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