mirror of
https://github.com/ikawrakow/ik_llama.cpp.git
synced 2026-06-28 04:30:15 -05:00
jinja: give each for-loop iteration a fresh scope (#2018)
`{% set %}` of a non-loop variable inside a `{% for %}` body leaks
across iterations when the assignment is conditionally skipped. Each
iteration should start with a clean scope, matching standard Jinja2
semantics.
This fixes the issue with GLM-5.2 chat template when:
* turn 1 is a tool call with reasoning
* turn 2 is a tool call without reasoning
In this case, the reasoning content for turn 1 would be wrongly
duplicated to turn 2, resulting in degraded model performance.
This commit is contained in:
parent
a7d35d51dc
commit
997b289d93
@ -596,11 +596,18 @@ value for_statement::execute_impl(context & ctx) {
|
|||||||
loop_obj->insert("length", mk_val<value_int>(filtered_items.size()));
|
loop_obj->insert("length", mk_val<value_int>(filtered_items.size()));
|
||||||
loop_obj->insert("previtem", i > 0 ? filtered_items[i - 1] : mk_val<value_undefined>("previtem"));
|
loop_obj->insert("previtem", i > 0 ? filtered_items[i - 1] : mk_val<value_undefined>("previtem"));
|
||||||
loop_obj->insert("nextitem", i < filtered_items.size() - 1 ? filtered_items[i + 1] : mk_val<value_undefined>("nextitem"));
|
loop_obj->insert("nextitem", i < filtered_items.size() - 1 ? filtered_items[i + 1] : mk_val<value_undefined>("nextitem"));
|
||||||
scope.set_val("loop", loop_obj);
|
// Use a fresh scope for each iteration so that {% set %} variables
|
||||||
scope_update_fns[i](scope);
|
// (including ones assigned only conditionally inside the body) do not
|
||||||
|
// leak across iterations. This matches standard Jinja2 semantics, where
|
||||||
|
// each loop iteration starts with a clean scope. State that must
|
||||||
|
// accumulate across iterations has to use namespace(), whose mutations
|
||||||
|
// are applied to the shared object referenced from the enclosing scope.
|
||||||
|
context iter_scope(scope);
|
||||||
|
iter_scope.set_val("loop", loop_obj);
|
||||||
|
scope_update_fns[i](iter_scope);
|
||||||
try {
|
try {
|
||||||
for (auto & stmt : body) {
|
for (auto & stmt : body) {
|
||||||
value val = stmt->execute(scope);
|
value val = stmt->execute(iter_scope);
|
||||||
result->push_back(val);
|
result->push_back(val);
|
||||||
}
|
}
|
||||||
} catch (const continue_statement::signal &) {
|
} catch (const continue_statement::signal &) {
|
||||||
|
|||||||
@ -360,6 +360,18 @@ static void test_loops(testing & t) {
|
|||||||
json::object(),
|
json::object(),
|
||||||
"012"
|
"012"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// {% set %} of a non-loop variable inside a loop body must not leak across
|
||||||
|
// iterations. When a variable is only assigned on some iterations (here via
|
||||||
|
// a conditional branch), the engine must still treat it as undefined on the
|
||||||
|
// iterations where the branch is not taken, rather than reusing the value
|
||||||
|
// from a previous iteration. This matches standard Jinja2 semantics, where
|
||||||
|
// each loop iteration starts with a fresh scope.
|
||||||
|
test_template(t, "conditional set does not leak across iterations",
|
||||||
|
"{%- for m in msgs %}{% if m.x %}{% set r = m.x %}{% endif %}[{{ r|default('-') }}]{% endfor %}",
|
||||||
|
{{"msgs", json::array({json{{"x", "a"}}, json::object(), json{{"x", "c"}}})}},
|
||||||
|
"[a][-][c]"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_expressions(testing & t) {
|
static void test_expressions(testing & t) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user