Fix _emit_string round-trip for number/date-shaped string values#68
Merged
Merged
Conversation
lexer._classify resolves a bare unquoted word as reserved-word tables -> _NUMBER_RE -> _DATE_RE -> UNKNOWN. renderer._emit_string didn't know about the number/date shapes, so a string value like "2025-07-01" or "75" rendered bare and re-lexed as a DATE/NUMBER token on the next parse instead of staying a string. Import _NUMBER_RE/_DATE_RE from .lexer rather than re-declaring them, mirroring how _emit_string already imports ALL_RESERVED from vocabulary instead of restating the reserved-word list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rmichaelthomas
added a commit
that referenced
this pull request
Jul 22, 2026
Publishes the _emit_string round-trip fix for number/date-shaped string values (PR #68), which landed on main after the v0.17.0 bump.
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.
The defect
renderer._emit_string(s)decides whether a string value needs quotes to survive the round-trip propertyparse(tokenize(render(ast))) == ast. The previous rule quoted iff the value contained a space, matched a reserved word, or differed from its lowercased form.That rule missed two self-typing lexical shapes.
lexer._classifyresolves a bare unquoted word as: reserved-word tables →_NUMBER_RE→_DATE_RE→UNKNOWN. So a string whose content is number-shaped or date-shaped, emitted bare, re-lexed as aNUMBERorDATEtoken — not a string:_emit_string("2025-07-01")→2025-07-01→ re-lexes asDATE→ parses toDateLiteral, not the original string._emit_string("75")→75→ re-lexes asNUMBER→ parses toNumberLiteral._emit_string("-3.5")→ same break (full_NUMBER_REshape: sign + decimal).This is a Calendar-Era (v29) regression in kind:
_DATE_REwas added to the lexer as a new self-typing shape, and_emit_stringwas never taught about it. The number case shares the root cause.The fix
Added one clause to
_emit_string's quoting condition: also quote when the value matches_NUMBER_REor_DATE_RE.Import, not copy.
_NUMBER_RE/_DATE_REare imported from.lexerrather than re-declared inrenderer.py— mirroring the function's existing pattern of importingALL_RESERVEDfromvocabularyinstead of restating the reserved-word list. A local copy would create a second source of truth that must be updated in lockstep every time the lexer's literal grammar grows, which is exactly the failure that produced this bug.lexer.pyimports onlyreandvocabulary(notrenderer), sorenderer → lexeris one-directional and adds no import cycle.Out of scope (per the build spec): no rename of
_NUMBER_RE/_DATE_RE, no changes tolexer.py, no changes to theQuotedString/BareWord/RememberValueNodewith-vs-fromrouting logic (a separate, already-correct round-trip guard), no vocabulary or AST changes.Invariants confirmed
grep -nE 'compile\(r?["\x27]\^' src/liminate/renderer.py→ no matches (regex sourced only via import, not copied).python3 -c "import liminate.renderer, liminate.lexer"→ imports cleanly, no circular import._emit_string('2025-07-01') → '"2025-07-01"',_emit_string('75') → '"75"',_emit_string('-3.5') → '"-3.5"',_emit_string('active') → 'active'(matches spec exactly).NumberLiteral/DateLiteralnodes still render bare (_emit_stringis only reached fromBareWord/QuotedStringbranches).Tests
Added to
tests/test_renderer.py:_emit_stringquoting check for date/number-shaped content plus the negative/decimal shape.ROUND_TRIP_SENTENCESparametrized suite.QuotedStringvalue stays aQuotedStringthrough render + re-parse (not aDateLiteral/NumberLiteral).NumberLiteral/DateLiteralnodes keep rendering bare.pytest tests/test_renderer.py -v→ 77 passed (8 new).pytest -q(full suite) → 1845 passed, no regressions.Release note
This warrants the dormant
0.16.1publish (per the build spec) — the fix changes rendered output for a previously-broken case, so it needs a version bump to propagate. PyPI release remains the architect's manual gate; not part of this PR.🤖 Generated with Claude Code