Skip to content

cfscript: support Lucee's ordered-struct literals ${…} and $[…] (#80) - #107

Closed
ghedwards wants to merge 3 commits into
masterfrom
claude/issue-80-ordered-struct-brace
Closed

ghedwards wants to merge 3 commits into
masterfrom
claude/issue-80-ordered-struct-brace

Conversation

@ghedwards

Copy link
Copy Markdown
Contributor

Closes #80.

Both spellings now yield (ordered_struct (pair …)); the existing empty forms [:] and [=] are unchanged.

+74 parse states (5272 → 5346), no new conflicts. Lucee's LDEV3133/test.cfm goes 4 → 0 error nodes.

They are one feature, and the issue's control was false

#80 was filed as "the bracket form parses, the brace form does not". But $[ x: 1 ] only appeared to parse — $ lexed as an ordinary identifier, so it gave exactly the tree q[ x: 1 ] gives:

a = $[ x: 1 ];   (subscript_expression object: (identifier)      <- the `$`
                                      index: (slice_expression start: (identifier) end: (number)))
a = q[ x: 1 ];   (subscript_expression object: (identifier) index: (slice_expression …))   <- identical

Error-free and wrong, which is why nothing caught it; two pairs then failed outright, and that is the only reason the gap was visible at all. ordered_struct in the grammar was only the empty [:] / [=].

