summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/concepts/expressions.md21
-rw-r--r--src/buildtool/build_engine/expression/evaluator.cpp122
-rw-r--r--test/buildtool/build_engine/expression/expression.test.cpp54
3 files changed, 197 insertions, 0 deletions
diff --git a/doc/concepts/expressions.md b/doc/concepts/expressions.md
index c5d120e4..aaea3d27 100644
--- a/doc/concepts/expressions.md
+++ b/doc/concepts/expressions.md
@@ -79,6 +79,27 @@ evaluation.
The value is the value of the key `"$1"` uninterpreted, if present,
and `null` otherwise.
+##### Quasi-Quoting: ``"`"``
+
+The value is the value of the key `"$1"` uninterpreted but replacing
+all outermost maps having a key `"type"` with the value either
+`","` or `",@"` in the following way.
+ - If the value for the key `"type"` is `","`, the value for the
+ key `"$1"` (or `null` if there is no key `"$1"`) is evaluated
+ and the map is replaced by the value of that evaluation.
+ - If the value for the key `"type"` is `",@"` it is an error if
+ that map is not an entry of a literal list. The value for the
+ key `"$1"` (or `[]` if there is no key `"$1"`) is evaluated;
+ the result of that evaluation has to be a list. The entries of
+ that list (i.e., the list obtained by evaluating the value for
+ `"$1"`) are inserted (not the list itself) into the surrounding
+ list replacing that map.
+For example, ``{"type": "`", "$1": [1, 2, {"type": ",@", "$1": [3, 4]}]}``
+evaluates to `[1, 2, 3, 4]` while
+``{"type": "`", "$1": [1, 2, {"type": ",", "$1": [3, 4]}]}``
+evaluates to `[1, 2, [3, 4]]`.
+
+
##### Sequential binding: `"let*"`
The key `"bindings"` (default `[]`) has to be (syntactically) a
diff --git a/src/buildtool/build_engine/expression/evaluator.cpp b/src/buildtool/build_engine/expression/evaluator.cpp
index 673b80b3..707e9dc6 100644
--- a/src/buildtool/build_engine/expression/evaluator.cpp
+++ b/src/buildtool/build_engine/expression/evaluator.cpp
@@ -499,6 +499,125 @@ auto VarExpr(SubExprEvaluator&& eval,
return result;
}
+auto OnlyInQuasiQuote(SubExprEvaluator&& /*eval*/,
+ ExpressionPtr const& /*expr*/,
+ Configuration const& /*env*/) -> ExpressionPtr {
+ throw Evaluator::EvaluationError{
+ R"("," and ",@" are only evaluated within quasi-quote ("`") environments.)"};
+}
+
+auto ExpandQuasiQuote(const SubExprEvaluator& eval,
+ ExpressionPtr const& expr,
+ Configuration const& env) -> ExpressionPtr;
+
+// NOLINTNEXTLINE(misc-no-recursion)
+auto ExpandQuasiQuoteListEntry(const SubExprEvaluator& eval,
+ ExpressionPtr const& expr,
+ Configuration const& env) -> ExpressionPtr {
+ if (expr->IsList()) {
+ auto result = Expression::list_t{};
+ for (auto const& entry : expr->List()) {
+ auto expanded = ExpandQuasiQuoteListEntry(eval, entry, env);
+ std::copy(expanded->List().begin(),
+ expanded->List().end(),
+ std::back_inserter(result));
+ }
+ return ExpressionPtr{Expression::list_t{ExpressionPtr{result}}};
+ }
+ if (expr->IsMap()) {
+ if (auto type_token = expr->At("type")) {
+ if (type_token->get()->IsString()) {
+ auto token = type_token->get()->String();
+ if (token == ",") {
+ auto arg = expr->At("$1");
+ if (not arg) {
+ return ExpressionPtr{Expression::kNone};
+ }
+ auto result = eval(*arg, env);
+ return ExpressionPtr{Expression::list_t{result}};
+ }
+ if (token == ",@") {
+ auto arg = expr->At("$1");
+ if (not arg) {
+ return Expression::kEmptyList;
+ }
+ auto result = eval(*arg, env);
+ if (not result->IsList()) {
+ throw Evaluator::EvaluationError{fmt::format(
+ "Argument of \",@\"-expresion {} should evaluate "
+ "to a list, but obtained {}",
+ expr->ToString(),
+ result->ToString())};
+ }
+ return result;
+ }
+ }
+ }
+ // regular map, walk through the entries and quasiquote expand them
+ auto result = Expression::map_t::underlying_map_t{};
+ for (auto const& el : expr->Map()) {
+ auto expanded = ExpandQuasiQuote(eval, el.second, env);
+ result[el.first] = expanded;
+ }
+ return ExpressionPtr{Expression::map_t{result}};
+ }
+ // not a container, spliced literally, i.e., return singleton list
+ return ExpressionPtr{Expression::list_t{expr}};
+}
+
+// NOLINTNEXTLINE(misc-no-recursion)
+auto ExpandQuasiQuote(const SubExprEvaluator& eval,
+ ExpressionPtr const& expr,
+ Configuration const& env) -> ExpressionPtr {
+ if (expr->IsList()) {
+ auto result = Expression::list_t{};
+ for (auto const& entry : expr->List()) {
+ auto expanded = ExpandQuasiQuoteListEntry(eval, entry, env);
+ std::copy(expanded->List().begin(),
+ expanded->List().end(),
+ std::back_inserter(result));
+ }
+ return ExpressionPtr{result};
+ }
+ if (expr->IsMap()) {
+ if (auto type_token = expr->At("type")) {
+ if (type_token->get()->IsString()) {
+ auto token = type_token->get()->String();
+ if (token == ",") {
+ auto arg = expr->At("$1");
+ if (not arg) {
+ return Expression::kNone;
+ }
+ return eval(*arg, env);
+ }
+ if (token == ",@") {
+ throw Evaluator::EvaluationError{fmt::format(
+ "\",@\"-expression found in non-list context: {}",
+ expr->ToString())};
+ }
+ }
+ }
+ // regular map, walk through the entries and quasiquote expand them
+ auto result = Expression::map_t::underlying_map_t{};
+ for (auto const& el : expr->Map()) {
+ auto expanded = ExpandQuasiQuote(eval, el.second, env);
+ result[el.first] = expanded;
+ }
+ return ExpressionPtr{Expression::map_t{result}};
+ }
+ // not a container, return literal anyway
+ return expr;
+}
+
+auto QuasiQuoteExpr(SubExprEvaluator&& eval,
+ ExpressionPtr const& expr,
+ Configuration const& env) -> ExpressionPtr {
+ if (auto const to_expand = expr->At("$1")) {
+ return ExpandQuasiQuote(eval, *to_expand, env);
+ }
+ return Expression::kNone;
+}
+
auto QuoteExpr(SubExprEvaluator&& /*eval*/,
ExpressionPtr const& expr,
Configuration const& /*env*/) -> ExpressionPtr {
@@ -1099,6 +1218,9 @@ auto AssertNonEmptyExpr(SubExprEvaluator&& eval,
auto const kBuiltInFunctions =
FunctionMap::MakePtr({{"var", VarExpr},
{"'", QuoteExpr},
+ {"`", QuasiQuoteExpr},
+ {",", OnlyInQuasiQuote},
+ {",@", OnlyInQuasiQuote},
{"if", IfExpr},
{"cond", CondExpr},
{"case", CaseExpr},
diff --git a/test/buildtool/build_engine/expression/expression.test.cpp b/test/buildtool/build_engine/expression/expression.test.cpp
index 888e7216..3ee12f79 100644
--- a/test/buildtool/build_engine/expression/expression.test.cpp
+++ b/test/buildtool/build_engine/expression/expression.test.cpp
@@ -405,6 +405,60 @@ TEST_CASE("Expression Evaluation", "[expression]") { // NOLINT
CHECK(result_empty == Expression::FromJson(R"(null)"_json));
}
+ SECTION("quasi-quote expression") {
+ auto expr = Expression::FromJson(R"({"type": "`", "$1":
+ { "foo": {"type": ",", "$1": {"type": "var", "name": "foo_var"}}
+ , "bar": [ 1, 2, ["deep", "literals"]
+ , {"type": ",@", "$1": {"type": "var", "name": "bar_var"}}
+ , 3
+ , {"type": ",", "$1": {"type": "var", "name": "bar_plain"}}
+ , 4, 5
+ , {"type": ",", "$1": {"type": "var", "name": "foo_var"}}
+ , [ "deep", "expansion"
+ , {"type": ",", "$1": {"type": "var", "name": "bar_plain"}}
+ , {"type": ",@", "$1": {"type": "var", "name": "bar_var"}}
+ , {"type": ",", "$1": {"type": "var", "name": "foo_var"}}
+ ]
+ ]
+ }
+ })"_json);
+ REQUIRE(expr);
+ env = env.Update("foo_var", "foo value"s);
+ env = env.Update("bar_var",
+ Expression::FromJson(R"(["b", "a", "r"])"_json));
+ env =
+ env.Update("bar_plain",
+ Expression::FromJson(R"(["kept", "as", "list"])"_json));
+ auto result = expr.Evaluate(env, fcts);
+ auto expected = Expression::FromJson(R"(
+ { "foo": "foo value"
+ , "bar": [ 1, 2, ["deep", "literals"]
+ , "b", "a", "r"
+ , 3
+ , ["kept", "as", "list"]
+ , 4, 5
+ , "foo value"
+ , [ "deep", "expansion"
+ , ["kept", "as", "list"]
+ , "b", "a", "r"
+ , "foo value"
+ ]
+ ]
+ })"_json);
+
+ CHECK(result == expected);
+
+ auto doc_example_a = Expression::FromJson(
+ R"({"type": "`", "$1": [1, 2, {"type": ",@", "$1": [3, 4]}]})"_json);
+ auto result_a = doc_example_a.Evaluate(env, fcts);
+ CHECK(result_a == Expression::FromJson(R"([1, 2, 3, 4])"_json));
+
+ auto doc_example_b = Expression::FromJson(
+ R"({"type": "`", "$1": [1, 2, {"type": ",", "$1": [3, 4]}]})"_json);
+ auto result_b = doc_example_b.Evaluate(env, fcts);
+ CHECK(result_b == Expression::FromJson(R"([1, 2, [3, 4]])"_json));
+ }
+
SECTION("if expression") {
auto expr = Expression::FromJson(R"(
{ "type": "if"