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"})}},