Commit 4e416ee73 for llama.cpp
commit 4e416ee7308dd6b581796f1a6241276cd5982691
Author: Si Chen <62344747+cs-fisha@users.noreply.github.com>
Date: Wed Sep 23 19:29:45 2026 +0800
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>
diff --git a/common/jinja/parser.cpp b/common/jinja/parser.cpp
index 2b25654a7..1ddceede9 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<unary_expression>(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 8b012ebdc..e2cab8aa6 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<value_int>(operand_val) || is_val<value_float>(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 00de91ddf..cd8fa723c 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"})}},