cfscript: support the ${ … } ordered-struct literal, keeping $[ … ] a subscript (#80) - #129
Merged
Merged
Conversation
`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
This was referenced Sep 18, 2026
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
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.
Closes #80. Supersedes #107 (now closed), which took the opposite decision on
$[ … ].animals = ${ Aardwolf: "…", aardvark: "…" };yields anordered_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 — asubscript_expression— and every jQuery-shaped spelling keeps the tree it had:animals = ${ a: "x", b: "y" };ordered_structwithpairs ← newx = ${};ordered_struct← newx = $[ 1 ];subscript_expression$ = 1;/x = $;/y = $.foo;/$( "sel" )a = [:];/b = [=];ordered_structx = `a ${ b } c`;template_substitutionx = "…?n=${trackingNumber}";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: onekey: valueinside brackets is Lucee's slice syntax and parses, while several comma-separated pairs are not an array reference at all.cfscript/subscript_multiple_pairs.cfcnow 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.npm testnpm run probenpm run lint/npm run fuzznpm run treediffvsmasterSTATE_COUNTPer-file, the whole delta is two files:
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, whichdocs/FAILING-PATTERNS.mdalready 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_substitutioninside 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
cfscript/ordered_struct_brace.cfcpin$as a variable —$[ 1 ],$ = 1,$( "x" )— alongside the literal, because that is the half this decision protects.LIMITATIONS.mdanddocs/FAILING-PATTERNS.md: the${entry becomes a record, and the subscript-multiple-pairs row states a decision instead of describing a gap.CHANGELOG.mdunder## [Unreleased], and the in-flight entry indocs/TODO.md.🤖 Generated with Claude Code
https://claude.ai/code/session_0117EvdX3EgLgDevVNrWZ5hr