Skip to content

cfscript: support the ${ … } ordered-struct literal, keeping $[ … ] a subscript (#80) - #129

Merged
ghedwards merged 3 commits into
masterfrom
claude/issue-80-brace-only
Sep 18, 2026
Merged

ghedwards merged 3 commits into
masterfrom
claude/issue-80-brace-only

Conversation

@ghedwards

@ghedwards ghedwards commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Closes #80. Supersedes #107 (now closed), which took the opposite decision on $[ … ].

animals = ${ Aardwolf: "…", aardvark: "…" }; yields an ordered_struct, the same node the empty [:] and [=] forms already produced.

The decision the issue was blocked on

$ stays an ordinary identifier. Only the brace form is the literal, so $[ … ] remains an ordinary array-style reference — a subscript_expression — and every jQuery-shaped spelling keeps the tree it had:

input tree
animals = ${ a: "x", b: "y" }; ordered_struct with pairs ← new
x = ${}; ordered_structnew
x = $[ 1 ]; subscript_expression
$ = 1; / x = $; / y = $.foo; / $( "sel" ) identifier, member, call
a = [:]; / b = [=]; ordered_struct
x = `a ${ b } c`; template_substitution
x = "…?n=${trackingNumber}"; string

The reason is which postfix operators exist, not how common each spelling is. [ subscripts any expression, so $[ … ] already means something and a literal reading would take that meaning away; { is not a postfix operator on anything, so a $ followed by a brace cannot be a reference at all and ${ … } has no competing reading to lose. Stated that way the trade needs no corpus counting, and it cannot be re-argued from prevalence: even if nobody subscripts a variable named $, the expression is still well-formed CFML and the literal still is not.

That also settles Lucee's bracket spelling of the same literal, $[ a: "…", b: "…" ], as deliberately unsupported: one key: value inside brackets is Lucee's slice syntax and parses, while several comma-separated pairs are not an array reference at all. cfscript/subscript_multiple_pairs.cfc now pins a decision rather than a gap.

Verification — re-run on today's master

Rebased onto 098916e (after #122 and #131 merged) and re-measured, because #122 moved the corpus baseline by one node.

check result
npm test 347/347 (117 cfml, 158 cfscript, 72 cfquery)
npm run probe 50/57, no drift
npm run lint / npm run fuzz clean
corpus scan, 15,392 files 641 → 646 error nodes across 122 files
npm run treediff vs master zero changed trees in both grammars
STATE_COUNT 5315 → 5361 (+46), no new conflicts

Per-file, the whole delta is two files:

  4 -> 3    lucee_Lucee/test/tickets/LDEV3133/test.cfm          the ${ half fixed, the $[ half declined
 12 -> 18   cfwheels/tools/vscode-ext/assets/templates/controller.cfc

The increase is a VS Code snippet template, not CFML — its ${modelNamePlural} placeholders sit in code position. It failed before (12 nodes) and fails after (18): recovery reshaped, validity unchanged, which docs/FAILING-PATTERNS.md already describes as not a reason to back out a fix. Nothing that parses moved, as the zero-change tree diff confirms.

'${' was already a token of this grammar — template_substitution inside a backtick string — so this admits an existing lexical form in a new position rather than adding one. Every other ${…} in the corpus sits inside a string literal (Java-style placeholders in Slatwall's shipping URLs, JS template literals in TestBox's coverage browser), and a string lexes as one token.

Also in the diff

  • Corpus test and probe cfscript/ordered_struct_brace.cfc pin $ as a variable — $[ 1 ], $ = 1, $( "x" ) — alongside the literal, because that is the half this decision protects.
  • LIMITATIONS.md and docs/FAILING-PATTERNS.md: the ${ entry becomes a record, and the subscript-multiple-pairs row states a decision instead of describing a gap.
  • CHANGELOG.md under ## [Unreleased], and the in-flight entry in docs/TODO.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_0117EvdX3EgLgDevVNrWZ5hr

`animals = ${ Aardwolf: "…", aardvark: "…" };` yields an ordered_struct,
the same node the empty `[:]` and `[=]` forms already produced. Lucee's
LDEV3133/test.cfm.

The issue was blocked on a decision rather than on cost, and the decision
is: `$` stays an ordinary identifier. Only the brace form is the literal,
so `$[ … ]` remains an array-style reference — a subscript_expression —
and `$ = 1`, `x = $`, `$.foo`, `$( "sel" )` and `$[ 1 ]` all keep the
trees they had. The jQuery-shaped spellings are common in the corpus and
nothing here asks them to change.

That also settles Lucee's bracket spelling of the same literal,
`$[ a: "…", b: "…" ]`, as deliberately unsupported: one `key: value`
inside brackets is Lucee's slice syntax and parses, while several
comma-separated pairs are not an array reference at all. LDEV3133/test.cfm
goes 4 -> 3 error nodes for exactly that reason — the `${` half fixed, the
`$[` half declined — and cfscript/subscript_multiple_pairs.cfc now pins a
decision rather than a gap.

`'${'` was already a token of this grammar (template_substitution, inside
a backtick string), so this admits an existing lexical form in a new
position rather than adding one. Every other ${…} in the corpus sits
inside a string literal — Java-style placeholders in Slatwall's shipping
URLs, JS template literals in TestBox's coverage browser — and a string
lexes as one token, so none of them reach the rule.

+46 parse states (5315 -> 5361), no new conflicts. Corpus 640 -> 645
error nodes across 121 files, and the increase is a single file:
cfwheels' tools/vscode-ext/assets/templates/controller.cfc, a VS Code
snippet template rather than CFML, whose ${modelNamePlural} placeholders
sit in code position. It failed before (12 nodes) and fails after (18) —
recovery reshaped, validity unchanged, which is the case
docs/FAILING-PATTERNS.md already says not to back a fix out for.
`npm run treediff` reports zero changed trees in both grammars, and
`npm run fuzz` passes.

The corpus test and probe pin `$` as a variable alongside the literal,
because that is the half the decision protects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117EvdX3EgLgDevVNrWZ5hr
…orm cannot (#80)

The justification recorded with the rule was incidental — that `'${'` was
already a token, and that other `${…}` in the corpus sit inside strings.
Both are true and neither is the reason.

The reason is which postfix operators the language has. `[` subscripts
any expression, so `$[ … ]` already means something — an array-style
reference on a variable named `$` — and taking it for a literal takes
that meaning away. `{` is not a postfix operator on anything, so a `$`
followed by a brace cannot be a reference at all and `${ … }` has no
competing reading to lose.

Stated that way the trade needs no corpus counting to settle, and it
cannot be re-argued from prevalence: even if nobody in the corpus
subscripts a variable named `$`, the expression is still well-formed
CFML and the literal still is not. That is the argument PR #107 is
missing when it accepts losing `$[ expr ]` on the grounds that the
construct occurs zero times.

No behaviour change: comments in cfscript/grammar.js, LIMITATIONS.md,
docs/FAILING-PATTERNS.md and CHANGELOG.md only. Generated files are
untouched, tests, probes and lint stay green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117EvdX3EgLgDevVNrWZ5hr
@ghedwards
ghedwards merged commit 8cc2807 into master Sep 18, 2026
8 checks passed
ghedwards pushed a commit that referenced this pull request Sep 18, 2026
Master has since taken #122 (#115), #131, #129 (#80), #128 (#116) and
#124 (#117). Conflicts were CHANGELOG.md, docs/TODO.md and
cfml/test/corpus/cfml.txt, all of them both-sides-added text; every
generated parser is byte-identical to master, since this change is
entirely in common/scanner.h.

Re-measured against the new baseline: corpus 644 -> 640 error nodes
across 120 -> 118 files (the same two files to zero, nothing else
moved), tree-shape diff zero changed files in both grammars, npm test /
probe / lint / fuzz green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117EvdX3EgLgDevVNrWZ5hr
ghedwards pushed a commit that referenced this pull request Sep 18, 2026
Master has since taken #122 (#115), #131, #129 (#80), #128 (#116) and
#124 (#117). cfscript/grammar.js and the corpus tests auto-merged;
conflicts were CHANGELOG.md, docs/TODO.md and the generated
cfscript/src/parser.c, which is regenerated rather than hand-merged.

Re-measured against the new baseline: STATE_COUNT 5436 -> 5489 (+53,
unchanged), corpus 644 -> 642 error nodes across 120 -> 119 files
(FunctionListener.cfc to zero, nothing else moved), tree-shape diff zero
changed files in both grammars, npm test / probe / lint / fuzz green.
The declared conflict was re-benchmarked: cfscript +0.5% against
untouched controls at +0.9% and -2.9%, so the subject moved less than
two grammars that did not change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117EvdX3EgLgDevVNrWZ5hr
ghedwards pushed a commit that referenced this pull request Sep 18, 2026
Master has since taken #122 (#115), #131, #129 (#80), #128 (#116),
#124 (#117) and #130 (#98). #128 landed a second external token in the
same three places this branch edits, so the real conflicts were the
externals list, the scanner's symbol enum and the scan() dispatch tail:

  - externals / enum: _empty_arrow_body keeps its position, _savecontent_kw
    appended after it, the two kept in the same order in both files.
  - dispatch tail: EMPTY_ARROW_BODY first, SAVECONTENT_KW last. Both are
    valid after `=>`, and savecontent consumes the word before it can tell
    whether it matched, so the zero-width one has to get its answer first.

cfscript/src/grammar.json and parser.c are regenerated, not hand-merged.
STATE_COUNT 5489 -> 5517 (+28, unchanged).

Also fixes a defect that was in the branch before this merge: the
lookahead past mark_end used skip(), which resets a token's start, so
_savecontent_kw came out zero-width on the `{` with the word covered by
no node. advance() is the correct lookahead once mark_end has fixed the
end. Nothing in the gate could see it - a corpus test compares
S-expressions and those carry no ranges - so the rule is recorded in the
parse-gap skill's references/scanner.md.

Re-measured: corpus 642 -> 641 error nodes across 119 -> 118 files
(_LDEV3623.cfc to zero, nothing else moved), tree-shape diff zero changed
files in both grammars, npm test / probe / lint / fuzz green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117EvdX3EgLgDevVNrWZ5hr
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.

cfscript: the ${ … } ordered-struct literal does not parse ($[ … ] does)

2 participants