Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,20 @@ missing rule).
- **Comma-less function parameters — fixed** ([#49](https://github.com/cfmleditor/tree-sitter-cfml/issues/49)). `function f( boolean a = false ⏎ boolean b = true )` now parses, giving the same tree as the comma form. Lucee, ACF and BoxLang all treat a newline between parameters as a soft separator; TestBox's `BaseSpec.cfc` `createMock` mixes the two, three commas then one omitted. **+34 parse states, no new conflicts, `cfscript` only.** Corpus 682 → 667 nodes across 140 → 130 files, 11 files improved. Two things here are worth keeping. First, the issue's premise was **wrong**: it said the rule already worked in the embedded CFScript of `common/define-grammar.js`, but that was measured by parsing a `<cfscript>` block with the `cfml` grammar, where the body is opaque `cf_script_content` and literal garbage "passes" too — where that copy is genuinely reachable it failed identically, and the two rule definitions were character-identical. Second, the separator **must** be newline-anchored and external: making the comma `optional(',')` does not generate at all, because with a bare `a b` the readings "type `a` named `b`" and "two parameters" are both valid, and the resulting conflict is live at every parameter list in the language.
- **`not` as a parameter name behind a type** — `function f( array not )`. The rest of the word-operator set parses since [#50](https://github.com/cfmleditor/tree-sitter-cfml/issues/50) was fixed, and `not` is the one word that cannot join them: every other entry in `_operator_shaped_name` is a *binary* operator, competing only with a reading that needs a left operand the name slot has not got, while `not` is `unary_operator`, so `function f( array not x )` is genuinely ambiguous. The only resolutions `generate` offers are a conflict or a precedence between `_operator_shaped_name` and `unary_operator`, both live at every `!`, `-` and `+` in the language. Not worth that for the least plausible name in the set.
- **A default on an operator-shaped parameter name** — `function f( array in = [] )`. The bare `array in` parses; giving it a default means aliasing a `seq` to `assignment_pattern`, because that rule's left is a `pattern` and no pattern can reach these words. That spelling generates without conflicts but produces a malformed tree — the name outside a nested, duplicated `assignment_pattern` — and adds 5% to the state table. No corpus file writes it.
- **A statement as an arrow-function body** — `list.each( (v) => if ( v < 0 ) throw( … ) )` ([#75](https://github.com/cfmleditor/tree-sitter-cfml/issues/75), Lucee `LDEV1819/test2.cfm`). **Implemented, measured and reverted**, like the dotted struct key before it. It needs two things: `$.if_statement` in the arrow body `choice`, and an automatic semicolon before `)` in `scan_automatic_semicolon` — without the second, only the assignment-position form (`x = (v) => if (v) y = 1;`) parses, and the reported argument-position form does not, because the body statement ends at the call's closing paren. Both together work, generate without conflicts and pass the fuzzer. The cost is invisible everywhere except the generated table: **STATE_COUNT 4984 → 10005 and `parser.c` 17.9 MB → 35.7 MB**, a 2× table and +17.7 MB of committed C compiled by every binding, for one file with one error node. Admitting a general `$.statement` instead of `if_statement` is worse, not better: a `statement_block` is itself a `$.statement`, so `(v) => { }` becomes ambiguous, the same collision the brace-less `try` body hit.
- **A statement as an arrow-function body** — `list.each( (v) => if ( v < 0 ) throw( … ) )` ([#75](https://github.com/cfmleditor/tree-sitter-cfml/issues/75), Lucee `LDEV1819/test2.cfm`). **Implemented, measured and reverted — three times now, by three different routes.** It needs `$.if_statement` in the arrow body `choice` and an automatic semicolon before `)` in `scan_automatic_semicolon`; without the second, only the assignment-position form parses, because the body statement runs to the call's closing paren.

**Re-measured on today's base, and the original figure holds.** This matters because [#98](https://github.com/cfmleditor/tree-sitter-cfml/issues/98)'s parked cost turned out to have expired once a related change landed, so a parked number is worth re-taking rather than trusting. Here it did not move:

| route | `STATE_COUNT` | `parser.c` |
|---|---|---|
| base | 5,315 | 19.5 MB |
| `$.if_statement` in the body `choice` | **10,704 (+101%)** | 39.3 MB |
| the same, gated on an external zero-width marker | **10,758** | 39.4 MB |
| a purpose-built conditional whose branches are expressions, at `'member'` | **7,330 (+38%)** | 27.5 MB |

**The middle row is the useful new finding, and it delimits a technique.** The external marker is what made `savecontent` as an expression and the empty arrow body affordable — the scanner looks ahead one token where the grammar cannot. It buys **nothing** here, and slightly less than nothing. That is because the cost is not ambiguity at the decision point: it is the `if_statement` subtree becoming reachable in expression position at all, which duplicates states across the whole expression hierarchy. **Reach for the marker when the problem is one token of lookahead; it does not help when a rule category becomes reachable in a new context.**

The third row shows even a hand-written conditional that admits only expressions in its branches costs +38% and +8 MB, because it still mentions `$.expression` in a new context. For one corpus file, none of these is worth it.
- **A `final` modifier on a parameter** — `function testFunc( final required s )` (Lucee `LDEV3671/test3671.cfc`). Surfaced by fixing [#77](https://github.com/cfmleditor/tree-sitter-cfml/issues/77): that file starts `final component`, so it used to degrade to plain text and this gap was never reachable. Adding `optional($._kw_final)` beside `optional(keyword('Required'))` in `_formal_parameter` fails to generate in all three grammars — `( final (` is ambiguous between the modifier and `final` as a `_reserved_identifier` expression, and that prefix is live at **every parameter list in the language**, the shape the cost table warns about. One file does not pay for it.
- **A multi-modifier component header** — `final abstract component { … }`. The shared scanner skips a *run* of modifiers when deciding a file is a component file, but `cfscript`'s `component` rule takes only one. Widening it to `repeat` makes `abstract` at the head of a component-body member ambiguous between an `access_type` and a nested component's modifier list, and the conflict then pulls in `variable_declaration` as well (`final MEMBER = "v"`). No corpus file writes two modifiers there; six write the single-modifier `final component`, which parses.
- **A `param` type spelled with a `_reserved_identifier` word** — `param query x;` and `param component x;` fail in *every* spelling, including `param query x default=1;`. Found while fixing #52 and unrelated to it: `tag_statement` takes its type through `$.identifier`, and `query` and `component` are in `_reserved_identifier`, so they lex as keywords in that slot. Every other type name tested parses (`string numeric any boolean date array struct xml binary guid void`). Narrow, and no corpus file writes it. Not yet filed.
Expand Down
10 changes: 9 additions & 1 deletion docs/FAILING-PATTERNS.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,15 @@ done twice.
half shipped; the residual listener target measures **+14 states** on today's
base and is blocked by the `? new X() :` collision, not by table size.
- [#75](https://github.com/cfmleditor/tree-sitter-cfml/issues/75) — implemented,
measured at 2× the table, reverted.
measured at 2× the table, reverted, and **re-measured on today's base after
#98 showed a parked cost can expire**. This one did not: `$.if_statement` in
the arrow body still doubles the table (5,315 → 10,704, `parser.c` 19.5 →
39.3 MB). Two further routes were tried and recorded in `LIMITATIONS.md`:
gating the arm on an external zero-width marker buys **nothing** (10,758),
and a purpose-built conditional whose branches are expressions still costs
**+38%** (7,330). The middle result is the one to remember — the marker
technique that made #82 and #116 affordable answers a one-token lookahead
problem, not a rule category becoming reachable in a new context.
- [#119](https://github.com/cfmleditor/tree-sitter-cfml/issues/119) — the fix and
the spelling that works today are mutually exclusive; the second shape would
need overlapping nodes and is not representable.