Skip to content

Commit 0751bc5

Browse files
sidmohan0ngxson
andauthored
jinja : add missing 'in' test to template engine (ggml-org#19004) (ggml-org#19239)
* jinja : add missing 'in' test to template engine (ggml-org#19004) The jinja template parser was missing the 'in' test from global_builtins(), causing templates using reject("in", ...), select("in", ...), or 'x is in(y)' to fail with "selectattr: unknown test 'in'". This broke tool-calling for Qwen3-Coder and any other model whose chat template uses the 'in' test. Added test_is_in supporting array, string, and object containment checks, mirroring the existing 'in' operator logic in runtime.cpp. Includes test cases for all three containment types plus reject/select filter usage. * reuse test_is_in in binary op --------- Co-authored-by: Sid Mohan <sidmohan0@users.noreply.github.com> Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
1 parent 07a7412 commit 0751bc5

3 files changed

Lines changed: 93 additions & 18 deletions

File tree

common/jinja/runtime.cpp

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,13 @@ value binary_expression::execute_impl(context & ctx) {
144144
return false;
145145
};
146146

147+
auto test_is_in = [&]() -> bool {
148+
func_args args(ctx);
149+
args.push_back(left_val);
150+
args.push_back(right_val);
151+
return global_builtins().at("test_is_in")(args)->as_bool();
152+
};
153+
147154
// Handle undefined and null values
148155
if (is_val<value_undefined>(left_val) || is_val<value_undefined>(right_val)) {
149156
if (is_val<value_undefined>(right_val) && (op.value == "in" || op.value == "not in")) {
@@ -223,19 +230,11 @@ value binary_expression::execute_impl(context & ctx) {
223230
return result;
224231
}
225232
} else if (is_val<value_array>(right_val)) {
226-
auto & arr = right_val->as_array();
227-
bool member = false;
228-
for (const auto & item : arr) {
229-
if (*left_val == *item) {
230-
member = true;
231-
break;
232-
}
233-
}
233+
// case: 1 in [0, 1, 2]
234+
bool member = test_is_in();
234235
if (op.value == "in") {
235-
JJ_DEBUG("Checking membership: %s in Array is %d", left_val->type().c_str(), member);
236236
return mk_val<value_bool>(member);
237237
} else if (op.value == "not in") {
238-
JJ_DEBUG("Checking non-membership: %s not in Array is %d", left_val->type().c_str(), !member);
239238
return mk_val<value_bool>(!member);
240239
}
241240
}
@@ -252,22 +251,23 @@ value binary_expression::execute_impl(context & ctx) {
252251

253252
// String membership
254253
if (is_val<value_string>(left_val) && is_val<value_string>(right_val)) {
255-
auto left_str = left_val->as_string().str();
256-
auto right_str = right_val->as_string().str();
254+
// case: "a" in "abc"
255+
bool member = test_is_in();
257256
if (op.value == "in") {
258-
return mk_val<value_bool>(right_str.find(left_str) != std::string::npos);
257+
return mk_val<value_bool>(member);
259258
} else if (op.value == "not in") {
260-
return mk_val<value_bool>(right_str.find(left_str) == std::string::npos);
259+
return mk_val<value_bool>(!member);
261260
}
262261
}
263262

264263
// Value key in object
265264
if (is_val<value_object>(right_val)) {
266-
bool has_key = right_val->has_key(left_val);
265+
// case: key in {key: value}
266+
bool member = test_is_in();
267267
if (op.value == "in") {
268-
return mk_val<value_bool>(has_key);
268+
return mk_val<value_bool>(member);
269269
} else if (op.value == "not in") {
270-
return mk_val<value_bool>(!has_key);
270+
return mk_val<value_bool>(!member);
271271
}
272272
}
273273

common/jinja/value.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,33 @@ const func_builtins & global_builtins() {
393393
{"test_is_lt", test_compare_fn<value_compare_op::lt>},
394394
{"test_is_lessthan", test_compare_fn<value_compare_op::lt>},
395395
{"test_is_ne", test_compare_fn<value_compare_op::ne>},
396+
{"test_is_in", [](const func_args & args) -> value {
397+
args.ensure_count(2);
398+
auto needle = args.get_pos(0);
399+
auto haystack = args.get_pos(1);
400+
if (is_val<value_undefined>(haystack)) {
401+
return mk_val<value_bool>(false);
402+
}
403+
if (is_val<value_array>(haystack)) {
404+
for (const auto & item : haystack->as_array()) {
405+
if (*needle == *item) {
406+
return mk_val<value_bool>(true);
407+
}
408+
}
409+
return mk_val<value_bool>(false);
410+
}
411+
if (is_val<value_string>(haystack)) {
412+
if (!is_val<value_string>(needle)) {
413+
throw raised_exception("'in' test expects args[1] as string when args[0] is string, got args[1] as " + needle->type());
414+
}
415+
return mk_val<value_bool>(
416+
haystack->as_string().str().find(needle->as_string().str()) != std::string::npos);
417+
}
418+
if (is_val<value_object>(haystack)) {
419+
return mk_val<value_bool>(haystack->has_key(needle));
420+
}
421+
throw raised_exception("'in' test expects iterable as first argument, got " + haystack->type());
422+
}},
396423
{"test_is_test", [](const func_args & args) -> value {
397424
args.ensure_vals<value_string>();
398425
auto & builtins = global_builtins();

tests/test-jinja.cpp

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,12 +189,24 @@ static void test_conditionals(testing & t) {
189189
"negated"
190190
);
191191

192-
test_template(t, "in operator",
192+
test_template(t, "in operator (element in array)",
193193
"{% if 'x' in items %}found{% endif %}",
194194
{{"items", json::array({"x", "y"})}},
195195
"found"
196196
);
197197

198+
test_template(t, "in operator (substring)",
199+
"{% if 'bc' in 'abcd' %}found{% endif %}",
200+
json::object(),
201+
"found"
202+
);
203+
204+
test_template(t, "in operator (object key)",
205+
"{% if 'key' in obj %}found{% endif %}",
206+
{{"obj", {{"key", 1}, {"other", 2}}}},
207+
"found"
208+
);
209+
198210
test_template(t, "is defined",
199211
"{% if x is defined %}yes{% else %}no{% endif %}",
200212
{{"x", 1}},
@@ -1036,6 +1048,42 @@ static void test_tests(testing & t) {
10361048
json::object(),
10371049
"yes"
10381050
);
1051+
1052+
test_template(t, "is in (array, true)",
1053+
"{{ 'yes' if 2 is in([1, 2, 3]) }}",
1054+
json::object(),
1055+
"yes"
1056+
);
1057+
1058+
test_template(t, "is in (array, false)",
1059+
"{{ 'yes' if 5 is in([1, 2, 3]) else 'no' }}",
1060+
json::object(),
1061+
"no"
1062+
);
1063+
1064+
test_template(t, "is in (string)",
1065+
"{{ 'yes' if 'bc' is in('abcde') }}",
1066+
json::object(),
1067+
"yes"
1068+
);
1069+
1070+
test_template(t, "is in (object keys)",
1071+
"{{ 'yes' if 'a' is in(obj) }}",
1072+
{{"obj", {{"a", 1}, {"b", 2}}}},
1073+
"yes"
1074+
);
1075+
1076+
test_template(t, "reject with in test",
1077+
"{{ items | reject('in', skip) | join(', ') }}",
1078+
{{"items", json::array({"a", "b", "c", "d"})}, {"skip", json::array({"b", "d"})}},
1079+
"a, c"
1080+
);
1081+
1082+
test_template(t, "select with in test",
1083+
"{{ items | select('in', keep) | join(', ') }}",
1084+
{{"items", json::array({"a", "b", "c", "d"})}, {"keep", json::array({"b", "c"})}},
1085+
"b, c"
1086+
);
10391087
}
10401088

10411089
static void test_string_methods(testing & t) {

0 commit comments

Comments
 (0)