Commit a681a53b6 for llama.cpp

commit a681a53b6092d57597b9e66f2fc9d4556155b9e4
Author: Xuan Son Nguyen <son@huggingface.co>
Date:   Sun Nov 15 19:33:06 2026 +0100

    jinja: add 4 missing features

diff --git a/common/jinja/lexer.cpp b/common/jinja/lexer.cpp
index bf2dfacda..7338d2eb1 100644
--- a/common/jinja/lexer.cpp
+++ b/common/jinja/lexer.cpp
@@ -6,6 +6,7 @@
 #include <charconv>
 #include <functional>
 #include <map>
+#include <set>
 #include <string>
 #include <vector>

@@ -272,6 +273,12 @@ lexer_result lexer::tokenize(const std::string & source) {
             if (last_token_type == token::text || last_token_type == token::eof) {
                 throw lexer_exception(std::string("unexpected character: ") + ch, source, pos);
             }
+            // keyword followed by an operand, e.g. `x if c else -1`; `a.else - 1` is still binary
+            static const std::set<std::string> operand_keywords = { "if", "else", "and", "or", "not", "in", "is" };
+            const bool is_attr = tokens.size() >= 2 && tokens[tokens.size() - 2].t == token::dot;
+            if (last_token_type == token::identifier && !is_attr && operand_keywords.count(tokens.back().value)) {
+                last_token_type = token::eof; // handle as unary below
+            }
             switch (last_token_type) {
                 case token::identifier:
                 case token::numeric_literal:
diff --git a/common/jinja/lexer.h b/common/jinja/lexer.h
index 3f4ebb62a..4a7ea83a3 100644
--- a/common/jinja/lexer.h
+++ b/common/jinja/lexer.h
@@ -141,6 +141,7 @@ struct lexer {
         {"~", token::additive_binary_operator},
         {"**", token::power_operator},
         {"*", token::multiplicative_binary_operator},
+        {"//", token::multiplicative_binary_operator},
         {"/", token::multiplicative_binary_operator},
         {"%", token::multiplicative_binary_operator},
         // Assignment operator
diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp
index 8bd8ab1c8..aae67cab0 100644
--- a/common/jinja/runtime.cpp
+++ b/common/jinja/runtime.cpp
@@ -196,6 +196,25 @@ static value exec_binary_op(context & ctx, const token & op, value left_val, val
         } else if (op.value == "/") {
             JJ_DEBUG("Division operation: %f / %f", a, b);
             return mk_val<value_float>(a / b);
+        } else if (op.value == "//") {
+            // python semantics: round toward -inf
+            if (is_val<value_int>(left_val) && is_val<value_int>(right_val)) {
+                const int64_t x = left_val->as_int();
+                const int64_t y = right_val->as_int();
+                if (y == 0) {
+                    throw std::runtime_error("integer division by zero");
+                }
+                if (y == -1) {
+                    // INT64_MIN / -1 is UB, wrap around instead
+                    return mk_val<value_int>((int64_t) (0 - (uint64_t) x));
+                }
+                int64_t q = x / y;
+                if (x % y != 0 && (x < 0) != (y < 0)) {
+                    q -= 1;
+                }
+                return mk_val<value_int>(q);
+            }
+            return mk_val<value_float>(std::floor(a / b));
         } else if (op.value == "%") {
             double rem = std::fmod(a, b);
             JJ_DEBUG("Modulo operation: %f %% %f = %f", a, b, rem);
@@ -274,12 +293,17 @@ static value exec_binary_op(context & ctx, const token & op, value left_val, val
     // String membership
     if (is_val<value_string>(left_val) && is_val<value_string>(right_val)) {
         // case: "a" in "abc"
-        bool member = test_is_in();
-        if (op.value == "in") {
-            return mk_val<value_bool>(member);
-        } else if (op.value == "not in") {
-            return mk_val<value_bool>(!member);
+        if (op.value == "in" || op.value == "not in") {
+            bool member = test_is_in();
+            return mk_val<value_bool>(op.value == "in" ? member : !member);
         }
+        // case: "a" < "b"; UTF-8 byte order is the same as codepoint order, like python
+        const std::string a = left_val->as_string().str();
+        const std::string b = right_val->as_string().str();
+        if (op.value == "<")  { return mk_val<value_bool>(a <  b); }
+        if (op.value == ">")  { return mk_val<value_bool>(a >  b); }
+        if (op.value == "<=") { return mk_val<value_bool>(a <= b); }
+        if (op.value == ">=") { return mk_val<value_bool>(a >= b); }
     }

     // Value key in object
diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp
index 561a4b86a..c75fa1d51 100644
--- a/common/jinja/value.cpp
+++ b/common/jinja/value.cpp
@@ -1101,8 +1101,25 @@ const func_builtins & value_bool_t::get_builtins() const {
     return builtins;
 }

-[[noreturn]] static value array_sum_not_implemented(const func_args &) {
-    throw not_implemented_exception("Array sum builtin not implemented");
+static value array_sum(const func_args & args) {
+    args.ensure_kwargs({"attribute", "start"});
+    args.ensure_count(1, 3);
+    args.ensure_vals<value_array>();
+    value undef     = mk_val<value_undefined>();
+    value attribute = args.get_kwarg_or_pos("attribute", 1);
+    value start     = args.get_kwarg_or_pos("start",     2);
+    // same as the + operator: compute in double, int result if all operands are int
+    bool   is_float = is_val<value_float>(start);
+    double sum      = start->is_undefined() ? 0.0 : start->as_float();
+    for (const auto & item : args.get_pos(0)->as_array()) {
+        value v = attribute->is_undefined() ? item : get_attribute(item, attribute, undef);
+        if (!is_val<value_int>(v) && !is_val<value_float>(v)) {
+            throw raised_exception("sum: unsupported operand type " + v->type());
+        }
+        is_float = is_float || is_val<value_float>(v);
+        sum += v->as_float();
+    }
+    return is_float ? value(mk_val<value_float>(sum)) : value(mk_val<value_int>(static_cast<int64_t>(sum)));
 }

 const func_builtins & value_array_t::get_builtins() const {
@@ -1171,7 +1188,7 @@ const func_builtins & value_array_t::get_builtins() const {
         {"select", select_or_reject<false, false>},
         {"rejectattr", select_or_reject<true, true>},
         {"reject", select_or_reject<true, false>},
-        {"sum", array_sum_not_implemented},
+        {"sum", array_sum},
         {"join", [](const func_args & args) -> value {
             args.ensure_kwargs({"d", "attribute"});
             args.ensure_count(1, 3);
diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp
index d52f1af9c..8fd275222 100644
--- a/tests/test-jinja.cpp
+++ b/tests/test-jinja.cpp
@@ -657,6 +657,30 @@ static void test_expressions(testing & t) {
         "20"
     );

+    test_template(t, "floor division",
+        "{{ 7 // 2 }},{{ -7 // 2 }},{{ 7 // -2 }},{{ 7.5 // 2 }},{{ a // b }}",
+        {{"a", 10}, {"b", 3}},
+        "3,-4,-4,3.0,3"
+    );
+
+    test_template(t, "string comparison",
+        "{{ 'b' > 'a' }},{{ 'VBD' > 'VB' }},{{ 'a' <= 'a' }},{{ 'Z' < 'a' }},{{ 'ab' >= 'b' }}",
+        json::object(),
+        "True,True,True,True,False"
+    );
+
+    test_template(t, "unary minus after keyword",
+        "{{ 1 if false else -1 }},{{ not -1 }},{{ x and -2 }},{{ -3 if x }}",
+        {{"x", true}},
+        "-1,False,-2,-3"
+    );
+
+    test_template(t, "binary minus after keyword attribute",
+        "{{ a.else - 1 }}",
+        {{"a", {{"else", 3}}}},
+        "2"
+    );
+
     test_template(t, "string concat ~",
         "{{ 'hello' ~ ' ' ~ 'world' }}",
         json::object(),
@@ -2190,6 +2214,12 @@ static void test_array_methods(testing & t) {
         "6"
     );

+    test_template(t, "array|sum",
+        "{{ [1, 2.5, 3]|sum }},{{ [1, 2]|sum(start=10) }},{{ items|sum(attribute='w') }},{{ []|sum }}",
+        {{"items", json::array({{{"w", 1}}, {{"w", 2}}})}},
+        "6.5,13,3,0"
+    );
+
     test_template(t, "array|min",
         "{{ [tool_calls_count, tool_sep_count]|min }}",
         {{"tool_calls_count", 2}, {"tool_sep_count", 1}},