diff options
-rw-r--r-- | doc/concepts/expressions.md | 21 | ||||
-rw-r--r-- | src/buildtool/build_engine/expression/evaluator.cpp | 122 | ||||
-rw-r--r-- | test/buildtool/build_engine/expression/expression.test.cpp | 54 |
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" |