Skip to content

fix(tokenizers): parse Jinja tuple literals in grouping parens (#409) - #415

Open
jamesburton wants to merge 1 commit into
devfrom
issue/409-jinja-tuple-literal
Open

fix(tokenizers): parse Jinja tuple literals in grouping parens (#409)#415
jamesburton wants to merge 1 commit into
devfrom
issue/409-jinja-tuple-literal

Conversation

@jamesburton

Copy link
Copy Markdown
Owner

Summary

JinjaParser.ParsePrimary treated any (...) as plain grouping and required exactly one
expression, so a comma-separated tuple literal such as (a, b, c) failed to parse with
Expected RightParen, got Comma. This sits in the unconditional reasoning-instructions prelude
of the real Qwen/Qwen3.8-27B chat_template.jinja (line 48:
resolved_reasoning_effort not in ('xhigh', 'medium', 'low')), so constructing a
JinjaChatTemplate from the unmodified template always threw.

  • ( now branches on whether a comma follows the first parsed expression:
    • (a) — plain grouping, returns a itself (NOT a tuple)
    • (a,) — one-element tuple (trailing comma is significant)
    • (a, b, c) / (a, b, c,) — tuple, trailing comma optional
    • () — empty tuple
  • Tuples reuse the existing ListExpr node (same representation as [...] list literals)
    rather than a parallel type, since the evaluator already treats it as an ordered sequence and
    in/not in membership testing works against it unchanged.

Files changed

Known limitation — PR #411 (#399) status

PR #411 (loop.previtem/loop.nextitem + is undefined support) is currently OPEN, not merged,
and this branch was cut from origin/dev before it landed, so it is not included here.

The acceptance test (JinjaQwen3_8_27BAcceptanceTests) therefore has three cases:

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.
  • Manually verified (via a temporary, unmerged local patch, reverted before this PR) that once
    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

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
Copilot AI lite review requested due to automatic review settings August 14, 2026 23:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ParsePrimary to parse () / (a,) / (a, b, c[,]) as tuple literals (represented as ListExpr), while keeping (a) as plain grouping.
  • Add parser + evaluator unit tests that discriminate (a) vs (a,), cover empty/multi-element tuples, trailing commas, and in/not in evaluation.
  • 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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants