fix(tokenizers): parse Jinja tuple literals in grouping parens (#409) - #415
Open
jamesburton wants to merge 1 commit into
Open
fix(tokenizers): parse Jinja tuple literals in grouping parens (#409)#415jamesburton wants to merge 1 commit into
jamesburton wants to merge 1 commit into
Conversation
JinjaParser.ParsePrimary treated any `(...)` as plain grouping and required
exactly one expression, so a comma-separated tuple literal such as
`(a, b, c)` failed with "Expected RightParen, got Comma". This is on the
unconditional reasoning-instructions prelude of the real
Qwen/Qwen3.8-27B chat_template.jinja (`resolved_reasoning_effort not in
('xhigh', 'medium', 'low')`), so constructing a JinjaChatTemplate from
that template always threw.
`(` now branches on whether a comma follows the first expression:
- `(a)` stays plain grouping (returns the inner expression, not a tuple)
- `(a,)` is a one-element tuple (trailing comma is significant)
- `(a, b, c)` / `(a, b, c,)` are tuples, trailing comma optional
- `()` is an empty tuple
Tuples reuse ListExpr (the existing `[...]` literal node) rather than a
parallel representation, since the evaluator already treats it as an
ordered sequence and In/NotIn membership testing works unchanged.
Added discriminating parser tests (including a test that `(a)` does NOT
become a one-element tuple), in/not-in evaluation tests against a tuple,
and an end-to-end acceptance test that parses+constructs a
JinjaChatTemplate from the real, unmodified Qwen/Qwen3.8-27B
chat_template.jinja fixture. Full rendering still depends on #399
(loop.previtem/nextitem, `is undefined`) which is open as PR #411 and not
yet merged into origin/dev; the acceptance test documents that known,
tracked gap rather than silently skipping.
Closes #409
There was a problem hiding this comment.
Pull request overview
This PR fixes the Jinja subset parser in DotLLM.Tokenizers to correctly accept parenthesized tuple literals (e.g. ('xhigh', 'medium', 'low')) while preserving normal grouping semantics for (expr). This unblocks parsing the unmodified Qwen3.8-27B chat_template.jinja, which uses not in (...) in an unconditional prelude.
Changes:
- Update
JinjaParser.ParsePrimaryto parse()/(a,)/(a, b, c[,])as tuple literals (represented asListExpr), while keeping(a)as plain grouping. - Add parser + evaluator unit tests that discriminate
(a)vs(a,), cover empty/multi-element tuples, trailing commas, andin/not inevaluation. - Add an end-to-end unit acceptance test and a verbatim Qwen3.8-27B chat template fixture to ensure the real template at least parses successfully.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/DotLLM.Tokenizers/ChatTemplates/JinjaParser.cs | Extends LeftParen primary parsing to support tuple-literal semantics while preserving grouping. |
| tests/DotLLM.Tests.Unit/Tokenizers/ChatTemplates/JinjaParserTests.cs | Adds discriminating tuple parsing/evaluation tests, including the real not in (...) construct. |
| tests/DotLLM.Tests.Unit/Tokenizers/ChatTemplates/Fixtures/qwen3.8-27b-chat-template.jinja | Adds the real Qwen3.8-27B template fixture used for regression/acceptance coverage. |
| tests/DotLLM.Tests.Unit/Tokenizers/ChatTemplates/JinjaQwen3_8_27BAcceptanceTests.cs | Adds end-to-end acceptance coverage ensuring the full template parses and tracks the known render-gap. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+84
to
+86
| // Must be the known, tracked #399 gap: an "is undefined"-style test name the | ||
| // evaluator doesn't recognize yet (see JinjaEvaluator.EvalIsTest). | ||
| Assert.Contains("Unknown test", ex.Message); |
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
JinjaParser.ParsePrimarytreated any(...)as plain grouping and required exactly oneexpression, so a comma-separated tuple literal such as
(a, b, c)failed to parse withExpected RightParen, got Comma. This sits in the unconditional reasoning-instructions preludeof the real
Qwen/Qwen3.8-27Bchat_template.jinja(line 48:resolved_reasoning_effort not in ('xhigh', 'medium', 'low')), so constructing aJinjaChatTemplatefrom the unmodified template always threw.(now branches on whether a comma follows the first parsed expression:(a)— plain grouping, returnsaitself (NOT a tuple)(a,)— one-element tuple (trailing comma is significant)(a, b, c)/(a, b, c,)— tuple, trailing comma optional()— empty tupleListExprnode (same representation as[...]list literals)rather than a parallel type, since the evaluator already treats it as an ordered sequence and
in/not inmembership testing works against it unchanged.Files changed
src/DotLLM.Tokenizers/ChatTemplates/JinjaParser.cs— the fix (ParsePrimary'sLeftParencase)tests/DotLLM.Tests.Unit/Tokenizers/ChatTemplates/JinjaParserTests.cs— discriminating parsertests:
(a)vs(a,), multi-element tuples, trailing comma, empty tuple,in/not intrue/false evaluation against a tuple, and the exact
not in (...)construct from line 48 ofthe real template.
tests/DotLLM.Tests.Unit/Tokenizers/ChatTemplates/Fixtures/qwen3.8-27b-chat-template.jinja—byte-for-byte copy of the real template fetched from
https://huggingface.co/Qwen/Qwen3.8-27B/raw/main/chat_template.jinja.
tests/DotLLM.Tests.Unit/Tokenizers/ChatTemplates/JinjaQwen3_8_27BAcceptanceTests.cs— theend-to-end acceptance test for fix(tokenizers): Jinja parser rejects parenthesized tuple literals (blocks Qwen3.8-27B template parse) #409.
Known limitation — PR #411 (#399) status
PR #411 (loop.previtem/loop.nextitem +
is undefinedsupport) is currently OPEN, not merged,and this branch was cut from
origin/devbefore it landed, so it is not included here.The acceptance test (
JinjaQwen3_8_27BAcceptanceTests) therefore has three cases:FullTemplate_Parses_WithoutThrowingandFullTemplate_ConstructsAsJinjaChatTemplate_WithoutThrowing— assert the tuple fix alone is sufficient for the full template to parse. These pass today.
FullTemplate_Render_KnownRemainingGapIsTracked— attempts a full render. Rendering stillneeds feat(tokenizers): Jinja loop.previtem / loop.nextitem — Qwen3.8 tool-response branch cannot render #399's
is undefinedtest support (used unconditionally at line 46:enable_thinking is undefined or enable_thinking is true), so it currently throws. The testasserts the failure is specifically the known, tracked
Unknown test: undefinederror — not atuple/comma parsing error — so a regression in this fix would still be caught. Once feat(tokenizers): Jinja loop.previtem / loop.nextitem #411 merges
and this branch (or
dev) picks it up, thetrybranch renders successfully and thecatchbranch is simply never reached (verified locally by temporarily patching the evaluator with the
same shims feat(tokenizers): Jinja loop.previtem / loop.nextitem #411 is expected to add —
is undefinedandloop.previtem/loop.nextitem— afterwhich the full template, including a tool-call/tool-response conversation, renders end-to-end
with no further errors. No other unsupported constructs were found in the template beyond the
known feat(tokenizers): Jinja loop.previtem / loop.nextitem — Qwen3.8 tool-response branch cannot render #399 gap, so no follow-up issue was needed.)
Test plan
dotnet build -c Release— clean, 0 errors.dotnet test --filter "FullyQualifiedName~ChatTemplates"— 171 passed, 0 failed.dotnet test --filter "FullyQualifiedName~Tokenizers"— 373 passed, 12 skipped (pre-existing,unrelated fixture-file-dependent tests), 0 failed.
feat(tokenizers): Jinja loop.previtem / loop.nextitem — Qwen3.8 tool-response branch cannot render #399's gap is closed, the full unmodified template renders successfully with tools and a
tool-call/tool-response conversation, with no further parsing/evaluation errors.
Closes #409