Credit where due: LIMITATIONS.md already had this right — it recorded the $[ misparse and even anticipated the design decision. It is the issue text that was wrong. Lucee's own LDEV3133 writes both spellings side by side as scenes 4 and 5, asserting structKeyList comes back in source order.

The entry list is spelled out rather than reusing object's

Reusing object's list wholesale cost +117 states. Measured arm by arm:

entry list states
pair only +73
+ cf_pair +74
+ spread_element +74 (free)
+ shorthand_property_identifier +117

The whole difference is shorthand_property_identifier (${ a, b }), a JavaScript-ism carried over with the fork rather than CFML — dropping it saves 43 states. spread_element measured free and cf_pair (${ k = "v" }) cost one, so both stay.

Known cost, accepted deliberately: $[ expr ] no longer parses

Both openers carry their bracket or brace inside the token. That is what keeps $ an ordinary identifier everywhere else — $.foo, $ = 1 and x = $ all still work, and Mura alone writes 3,775 $.method( calls. But a variable named $ being subscripted is now out-lexed.

Checked rather than assumed: that construct occurs zero times in real code across the 15,083-file corpus. The only two $[ hits are Lucee's ordered struct itself and a $[ inside a regex string literal. It trades an error for what used to be a silently wrong tree, which is the better failure. Recorded in LIMITATIONS.md, with the escape route ($[ would have to become an external token that looks ahead for a pair) if it ever matters.

Verification

check result
npm test pass
npm run probe pass (43/51)
npm run lint pass
npm run testbindings pass
npm run fuzz pass
scripts/check-generated.js no drift
corpus scan 722 → 724 nodes, 143 → 142 files
tree diff 0 changed files in both grammars

The corpus numbers move in opposite directions, and both halves deserve stating. LDEV3133/test.cfm goes 4 → 0. Against that, tools/vscode-ext/assets/templates/controller.cfc goes 12 → 18 — that file is a VS Code snippet template, not CFML, where ${modelNamePlural} is a snippet placeholder. It does not parse on master either, and the change only moves where error recovery gives up. docs/FAILING-PATTERNS.md describes exactly this case as not a reason to back out a fix; a file that parses getting worse would be.

The tree diff confirms that: zero changed files in both grammars, so nothing that actually parses moved.

A corpus test pins both new forms alongside every neighbour they could have swallowed — { x: 1 } as object, [ 1, 2 ] as array, q[ x: 1 ] as subscript_expression, $ as identifier, $.foo as member_expression, and [:]/[=] unchanged. A new probe carries the Lucee reduction.

Implementation note

The entry list is a module-scope helper inlined at both sites, not a grammar rule: commaSep(optional(…)) matches the empty string, and tree-sitter rejects a named rule that can. It is legal inlined because the surrounding ${/} make the enclosing rule non-empty — which is how object already spells it.


🤖 Generated with Claude Code

https://claude.ai/code/session_01M3JjZSaiDNzZVg866si8Gj


Generated by Claude Code

Both spellings now yield `(ordered_struct (pair …))`; the existing empty forms
`[:]` and `[=]` are unchanged. +74 parse states (5272 -> 5346), no new
conflicts. Lucee's LDEV3133/test.cfm goes 4 -> 0 error nodes.

They are one feature, and the issue's control was false. #80 was filed as "the
bracket form parses, the brace form does not", but `$[ x: 1 ]` only APPEARED
to parse: `$` lexed as an ordinary identifier, so it gave exactly the tree
`q[ x: 1 ]` gives — a subscript_expression whose index is a slice_expression.
Error-free and wrong, which is why nothing caught it; two pairs then failed
outright, and that is the only reason the gap was visible at all.
LIMITATIONS.md already had this right. Lucee's own test writes both spellings
side by side as scenes 4 and 5, asserting structKeyList comes back in source
order.

The entry list is spelled out rather than reusing `object`'s. Reusing it
wholesale cost +117 states, and the difference is entirely
`shorthand_property_identifier` (`${ a, b }`) — a JavaScript-ism carried over
with the fork rather than CFML. Dropping it saves 43 states. `spread_element`
measured free and `cf_pair` cost one, so both stay.

Known cost, accepted deliberately: `$[ expr ]` no longer parses. Both openers
carry their bracket or brace inside the token, which is what keeps `$` an
ordinary identifier everywhere else — `$.foo`, `$ = 1` and `x = $` all still
work, and Mura alone writes 3,775 `$.method(` calls. A variable named `$`
being SUBSCRIPTED is now out-lexed. That occurs zero times in real code across
the 15,083-file corpus: the only two `$[` hits are Lucee's ordered struct
itself and a `$[` inside a regex string literal. It trades an error for a
silently wrong tree, which is the better failure. Recorded in LIMITATIONS.md.

Corpus 722 -> 724 error nodes across 143 -> 142 files. Both halves matter:
LDEV3133/test.cfm goes 4 -> 0, while cfwheels'
tools/vscode-ext/assets/templates/controller.cfc goes 12 -> 18. That file is a
VS Code snippet template, not CFML — `${modelNamePlural}` is a snippet
placeholder — it does not parse on master either, and the change only moves
where error recovery gives up. The tree-shape diff reports ZERO changed files
in both grammars, so no file that actually parses moved.

A `commaSep(optional(…))` entry list cannot be a named rule (it matches the
empty string and tree-sitter rejects that), so it is a module-scope helper
inlined at both sites — which is how `object` already spells it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M3JjZSaiDNzZVg866si8Gj
`66da710` on master added an `(ordered_struct ["[" ":" "]"])` punctuation rule
while this branch was open. That rule still validates against the merged
grammar — `[:]` and `[=]` keep those tokens — but it does not reach the two
openers this branch adds, so `${` / `$[` / `}` went unhighlighted while the
bracket forms beside them were coloured.

Adds them to the same list, plus the `=` of `[=]`, which the master rule also
missed. Query files are validated by `npm test`, which passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M3JjZSaiDNzZVg866si8Gj
ghedwards pushed a commit that referenced this pull request Sep 16, 2026
…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

Copy link
Copy Markdown
Contributor Author

Closing in favour of #129, which implements #80 with the opposite decision on $[ … ].

This PR makes both spellings literals by putting the bracket and brace inside the opener tokens, and accepts the consequence it names honestly in its own body: a variable named $ can no longer be subscripted. The justification offered is that $[ expr ] occurs zero times in the 15,083-file corpus.

The maintainer's decision is the other way, and the reason is about the language rather than about prevalence: [ is a postfix operator on any expression, so $[ … ] already means something — an array-style reference — and a literal reading 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. Even with zero corpus sites, $[ expr ] is still well-formed CFML and ${ … } still is not, so frequency does not settle it.

#129 therefore supports ${ … } only, leaves $[ … ] as a subscript_expression, and records Lucee's $[ a: …, b: … ] spelling of the literal as deliberately unsupported. LDEV3133/test.cfm lands at 3 error nodes rather than 0, which is the visible cost of that choice.

Two things from this PR that outlived it and are worth keeping in view:

This branch is also dirty against master and based on v0.26.34, so nothing here is lost by closing rather than rebasing.


Generated by Claude Code

@ghedwards ghedwards closed this Sep 16, 2026
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