execute method
Executes the statement within the given ctx and returns the result as a JinjaValue.
Implementation
@override
JinjaValue execute(Context ctx) {
// New scope for loop
// Evaluate iterable
// Handle SelectExpression (filtering) if present in iterable
Expression iterExpr = iterable;
Expression? testExpr;
if (iterable is SelectExpression) {
final sel = iterable as SelectExpression;
iterExpr = sel.lhs;
testExpr = sel.test;
}
JinjaValue iterableVal = iterExpr.execute(ctx);
if (iterableVal.isUndefined) {
iterableVal = const JinjaList([]);
}
if (!iterableVal.isList && !iterableVal.isMap && !iterableVal.isString) {
final got = iterableVal is JinjaFunction
? "${iterableVal.typeName} '${iterableVal.name}'"
: iterableVal.typeName;
throw Exception('Expected iterable in for loop: got $got');
}
List<JinjaValue> items = [];
if (iterableVal is JinjaMap) {
// Loop over keys
// Sort keys for deterministic output if needed, though Jinja2 doesn't strictly guarantee order, usually tests expect sorted keys or we should sort.
// Standard Jinja2 iterates keys.
// llama.cpp tests seem to expect keys.
// Keys with the same string form are iterated once, as in asMap.
final keys = <String, JinjaValue>{
for (final k in iterableVal.items.keys) k.toString(): k,
};
items = [
for (final MapEntry(:key, :value) in keys.entries)
value is JinjaStringValue && hasRawInput(value.value)
? value
: JinjaStringValue.fromString(key),
];
} else if (iterableVal is JinjaList) {
items = iterableVal.items;
} else if (iterableVal is JinjaTuple) {
items = iterableVal.items;
} else if (iterableVal is JinjaStringValue) {
items = charsOf(iterableVal.value);
deriveRendered(JinjaList(items), [iterableVal]);
}
// Filter items first if testExpr
if (testExpr != null) {
List<JinjaValue> filtered = [];
for (final item in items) {
// Create temp context to evaluate test
// We need to bind loop var to item
final tempCtx = ctx.derive();
_bindLoopVar(tempCtx, loopVar, item);
if (testExpr.execute(tempCtx).asBool) {
filtered.add(item);
}
}
items = filtered;
}
// Loop
final output = <JinjaString>[];
// As in Jinja2 and llama.cpp, the else block runs unless a pass finishes
// without `break` or `continue`.
var iterated = false;
final loopCtx = ctx.derive(); // Scope for loop execution
for (int i = 0; i < items.length; i++) {
final item = items[i];
// Loop object
final loopObj = {
'index': JinjaInteger(i + 1),
'index0': JinjaInteger(i),
'revindex': JinjaInteger(items.length - i),
'revindex0': JinjaInteger(items.length - i - 1),
'first': JinjaBoolean(i == 0),
'last': JinjaBoolean(i == items.length - 1),
'length': JinjaInteger(items.length),
'previtem': i > 0 ? items[i - 1] : const JinjaUndefined(),
'nextitem': i < items.length - 1
? items[i + 1]
: const JinjaUndefined(),
};
loopCtx.set('loop', JinjaLoopContext(loopObj));
_bindLoopVar(loopCtx, loopVar, item);
try {
final result = execStatements(body, loopCtx);
if (result is JinjaStringValue) output.add(result.value);
iterated = true;
} on ContinueSignal catch (signal) {
output.addAll(signal.output);
} on BreakSignal catch (signal) {
output.addAll(signal.output);
break;
}
}
if (!iterated) {
try {
final result = execStatements(defaultBlock, ctx);
if (result is JinjaStringValue) output.add(result.value);
} on LoopSignal catch (signal) {
// A `break` or `continue` in the else block belongs to an enclosing
// loop and keeps the output of this one, as in Jinja2.
signal.output.insertAll(0, output);
rethrow;
}
}
return JinjaStringValue(_joinOutput(output));
}