Skip to content

cfscript: support a new expression as a function-listener target (#98) - #130

Open
ghedwards wants to merge 1 commit into
masterfrom
claude/issue-98-new-listener-target-v2
Open

ghedwards wants to merge 1 commit into
masterfrom
claude/issue-98-new-listener-target-v2

Conversation

@ghedwards

Copy link
Copy Markdown
Contributor

Closes #98 — the residual half, after #102 shipped the arguments-required change.

threadName = new Query():function( result, error ) { … }; parses. That completes the eleven forms in Lucee's Function Listeners recipe; the other ten landed in #96 and #97.

The number this issue was parked on had expired

#98 records the widening at +591 states and parks the issue on that. That figure was measured when new_expression could complete on the bare keyword new — and #102 removed exactly that by requiring its arguments. Re-measured on today's base, the same widening costs +14.

The transferable part: a cost taken before a related change is not evidence about after it. Nothing about this feature got cheaper; the measurement simply stopped describing the grammar it was taken from.

What actually blocked it was precedence, not size

With the target widened, new Foo() before a contested : is either a listener target or a ternary consequence, and only an open ? tells them apart — which is what GLR carries. The rule shipped in #87 sits at prec.right('call', …), and 'call' binds tighter than 'ternary', so the listener reading won inside c ? new Foo() : obj and that ternary stopped parsing. Every earlier attempt failed this way, including the +260 variant that restricted the listener side to a function literal.

The fix is a second arm below 'ternary', plus the conflict [$.expression, $.function_listener_expression] that tree-sitter generate asks for. Where a ? is open the ternary wins; where none is, nothing competes for the colon. The call-target arm keeps 'call' and is untouched.

input before after
new Query():function( r, e ) { … } ERROR function_listener_expression with a new_expression target
new org.lucee.cfml.Query():callback ERROR parses
c ? new Foo() : obj ternary ternary
isNull(o) ? new() : o ternary ternary
Query():function( … ) (call target) parses parses
x = new.foo, var new = 1, obj.new() identifier reads identifier reads
a[ f() : g() ] slice, case f(): parse parse

Verification

check result
npm test 346/346 (116 cfml, 158 cfscript, 72 cfquery)
npm run probe 50/56, no drift; cfscript/function_listener_new.cfc flips to pass
npm run lint clean
corpus scan, 15,392 files 640 → 638 error nodes across 121 → 120 filesFunctionListener.cfc to zero, nothing regressed
npm run treediff vs master zero changed trees in both grammars
STATE_COUNT 5315 → 5368 (+53), one declared conflict
npm run bench cfscript +1.0% against untouched controls spanning 4.2 points — cfml +2.5%, cfquery −1.7%

The benchmark is the gate CLAUDE.md requires for a declared conflict, and the controls are the point: the subject moved less than two grammars that did not change at all, so the cost is inside the noise floor. That matches what the prefix predicts — this conflict is live on a new_expression followed by : and nowhere else, unlike the identifier ( shapes that cost 2.8×.

Also in the diff

  • Corpus test function listener on a new target (#98) pinning both ternary controls beside the construct, because they are what every earlier attempt broke.
  • LIMITATIONS.md and docs/FAILING-PATTERNS.md rewritten from rejection to record, keeping the expired-measurement lesson and the still-valid reasons new is not reserved (395 .new( call sites across 160 corpus files; Lucee's reserved-word list is null, true, false).
  • CHANGELOG.md under ## [Unreleased], and the in-flight entry in docs/TODO.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_0117EvdX3EgLgDevVNrWZ5hr


Generated by Claude Code

`threadName = new Query():function( result, error ) { … };`. That
completes the eleven forms in Lucee's Function Listeners recipe; the
other ten landed in #96 and #97.

The +591 this issue was parked on had expired. It was measured when
new_expression could complete on the bare keyword `new`; requiring its
arguments — shipped separately — removed that, and the same widening
then measured +14. A cost taken before a related change is not evidence
about after it.

What actually blocked it was precedence, not size. With the target
widened, `new Foo()` before a contested `:` is either a listener target
or a ternary consequence, and only an open `?` tells them apart. The rule
shipped in #87 sits at prec.right('call', …), and 'call' binds tighter
than 'ternary', so the listener reading won inside `c ? new Foo() : obj`
and that ternary stopped parsing — the failure every earlier attempt hit,
including the +260 variant that restricted the listener side to a
function literal.

So the `new` target is a second arm below 'ternary', with the conflict
[$.expression, $.function_listener_expression] that tree-sitter generate
asks for. Where a `?` is open the ternary wins; where none is, nothing
competes for the colon. The call-target arm keeps 'call' and is
untouched.

+53 parse states (5315 -> 5368). Corpus 640 -> 638 error nodes across
121 -> 120 files, FunctionListener.cfc going to zero and nothing
regressing. `npm run treediff` reports zero changed trees in both
grammars.

The conflict was benchmarked, as a declared conflict must be: cfscript
+1.0% against untouched controls spanning 4.2 points (cfml +2.5%,
cfquery -1.7%), so inside the noise floor. That is what the prefix
predicts — the conflict is live on a new_expression followed by `:` and
nowhere else.

Both ternary controls, `c ? new Foo() : obj` and `isNull(o) ? new() : o`,
are pinned by a corpus test alongside the construct, because they are
what every earlier attempt broke. Probe
cfscript/function_listener_new.cfc flips to pass.

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: a new expression as a function-listener target — new Query():callback

2 participants