Found during independent review of PR #411 (issue #399). Pre-existing — not introduced by that PR.
Problem
JinjaEvaluator.EvalMemberAccess (:272-273) returns Undefined when accessing an attribute on an Undefined value. Real Jinja2 raises UndefinedError.
{{ loop.nextitem.role }} {# last iteration #}
- Jinja2:
UndefinedError: 'jinja2.Utils.Undefined object' has no attribute 'role'
- dotLLM: renders
""
So a template bug that Jinja2 would surface loudly renders as an empty string here — and an empty string in a chat template is a silently malformed prompt, which then shows up as a quality regression attributed to the model or the kernels.
This is the same failure class the repo keeps paying for: #373 (unknown pre silently disabling pre-tokenization), #392 (skips counted as failures masking real ones), #261 (recurrent-state pollution silently corrupting perplexity). The pattern is always degrade quietly rather than fail loudly.
Why it hasn't bitten yet
Qwen3.8-27B's template guards both access sites via short-circuit (EvalBinary:314-318), so it is currently harmless for that template. It is a latent leniency in the evaluator generally, and templates are third-party inputs we do not control — every new model ships a new one.
Related leniency (same root)
Tuples stringify as ['k1', 1] where Jinja2 gives ('k1', 1). Cosmetic today; noted because it shares the cause — the evaluator approximates Jinja2 rather than matching it, and the divergences are undocumented.
Suggested shape
Evidence
Verified against reference Jinja2 3.1.6 during review of PR #411. Related: #399, #409 (parenthesized tuple literals), #373.
Found during independent review of PR #411 (issue #399). Pre-existing — not introduced by that PR.
Problem
JinjaEvaluator.EvalMemberAccess(:272-273) returnsUndefinedwhen accessing an attribute on anUndefinedvalue. Real Jinja2 raisesUndefinedError.{{ loop.nextitem.role }} {# last iteration #}UndefinedError: 'jinja2.Utils.Undefined object' has no attribute 'role'""So a template bug that Jinja2 would surface loudly renders as an empty string here — and an empty string in a chat template is a silently malformed prompt, which then shows up as a quality regression attributed to the model or the kernels.
This is the same failure class the repo keeps paying for: #373 (unknown
presilently disabling pre-tokenization), #392 (skips counted as failures masking real ones), #261 (recurrent-state pollution silently corrupting perplexity). The pattern is always degrade quietly rather than fail loudly.Why it hasn't bitten yet
Qwen3.8-27B's template guards both access sites via short-circuit (
EvalBinary:314-318), so it is currently harmless for that template. It is a latent leniency in the evaluator generally, and templates are third-party inputs we do not control — every new model ships a new one.Related leniency (same root)
Tuples stringify as
['k1', 1]where Jinja2 gives('k1', 1). Cosmetic today; noted because it shares the cause — the evaluator approximates Jinja2 rather than matching it, and the divergences are undocumented.Suggested shape
Undefinedthrows aJinjaExceptionnaming the expression and line, matching Jinja2's behaviour.docs/TOKENIZERS.mdwhich Jinja2 behaviours this evaluator deliberately approximates, so the next divergence is a known deviation rather than a surprise.Evidence
Verified against reference Jinja2 3.1.6 during review of PR #411. Related: #399, #409 (parenthesized tuple literals), #373.