From 4e416ee7308dd6b581796f1a6241276cd5982691 Mon Sep 17 00:00:00 2001 From: Si Chen <62344747+cs-fisha@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:29:45 +0800 Subject: [PATCH] jinja : parse unary +/- before variables (#29244) * jinja : parse unary +/- before variables Lexer already emits unary_operator for -n / +n, and runtime executes unary -. Parse them at multiplicative precedence so slices like items[:-n] and GigaChat indent[:-indent_factor] work. * jinja : keep filters/tests outside unary operands Unary +/- must bind only the primary/postfix operand so -n|abs is (-n)|abs, not -(n|abs). Add unary + and filter/test regression coverage. Signed-off-by: sinksilk <785976238@qq.com> --------- Signed-off-by: sinksilk <785976238@qq.com> --- common/jinja/parser.cpp | 12 ++++++++++- common/jinja/runtime.cpp | 5 +++++ tests/test-jinja.cpp | 43 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/common/jinja/parser.cpp b/common/jinja/parser.cpp index 2b25654a7a..1ddceede91 100644 --- a/common/jinja/parser.cpp +++ b/common/jinja/parser.cpp @@ -437,7 +437,8 @@ private: } statement_ptr parse_filter_expression() { - auto operand = parse_call_member_expression(); + // Filters/tests bind outside unary so -n|abs is (-n)|abs, not -(n|abs). + auto operand = parse_unary_expression(); while (is(token::pipe)) { size_t start_pos = current; ++current; // consume pipe @@ -448,6 +449,15 @@ private: return operand; } + statement_ptr parse_unary_expression() { + if (is(token::unary_operator)) { + size_t start_pos = current; + auto op = next(); + return mk_stmt(start_pos, op, parse_unary_expression()); + } + return parse_call_member_expression(); + } + statement_ptr parse_call_member_expression() { // Handle member expressions recursively auto member = parse_member_expression(parse_primary_expression()); diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp index 8b012ebdc5..e2cab8aa64 100644 --- a/common/jinja/runtime.cpp +++ b/common/jinja/runtime.cpp @@ -450,6 +450,11 @@ value unary_expression::execute_impl(context & ctx) const { } else { throw std::runtime_error("Unary - operator requires numeric operand"); } + } else if (op.value == "+") { + if (is_val(operand_val) || is_val(operand_val)) { + return operand_val; + } + throw std::runtime_error("Unary + operator requires numeric operand"); } throw std::runtime_error("Unknown unary operator '" + op.value + "'"); diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 00de91ddf9..cd8fa723ca 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -458,6 +458,49 @@ static void test_expressions(testing & t) { "['b']" ); + test_template(t, "array slice negative variable", + "{{ items[:-n]|string }}", + {{"items", json::array({"a", "b", "c"})}, {"n", 1}}, + "['a', 'b']" + ); + + test_template(t, "array slice negative variable indent", + "{{ indent[:-indent_factor] }}", + {{"indent", " "}, {"indent_factor", 2}}, + " " + ); + + test_template(t, "unary minus variable", + "{{ -n }}", + {{"n", 3}}, + "-3" + ); + + test_template(t, "unary plus variable", + "{{ +n }}", + {{"n", -3}}, + "-3" + ); + + test_template(t, "unary plus float", + "{{ +x }}", + {{"x", -1.5}}, + "-1.5" + ); + + // Unary binds tighter than filter: -n|abs == (-n)|abs, not -(n|abs) + test_template(t, "unary minus then abs filter", + "{{ -n|abs }}", + {{"n", -3}}, + "3" + ); + + test_template(t, "unary minus then number test", + "{{ -n is number }}", + {{"n", 3}}, + "True" + ); + test_template(t, "array slice step", "{{ items[::2]|string }}", {{"items", json::array({"a", "b", "c"})}},