Commit ca2e2037b for llama.cpp
commit ca2e2037b68ae7d821113d538110e6ce6903864c
Author: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>
Date: Wed Sep 30 15:33:26 2026 +0200
jinja : support coerced array attributes (#29574)
* support coerced array attributes
* add tests
diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp
index e2cab8aa6..a44d63c0c 100644
--- a/common/jinja/runtime.cpp
+++ b/common/jinja/runtime.cpp
@@ -888,7 +888,7 @@ value member_expression::execute_impl(context & ctx) const {
JJ_DEBUG("Accessed property '%s' value, got type: %s", key.c_str(), val->type().c_str());
} else if (is_val<value_array>(object) || is_val<value_string>(object)) {
- if (is_val<value_int>(property)) {
+ if (is_val<value_int>(property) || is_val<value_bool>(property)) {
int64_t index = property->as_int();
JJ_DEBUG("Accessing %s index %d", object->type().c_str(), (int)index);
if (is_val<value_array>(object)) {
@@ -911,8 +911,6 @@ value member_expression::execute_impl(context & ctx) const {
JJ_DEBUG("Accessing %s built-in '%s'", is_val<value_array>(object) ? "array" : "string", key.c_str());
val = try_builtin_func(ctx, key, object, true);
- } else {
- throw std::runtime_error("Cannot access property with non-string/non-number: got " + property->type());
}
} else {
if (!is_val<value_string>(property)) {
@@ -926,10 +924,10 @@ value member_expression::execute_impl(context & ctx) const {
value_t::stats_t::mark_used(val);
value_t::stats_t::mark_used(object);
value_t::stats_t::mark_used(property);
- if (is_val<value_int>(property)) {
- object->stats.ops.insert("array_access");
- } else if (is_val<value_string>(property)) {
+ if (is_val<value_object>(object) || is_val<value_string>(property) || is_val<value_float>(property) || is_val<value_array>(property) || is_val<value_none>(property)) {
object->stats.ops.insert("object_access");
+ } else if (is_val<value_int>(property) || is_val<value_bool>(property)) {
+ object->stats.ops.insert("array_access");
}
}
diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp
index 10130a6b0..6edd8b96f 100644
--- a/common/jinja/value.cpp
+++ b/common/jinja/value.cpp
@@ -149,6 +149,13 @@ static value test_type_fn(const func_args & args) {
JJ_DEBUG("test_type_fn: type=%s, %s or %s result=%d", typeid(T).name(), typeid(U).name(), typeid(V).name(), is_type ? 1 : 0);
return mk_val<value_bool>(is_type);
}
+template<typename T, typename U, typename V, typename W>
+static value test_type_fn(const func_args & args) {
+ args.ensure_count(1);
+ bool is_type = is_val<T>(args.get_pos(0)) || is_val<U>(args.get_pos(0)) || is_val<V>(args.get_pos(0)) || is_val<W>(args.get_pos(0));
+ JJ_DEBUG("test_type_fn: type=%s, %s, %s or %s result=%d", typeid(T).name(), typeid(U).name(), typeid(V).name(), typeid(W).name(), is_type ? 1 : 0);
+ return mk_val<value_bool>(is_type);
+}
template<value_compare_op op>
static value test_compare_fn(const func_args & args) {
args.ensure_count(2, 2);
@@ -261,6 +268,30 @@ static value tojson(const func_args & args) {
return mk_val<value_string>(json_str);
}
+static value & get_attribute(const value & val, const value & attr, value & default_val) {
+ if (!attr->is_undefined()) {
+ if (is_val<value_array>(val)) {
+ value idx = attr;
+
+ if (is_val<value_string>(attr)) {
+ const std::string s = attr->as_string().str();
+ if (!s.empty() && std::all_of(s.begin(), s.end(), [](unsigned char c) { return std::isdigit(c); })) {
+ try {
+ idx = mk_val<value_int>(std::stoll(s));
+ } catch (...) {
+ idx = mk_val<value_undefined>();
+ }
+ }
+ }
+
+ return val->at(idx, default_val);
+ } else if (is_val<value_object>(val)) {
+ return val->at(attr, default_val);
+ }
+ }
+ return default_val;
+}
+
template<bool is_reject>
static value selectattr(const func_args & args) {
args.ensure_count(2, 4);
@@ -274,10 +305,7 @@ static value selectattr(const func_args & args) {
if (args.count() == 2) {
// example: array | selectattr("active")
for (const auto & item : arr) {
- if (!is_val<value_object>(item)) {
- throw raised_exception("selectattr: item is not an object");
- }
- value attr_val = item->at(attribute, val_default);
+ value attr_val = get_attribute(item, attribute, val_default);
bool is_selected = attr_val->as_bool();
if constexpr (is_reject) is_selected = !is_selected;
if (is_selected) out->push_back(item);
@@ -318,10 +346,7 @@ static value selectattr(const func_args & args) {
}
auto test_fn = it->second;
for (const auto & item : arr) {
- if (!is_val<value_object>(item)) {
- throw raised_exception("selectattr: item is not an object");
- }
- value attr_val = item->at(attribute, val_default);
+ value attr_val = get_attribute(item, attribute, val_default);
func_args test_args(args.ctx);
test_args.push_back(attr_val); // attribute value
test_args.push_back(extra_arg); // extra argument
@@ -478,8 +503,8 @@ const func_builtins & global_builtins() {
{"test_is_integer", test_type_fn<value_int>},
{"test_is_float", test_type_fn<value_float>},
{"test_is_number", test_type_fn<value_int, value_float>},
- {"test_is_iterable", test_type_fn<value_array, value_string, value_undefined>},
- {"test_is_sequence", test_type_fn<value_array, value_string, value_undefined>},
+ {"test_is_iterable", test_type_fn<value_object, value_array, value_string, value_undefined>},
+ {"test_is_sequence", test_type_fn<value_object, value_array, value_string, value_undefined>},
{"test_is_mapping", test_type_fn<value_object>},
{"test_is_lower", [](const func_args & args) -> value {
args.ensure_vals<value_string>();
@@ -1068,22 +1093,14 @@ const func_builtins & value_array_t::get_builtins() const {
}
value val_delim = args.get_kwarg_or_pos("d", 1);
value attribute = args.get_kwarg_or_pos("attribute", 2);
+ value undef = mk_val<value_undefined>();
const auto & arr = args.get_pos(0)->as_array();
- const bool attr_is_int = is_val<value_int>(attribute);
- if (!attribute->is_undefined() && !is_val<value_string>(attribute) && !attr_is_int) {
- throw raised_exception("join() attribute must be string or integer");
- }
- const int64_t attr_int = attr_is_int ? attribute->as_int() : 0;
const std::string delim = val_delim->is_undefined() ? "" : val_delim->as_string().str();
std::string result;
for (size_t i = 0; i < arr.size(); ++i) {
value val_arr = arr[i];
if (!attribute->is_undefined()) {
- if (attr_is_int && is_val<value_array>(val_arr)) {
- val_arr = val_arr->at(attr_int);
- } else if (!attr_is_int && is_val<value_object>(val_arr)) {
- val_arr = val_arr->at(attribute);
- }
+ val_arr = get_attribute(val_arr, attribute, undef);
}
if (!is_val<value_string>(val_arr) && !is_val<value_int>(val_arr) && !is_val<value_float>(val_arr)) {
throw raised_exception("join() can only join arrays of strings or numerics");
@@ -1115,21 +1132,11 @@ const func_builtins & value_array_t::get_builtins() const {
}
value val = args.get_pos(0);
value attribute = args.get_kwarg_or_pos("attribute", 1);
- const bool attr_is_int = is_val<value_int>(attribute);
- if (!is_val<value_string>(attribute) && !attr_is_int) {
- throw raised_exception("map: attribute must be string or integer");
- }
- const int64_t attr_int = attr_is_int ? attribute->as_int() : 0;
value default_val = args.get_kwarg("default", mk_val<value_undefined>());
auto out = mk_val<value_array>();
auto arr = val->as_array();
for (const auto & item : arr) {
- value attr_val;
- if (attr_is_int) {
- attr_val = is_val<value_array>(item) ? item->at(attr_int, default_val) : default_val;
- } else {
- attr_val = is_val<value_object>(item) ? item->at(attribute, default_val) : default_val;
- }
+ value attr_val = get_attribute(item, attribute, default_val);
out->push_back(attr_val);
}
return is_val<value_tuple>(val) ? mk_val<value_tuple>(std::move(out->as_array())) : out;
@@ -1166,22 +1173,14 @@ const func_builtins & value_array_t::get_builtins() const {
// FIXME: sorting is currently always case sensitive
//const bool case_sensitive = val_case->as_bool(); // undefined == false
const bool reverse = val_reverse->as_bool(); // undefined == false
- const bool attr_is_int = is_val<value_int>(attribute);
- const int64_t attr_int = attr_is_int ? attribute->as_int() : 0;
+ value undef = mk_val<value_undefined>();
std::vector<value> arr = val->as_array(); // copy
std::sort(arr.begin(), arr.end(),[&](const value & a, const value & b) {
value val_a = a;
value val_b = b;
if (!attribute->is_undefined()) {
- if (attr_is_int && is_val<value_array>(a) && is_val<value_array>(b)) {
- val_a = a->at(attr_int);
- val_b = b->at(attr_int);
- } else if (!attr_is_int && is_val<value_object>(a) && is_val<value_object>(b)) {
- val_a = a->at(attribute);
- val_b = b->at(attribute);
- } else {
- throw raised_exception("sort: unsupported object attribute comparison between " + a->type() + " and " + b->type());
- }
+ val_a = get_attribute(a, attribute, undef);
+ val_b = get_attribute(b, attribute, undef);
}
return value_compare(val_a, val_b, reverse ? value_compare_op::gt : value_compare_op::lt);
});
@@ -1199,19 +1198,23 @@ const func_builtins & value_array_t::get_builtins() const {
args.ensure_vals<value_array>();
value val_case = args.get_kwarg_or_pos("case_sensitive", 1);
value attribute = args.get_kwarg_or_pos("attribute", 2);
- if (!attribute->is_undefined()) {
- throw not_implemented_exception("min: attribute not implemented");
- }
// FIXME: min is currently always case sensitive
(void) val_case;
+ value undef = mk_val<value_undefined>();
const auto & arr = args.get_pos(0)->as_array();
if (arr.empty()) {
- return mk_val<value_undefined>();
+ return undef;
}
value result = arr[0];
- for (size_t i = 1; i < arr.size(); ++i) {
- if (value_compare(arr[i], result, value_compare_op::lt)) {
- result = arr[i];
+ for (const auto & item : arr) {
+ value val_arr = item;
+ value val_cmp = result;
+ if (!attribute->is_undefined()) {
+ val_arr = get_attribute(val_arr, attribute, undef);
+ val_cmp = get_attribute(val_cmp, attribute, undef);
+ }
+ if (value_compare(val_arr, val_cmp, value_compare_op::lt)) {
+ result = item;
}
}
return result;
@@ -1221,19 +1224,23 @@ const func_builtins & value_array_t::get_builtins() const {
args.ensure_vals<value_array>();
value val_case = args.get_kwarg_or_pos("case_sensitive", 1);
value attribute = args.get_kwarg_or_pos("attribute", 2);
- if (!attribute->is_undefined()) {
- throw not_implemented_exception("max: attribute not implemented");
- }
// FIXME: max is currently always case sensitive
(void) val_case;
+ value undef = mk_val<value_undefined>();
const auto & arr = args.get_pos(0)->as_array();
if (arr.empty()) {
- return mk_val<value_undefined>();
+ return undef;
}
value result = arr[0];
- for (size_t i = 1; i < arr.size(); ++i) {
- if (value_compare(arr[i], result, value_compare_op::gt)) {
- result = arr[i];
+ for (const auto & item : arr) {
+ value val_arr = item;
+ value val_cmp = result;
+ if (!attribute->is_undefined()) {
+ val_arr = get_attribute(val_arr, attribute, undef);
+ val_cmp = get_attribute(val_cmp, attribute, undef);
+ }
+ if (value_compare(val_arr, val_cmp, value_compare_op::gt)) {
+ result = item;
}
}
return result;
diff --git a/common/jinja/value.h b/common/jinja/value.h
index 4926fb680..142943dfd 100644
--- a/common/jinja/value.h
+++ b/common/jinja/value.h
@@ -433,6 +433,12 @@ struct value_array_t : public value_t {
}
return val_arr[index];
}
+ virtual value & at(const value & index, value & default_val) override {
+ if (!is_val<value_int>(index) && !is_val<value_bool>(index)) {
+ return default_val;
+ }
+ return at(index->as_int(), default_val);
+ }
virtual const func_builtins & get_builtins() const override;
virtual bool is_hashable() const override {
if (std::all_of(val_arr.begin(), val_arr.end(), [&](auto & val) -> bool {
diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp
index 891b785c4..7649e3624 100644
--- a/tests/test-jinja.cpp
+++ b/tests/test-jinja.cpp
@@ -452,6 +452,18 @@ static void test_expressions(testing & t) {
"c"
);
+ test_template(t, "array bool access",
+ "{{ items[true] }}",
+ {{"items", json::array({"a", "b", "c"})}},
+ "b"
+ );
+
+ test_template(t, "array non-index access",
+ "{{ items[1.0] is undefined }}",
+ {{"items", json::array({"a", "b", "c"})}},
+ "True"
+ );
+
test_template(t, "array slice",
"{{ items[1:-1]|string }}",
{{"items", json::array({"a", "b", "c"})}},
@@ -735,6 +747,16 @@ static void test_filters(testing & t) {
"xyz"
);
+ test_template(t, "sort with numeric-like attribute",
+ "{{ items|sort(attribute='01')|join(attribute=1) }}",
+ {{"items", json::array({
+ json::array({1, "z"}),
+ json::array({2, "x"}),
+ json::array({3, "y"}),
+ })}},
+ "xyz"
+ );
+
test_template(t, "join",
"{{ items|join(', ') }}",
{{"items", json::array({"a", "b", "c"})}},
@@ -1590,6 +1612,16 @@ static void test_array_methods(testing & t) {
"b c "
);
+ test_template(t, "array|selectattr numeric-like with operator",
+ "{% for item in items|selectattr('0', 'gt', 1) %}{{ item.1 }} {% endfor %}",
+ {{"items", json::array({
+ json::array({3, "z"}),
+ json::array({1, "x"}),
+ json::array({2, "y"}),
+ })}},
+ "z y "
+ );
+
test_template(t, "array|tojson",
"{{ arr|tojson }}",
{{"arr", json::array({1, 2, 3})}},
@@ -1654,6 +1686,12 @@ static void test_array_methods(testing & t) {
"123"
);
+ test_template(t, "array|join numeric-like attribute",
+ "{{ arr|join(attribute='0') }}",
+ {{"arr", json::array({json::array({1}), json::array({2}), json::array({3})})}},
+ "123"
+ );
+
test_template(t, "array.pop() last",
"{{ arr.pop() }}-{{ arr|join(',') }}",
{{"arr", json::array({"a", "b", "c"})}},
@@ -1712,6 +1750,16 @@ static void test_array_methods(testing & t) {
"10 20 30 "
);
+ test_template(t, "array|map with numeric-like attribute",
+ "{% for v in arr|map(attribute='1') %}{{ v }} {% endfor %}",
+ {{"arr", json::array({
+ json::array({10, "x"}),
+ json::array({20, "y"}),
+ json::array({30, "z"}),
+ })}},
+ "x y z "
+ );
+
test_template(t, "array|map with negative attribute",
"{% for v in arr|map(attribute=-1) %}{{ v }} {% endfor %}",
{{"arr", json::array({
@@ -1741,21 +1789,21 @@ static void test_array_methods(testing & t) {
);
test_template(t, "array|min attribute",
- "{{ items|min(attribute='x') }}",
+ "{{ items|min(attribute='x')|tojson }}",
{{"items", json::array({
json({{"x", 2}}),
json({{"x", 1}}),
})}},
- "{'x': 1}"
+ "{\"x\": 1}"
);
test_template(t, "array|max attribute",
- "{{ items|max(attribute='x') }}",
+ "{{ items|max(attribute='x')|tojson }}",
{{"items", json::array({
json({{"x", 2}}),
json({{"x", 1}}),
})}},
- "{'x': 2}"
+ "{\"x\": 2}"
);
// not used by any chat templates