Found while fixing #77 (§82's escape hatch surviving canonical rendering). That one is closed. This is the same class of defect in a second position, and here it changes a value rather than a node kind.
Reproduction
remember a number called total with 75
remember a string called label with "total"
show label
|
output |
| as written |
total |
| after canonical rendering |
75 |
The middle line renders canonically as remember a string called label with total. On re-parse the bare word is a BareWord, which does symbol-table fallback; total is in scope as a number, so the string literal becomes the number 75.
Why the §77 fix does not cover it
QuotedString and BareWord are different nodes with different evaluation semantics — BareWord falls back to the symbol table, QuotedString never does. §90 treats them as interchangeable spellings of the same value, which is true right up until a name in scope makes the bare form resolve.
§77's fix was narrow on purpose: the parser holds the predicate table, so it can mark that one collision and the renderer honours the mark. The symbol table is not available at parse time, so the same trick does not reach this case.
Why this is a decision, not a patch
Unlike §82-vs-§90, this is not a gap between two rules that never saw each other. §90 says single-word quotes drop by design, and three tests assert it by name:
tests/test_assign.py::test_render_round_trip_single_word_quote_drops
tests/test_integration_includes_remove.py::test_remove_renders_canonically
tests/test_renderer.py::test_render_quoted_string_with_uppercase_keeps_quotes
Closing it means reversing a locked decision about what canonical form is, and canonical renderings are a stored contract surface (Receipts, and any consumer that has persisted one).
Measured cost
Making QuotedString always render with quotes: 6 failures of 1899. Three are the genuine spec assertions above; three are regeneration artifacts (grammar/, the conformance fixture).
Options
- Reverse §90 for
QuotedString — always quote. One line, six test updates, and every stored canonical form containing a quoted single word now differs from what the renderer would produce today.
- Mark the collision at analysis time — the analyzer holds the symbol table, so it could set the same
shadows_a_predicate-style flag §77 introduced (call it what it is: shadows a name in scope). Preserves §90, closes the hole, but only for pipelines that analyze before rendering — and render() is called without analyze() in several places.
- Leave it and document the sharp edge — the collision requires a quoted string whose content exactly matches a name in scope, which is rare in practice.
My read: (2) is the right shape and matches the fix that just landed, but its coverage gap (render-without-analyze) is real and needs closing first or the fix is conditional. (1) is honest and complete but is a migration.
Not urgent
No known program hits it. Raised because it was found, measured, and would otherwise be re-found.
🤖 Generated with Claude Code
https://claude.ai/code/session_01E6qcDj1e1dEYvc8jLnpGZy
Found while fixing #77 (§82's escape hatch surviving canonical rendering). That one is closed. This is the same class of defect in a second position, and here it changes a value rather than a node kind.
Reproduction
total75The middle line renders canonically as
remember a string called label with total. On re-parse the bare word is aBareWord, which does symbol-table fallback;totalis in scope as a number, so the string literal becomes the number 75.Why the §77 fix does not cover it
QuotedStringandBareWordare different nodes with different evaluation semantics —BareWordfalls back to the symbol table,QuotedStringnever does. §90 treats them as interchangeable spellings of the same value, which is true right up until a name in scope makes the bare form resolve.§77's fix was narrow on purpose: the parser holds the predicate table, so it can mark that one collision and the renderer honours the mark. The symbol table is not available at parse time, so the same trick does not reach this case.
Why this is a decision, not a patch
Unlike §82-vs-§90, this is not a gap between two rules that never saw each other. §90 says single-word quotes drop by design, and three tests assert it by name:
tests/test_assign.py::test_render_round_trip_single_word_quote_dropstests/test_integration_includes_remove.py::test_remove_renders_canonicallytests/test_renderer.py::test_render_quoted_string_with_uppercase_keeps_quotesClosing it means reversing a locked decision about what canonical form is, and canonical renderings are a stored contract surface (Receipts, and any consumer that has persisted one).
Measured cost
Making
QuotedStringalways render with quotes: 6 failures of 1899. Three are the genuine spec assertions above; three are regeneration artifacts (grammar/, the conformance fixture).Options
QuotedString— always quote. One line, six test updates, and every stored canonical form containing a quoted single word now differs from what the renderer would produce today.shadows_a_predicate-style flag §77 introduced (call it what it is: shadows a name in scope). Preserves §90, closes the hole, but only for pipelines that analyze before rendering — andrender()is called withoutanalyze()in several places.My read: (2) is the right shape and matches the fix that just landed, but its coverage gap (render-without-analyze) is real and needs closing first or the fix is conditional. (1) is honest and complete but is a migration.
Not urgent
No known program hits it. Raised because it was found, measured, and would otherwise be re-found.
🤖 Generated with Claude Code
https://claude.ai/code/session_01E6qcDj1e1dEYvc8jLnpGZy