rip-lang 3.7.3 → 3.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2412 @@
1
+ # ==============================================================================
2
+ # Lunar - Predictive Recursive Descent Parser Generator
3
+ #
4
+ # Companion to Solar. Given the same grammar, Solar generates SLR(1) table
5
+ # parsers and Lunar generates predictive recursive descent parsers.
6
+ #
7
+ # Author: Steve Shreeve <steve.shreeve@gmail.com>
8
+ # Date: February 13, 2026
9
+ # ==============================================================================
10
+
11
+ export install = (Generator) ->
12
+ Generator::generateRD = ->
13
+ infra = @_generateRDInfra()
14
+ parsers = @_generateRDParsers()
15
+ shell = @_generateRDShell()
16
+
17
+ """
18
+ // Predictive Recursive Descent Parser generated by Lunar
19
+
20
+ #{infra}
21
+
22
+ #{parsers}
23
+
24
+ #{shell}
25
+ """
26
+
27
+ # --- Token management layer (static template) ---
28
+ Generator::_generateRDInfra = ->
29
+ """
30
+ const ruleActions = #{@ruleActions};
31
+
32
+ let token, tokenText, tokenLoc, lexerAdapter;
33
+ const EOF = 1;
34
+
35
+ function advance() {
36
+ token = lexerAdapter.lex() || EOF;
37
+ tokenText = lexerAdapter.text;
38
+ tokenLoc = lexerAdapter.loc || {};
39
+ return tokenText;
40
+ }
41
+
42
+ function expect(tag) {
43
+ if (token !== tag) {
44
+ const got = token === EOF ? 'end of input' : ("'" + token + "'");
45
+ const line = (tokenLoc && tokenLoc.r || 0) + 1;
46
+ throw new Error("Parse error on line " + line + ": expected '" + tag + "', got " + got);
47
+ }
48
+ const val = tokenText;
49
+ advance();
50
+ return val;
51
+ }
52
+
53
+ function match(tag) {
54
+ if (token === tag) {
55
+ const val = tokenText;
56
+ advance();
57
+ return val;
58
+ }
59
+ }
60
+
61
+ function loc() {
62
+ return tokenLoc ? { r: tokenLoc.r, c: tokenLoc.c } : { r: 0, c: 0 };
63
+ }
64
+
65
+ function withLoc(node, l) {
66
+ if (Array.isArray(node)) node.loc = l || loc();
67
+ return node;
68
+ }
69
+
70
+ function mark() {
71
+ return { pos: lexerAdapter.pos, token, tokenText, tokenLoc: tokenLoc ? { r: tokenLoc.r, c: tokenLoc.c } : {} };
72
+ }
73
+
74
+ function reset(saved) {
75
+ lexerAdapter.pos = saved.pos;
76
+ token = saved.token;
77
+ tokenText = saved.tokenText;
78
+ tokenLoc = saved.tokenLoc;
79
+ }
80
+
81
+ function speculate(fn) {
82
+ const saved = mark();
83
+ try { return fn(); }
84
+ catch (e) { reset(saved); return null; }
85
+ }
86
+ """
87
+
88
+ # --- Parser shell and exports (static template) ---
89
+ Generator::_generateRDShell = ->
90
+ """
91
+ const parserInstance = {
92
+ parse(input) {
93
+ lexerAdapter = Object.create(this.lexer);
94
+ const sharedState = { ctx: {} };
95
+ for (const k in this.ctx) sharedState.ctx[k] = this.ctx[k];
96
+ lexerAdapter.setInput(input, sharedState.ctx);
97
+ sharedState.ctx.lexer = lexerAdapter;
98
+ sharedState.ctx.parser = this;
99
+ if (!lexerAdapter.loc) lexerAdapter.loc = {};
100
+ advance();
101
+ return parseRoot();
102
+ },
103
+ lexer: null,
104
+ ctx: {},
105
+ };
106
+
107
+ const createParser = (init = {}) => {
108
+ const p = Object.create(parserInstance);
109
+ Object.defineProperty(p, "ctx", {
110
+ value: { ...init },
111
+ enumerable: false,
112
+ writable: true,
113
+ configurable: true,
114
+ });
115
+ return p;
116
+ };
117
+
118
+ const parser = /*#__PURE__*/createParser();
119
+
120
+ export { parser };
121
+ export const Parser = createParser;
122
+ export const parse = parser.parse.bind(parser);
123
+ export default parser;
124
+ """
125
+
126
+ # --- All nonterminal parsing functions ---
127
+ Generator::_generateRDParsers = ->
128
+ lines = []
129
+
130
+ # Build helper data structures
131
+ @_buildFirstSets()
132
+
133
+ # Analyze grammar for Pratt parser generation
134
+ @_analyzeExpressionRules()
135
+
136
+ # Generate Pratt binding power table and expression parsers
137
+ lines.push @_generateRDBindingPowers()
138
+ lines.push @_generateRDUnaryGeneric()
139
+ lines.push @_generateRDPostfixForGeneric()
140
+
141
+ # Generate each nonterminal parser
142
+ # Some nonterminals need specialized implementations
143
+ specialized = new Set ['For', 'Object', 'AssignObj']
144
+
145
+ for own name of @types when name isnt '$accept'
146
+ if specialized.has name
147
+ lines.push @_generateRDSpecialized name
148
+ continue
149
+
150
+ category = @_classifyNonterminal name
151
+ switch category
152
+ when 'root' then lines.push @_generateRDRoot()
153
+ when 'body-list' then lines.push @_generateRDBodyList name
154
+ when 'comma-list' then lines.push @_generateRDCommaList name
155
+ when 'concat-list' then lines.push @_generateRDConcatList name
156
+ when 'expression' then lines.push @_generateRDExpression()
157
+ when 'expression-line' then lines.push @_generateRDExpressionLine()
158
+ when 'operation' then lines.push @_generateRDOperation()
159
+ when 'operation-line' then lines.push @_generateRDOperationLine()
160
+ when 'choice' then lines.push @_generateRDChoice name
161
+ when 'token' then lines.push @_generateRDToken name
162
+ when 'keyword' then lines.push @_generateRDKeyword name
163
+ when 'left-rec-loop' then lines.push @_generateRDLeftRecLoop name
164
+ when 'sequence' then lines.push @_generateRDSequence name
165
+ else
166
+ lines.push @_generateRDGeneric name
167
+
168
+ lines.join '\n\n'
169
+
170
+ # --- Build FIRST set lookup for terminals ---
171
+ Generator::_buildFirstSets = ->
172
+ @firsts = {}
173
+ for own name, type of @types
174
+ tokens = new Set
175
+ type.firsts.forEach (t) => tokens.add t
176
+ @firsts[name] = tokens
177
+
178
+ # --- Classify a nonterminal by its grammar pattern (fully generic) ---
179
+ Generator::_classifyNonterminal = (name) ->
180
+ type = @types[name]
181
+ return null unless type
182
+ rules = type.rules
183
+
184
+ # Start symbol → root
185
+ return 'root' if name is @start
186
+
187
+ # Known expression-related roles (detected by _analyzeExpressionRules)
188
+ return 'expression' if name is @_exprNT
189
+ return 'expression-line' if name is @_exprLineNT
190
+ return 'operation' if name is @_operationNT
191
+ return 'operation-line' if name is @_operationLineNT
192
+
193
+ # Left-recursive with TERMINATOR separator → body-list
194
+ if rules.some((r) => r.symbols[0] is name and r.symbols[1] is 'TERMINATOR')
195
+ return 'body-list'
196
+
197
+ # Left-recursive with comma separator → comma-list
198
+ if rules.some((r) => r.symbols[0] is name and r.symbols[1] is ',')
199
+ return 'comma-list'
200
+
201
+ # Left-recursive with no separator (concatenation) → concat-list
202
+ hasConcatRec = rules.some((r) => r.symbols.length is 2 and r.symbols[0] is name and @types[r.symbols[1]])
203
+ hasSepRec = rules.some((r) => r.symbols[0] is name and (r.symbols[1] is ',' or r.symbols[1] is 'TERMINATOR'))
204
+ return 'concat-list' if hasConcatRec and not hasSepRec
205
+
206
+ # Left-recursive with terminal continuation (e.g., IfBlock → IfBlock ELSE IF ...) → left-rec-loop
207
+ hasLeftRec = rules.some((r) => r.symbols[0] is name and r.symbols.length >= 3 and not @types[r.symbols[1]])
208
+ hasBase = rules.some((r) => r.symbols[0] isnt name)
209
+ return 'left-rec-loop' if hasLeftRec and hasBase and not hasSepRec
210
+
211
+ # Single rule with single token → token passthrough
212
+ if rules.length is 1 and rules[0].symbols.length is 1 and not @types[rules[0].symbols[0]]
213
+ return 'token'
214
+
215
+ # All rules start with a unique keyword token → keyword-prefixed
216
+ firstTokens = rules.map (r) => r.symbols[0]
217
+ if firstTokens.every((s) => s and s isnt '' and not @types[s])
218
+ uniqueFirsts = new Set firstTokens
219
+ if uniqueFirsts.size is rules.length or firstTokens[0] is firstTokens[1]
220
+ return 'keyword'
221
+
222
+ # All rules are single-symbol nonterminal passthrough → choice
223
+ if rules.every((r) => r.symbols.length is 1 and @types[r.symbols[0]])
224
+ return 'choice'
225
+
226
+ # Mixed choice — but check if a passthrough nonterminal is also the prefix
227
+ # of a longer rule (e.g., ObjAssignable AND ObjAssignable : Expression).
228
+ # If so, it's a shared-prefix sequence, not a choice.
229
+ if rules.some((r) => r.symbols.length is 1 and @types[r.symbols[0]])
230
+ passthroughs = (r.symbols[0] for r in rules when r.symbols.length is 1 and @types[r.symbols[0]])
231
+ longerRules = (r for r in rules when r.symbols.length > 1)
232
+ hasSharedPrefix = passthroughs.some (nt) =>
233
+ # Only count as shared prefix if the longer rule starts with the SAME nonterminal
234
+ # AND none of the longer rules start with the expression nonterminal (which would cause infinite recursion)
235
+ longerRules.some((r) => r.symbols[0] is nt) and not longerRules.some((r) => r.symbols[0] is @_exprNT or r.symbols[0] is @_operationNT)
236
+ return 'sequence' if hasSharedPrefix
237
+ return 'choice'
238
+
239
+ 'sequence'
240
+
241
+ # --- Find the item nonterminal for a list pattern ---
242
+ Generator::_findListItem = (listName, separator) ->
243
+ type = @types[listName]
244
+ return @_exprNT unless type
245
+
246
+ # Look for the non-recursive single-symbol alternative: List → Item
247
+ for rule in type.rules
248
+ if rule.symbols.length is 1 and @types[rule.symbols[0]] and rule.symbols[0] isnt listName
249
+ return rule.symbols[0]
250
+
251
+ # Look for the item in the recursive rule: List → List separator Item
252
+ for rule in type.rules
253
+ syms = rule.symbols
254
+ if syms[0] is listName and syms[1] is separator and syms.length >= 3
255
+ lastSym = syms[syms.length - 1]
256
+ return lastSym if @types[lastSym]
257
+
258
+ @_exprNT # fallback
259
+
260
+ # ============================================================================
261
+ # Generic Expression Analysis — derives Pratt handlers from grammar rules
262
+ # ============================================================================
263
+
264
+ Generator::_analyzeExpressionRules = ->
265
+ # Detect nonterminal roles from grammar structure (no hardcoded names)
266
+ @_exprNT = null # The main expression nonterminal
267
+ @_exprLineNT = null # Single-line expression variant
268
+ @_operationNT = null # Nonterminal with binary operator rules
269
+ @_operationLineNT = null # Single-line operation variant
270
+ @_valueNT = null # Nonterminal with atom alternatives
271
+ @_codeNT = null # Arrow function nonterminal
272
+
273
+ # Find the operation nonterminal — the one with the MOST binary operator
274
+ # rules of the form NT OP NT where both operands are the same nonterminal
275
+ # and OP has defined precedence (e.g., Expression + Expression)
276
+ bestCount = 0
277
+ for own name, type of @types
278
+ binOpCount = 0
279
+ for rule in type.rules
280
+ syms = rule.symbols
281
+ if syms.length >= 3 and @types[syms[0]] and @types[syms[2]] and syms[0] is syms[2] and not @types[syms[1]] and @operators[syms[1]]
282
+ binOpCount++
283
+ if binOpCount > bestCount
284
+ bestCount = binOpCount
285
+ @_operationNT = name
286
+
287
+ # Find the expression nonterminal (has the operation NT as a single-symbol alternative)
288
+ if @_operationNT
289
+ for own name, type of @types when name isnt @_operationNT
290
+ hasOperationAlt = type.rules.some (r) => r.symbols.length is 1 and r.symbols[0] is @_operationNT
291
+ if hasOperationAlt
292
+ @_exprNT ?= name
293
+
294
+ # Find Value (atom container) and Code (arrow functions) from Expression alternatives
295
+ if @_exprNT
296
+ for rule in @types[@_exprNT].rules when rule.symbols.length is 1
297
+ altName = rule.symbols[0]
298
+ altType = @types[altName]
299
+ continue unless altType
300
+ continue if altName is @_operationNT
301
+ # Value: pure choice nonterminal (all rules are single-nonterminal passthroughs)
302
+ # with the most alternatives — this is the main "atom container"
303
+ allPassthrough = altType.rules.every (r) => r.symbols.length is 1 and @types[r.symbols[0]]
304
+ if allPassthrough and altType.rules.length >= 3
305
+ if not @_valueNT or altType.rules.length > @types[@_valueNT].rules.length
306
+ @_valueNT = altName
307
+ # Code: FIRST set contains function glyphs (-> or =>)
308
+ altFirsts = @firsts[altName]
309
+ if altFirsts and (altFirsts.has('->') or altFirsts.has('=>'))
310
+ @_codeNT ?= altName unless allPassthrough # Code is a sequence, not a choice
311
+
312
+ # Find expression-line and operation-line variants
313
+ # These are nonterminals that mirror expression/operation but for single-line forms
314
+ if @_operationNT
315
+ for own name, type of @types when name isnt @_operationNT
316
+ # Operation-line: has prefix rules referencing a "line" nonterminal, not left-recursive binops
317
+ hasPrefixLine = type.rules.some (r) => r.symbols.length is 2 and not @types[r.symbols[0]] and @types[r.symbols[1]] and r.symbols[1] isnt @_exprNT
318
+ hasNoBinOps = not type.rules.some (r) => r.symbols.length is 3 and r.symbols[0] is name
319
+ if hasPrefixLine and hasNoBinOps and type.rules.length <= 5
320
+ # Check if it's an alternative of a "line" expression nonterminal
321
+ for own pName, pType of @types
322
+ isAlt = pType.rules.some (r) => r.symbols.length is 1 and r.symbols[0] is name
323
+ isExprAlt = pType.rules.some (r) => r.symbols.length is 1 and (r.symbols[0] is @_exprNT or r.symbols[0] is @_operationNT)
324
+ if isAlt and isExprAlt and pName isnt @_exprNT
325
+ @_operationLineNT ?= name
326
+ @_exprLineNT ?= pName
327
+
328
+ @_operationNT ?= @_exprNT
329
+ @_valueNT ?= @_exprNT
330
+ @_exprNT ?= @start
331
+
332
+ # Classify Operation rules
333
+ @_infixOps = [] # {token, bp, assoc, rule, controlTarget}
334
+ @_prefixOps = [] # {token, bp, rule}
335
+ @_postfixOps = [] # {token, bp, rule}
336
+ @_ternaryOps = [] # {token, separatorToken, bp, assoc, rule}
337
+ @_specialOps = [] # rules that don't fit simple patterns
338
+
339
+ operationType = @types[@_operationNT]
340
+ if operationType
341
+ for rule in operationType.rules
342
+ @_classifyOperationRule rule
343
+
344
+ # Classify assignment rules — detect Expression alternatives where ALL rules
345
+ # follow the pattern: LHS-NT TOKEN RHS-NT (with TERMINATOR/INDENT variants)
346
+ # and share the same LHS nonterminal and operator token
347
+ @_assignOps = []
348
+ exprAlts = new Set
349
+ if @_exprNT
350
+ for rule in @types[@_exprNT].rules when rule.symbols.length is 1
351
+ exprAlts.add rule.symbols[0]
352
+
353
+ for altName as exprAlts
354
+ altType = @types[altName]
355
+ continue unless altType
356
+ continue if altName is @_operationNT or altName is @_valueNT or altName is @_codeNT
357
+ rules = altType.rules
358
+ # All rules must start with a nonterminal and have a consistent second token
359
+ continue unless rules.length >= 1
360
+ firstLHS = rules[0].symbols[0]
361
+ firstOP = rules[0].symbols[1]
362
+ continue unless @types[firstLHS] and firstOP and not @types[firstOP]
363
+ # Check consistency: most rules start with the same LHS and operator
364
+ # (some nonterminals have a "fire-and-forget" form that starts with the op directly)
365
+ consistentCount = rules.filter((r) => r.symbols[0] is firstLHS and r.symbols[1] is firstOP).length
366
+ if consistentCount >= rules.length / 2
367
+ existing = @_assignOps.find (a) -> a.token is firstOP
368
+ unless existing
369
+ @_assignOps.push {nonterminal: altName, token: firstOP, rule: rules[0]}
370
+
371
+ # Find prefix starters — Expression alternatives that start with keywords
372
+ @_prefixStarters = []
373
+ # Tokens already handled as prefix operators
374
+ handledTokens = new Set
375
+ for {token: tok} in @_prefixOps
376
+ handledTokens.add tok
377
+
378
+ exprType = @types[@_exprNT]
379
+ if exprType
380
+ for rule in exprType.rules when rule.symbols.length is 1
381
+ altName = rule.symbols[0]
382
+ altType = @types[altName]
383
+ continue unless altType
384
+ # Skip Operation (handled by Pratt), Value (handled by atoms), Code (arrow functions)
385
+ continue if altName is @_operationNT or altName is @_valueNT or altName is @_codeNT
386
+ # Skip assignment nonterminals (handled by assignment logic)
387
+ continue if @_assignOps.some (a) -> a.nonterminal is altName
388
+ # Find unique keyword tokens that start this nonterminal's rules
389
+ # (not from left-recursive rules that start with Expression/Value)
390
+ @_findKeywordTokens altType, altName, handledTokens, new Set
391
+
392
+ # Collect postfix chain rules — find nonterminals with left-recursive
393
+ # property access / indexing / call rules (Value . Property, Value Arguments, etc.)
394
+ @_postfixChains = []
395
+ valueAlts = new Set
396
+ if @_valueNT
397
+ # Recursively collect all nonterminals reachable through single-NT passthroughs
398
+ # (Value → Assignable → SimpleAssignable, Value → Invocation, etc.)
399
+ queue = [@_valueNT]
400
+ while queue.length
401
+ ntName = queue.shift()
402
+ continue if valueAlts.has ntName
403
+ valueAlts.add ntName
404
+ ntType = @types[ntName]
405
+ continue unless ntType
406
+ for rule in ntType.rules when rule.symbols.length is 1 and @types[rule.symbols[0]]
407
+ queue.push rule.symbols[0]
408
+
409
+ for altName as valueAlts
410
+ altType = @types[altName]
411
+ continue unless altType
412
+ for rule in altType.rules
413
+ syms = rule.symbols
414
+ # Left-recursive through Value chain: first symbol is reachable from Value
415
+ if syms.length >= 2 and @types[syms[0]] and (valueAlts.has(syms[0]) or syms[0] is @_codeNT)
416
+ @_postfixChains.push {nonterminal: altName, rule}
417
+
418
+ # Collect atom types (terminals that can start a Value)
419
+ @_atomTokens = []
420
+ valueType = @types[@_valueNT]
421
+ if valueType
422
+ @_collectAtomTokens valueType, new Set
423
+
424
+ # Build set of tokens that parseExpression() directly handles.
425
+ # Used by the choice generator to detect redundant alternatives.
426
+ @_exprHandledTokens = new Set
427
+ for {token: tok} in @_prefixStarters
428
+ @_exprHandledTokens.add tok
429
+ for {token: tok} in @_prefixOps
430
+ @_exprHandledTokens.add tok
431
+ for {token: tok} in @_atomTokens
432
+ @_exprHandledTokens.add tok
433
+ if @_codeNT and @firsts[@_codeNT]
434
+ @firsts[@_codeNT].forEach (t) => @_exprHandledTokens.add t
435
+ if @_valueNT and @firsts[@_valueNT]
436
+ @firsts[@_valueNT].forEach (t) => @_exprHandledTokens.add t
437
+ # Statement tokens handled by parseUnary (for 'break if done', 'return x unless err')
438
+ @_exprHandledTokens.add 'STATEMENT'
439
+ @_exprHandledTokens.add 'RETURN'
440
+ @_exprHandledTokens.add 'REACT_ASSIGN'
441
+
442
+ Generator::_findKeywordTokens = (type, dispatchName, handledTokens, visited) ->
443
+ return if visited.has type.name
444
+ visited.add type.name
445
+
446
+ # Build set of nonterminals that are direct Expression/Statement alternatives
447
+ # (these should not be followed as keyword prefixes in multi-symbol rules)
448
+ @_exprStatementAlts ?= do =>
449
+ alts = new Set
450
+ for ntName in [@_exprNT, 'Statement', 'Line']
451
+ ntType = @types[ntName]
452
+ continue unless ntType
453
+ alts.add ntName
454
+ for rule in ntType.rules when rule.symbols.length is 1 and @types[rule.symbols[0]]
455
+ alts.add rule.symbols[0]
456
+ alts
457
+
458
+ for rule in type.rules
459
+ syms = rule.symbols
460
+ first = syms[0]
461
+ continue unless first and first isnt ''
462
+ if @types[first]
463
+ isExprLike = first is @_exprNT or first is @_operationNT or first is @_valueNT or first is @_codeNT
464
+ continue if isExprLike
465
+ # In multi-symbol rules, don't follow nonterminals that are Expression/Statement alternatives
466
+ # (e.g., Statement in "Statement POST_IF Expression" — RETURN shouldn't be a prefix for If)
467
+ continue if syms.length > 1 and @_exprStatementAlts.has(first)
468
+ continue if syms.length > 1 and first is type.name # skip self-recursive
469
+ @_findKeywordTokens @types[first], dispatchName, handledTokens, visited
470
+ else
471
+ # Terminal — this is a keyword token for this nonterminal
472
+ unless handledTokens.has first
473
+ existing = @_prefixStarters.find (p) -> p.token is first
474
+ unless existing
475
+ @_prefixStarters.push {token: first, nonterminal: dispatchName}
476
+ handledTokens.add first
477
+
478
+ Generator::_collectAtomTokens = (type, visited) ->
479
+ return if visited.has type.name
480
+ visited.add type.name
481
+ for rule in type.rules
482
+ syms = rule.symbols
483
+ if syms.length is 1 and not @types[syms[0]]
484
+ # Single terminal — it's an atom
485
+ @_atomTokens.push {token: syms[0], rule} unless @_atomTokens.some (a) -> a.token is syms[0]
486
+ else if syms.length is 1 and @types[syms[0]]
487
+ # Single nonterminal passthrough — recurse
488
+ @_collectAtomTokens @types[syms[0]], visited
489
+ else if syms.length >= 1 and not @types[syms[0]] and syms[0] isnt ''
490
+ # Multi-symbol starting with terminal — it's an atom production
491
+ @_atomTokens.push {token: syms[0], rule, nonterminal: type.name} unless @_atomTokens.some (a) -> a.token is syms[0]
492
+
493
+ Generator::_classifyOperationRule = (rule) ->
494
+ syms = rule.symbols
495
+ return unless syms.length >= 2
496
+
497
+ # Infix: NT TOKEN NT
498
+ if syms.length is 3 and @types[syms[0]] and not @types[syms[1]] and @types[syms[2]]
499
+ op = @operators[syms[1]]
500
+ # Check for control flow: Expression || Return, Expression && Throw, etc.
501
+ # Only flag as control-flow when the right operand is a small, keyword-led
502
+ # nonterminal (like Return, Throw) — not when it's just a broader expression
503
+ # nonterminal (like Expression in SimpleAssignable COMPOUND_ASSIGN Expression)
504
+ controlTarget = null
505
+ if syms[2] isnt syms[0]
506
+ rightType = @types[syms[2]]
507
+ if rightType and rightType.rules.length <= 4
508
+ allKeywordLed = rightType.rules.every (r) => r.symbols[0] and not @types[r.symbols[0]]
509
+ controlTarget = syms[2] if allKeywordLed
510
+ @_infixOps.push {token: syms[1], bp: op?.precedence or 0, assoc: op?.assoc or 'left', rule, controlTarget}
511
+ return
512
+
513
+ # Prefix: TOKEN NT
514
+ if syms.length is 2 and not @types[syms[0]] and @types[syms[1]]
515
+ op = @operators[syms[0]]
516
+ # Use rule.precedence if set (prec override, e.g., '- Expression' with prec: 'UNARY_MATH')
517
+ bp = rule.precedence or op?.precedence or 0
518
+ @_prefixOps.push {token: syms[0], bp, rule}
519
+ return
520
+
521
+ # Postfix: NT TOKEN (e.g., Value ?, SimpleAssignable ++)
522
+ if syms.length is 2 and @types[syms[0]] and not @types[syms[1]]
523
+ op = @operators[syms[1]]
524
+ @_postfixOps.push {token: syms[1], bp: op?.precedence or 0, rule, leftNT: syms[0]}
525
+ return
526
+
527
+ # Postfix with continuation: NT TOKEN ... (e.g., SimpleAssignable COMPOUND_ASSIGN Expression)
528
+ if syms.length >= 3 and @types[syms[0]] and not @types[syms[1]]
529
+ op = @operators[syms[1]]
530
+ @_postfixOps.push {token: syms[1], bp: op?.precedence or 0, rule, leftNT: syms[0]}
531
+ return
532
+
533
+ # Ternary: NT TOKEN NT TOKEN NT (5 symbols)
534
+ if syms.length is 5 and @types[syms[0]] and not @types[syms[1]] and @types[syms[2]] and not @types[syms[3]] and @types[syms[4]]
535
+ op = @operators[syms[1]]
536
+ @_ternaryOps.push {token: syms[1], separator: syms[3], bp: op?.precedence or 0, assoc: op?.assoc or 'right', rule}
537
+ return
538
+
539
+ # Special/unclassified
540
+ @_specialOps.push {rule}
541
+
542
+ # --- Generate Pratt binding power table ---
543
+ Generator::_generateRDBindingPowers = ->
544
+ lines = []
545
+ lines.push 'const BP = {};'
546
+
547
+ for own sym, info of @operators
548
+ # Use precedence * 2 to leave room for right-associative adjustments
549
+ bp = info.precedence * 2
550
+ lines.push "BP[#{JSON.stringify sym}] = #{bp};"
551
+
552
+ lines.join '\n'
553
+
554
+ # --- Root parser ---
555
+ Generator::_generateRDRoot = ->
556
+ """
557
+ function parseRoot() {
558
+ if (token === EOF) return withLoc(["program"]);
559
+ const body = parseBody();
560
+ return withLoc(["program", ...body]);
561
+ }
562
+ """
563
+
564
+ # --- Body-style list (TERMINATOR-separated) ---
565
+ Generator::_generateRDBodyList = (name) ->
566
+ fnName = "parse#{name}"
567
+ # Find the item nonterminal from the non-recursive single-symbol alternative
568
+ itemNT = @_findListItem name, 'TERMINATOR'
569
+ itemParser = "parse#{itemNT}"
570
+ """
571
+ function #{fnName}() {
572
+ const items = [#{itemParser}()];
573
+ while (token === 'TERMINATOR') {
574
+ advance();
575
+ if (token !== EOF && token !== 'OUTDENT') {
576
+ items.push(#{itemParser}());
577
+ }
578
+ }
579
+ return items;
580
+ }
581
+ """
582
+
583
+ # --- Comma-separated list ---
584
+ Generator::_generateRDCommaList = (name) ->
585
+ fnName = "parse#{name}"
586
+
587
+ # Find the item nonterminal from the non-recursive single-symbol alternative
588
+ itemNT = @_findListItem name, ','
589
+ itemParser = "parse#{itemNT}"
590
+
591
+ # Check if this list supports INDENT blocks (has rules with INDENT in them)
592
+ type = @types[name]
593
+ hasIndent = type.rules.some (r) =>
594
+ r.symbols.indexOf('INDENT') >= 0 or r.symbols.indexOf('OUTDENT') >= 0
595
+
596
+ if hasIndent
597
+ # Compute list boundary tokens from FOLLOW set (tokens that can follow this list)
598
+ followTokens = []
599
+ type.follows?.forEach (t) =>
600
+ followTokens.push t unless t is ',' or t is 'TERMINATOR'
601
+ # Always include OUTDENT as a boundary
602
+ followTokens.push 'OUTDENT' unless followTokens.indexOf('OUTDENT') >= 0
603
+ boundaryCond = followTokens.map((t) -> "token === #{JSON.stringify t}").join ' || '
604
+
605
+ """
606
+ function #{fnName}() {
607
+ if (token === 'INDENT') {
608
+ advance();
609
+ const inner = #{fnName}();
610
+ match(',');
611
+ expect('OUTDENT');
612
+ return inner;
613
+ }
614
+ const result = [#{itemParser}()];
615
+ while (true) {
616
+ if (token === ',') {
617
+ advance();
618
+ if (#{boundaryCond}) break;
619
+ if (token === 'TERMINATOR') {
620
+ advance();
621
+ if (#{boundaryCond}) break;
622
+ }
623
+ result.push(#{itemParser}());
624
+ } else if (token === 'TERMINATOR') {
625
+ advance();
626
+ if (#{boundaryCond}) break;
627
+ result.push(#{itemParser}());
628
+ } else if (token === 'INDENT') {
629
+ advance();
630
+ const inner = #{fnName}();
631
+ match(',');
632
+ expect('OUTDENT');
633
+ result.push(...inner);
634
+ } else {
635
+ break;
636
+ }
637
+ }
638
+ return result;
639
+ }
640
+ """
641
+ else
642
+ # Simple comma-separated list (no INDENT support)
643
+ """
644
+ function #{fnName}() {
645
+ const result = [#{itemParser}()];
646
+ while (token === ',') {
647
+ advance();
648
+ result.push(#{itemParser}());
649
+ }
650
+ return result;
651
+ }
652
+ """
653
+
654
+ # --- Concatenation list (no separator, just sequential items) ---
655
+ Generator::_generateRDConcatList = (name) ->
656
+ fnName = "parse#{name}"
657
+
658
+ # Find the item nonterminal from the concat-recursive rule: Name Item → [Name, Item]
659
+ type = @types[name]
660
+ itemNT = null
661
+ for rule in type.rules
662
+ if rule.symbols.length is 2 and rule.symbols[0] is name and @types[rule.symbols[1]]
663
+ itemNT = rule.symbols[1]
664
+ break
665
+ itemNT ?= type.rules[0]?.symbols[0] if type.rules[0]?.symbols.length is 1
666
+ itemParser = "parse#{itemNT}"
667
+
668
+ # Determine continuation condition from the item's FIRST set
669
+ itemFirsts = @firsts[itemNT]
670
+ if itemFirsts?.size
671
+ firstTokens = []
672
+ itemFirsts.forEach (t) => firstTokens.push t
673
+ cond = firstTokens.map((t) -> "token === #{JSON.stringify t}").join ' || '
674
+ """
675
+ function #{fnName}() {
676
+ const items = [#{itemParser}()];
677
+ while (#{cond}) {
678
+ items.push(#{itemParser}());
679
+ }
680
+ return items;
681
+ }
682
+ """
683
+ else
684
+ """
685
+ function #{fnName}() {
686
+ const items = [#{itemParser}()];
687
+ while (token !== EOF && token !== 'OUTDENT') {
688
+ items.push(#{itemParser}());
689
+ }
690
+ return items;
691
+ }
692
+ """
693
+
694
+ # --- Choice nonterminal (dispatch on FIRST set) ---
695
+ Generator::_generateRDChoice = (name) ->
696
+ fnName = "parse#{name}"
697
+ type = @types[name]
698
+ rules = type.rules
699
+
700
+ lines = []
701
+ lines.push "function #{fnName}() {"
702
+
703
+ # Collect alternatives with their FIRST sets
704
+ # Skip multi-symbol rules that start with Expression/Statement — those are postfix
705
+ # patterns handled by the Pratt parser (e.g., Expression POST_IF Expression in If)
706
+ alternatives = []
707
+ fallthrough = null
708
+
709
+ for rule in rules
710
+ if rule.symbols.length is 1 and rule.symbols[0] is ''
711
+ # Epsilon rule — used as default/fallthrough
712
+ fallthrough = rule
713
+ continue
714
+
715
+ sym = rule.symbols[0]
716
+ # Skip multi-symbol rules starting with Expression, Statement, or direct Statement alternatives
717
+ # (postfix patterns like "Expression POST_IF Expression" are handled by the Pratt parser)
718
+ if rule.symbols.length > 1 and @types[sym]
719
+ continue if sym is @_exprNT or sym is @_operationNT
720
+ # Also skip rules starting with Statement or its direct alternatives (Return, Import, etc.)
721
+ statementNT = @types['Statement']
722
+ if statementNT
723
+ isStatementAlt = sym is 'Statement' or statementNT.rules.some((r) -> r.symbols.length is 1 and r.symbols[0] is sym)
724
+ continue if isStatementAlt
725
+ if @types[sym]
726
+ # Nonterminal passthrough (single-symbol) or multi-symbol with structural first NT
727
+ alternatives.push {type: 'nonterminal', name: sym, rule}
728
+ else if sym
729
+ # Terminal-prefixed
730
+ alternatives.push {type: 'terminal', token: sym, rule}
731
+
732
+ # Sort nonterminal alternatives: smallest FIRST set first (most specific → default last)
733
+ ntAlts = (alt for alt in alternatives when alt.type is 'nonterminal')
734
+ ntAlts.sort (a, b) =>
735
+ sizeA = @firsts[a.name]?.size or 0
736
+ sizeB = @firsts[b.name]?.size or 0
737
+ sizeA - sizeB # smallest first
738
+
739
+ # Start with terminal-prefixed (most specific)
740
+ # But check for overlap with nonterminal alternatives (e.g., '...' overlaps with Splat)
741
+ firstUsed = new Set
742
+ for alt in alternatives when alt.type is 'terminal'
743
+ # Check if this terminal also starts a nonterminal alternative
744
+ overlappingNT = null
745
+ for ntAlt in alternatives when ntAlt.type is 'nonterminal'
746
+ if @firsts[ntAlt.name]?.has(alt.token)
747
+ overlappingNT = ntAlt.name
748
+ break
749
+ if alt.rule.symbols.length is 1 and overlappingNT
750
+ # Terminal overlaps with nonterminal — try nonterminal first, fall back to terminal
751
+ lines.push " if (token === #{JSON.stringify alt.token}) {"
752
+ lines.push " const _spec = speculate(() => parse#{overlappingNT}());"
753
+ lines.push " if (_spec !== null) return _spec;"
754
+ action = @_getRDAction alt.rule
755
+ lines.push " #{action}"
756
+ lines.push " }"
757
+ else if alt.rule.symbols.length is 1
758
+ lines.push " if (token === #{JSON.stringify alt.token}) {"
759
+ lines.push " const v = tokenText; advance(); return v;"
760
+ lines.push " }"
761
+ else
762
+ lines.push " if (token === #{JSON.stringify alt.token}) {"
763
+ lines.push " const l = loc();"
764
+ lines.push @_generateRDRuleBody alt.rule, ' '
765
+ lines.push " }"
766
+ firstUsed.add alt.token
767
+
768
+ # Then nonterminal alternatives, smallest FIRST set first
769
+ # The last (broadest) becomes the default
770
+ # Check if the broadest alternative is the expression nonterminal
771
+ broadestIsExpr = ntAlts.length >= 2 and ntAlts[ntAlts.length - 1].name is @_exprNT
772
+
773
+ # Collect multi-symbol rules grouped by their starting nonterminal
774
+ # (for generating continuations after passthrough dispatch)
775
+ continuationRules = {}
776
+ for alt in alternatives when alt.rule.symbols.length > 1 and @types[alt.rule.symbols[0]]
777
+ ntName = alt.rule.symbols[0]
778
+ continuationRules[ntName] ?= []
779
+ continuationRules[ntName].push alt.rule
780
+
781
+ for alt, i in ntAlts
782
+ if i is ntAlts.length - 1
783
+ # Broadest — use as default (skip FIRST set check)
784
+ continue
785
+ firsts = @firsts[alt.name]
786
+ if firsts
787
+ uniqueTokens = []
788
+ firsts.forEach (t) =>
789
+ if not firstUsed.has(t)
790
+ uniqueTokens.push t
791
+ # If the broadest is Expression, filter out tokens that parseExpression handles directly
792
+ # (they're redundant — parseExpression's Pratt loop handles postfix patterns like break if done)
793
+ if broadestIsExpr
794
+ uniqueTokens = uniqueTokens.filter (t) => not @_exprHandledTokens.has(t)
795
+ continue unless uniqueTokens.length # Skip entirely if all tokens are handled
796
+ if uniqueTokens.length
797
+ cond = uniqueTokens.map((t) -> "token === #{JSON.stringify t}").join ' || '
798
+ # Check if this alternative also has multi-symbol continuation rules
799
+ contRules = continuationRules[alt.name]
800
+ if contRules?.length
801
+ lines.push " if (#{cond}) {"
802
+ lines.push " const l = loc();"
803
+ lines.push " let _node_ = parse#{alt.name}();"
804
+ # Generate continuation checks (resolve nonterminals to FIRST sets)
805
+ for contRule in contRules
806
+ contSym = contRule.symbols[1]
807
+ if @types[contSym]
808
+ contTokens = []
809
+ @firsts[contSym]?.forEach (t) => contTokens.push t
810
+ contCond = contTokens.map((t) -> "token === #{JSON.stringify t}").join ' || '
811
+ else
812
+ contCond = "token === #{JSON.stringify contSym}"
813
+ lines.push " if (#{contCond}) {"
814
+ lines.push " const _vals_ = [_node_];"
815
+ for sym, si in contRule.symbols when si > 0
816
+ if @types[sym] then lines.push " _vals_.push(parse#{sym}());"
817
+ else lines.push " _vals_.push(expect(#{JSON.stringify sym}));"
818
+ lines.push " const $ = _vals_, $0 = _vals_.length - 1;"
819
+ lines.push " const _r = ruleActions(#{contRule.id}, _vals_, [], {});"
820
+ lines.push " return _r != null ? withLoc(_r, l) : _node_;"
821
+ lines.push " }"
822
+ lines.push " return _node_;"
823
+ lines.push " }"
824
+ else
825
+ lines.push " if (#{cond}) return parse#{alt.name}();"
826
+ for t in uniqueTokens
827
+ firstUsed.add t
828
+
829
+ # Default case — broadest nonterminal alternative (with continuation handling)
830
+ if fallthrough
831
+ action = @_getRDAction fallthrough
832
+ lines.push " #{action}"
833
+ else if ntAlts.length
834
+ defaultAlt = ntAlts[ntAlts.length - 1]
835
+ defaultContRules = continuationRules[defaultAlt.name]
836
+ if defaultContRules?.length
837
+ lines.push " {"
838
+ lines.push " const l = loc();"
839
+ lines.push " let _node_ = parse#{defaultAlt.name}();"
840
+ for contRule in defaultContRules
841
+ contSym = contRule.symbols[1]
842
+ if @types[contSym]
843
+ contTokens = []
844
+ @firsts[contSym]?.forEach (t) => contTokens.push t
845
+ contCond = contTokens.map((t) -> "token === #{JSON.stringify t}").join ' || '
846
+ else
847
+ contCond = "token === #{JSON.stringify contSym}"
848
+ lines.push " if (#{contCond}) {"
849
+ lines.push " const _vals_ = [_node_];"
850
+ for sym, si in contRule.symbols when si > 0
851
+ if @types[sym] then lines.push " _vals_.push(parse#{sym}());"
852
+ else lines.push " _vals_.push(expect(#{JSON.stringify sym}));"
853
+ lines.push " const $ = _vals_, $0 = _vals_.length - 1;"
854
+ lines.push " const _r = ruleActions(#{contRule.id}, _vals_, [], {});"
855
+ lines.push " return _r != null ? withLoc(_r, l) : _node_;"
856
+ lines.push " }"
857
+ lines.push " return _node_;"
858
+ lines.push " }"
859
+ else
860
+ lines.push " return parse#{defaultAlt.name}();"
861
+ else if alternatives.length
862
+ last = alternatives[alternatives.length - 1]
863
+ lines.push " const v = tokenText; advance(); return v;" if last.type is 'terminal'
864
+ else
865
+ lines.push " throw new Error('Parse error: unexpected token ' + token + ' in #{name}');"
866
+
867
+ lines.push "}"
868
+ lines.join '\n'
869
+
870
+ # --- Token passthrough (single-token nonterminal) ---
871
+ Generator::_generateRDToken = (name) ->
872
+ fnName = "parse#{name}"
873
+ rule = @types[name].rules[0]
874
+ tokenTag = rule.symbols[0]
875
+ """
876
+ function #{fnName}() {
877
+ return expect(#{JSON.stringify tokenTag});
878
+ }
879
+ """
880
+
881
+ # --- Left-recursive loop (e.g., IfBlock → IF Expr Block | IfBlock ELSE IF Expr Block) ---
882
+ Generator::_generateRDLeftRecLoop = (name) ->
883
+ fnName = "parse#{name}"
884
+ type = @types[name]
885
+ rules = type.rules
886
+
887
+ # Separate base rules (non-recursive) and recursive rules
888
+ baseRules = (r for r in rules when r.symbols[0] isnt name)
889
+ recRules = (r for r in rules when r.symbols[0] is name)
890
+
891
+ lines = []
892
+ lines.push "function #{fnName}() {"
893
+ lines.push " const l = loc();"
894
+
895
+ # Parse base case
896
+ lines.push " // Base case"
897
+ lines.push " const _vals_ = [];"
898
+ if baseRules.length is 1
899
+ for sym in baseRules[0].symbols
900
+ if @types[sym] then lines.push " _vals_.push(parse#{sym}());"
901
+ else lines.push " _vals_.push(expect(#{JSON.stringify sym}));"
902
+ lines.push " let _node_ = (function() { #{@_getRDAction baseRules[0], '_vals_'} }).call({});"
903
+ else
904
+ lines.push @_generateRDSharedPrefix baseRules, ' '
905
+
906
+ # Generate continuation loop for recursive rules
907
+ if recRules.length
908
+ # Find the continuation token (first terminal after the self-reference)
909
+ contToken = recRules[0].symbols[1]
910
+ contFirsts = if @types[contToken]
911
+ tokens = []
912
+ @firsts[contToken]?.forEach (t) => tokens.push t
913
+ tokens
914
+ else
915
+ [contToken]
916
+
917
+ cond = contFirsts.map((t) -> "token === #{JSON.stringify t}").join ' || '
918
+ lines.push " // Recursive continuation → loop"
919
+
920
+ # Check if continuation needs lookahead: if the first 2+ symbols are terminals,
921
+ # we need mark/reset to avoid consuming the first token when the second doesn't match
922
+ # (e.g., IfBlock: ELSE IF — don't consume ELSE unless IF follows)
923
+ needsLookahead = false
924
+ if recRules.length is 1 and recRules[0].symbols.length >= 3
925
+ contSyms = recRules[0].symbols[1..]
926
+ needsLookahead = contSyms.length >= 2 and not @types[contSyms[0]] and not @types[contSyms[1]]
927
+
928
+ lines.push " while (#{cond}) {"
929
+
930
+ if needsLookahead
931
+ secondToken = recRules[0].symbols[2]
932
+ lines.push " const _saved_ = mark();"
933
+ lines.push " advance(); // consume #{contToken}"
934
+ lines.push " if (token !== #{JSON.stringify secondToken}) { reset(_saved_); break; }"
935
+
936
+ if recRules.length is 1
937
+ recRule = recRules[0]
938
+ continuation = recRule.symbols[1..] # everything after the self-reference
939
+ lines.push " const _rvals_ = [_node_];"
940
+ if needsLookahead
941
+ # We already advanced past the first token; now consume the second via expect
942
+ # The first token's value was consumed by advance(), push its tag as value
943
+ lines.push " _rvals_.push(#{JSON.stringify contToken}); // already consumed"
944
+ for sym, si in continuation when si >= 1 # skip first (already consumed)
945
+ if @types[sym] then lines.push " _rvals_.push(parse#{sym}());"
946
+ else lines.push " _rvals_.push(expect(#{JSON.stringify sym}));"
947
+ else
948
+ for sym in continuation
949
+ if @types[sym] then lines.push " _rvals_.push(parse#{sym}());"
950
+ else lines.push " _rvals_.push(expect(#{JSON.stringify sym}));"
951
+ lines.push " const $ = _rvals_, $0 = _rvals_.length - 1;"
952
+ lines.push " const _r = ruleActions(#{recRule.id}, _rvals_, [], {});"
953
+ lines.push " _node_ = _r != null ? withLoc(_r, l) : _node_;"
954
+ else
955
+ # Multiple recursive rules — disambiguate by next token after continuation
956
+ lines.push " const _rvals_ = [_node_];"
957
+ # Find shared continuation prefix
958
+ contPrefixLen = 1 # skip the self-reference (index 0)
959
+ loop
960
+ sym = recRules[0].symbols[contPrefixLen]
961
+ break unless sym
962
+ break unless recRules.every (r) -> r.symbols.length > contPrefixLen and r.symbols[contPrefixLen] is sym
963
+ contPrefixLen++
964
+ for i in [1...contPrefixLen]
965
+ sym = recRules[0].symbols[i]
966
+ if @types[sym] then lines.push " _rvals_.push(parse#{sym}());"
967
+ else lines.push " _rvals_.push(expect(#{JSON.stringify sym}));"
968
+ # Disambiguate remaining
969
+ subFirst = true
970
+ for recRule in recRules.sort((a, b) -> b.symbols.length - a.symbols.length)
971
+ remaining = recRule.symbols[contPrefixLen..]
972
+ if remaining.length is 0
973
+ lines.push " else {" unless subFirst
974
+ lines.push " #{if subFirst then '' else ' '}const $ = _rvals_, $0 = _rvals_.length - 1;"
975
+ lines.push " #{if subFirst then '' else ' '}const _r = ruleActions(#{recRule.id}, _rvals_, [], {});"
976
+ lines.push " #{if subFirst then '' else ' '}_node_ = _r != null ? withLoc(_r, l) : _node_;"
977
+ lines.push " }" unless subFirst
978
+ else
979
+ nextSym = remaining[0]
980
+ c = if @types[nextSym] then "true" else "token === #{JSON.stringify nextSym}"
981
+ p = if subFirst then "if" else "else if"
982
+ subFirst = false
983
+ lines.push " #{p} (#{c}) {"
984
+ for sym in remaining
985
+ if @types[sym] then lines.push " _rvals_.push(parse#{sym}());"
986
+ else lines.push " _rvals_.push(expect(#{JSON.stringify sym}));"
987
+ lines.push " const $ = _rvals_, $0 = _rvals_.length - 1;"
988
+ lines.push " const _r = ruleActions(#{recRule.id}, _rvals_, [], {});"
989
+ lines.push " _node_ = _r != null ? withLoc(_r, l) : _node_;"
990
+ lines.push " }"
991
+ subFirst = false
992
+
993
+ lines.push " }"
994
+
995
+ lines.push " return _node_;"
996
+ lines.push "}"
997
+ lines.join '\n'
998
+
999
+ # --- Keyword-prefixed nonterminal ---
1000
+ Generator::_generateRDKeyword = (name) ->
1001
+ fnName = "parse#{name}"
1002
+ type = @types[name]
1003
+ rules = type.rules
1004
+
1005
+ lines = []
1006
+ lines.push "function #{fnName}() {"
1007
+ lines.push " const l = loc();"
1008
+
1009
+ # Group rules by first symbol
1010
+ groups = {}
1011
+ for rule in rules
1012
+ first = rule.symbols[0]
1013
+ groups[first] ?= []
1014
+ groups[first].push rule
1015
+
1016
+ firstKey = true
1017
+ for own firstToken, ruleGroup of groups
1018
+ prefix = if firstKey then "if" else "else if"
1019
+ firstKey = false
1020
+ lines.push " #{prefix} (token === #{JSON.stringify firstToken}) {"
1021
+
1022
+ if ruleGroup.length is 1
1023
+ lines.push @_generateRDRuleBody ruleGroup[0], ' '
1024
+ else
1025
+ # Multiple rules with same first token — disambiguate by lookahead
1026
+ lines.push @_generateRDSharedPrefix ruleGroup, ' '
1027
+
1028
+ lines.push " }"
1029
+
1030
+ lines.push " else {"
1031
+ lines.push " throw new Error('Parse error: unexpected token ' + token + ' in #{name}');"
1032
+ lines.push " }"
1033
+ lines.push "}"
1034
+ lines.join '\n'
1035
+
1036
+ # --- Sequence nonterminal (rules are sequences of symbols) ---
1037
+ Generator::_generateRDSequence = (name) ->
1038
+ fnName = "parse#{name}"
1039
+ type = @types[name]
1040
+ rules = type.rules
1041
+
1042
+ lines = []
1043
+ lines.push "function #{fnName}() {"
1044
+ lines.push " const l = loc();"
1045
+
1046
+ if rules.length is 1
1047
+ lines.push @_generateRDRuleBody rules[0], ' '
1048
+ else
1049
+ # Group rules by first symbol
1050
+ groups = {}
1051
+ order = []
1052
+ for rule in rules
1053
+ first = rule.symbols[0]
1054
+ key = first or '$epsilon'
1055
+ unless groups[key]
1056
+ groups[key] = []
1057
+ order.push key
1058
+ groups[key].push rule
1059
+
1060
+ firstCond = true
1061
+ epsilonGroup = null
1062
+ terminalGroups = []
1063
+ nonterminalGroups = []
1064
+
1065
+ for key in order
1066
+ ruleGroup = groups[key]
1067
+ if key is '$epsilon'
1068
+ epsilonGroup = ruleGroup[0]
1069
+ else if @types[key]
1070
+ nonterminalGroups.push {key, ruleGroup}
1071
+ else
1072
+ terminalGroups.push {key, ruleGroup}
1073
+
1074
+ # Process terminal-first groups first (most specific)
1075
+ for {key, ruleGroup} in terminalGroups
1076
+ prefix = if firstCond then "if" else "else if"
1077
+ firstCond = false
1078
+ lines.push " #{prefix} (token === #{JSON.stringify key}) {"
1079
+ if ruleGroup.length is 1
1080
+ lines.push @_generateRDRuleBody ruleGroup[0], ' '
1081
+ else
1082
+ lines.push @_generateRDSharedPrefix ruleGroup, ' '
1083
+ lines.push " }"
1084
+
1085
+ # Then nonterminal-first groups — sort by FIRST set size (most specific first)
1086
+ # and check if groups overlap (merge overlapping into the broadest)
1087
+ if nonterminalGroups.length
1088
+ nonterminalGroups.sort (a, b) =>
1089
+ sizeA = @firsts[a.key]?.size or 0
1090
+ sizeB = @firsts[b.key]?.size or 0
1091
+ sizeA - sizeB # smallest first
1092
+
1093
+ for {key, ruleGroup}, i in nonterminalGroups
1094
+ firsts = @firsts[key]
1095
+ if i is nonterminalGroups.length - 1
1096
+ # Broadest group — use as default, merge all remaining rules
1097
+ allNTRules = []
1098
+ for {ruleGroup: rg} in nonterminalGroups[i..]
1099
+ allNTRules.push ...rg
1100
+ if firstCond
1101
+ lines.push @_generateRDSharedPrefix allNTRules, ' '
1102
+ firstCond = false
1103
+ else
1104
+ lines.push " else {"
1105
+ lines.push @_generateRDSharedPrefix allNTRules, ' '
1106
+ lines.push " }"
1107
+ break
1108
+ else if firsts?.size
1109
+ # Specific group — dispatch on unique FIRST tokens
1110
+ uniqueTokens = []
1111
+ firsts.forEach (t) =>
1112
+ # Only emit if this token isn't in broader groups
1113
+ unique = true
1114
+ for {key: otherKey} in nonterminalGroups[i + 1..]
1115
+ otherFirsts = @firsts[otherKey]
1116
+ if otherFirsts?.has(t)
1117
+ unique = false
1118
+ break
1119
+ uniqueTokens.push t if unique
1120
+ if uniqueTokens.length
1121
+ cond = uniqueTokens.map((t) -> "token === #{JSON.stringify t}").join ' || '
1122
+ prefix = if firstCond then "if" else "else if"
1123
+ firstCond = false
1124
+ lines.push " #{prefix} (#{cond}) {"
1125
+ lines.push @_generateRDSharedPrefix ruleGroup, ' '
1126
+ lines.push " }"
1127
+
1128
+ if epsilonGroup
1129
+ if firstCond
1130
+ lines.push @_generateRDRuleBody epsilonGroup, ' '
1131
+ else
1132
+ lines.push " else {"
1133
+ lines.push @_generateRDRuleBody epsilonGroup, ' '
1134
+ lines.push " }"
1135
+
1136
+ lines.push "}"
1137
+ lines.join '\n'
1138
+
1139
+ # --- Generate code for rules sharing a common prefix ---
1140
+ Generator::_generateRDSharedPrefix = (rules, indent) ->
1141
+ return @_generateRDRuleBody rules[0], indent if rules.length is 1
1142
+
1143
+ # Find longest common prefix
1144
+ prefixLen = 0
1145
+ loop
1146
+ sym = rules[0].symbols[prefixLen]
1147
+ break unless sym and sym isnt ''
1148
+ break unless rules.every (r) -> r.symbols.length > prefixLen and r.symbols[prefixLen] is sym
1149
+ prefixLen++
1150
+
1151
+ lines = []
1152
+
1153
+ if prefixLen is 0
1154
+ # No common prefix — just use first rule as default
1155
+ lines.push @_generateRDRuleBody rules[0], indent
1156
+ return lines.join '\n'
1157
+
1158
+ # Parse common prefix into vals
1159
+ lines.push "#{indent}const _vals_ = [];"
1160
+ for i in [0...prefixLen]
1161
+ sym = rules[0].symbols[i]
1162
+ if @types[sym]
1163
+ lines.push "#{indent}_vals_.push(parse#{sym}());"
1164
+ else
1165
+ lines.push "#{indent}_vals_.push(expect(#{JSON.stringify sym}));"
1166
+
1167
+ # Categorize suffixes (what comes after the common prefix)
1168
+ sorted = rules[..].sort (a, b) -> b.symbols.length - a.symbols.length
1169
+ terminalSuffixes = []
1170
+ nonterminalSuffixes = []
1171
+ emptyRule = null
1172
+
1173
+ for rule in sorted
1174
+ remaining = rule.symbols[prefixLen..]
1175
+ if remaining.length is 0
1176
+ emptyRule = rule
1177
+ else if @types[remaining[0]]
1178
+ nonterminalSuffixes.push rule
1179
+ else
1180
+ terminalSuffixes.push rule
1181
+
1182
+ # Check if suffixes form an "optional chain" pattern:
1183
+ # Multiple rules that are supersets of shorter rules with an empty base case.
1184
+ # Uses rule-length-based dispatch to select the correct semantic action.
1185
+ # Only valid when ALL suffix symbols appear in the longest rule's suffix
1186
+ # (otherwise nonterminal suffixes like [Expression] would be missed).
1187
+ isOptionalChain = false
1188
+ if emptyRule and (nonterminalSuffixes.length + terminalSuffixes.length) >= 2
1189
+ longestSuffix = sorted[0].symbols[prefixLen..]
1190
+ longestSyms = new Set longestSuffix
1191
+ isOptionalChain = sorted.every (rule) =>
1192
+ suffix = rule.symbols[prefixLen..]
1193
+ suffix.every (sym) => longestSyms.has sym
1194
+
1195
+ if isOptionalChain
1196
+ # Optional chain: parse each optional part if its FIRST token matches
1197
+ # Then select the correct rule based on how many symbols were parsed
1198
+ longestRule = sorted[0]
1199
+ longestRemaining = longestRule.symbols[prefixLen..]
1200
+
1201
+ # Generate optional checks using FIRST sets
1202
+ for sym in longestRemaining
1203
+ if @types[sym]
1204
+ firsts = @firsts[sym]
1205
+ if firsts?.size
1206
+ firstTokens = []
1207
+ firsts.forEach (t) => firstTokens.push t
1208
+ cond = firstTokens.map((t) -> "token === #{JSON.stringify t}").join ' || '
1209
+ lines.push "#{indent}if (#{cond}) _vals_.push(parse#{sym}()); // optional"
1210
+ else
1211
+ lines.push "#{indent}_vals_.push(parse#{sym}());"
1212
+ else
1213
+ lines.push "#{indent}if (token === #{JSON.stringify sym}) _vals_.push(expect(#{JSON.stringify sym})); // optional"
1214
+
1215
+ # Select the correct rule based on how many symbols were actually parsed
1216
+ # Build a lookup from vals length to rule ID
1217
+ ruleEntries = []
1218
+ for rule in sorted
1219
+ ruleEntries.push "#{rule.symbols.length}:#{rule.id}"
1220
+ mapStr = "{" + ruleEntries.join(",") + "}"
1221
+ lines.push "#{indent}const _rid_ = (#{mapStr})[_vals_.length];"
1222
+ lines.push "#{indent}if (_rid_ !== undefined) {"
1223
+ lines.push "#{indent} const $ = _vals_, $0 = _vals_.length - 1;"
1224
+ lines.push "#{indent} const _r = ruleActions(_rid_, _vals_, [], {});"
1225
+ lines.push "#{indent} return _r != null ? withLoc(_r, l) : withLoc($[$0], l);"
1226
+ lines.push "#{indent}}"
1227
+ lines.push "#{indent}#{@_getRDAction longestRule, '_vals_'}"
1228
+ else
1229
+ # Standard disambiguation: terminal suffixes first, then nonterminal default
1230
+ firstCond = true
1231
+
1232
+ # Group terminal suffixes by their first token (to avoid duplicate branches)
1233
+ termGroups = {}
1234
+ termOrder = []
1235
+ for rule in terminalSuffixes
1236
+ remaining = rule.symbols[prefixLen..]
1237
+ tok = remaining[0]
1238
+ unless termGroups[tok]
1239
+ termGroups[tok] = []
1240
+ termOrder.push tok
1241
+ termGroups[tok].push rule
1242
+
1243
+ for tok in termOrder
1244
+ group = termGroups[tok]
1245
+ prefix = if firstCond then "if" else "else if"
1246
+ firstCond = false
1247
+ lines.push "#{indent}#{prefix} (token === #{JSON.stringify tok}) {"
1248
+ if group.length is 1
1249
+ remaining = group[0].symbols[prefixLen..]
1250
+ for sym in remaining
1251
+ if @types[sym]
1252
+ lines.push "#{indent} _vals_.push(parse#{sym}());"
1253
+ else
1254
+ lines.push "#{indent} _vals_.push(expect(#{JSON.stringify sym}));"
1255
+ lines.push "#{indent} #{@_getRDAction group[0], '_vals_'}"
1256
+ else
1257
+ # Multiple rules with same first token — find deeper shared prefix
1258
+ # and disambiguate at the divergence point
1259
+ subPrefixLen = prefixLen
1260
+ loop
1261
+ sym = group[0].symbols[subPrefixLen]
1262
+ break unless sym
1263
+ break unless group.every (r) -> r.symbols.length > subPrefixLen and r.symbols[subPrefixLen] is sym
1264
+ subPrefixLen++
1265
+ # Parse the shared sub-prefix
1266
+ for i in [prefixLen...subPrefixLen]
1267
+ sym = group[0].symbols[i]
1268
+ if @types[sym]
1269
+ lines.push "#{indent} _vals_.push(parse#{sym}());"
1270
+ else
1271
+ lines.push "#{indent} _vals_.push(expect(#{JSON.stringify sym}));"
1272
+ # Disambiguate at the divergence point
1273
+ subFirst = true
1274
+ for rule in group.sort((a, b) -> b.symbols.length - a.symbols.length)
1275
+ remaining = rule.symbols[subPrefixLen..]
1276
+ if remaining.length is 0
1277
+ if subFirst
1278
+ lines.push "#{indent} #{@_getRDAction rule, '_vals_'}"
1279
+ else
1280
+ lines.push "#{indent} else {"
1281
+ lines.push "#{indent} #{@_getRDAction rule, '_vals_'}"
1282
+ lines.push "#{indent} }"
1283
+ else
1284
+ nextSym = remaining[0]
1285
+ cond = if @types[nextSym]
1286
+ firsts = @firsts[nextSym]
1287
+ if firsts?.size
1288
+ tokens = []
1289
+ firsts.forEach (t) => tokens.push t
1290
+ tokens.map((t) -> "token === #{JSON.stringify t}").join ' || '
1291
+ else
1292
+ "true"
1293
+ else
1294
+ "token === #{JSON.stringify nextSym}"
1295
+ p = if subFirst then "if" else "else if"
1296
+ subFirst = false
1297
+ lines.push "#{indent} #{p} (#{cond}) {"
1298
+ for sym in remaining
1299
+ if @types[sym]
1300
+ lines.push "#{indent} _vals_.push(parse#{sym}());"
1301
+ else
1302
+ lines.push "#{indent} _vals_.push(expect(#{JSON.stringify sym}));"
1303
+ lines.push "#{indent} #{@_getRDAction rule, '_vals_'}"
1304
+ lines.push "#{indent} }"
1305
+ subFirst = false
1306
+ lines.push "#{indent}}"
1307
+
1308
+ # Nonterminal suffixes as default (or with FIRST check when emptyRule exists)
1309
+ if nonterminalSuffixes.length
1310
+ inner = if firstCond then indent else "#{indent} "
1311
+ lines.push "#{indent}else {" unless firstCond
1312
+
1313
+ if nonterminalSuffixes.length is 1
1314
+ remaining = nonterminalSuffixes[0].symbols[prefixLen..]
1315
+ # When emptyRule exists, wrap nonterminal in FIRST check so empty is the default
1316
+ if emptyRule and remaining.length and remaining[0] and @types[remaining[0]]
1317
+ ntFirsts = @firsts[remaining[0]]
1318
+ if ntFirsts?.size
1319
+ ntTokens = []
1320
+ ntFirsts.forEach (t) => ntTokens.push t
1321
+ ntCond = ntTokens.map((t) -> "token === #{JSON.stringify t}").join ' || '
1322
+ lines.push "#{inner}if (#{ntCond}) {"
1323
+ for sym in remaining
1324
+ if @types[sym] then lines.push "#{inner} _vals_.push(parse#{sym}());"
1325
+ else lines.push "#{inner} _vals_.push(expect(#{JSON.stringify sym}));"
1326
+ lines.push "#{inner} #{@_getRDAction nonterminalSuffixes[0], '_vals_'}"
1327
+ lines.push "#{inner}}"
1328
+ # emptyRule as else of the FIRST check
1329
+ lines.push "#{inner}else {"
1330
+ lines.push "#{inner} #{@_getRDAction emptyRule, '_vals_'}"
1331
+ lines.push "#{inner}}"
1332
+ emptyRule = null # Consumed — don't emit again below
1333
+ else
1334
+ for sym in remaining
1335
+ if @types[sym] then lines.push "#{inner}_vals_.push(parse#{sym}());"
1336
+ else lines.push "#{inner}_vals_.push(expect(#{JSON.stringify sym}));"
1337
+ lines.push "#{inner}#{@_getRDAction nonterminalSuffixes[0], '_vals_'}"
1338
+ else
1339
+ for sym in remaining
1340
+ if @types[sym] then lines.push "#{inner}_vals_.push(parse#{sym}());"
1341
+ else lines.push "#{inner}_vals_.push(expect(#{JSON.stringify sym}));"
1342
+ lines.push "#{inner}#{@_getRDAction nonterminalSuffixes[0], '_vals_'}"
1343
+ else
1344
+ # Multiple nonterminal suffixes — find deeper shared prefix (extending
1345
+ # through nonterminals) until we reach a terminal divergence point
1346
+ subPrefixLen = prefixLen
1347
+ loop
1348
+ sym = nonterminalSuffixes[0].symbols[subPrefixLen]
1349
+ break unless sym
1350
+ break unless nonterminalSuffixes.every (r) -> r.symbols.length > subPrefixLen and r.symbols[subPrefixLen] is sym
1351
+ subPrefixLen++
1352
+ for i in [prefixLen...subPrefixLen]
1353
+ sym = nonterminalSuffixes[0].symbols[i]
1354
+ if @types[sym] then lines.push "#{inner}_vals_.push(parse#{sym}());"
1355
+ else lines.push "#{inner}_vals_.push(expect(#{JSON.stringify sym}));"
1356
+ # Re-categorize remaining suffixes at the new divergence point
1357
+ subTerminals = []
1358
+ subNonterminals = []
1359
+ subEmpty = null
1360
+ for rule in nonterminalSuffixes.sort((a, b) -> b.symbols.length - a.symbols.length)
1361
+ remaining = rule.symbols[subPrefixLen..]
1362
+ if remaining.length is 0 then subEmpty = rule
1363
+ else if @types[remaining[0]] then subNonterminals.push rule
1364
+ else subTerminals.push rule
1365
+ # Generate terminal branches first
1366
+ subFirst = true
1367
+ subTermGroups = {}
1368
+ subTermOrder = []
1369
+ for rule in subTerminals
1370
+ tok = rule.symbols[subPrefixLen]
1371
+ unless subTermGroups[tok]
1372
+ subTermGroups[tok] = []
1373
+ subTermOrder.push tok
1374
+ subTermGroups[tok].push rule
1375
+ for tok in subTermOrder
1376
+ group = subTermGroups[tok]
1377
+ p = if subFirst then "if" else "else if"
1378
+ subFirst = false
1379
+ lines.push "#{inner}#{p} (token === #{JSON.stringify tok}) {"
1380
+ if group.length is 1
1381
+ for sym in group[0].symbols[subPrefixLen..]
1382
+ if @types[sym] then lines.push "#{inner} _vals_.push(parse#{sym}());"
1383
+ else lines.push "#{inner} _vals_.push(expect(#{JSON.stringify sym}));"
1384
+ lines.push "#{inner} #{@_getRDAction group[0], '_vals_'}"
1385
+ else
1386
+ # Deeper shared prefix within this group
1387
+ deepLen = subPrefixLen
1388
+ loop
1389
+ sym = group[0].symbols[deepLen]
1390
+ break unless sym
1391
+ break unless group.every (r) -> r.symbols.length > deepLen and r.symbols[deepLen] is sym
1392
+ deepLen++
1393
+ for i in [subPrefixLen...deepLen]
1394
+ sym = group[0].symbols[i]
1395
+ if @types[sym] then lines.push "#{inner} _vals_.push(parse#{sym}());"
1396
+ else lines.push "#{inner} _vals_.push(expect(#{JSON.stringify sym}));"
1397
+ # Check for optional chain within group (empty remaining = shortest)
1398
+ hasShort = group.some (r) -> r.symbols.length is deepLen
1399
+ if hasShort
1400
+ longest = group.sort((a, b) -> b.symbols.length - a.symbols.length)[0]
1401
+ for sym2 in longest.symbols[deepLen..]
1402
+ if @types[sym2]
1403
+ firsts2 = @firsts[sym2]
1404
+ if firsts2?.size
1405
+ toks2 = []
1406
+ firsts2.forEach (t2) => toks2.push t2
1407
+ cond2 = toks2.map((t2) -> "token === #{JSON.stringify t2}").join ' || '
1408
+ lines.push "#{inner} if (#{cond2}) _vals_.push(parse#{sym2}());"
1409
+ else
1410
+ lines.push "#{inner} _vals_.push(parse#{sym2}());"
1411
+ else
1412
+ lines.push "#{inner} if (token === #{JSON.stringify sym2}) _vals_.push(expect(#{JSON.stringify sym2}));"
1413
+ lines.push "#{inner} #{@_getRDAction longest, '_vals_'}"
1414
+ else
1415
+ lines.push "#{inner} #{@_getRDAction group[0], '_vals_'}"
1416
+ lines.push "#{inner}}"
1417
+ # Nonterminal suffixes — group by first symbol and dispatch on FIRST sets
1418
+ if subNonterminals.length
1419
+ # Group by first remaining symbol
1420
+ ntGroups = {}
1421
+ ntOrder = []
1422
+ for rule in subNonterminals
1423
+ key = rule.symbols[subPrefixLen]
1424
+ unless ntGroups[key]
1425
+ ntGroups[key] = []
1426
+ ntOrder.push key
1427
+ ntGroups[key].push rule
1428
+ # Sort by FIRST set size (smallest first, largest = default)
1429
+ ntOrder.sort (a, b) => (@firsts[a]?.size or 0) - (@firsts[b]?.size or 0)
1430
+ for ntKey, ni in ntOrder
1431
+ group = ntGroups[ntKey]
1432
+ if ni is ntOrder.length - 1 and ntOrder.length is 1
1433
+ # Only one group — use as default
1434
+ base = if subFirst and not subEmpty then inner else "#{inner} "
1435
+ lines.push "#{inner}else {" unless subFirst and not subEmpty
1436
+ if group.length is 1
1437
+ for sym in group[0].symbols[subPrefixLen..]
1438
+ if @types[sym] then lines.push "#{base}_vals_.push(parse#{sym}());"
1439
+ else lines.push "#{base}_vals_.push(expect(#{JSON.stringify sym}));"
1440
+ lines.push "#{base}#{@_getRDAction group[0], '_vals_'}"
1441
+ else
1442
+ # Multiple rules — find deeper shared prefix within group, then disambiguate
1443
+ deepLen = subPrefixLen
1444
+ loop
1445
+ sym = group[0].symbols[deepLen]
1446
+ break unless sym
1447
+ break unless group.every (r) -> r.symbols.length > deepLen and r.symbols[deepLen] is sym
1448
+ deepLen++
1449
+ for i in [subPrefixLen...deepLen]
1450
+ sym = group[0].symbols[i]
1451
+ if @types[sym] then lines.push "#{base}_vals_.push(parse#{sym}());"
1452
+ else lines.push "#{base}_vals_.push(expect(#{JSON.stringify sym}));"
1453
+ # Disambiguate remaining by terminal token
1454
+ deepFirst = true
1455
+ deepSorted = group[..].sort (a, b) -> b.symbols.length - a.symbols.length
1456
+ deepEmpty = null
1457
+ for rule in deepSorted
1458
+ remaining = rule.symbols[deepLen..]
1459
+ if remaining.length is 0
1460
+ deepEmpty = rule
1461
+ else
1462
+ tok = remaining[0]
1463
+ cond = if @types[tok]
1464
+ toks = []
1465
+ @firsts[tok]?.forEach (t) => toks.push t
1466
+ toks.map((t) -> "token === #{JSON.stringify t}").join(' || ') or "true"
1467
+ else "token === #{JSON.stringify tok}"
1468
+ p = if deepFirst then "if" else "else if"
1469
+ deepFirst = false
1470
+ lines.push "#{base}#{p} (#{cond}) {"
1471
+ for sym in remaining
1472
+ if @types[sym] then lines.push "#{base} _vals_.push(parse#{sym}());"
1473
+ else lines.push "#{base} _vals_.push(expect(#{JSON.stringify sym}));"
1474
+ lines.push "#{base} #{@_getRDAction rule, '_vals_'}"
1475
+ lines.push "#{base}}"
1476
+ if deepEmpty
1477
+ if deepFirst
1478
+ lines.push "#{base}#{@_getRDAction deepEmpty, '_vals_'}"
1479
+ else
1480
+ lines.push "#{base}else { #{@_getRDAction deepEmpty, '_vals_'} }"
1481
+ else unless deepFirst
1482
+ lines.push "#{base}#{@_getRDAction deepSorted[0], '_vals_'}"
1483
+ lines.push "#{inner}}" unless subFirst and not subEmpty
1484
+ else
1485
+ # Specific group — dispatch on unique FIRST tokens
1486
+ firsts = @firsts[ntKey]
1487
+ if firsts?.size
1488
+ uniqueTokens = []
1489
+ firsts.forEach (t) => uniqueTokens.push t
1490
+ cond = uniqueTokens.map((t) -> "token === #{JSON.stringify t}").join ' || '
1491
+ p = if subFirst then "if" else "else if"
1492
+ subFirst = false
1493
+ lines.push "#{inner}#{p} (#{cond}) {"
1494
+ if group.length is 1
1495
+ for sym in group[0].symbols[subPrefixLen..]
1496
+ if @types[sym] then lines.push "#{inner} _vals_.push(parse#{sym}());"
1497
+ else lines.push "#{inner} _vals_.push(expect(#{JSON.stringify sym}));"
1498
+ lines.push "#{inner} #{@_getRDAction group[0], '_vals_'}"
1499
+ else
1500
+ # Multiple rules — find deeper shared prefix, then disambiguate
1501
+ deepLen = subPrefixLen
1502
+ loop
1503
+ sym = group[0].symbols[deepLen]
1504
+ break unless sym
1505
+ break unless group.every (r) -> r.symbols.length > deepLen and r.symbols[deepLen] is sym
1506
+ deepLen++
1507
+ for i in [subPrefixLen...deepLen]
1508
+ sym = group[0].symbols[i]
1509
+ if @types[sym] then lines.push "#{inner} _vals_.push(parse#{sym}());"
1510
+ else lines.push "#{inner} _vals_.push(expect(#{JSON.stringify sym}));"
1511
+ deepFirst = true
1512
+ for rule in group.sort((a, b) -> b.symbols.length - a.symbols.length)
1513
+ remaining = rule.symbols[deepLen..]
1514
+ if remaining.length is 0
1515
+ unless deepFirst
1516
+ lines.push "#{inner} else {"
1517
+ lines.push "#{inner} #{if deepFirst then '' else ' '}#{@_getRDAction rule, '_vals_'}"
1518
+ lines.push "#{inner} }" unless deepFirst
1519
+ else
1520
+ tok2 = remaining[0]
1521
+ cond2 = if @types[tok2]
1522
+ t2 = []; @firsts[tok2]?.forEach (t) => t2.push t
1523
+ t2.map((t) -> "token === #{JSON.stringify t}").join(' || ') or "true"
1524
+ else "token === #{JSON.stringify tok2}"
1525
+ dp = if deepFirst then "if" else "else if"
1526
+ deepFirst = false
1527
+ lines.push "#{inner} #{dp} (#{cond2}) {"
1528
+ for sym in remaining
1529
+ if @types[sym] then lines.push "#{inner} _vals_.push(parse#{sym}());"
1530
+ else lines.push "#{inner} _vals_.push(expect(#{JSON.stringify sym}));"
1531
+ lines.push "#{inner} #{@_getRDAction rule, '_vals_'}"
1532
+ lines.push "#{inner} }"
1533
+ deepFirst = false
1534
+ lines.push "#{inner}}"
1535
+ else if subEmpty
1536
+ if subFirst
1537
+ lines.push "#{inner}#{@_getRDAction subEmpty, '_vals_'}"
1538
+ else
1539
+ lines.push "#{inner}else {"
1540
+ lines.push "#{inner} #{@_getRDAction subEmpty, '_vals_'}"
1541
+ lines.push "#{inner}}"
1542
+
1543
+ lines.push "#{indent}}" unless firstCond
1544
+ else if emptyRule
1545
+ if firstCond
1546
+ lines.push "#{indent}#{@_getRDAction emptyRule, '_vals_'}"
1547
+ else
1548
+ lines.push "#{indent}else {"
1549
+ lines.push "#{indent} #{@_getRDAction emptyRule, '_vals_'}"
1550
+ lines.push "#{indent}}"
1551
+
1552
+ lines.join '\n'
1553
+
1554
+ # --- Generate body for a single grammar rule ---
1555
+ Generator::_generateRDRuleBody = (rule, indent = ' ') ->
1556
+ {symbols} = rule
1557
+ lines = []
1558
+ valsVar = '_vals_'
1559
+
1560
+ # Handle epsilon
1561
+ if symbols.length is 0 or (symbols.length is 1 and symbols[0] is '')
1562
+ action = @_getRDAction rule
1563
+ lines.push "#{indent}#{action}"
1564
+ return lines.join '\n'
1565
+
1566
+ lines.push "#{indent}const #{valsVar} = [];"
1567
+
1568
+ for sym in symbols
1569
+ if @types[sym]
1570
+ lines.push "#{indent}#{valsVar}.push(parse#{sym}());"
1571
+ else
1572
+ lines.push "#{indent}#{valsVar}.push(expect(#{JSON.stringify sym}));"
1573
+
1574
+ # Apply semantic action
1575
+ action = @_getRDAction rule, valsVar
1576
+ lines.push "#{indent}#{action}"
1577
+
1578
+ lines.join '\n'
1579
+
1580
+ # --- Get the semantic action code for a rule ---
1581
+ Generator::_getRDAction = (rule, valsVar = '_vals_') ->
1582
+ {symbols} = rule
1583
+ len = if symbols[0] is '' then 0 else symbols.length
1584
+
1585
+ if len is 0
1586
+ "{ const $ = [], $0 = -1; const _r = ruleActions(#{rule.id}, [], [], {}); return _r != null ? withLoc(_r, l) : withLoc([], l); }"
1587
+ else
1588
+ "{ const $ = #{valsVar}, $0 = #{valsVar}.length - 1; const _r = ruleActions(#{rule.id}, #{valsVar}, [], {}); return _r != null ? withLoc(_r, l) : withLoc($[$0], l); }"
1589
+
1590
+ # --- Expression parser (Pratt-based, GENERATED from grammar rules) ---
1591
+ Generator::_generateRDExpression = ->
1592
+ lines = []
1593
+ lines.push "function parseExpression(minBP) {"
1594
+ lines.push " if (minBP === undefined) minBP = 0;"
1595
+ lines.push " const l = loc();"
1596
+ lines.push ""
1597
+
1598
+ # Prefix starters — keyword-led expressions (IF, FOR, CLASS, etc.)
1599
+ lines.push " // Statement-like expression starters (derived from Expression alternatives)"
1600
+ for {token, nonterminal} in @_prefixStarters
1601
+ lines.push " if (token === #{JSON.stringify token}) return parse#{nonterminal}();"
1602
+ lines.push ""
1603
+
1604
+ # Arrow function detection
1605
+ codeFirsts = if @_codeNT then @firsts[@_codeNT] else null
1606
+ if codeFirsts
1607
+ codeTokens = []
1608
+ codeFirsts.forEach (t) => codeTokens.push t
1609
+ if codeTokens.length
1610
+ cond = codeTokens.map((t) -> "token === #{JSON.stringify t}").join ' || '
1611
+ lines.push " // Arrow functions"
1612
+ lines.push " if (#{cond}) return parse#{@_codeNT}();"
1613
+ lines.push ""
1614
+
1615
+ lines.push " // Pratt expression parser"
1616
+ lines.push " let left = parseUnary();"
1617
+ lines.push ""
1618
+ lines.push " while (true) {"
1619
+
1620
+ # Assignment operators (lowest precedence, right-associative)
1621
+ if @_assignOps.length
1622
+ lines.push " // Assignment operators (derived from grammar rules)"
1623
+ lines.push " if (minBP === 0) {"
1624
+ for {token: tok, rule} in @_assignOps
1625
+ lines.push " if (token === #{JSON.stringify tok}) {"
1626
+ lines.push " const _op = tokenText; advance();"
1627
+ lines.push " let right;"
1628
+ lines.push " if (token === 'TERMINATOR') { advance(); right = parseExpression(0); }"
1629
+ lines.push " else if (token === 'INDENT') { advance(); right = parseExpression(0); expect('OUTDENT'); }"
1630
+ lines.push " else { right = parseExpression(0); }"
1631
+ # Use ruleActions with constructed vals
1632
+ lines.push " const _vals_ = [left, _op, right];"
1633
+ lines.push " const $ = _vals_, $0 = _vals_.length - 1;"
1634
+ lines.push " const _r = ruleActions(#{rule.id}, _vals_, [], {});"
1635
+ lines.push " left = _r != null ? withLoc(_r, l) : withLoc(left, l);"
1636
+ lines.push " continue;"
1637
+ lines.push " }"
1638
+ lines.push " }"
1639
+ lines.push ""
1640
+
1641
+ # Postfix operators (from Operation rules)
1642
+ # Skip tokens that also appear as infix ops (they're handled there with INDENT/TERMINATOR variants)
1643
+ infixTokens = new Set
1644
+ for {token: t} in @_infixOps
1645
+ infixTokens.add t
1646
+ if @_postfixOps.length
1647
+ lines.push " // Postfix operators (derived from Operation rules)"
1648
+ seen = new Set
1649
+ for {token: tok, bp, rule, leftNT} in @_postfixOps
1650
+ continue if seen.has tok
1651
+ continue if infixTokens.has tok
1652
+ seen.add tok
1653
+ lines.push " if (token === #{JSON.stringify tok} && (BP[#{JSON.stringify tok}] || 0) >= minBP) {"
1654
+ # Build vals for the action
1655
+ syms = rule.symbols
1656
+ lines.push " const _vals_ = [left];"
1657
+ for sym, i in syms when i > 0
1658
+ if @types[sym]
1659
+ lines.push " _vals_.push(parse#{sym}());"
1660
+ else
1661
+ lines.push " _vals_.push(expect(#{JSON.stringify sym}));"
1662
+ lines.push " const $ = _vals_, $0 = _vals_.length - 1;"
1663
+ lines.push " const _r = ruleActions(#{rule.id}, _vals_, [], {});"
1664
+ lines.push " left = _r != null ? withLoc(_r, l) : left;"
1665
+ lines.push " continue;"
1666
+ lines.push " }"
1667
+
1668
+ # Postfix chains (property access, indexing, calls — from SimpleAssignable/Invocation)
1669
+ if @_postfixChains.length
1670
+ lines.push ""
1671
+ lines.push " // Postfix chains (derived from SimpleAssignable/Invocation rules)"
1672
+
1673
+ # Group rules by effective dispatch token (resolving nonterminals to FIRST sets)
1674
+ chainGroups = {}
1675
+ chainOrder = []
1676
+ for {nonterminal, rule} in @_postfixChains
1677
+ syms = rule.symbols
1678
+ chainSym = syms[1]
1679
+ # Resolve nonterminal to its FIRST set tokens
1680
+ tokens = []
1681
+ if @types[chainSym]
1682
+ @firsts[chainSym]?.forEach (t) => tokens.push t
1683
+ else
1684
+ tokens.push chainSym
1685
+ for tok in tokens
1686
+ unless chainGroups[tok]
1687
+ chainGroups[tok] = []
1688
+ chainOrder.push tok
1689
+ # Avoid duplicate rules in same group
1690
+ unless chainGroups[tok].some (entry) -> entry.rule.id is rule.id
1691
+ chainGroups[tok].push {nonterminal, rule}
1692
+
1693
+ emittedChains = new Set
1694
+ for tok in chainOrder
1695
+ group = chainGroups[tok]
1696
+ continue if emittedChains.has tok
1697
+ emittedChains.add tok
1698
+ # Use operator binding power if available, otherwise always match
1699
+ bp = @operators[tok]
1700
+ bpCheck = if bp then " && (BP[#{JSON.stringify tok}] || 0) >= minBP" else ""
1701
+ lines.push " if (token === #{JSON.stringify tok}#{bpCheck}) {"
1702
+ if group.length is 1
1703
+ {rule} = group[0]
1704
+ syms = rule.symbols
1705
+ lines.push " const _vals_ = [left];"
1706
+ for sym, i in syms when i > 0
1707
+ if @types[sym]
1708
+ lines.push " _vals_.push(parse#{sym}());"
1709
+ else
1710
+ lines.push " _vals_.push(expect(#{JSON.stringify sym}));"
1711
+ lines.push " const $ = _vals_, $0 = _vals_.length - 1;"
1712
+ lines.push " const _r = ruleActions(#{rule.id}, _vals_, [], {});"
1713
+ lines.push " left = _r != null ? withLoc(_r, l) : left;"
1714
+ else
1715
+ # Check if this group has both Expression and Slice rules (INDEX_START ambiguity)
1716
+ # If so, use speculation: try each rule with Slice first, fall back to Expression
1717
+ hasSlice = group.some (g) => g.rule.symbols.some (s) => s is 'Slice'
1718
+ hasExpr = group.some (g) => g.rule.symbols.some (s) => s is @_exprNT
1719
+ if hasSlice and hasExpr
1720
+ # Index with Slice/Expression ambiguity: parse Expression, then check for RangeDots
1721
+ # If RangeDots follows, it's a Slice; otherwise it's a plain index
1722
+ exprRules = (g.rule for g in group when not g.rule.symbols.some (s) => s is 'Slice')
1723
+ sliceRules = (g.rule for g in group when g.rule.symbols.some (s) => s is 'Slice')
1724
+ sliceRule = sliceRules.sort((a, b) -> a.symbols.length - b.symbols.length)[0]
1725
+ exprRule = exprRules.sort((a, b) -> a.symbols.length - b.symbols.length)[0]
1726
+ lines.push " const _vals_ = [left];"
1727
+ lines.push " _vals_.push(expect(#{JSON.stringify tok}));"
1728
+ # Check for RangeDots first (Slice starting with ..)
1729
+ lines.push " if (token === '..' || token === '...') {"
1730
+ lines.push " const _slice = parseRangeDots();"
1731
+ lines.push " if (token !== 'INDEX_END') { _vals_.push([_slice, null, parseExpression()]); }"
1732
+ lines.push " else { _vals_.push([_slice, null, null]); }"
1733
+ lines.push " _vals_.push(expect('INDEX_END'));"
1734
+ lines.push " const $ = _vals_, $0 = _vals_.length - 1;"
1735
+ lines.push " const _r = ruleActions(#{sliceRule.id}, _vals_, [], {});"
1736
+ lines.push " left = _r != null ? withLoc(_r, l) : left; continue;"
1737
+ lines.push " }"
1738
+ # Parse Expression, then check for RangeDots (Slice: expr..expr, expr..)
1739
+ lines.push " const _expr = parseExpression();"
1740
+ lines.push " if (token === '..' || token === '...') {"
1741
+ lines.push " const _dots = parseRangeDots();"
1742
+ lines.push " if (token !== 'INDEX_END') {"
1743
+ lines.push " _vals_.push([_dots, _expr, parseExpression()]);"
1744
+ lines.push " } else {"
1745
+ lines.push " _vals_.push([_dots, _expr, null]);"
1746
+ lines.push " }"
1747
+ lines.push " _vals_.push(expect('INDEX_END'));"
1748
+ lines.push " const $ = _vals_, $0 = _vals_.length - 1;"
1749
+ lines.push " const _r = ruleActions(#{sliceRule.id}, _vals_, [], {});"
1750
+ lines.push " left = _r != null ? withLoc(_r, l) : left;"
1751
+ lines.push " } else {"
1752
+ lines.push " _vals_.push(_expr);"
1753
+ lines.push " _vals_.push(expect('INDEX_END'));"
1754
+ lines.push " const $ = _vals_, $0 = _vals_.length - 1;"
1755
+ lines.push " const _r = ruleActions(#{exprRule.id}, _vals_, [], {});"
1756
+ lines.push " left = _r != null ? withLoc(_r, l) : left;"
1757
+ lines.push " }"
1758
+ lines.push " continue;"
1759
+ lines.push " }"
1760
+ continue # Skip normal multi-rule handling for this group
1761
+
1762
+ # Multiple rules — find common prefix after syms[0] (the left NT), then disambiguate
1763
+ lines.push " const _vals_ = [left];"
1764
+ # Find shared prefix (starting from index 1)
1765
+ prefixLen = 1
1766
+ loop
1767
+ sym = group[0].rule.symbols[prefixLen]
1768
+ break unless sym
1769
+ break unless group.every (g) -> g.rule.symbols.length > prefixLen and g.rule.symbols[prefixLen] is sym
1770
+ prefixLen++
1771
+ for i in [1...prefixLen]
1772
+ sym = group[0].rule.symbols[i]
1773
+ if @types[sym] then lines.push " _vals_.push(parse#{sym}());"
1774
+ else lines.push " _vals_.push(expect(#{JSON.stringify sym}));"
1775
+ # Disambiguate remaining suffixes (deduplicate rules with identical suffixes)
1776
+ sorted = group[..].sort (a, b) -> b.rule.symbols.length - a.rule.symbols.length
1777
+ # Deduplicate: only keep one rule per unique suffix
1778
+ uniqueSorted = []
1779
+ seenSuffixes = new Set
1780
+ for entry in sorted
1781
+ suffix = entry.rule.symbols[prefixLen..].join(' ')
1782
+ unless seenSuffixes.has suffix
1783
+ seenSuffixes.add suffix
1784
+ uniqueSorted.push entry
1785
+ subFirst = true
1786
+ for {rule: r} in uniqueSorted
1787
+ remaining = r.symbols[prefixLen..]
1788
+ if remaining.length is 0
1789
+ unless subFirst
1790
+ lines.push " else {"
1791
+ lines.push "#{if subFirst then ' ' else ' '}const $ = _vals_, $0 = _vals_.length - 1;"
1792
+ lines.push "#{if subFirst then ' ' else ' '}const _r = ruleActions(#{r.id}, _vals_, [], {});"
1793
+ lines.push "#{if subFirst then ' ' else ' '}left = _r != null ? withLoc(_r, l) : left;"
1794
+ unless subFirst
1795
+ lines.push " }"
1796
+ else
1797
+ nextSym = remaining[0]
1798
+ cond = if @types[nextSym]
1799
+ firsts = @firsts[nextSym]
1800
+ if firsts?.size
1801
+ toks = []
1802
+ firsts.forEach (t) => toks.push t
1803
+ toks.map((t) -> "token === #{JSON.stringify t}").join ' || '
1804
+ else "true"
1805
+ else "token === #{JSON.stringify nextSym}"
1806
+ p = if subFirst then "if" else "else if"
1807
+ subFirst = false
1808
+ lines.push " #{p} (#{cond}) {"
1809
+ for sym in remaining
1810
+ if @types[sym] then lines.push " _vals_.push(parse#{sym}());"
1811
+ else lines.push " _vals_.push(expect(#{JSON.stringify sym}));"
1812
+ lines.push " const $ = _vals_, $0 = _vals_.length - 1;"
1813
+ lines.push " const _r = ruleActions(#{r.id}, _vals_, [], {});"
1814
+ lines.push " left = _r != null ? withLoc(_r, l) : left;"
1815
+ lines.push " }"
1816
+ subFirst = false
1817
+ lines.push " continue;"
1818
+ lines.push " }"
1819
+
1820
+ # Infix binary operators (from Operation rules)
1821
+ lines.push ""
1822
+ lines.push " // Binary operators (derived from Operation rules)"
1823
+ seen = new Set
1824
+ for {token: tok, bp, assoc, rule, controlTarget} in @_infixOps
1825
+ continue if seen.has tok
1826
+ bpExpr = "(BP[#{JSON.stringify tok}] || 0)"
1827
+ nextBP = if assoc is 'right' then bpExpr else "#{bpExpr} + 1"
1828
+
1829
+ # Check if this operator has control-flow variants (|| Return, && Throw, etc.)
1830
+ controlOps = @_infixOps.filter (op) -> op.token is tok and op.controlTarget
1831
+ normalRule = @_infixOps.find (op) -> op.token is tok and not op.controlTarget
1832
+
1833
+ if controlOps.length
1834
+ # Emit a merged handler: check each control target, fall through to normal binary
1835
+ seen.add tok
1836
+ lines.push " if (token === #{JSON.stringify tok} && #{bpExpr} >= minBP) {"
1837
+ lines.push " advance();"
1838
+ for ctrlOp, ci in controlOps
1839
+ prefix = if ci is 0 then "if" else "else if"
1840
+ lines.push " #{prefix} (token === #{JSON.stringify ctrlOp.controlTarget.toUpperCase()}) {"
1841
+ lines.push " const ctrl = parse#{ctrlOp.controlTarget}();"
1842
+ lines.push " const _vals_ = [left, #{JSON.stringify tok}, ctrl];"
1843
+ lines.push " const $ = _vals_, $0 = 2;"
1844
+ lines.push " const _r = ruleActions(#{ctrlOp.rule.id}, _vals_, [], {});"
1845
+ lines.push " left = _r != null ? withLoc(_r, l) : left; continue;"
1846
+ lines.push " }"
1847
+ if normalRule
1848
+ lines.push " else {"
1849
+ lines.push " const right = parseExpression(#{nextBP});"
1850
+ lines.push " const _vals_ = [left, #{JSON.stringify tok}, right];"
1851
+ lines.push " const $ = _vals_, $0 = 2;"
1852
+ lines.push " const _r = ruleActions(#{normalRule.rule.id}, _vals_, [], {});"
1853
+ lines.push " left = _r != null ? withLoc(_r, l) : left; continue;"
1854
+ lines.push " }"
1855
+ lines.push " }"
1856
+ continue
1857
+
1858
+ # No control-flow variants — normal binary operator
1859
+ seen.add tok
1860
+ lines.push " if (token === #{JSON.stringify tok} && #{bpExpr} >= minBP) {"
1861
+ # Check if token is a category token (MATH, COMPARE, etc.) where tokenText is the actual op
1862
+ isCategoryToken = tok is tok.toUpperCase() and tok.length > 2
1863
+ if isCategoryToken
1864
+ lines.push " const _op = tokenText; advance();"
1865
+ else
1866
+ lines.push " advance();"
1867
+ # Check if this operator has TERMINATOR/INDENT variants in the grammar
1868
+ hasIndentVariant = @_postfixOps.some (op) -> op.token is tok
1869
+ if hasIndentVariant
1870
+ lines.push " let right;"
1871
+ lines.push " if (token === 'TERMINATOR') { advance(); right = parseExpression(#{nextBP}); }"
1872
+ lines.push " else if (token === 'INDENT') { advance(); right = parseExpression(#{nextBP}); expect('OUTDENT'); }"
1873
+ lines.push " else { right = parseExpression(#{nextBP}); }"
1874
+ else
1875
+ lines.push " const right = parseExpression(#{nextBP});"
1876
+ lines.push " const _vals_ = [left, #{if isCategoryToken then '_op' else JSON.stringify tok}, right];"
1877
+ lines.push " const $ = _vals_, $0 = 2;"
1878
+ lines.push " const _r = ruleActions(#{rule.id}, _vals_, [], {});"
1879
+ lines.push " left = _r != null ? withLoc(_r, l) : left;"
1880
+ lines.push " continue;"
1881
+ lines.push " }"
1882
+
1883
+ # Ternary operators
1884
+ if @_ternaryOps.length
1885
+ lines.push ""
1886
+ lines.push " // Ternary operators (derived from Operation rules)"
1887
+ for {token: tok, separator, bp, assoc, rule} in @_ternaryOps
1888
+ bpExpr = "(BP[#{JSON.stringify tok}] || 0)"
1889
+ lines.push " if (token === #{JSON.stringify tok} && #{bpExpr} >= minBP) {"
1890
+ lines.push " advance();"
1891
+ lines.push " const middle = parseExpression(0);"
1892
+ lines.push " expect(#{JSON.stringify separator});"
1893
+ lines.push " const right = parseExpression(0);"
1894
+ lines.push " const _vals_ = [left, #{JSON.stringify tok}, middle, #{JSON.stringify separator}, right];"
1895
+ lines.push " const $ = _vals_, $0 = 4;"
1896
+ lines.push " const _r = ruleActions(#{rule.id}, _vals_, [], {});"
1897
+ lines.push " left = _r != null ? withLoc(_r, l) : left;"
1898
+ lines.push " continue;"
1899
+ lines.push " }"
1900
+
1901
+ # Scan Expression alternatives for postfix patterns:
1902
+ # Rules like "Expression TOKEN Expression" or "Statement TOKEN Expression"
1903
+ # where TOKEN has operator precedence (postfix if, postfix unless, etc.)
1904
+ # and rules where Expression appears first (postfix for, postfix while)
1905
+ if @_exprNT
1906
+ postfixTokensEmitted = new Set
1907
+ for exprRule in @types[@_exprNT].rules when exprRule.symbols.length is 1
1908
+ altName = exprRule.symbols[0]
1909
+ altType = @types[altName]
1910
+ continue unless altType
1911
+ continue if altName is @_operationNT or altName is @_valueNT or altName is @_codeNT
1912
+
1913
+ for rule in altType.rules
1914
+ syms = rule.symbols
1915
+ continue unless syms.length >= 2
1916
+
1917
+ # Pattern: Expression/Statement POSTFIX_TOKEN NT — postfix operator (3+ symbols)
1918
+ # Only match when the left operand is Expression or Statement (not structural NTs like IfBlock)
1919
+ if syms.length >= 3 and @types[syms[0]] and not @types[syms[1]] and @types[syms[2]] and (syms[0] is @_exprNT or @_exprStatementAlts?.has(syms[0]))
1920
+ postToken = syms[1]
1921
+ continue if postfixTokensEmitted.has postToken
1922
+ # Skip if this token is a prefix starter — those get full handlers below (parsePostfixFor etc.)
1923
+ continue if @_prefixStarters.some((p) -> p.token is postToken)
1924
+ postfixTokensEmitted.add postToken
1925
+ bp = (@operators[postToken]?.precedence or 0) * 2
1926
+
1927
+ # Collect ALL rules for this postfix token (including longer variants like ELSE continuation)
1928
+ allPostRules = []
1929
+ for r2 in altType.rules
1930
+ s2 = r2.symbols
1931
+ if s2.length >= 3 and s2[1] is postToken and @types[s2[0]] and (s2[0] is @_exprNT or @_exprStatementAlts?.has(s2[0]))
1932
+ allPostRules.push r2
1933
+
1934
+ # Sort by length: shortest first (base), then longer variants
1935
+ allPostRules.sort (a, b) -> a.symbols.length - b.symbols.length
1936
+ baseRule = allPostRules[0]
1937
+ longerRules = (r for r in allPostRules when r.symbols.length > baseRule.symbols.length)
1938
+
1939
+ lines.push ""
1940
+ lines.push " // Postfix #{postToken} (derived from #{altName} rules)"
1941
+ lines.push " if (token === #{JSON.stringify postToken} && #{bp} >= minBP) {"
1942
+ lines.push " const _pvals_ = [left];"
1943
+ lines.push " _pvals_.push(expect(#{JSON.stringify postToken}));"
1944
+ for sym in baseRule.symbols[2..]
1945
+ if @types[sym] then lines.push " _pvals_.push(parse#{sym}());"
1946
+ else lines.push " _pvals_.push(expect(#{JSON.stringify sym}));"
1947
+
1948
+ # Check for longer continuation variants (e.g., POST_IF Expr ELSE INDENT Expr OUTDENT)
1949
+ if longerRules.length
1950
+ contRule = longerRules[0]
1951
+ contStart = baseRule.symbols.length
1952
+ contSym = contRule.symbols[contStart]
1953
+ if contSym and not @types[contSym]
1954
+ lines.push " if (token === #{JSON.stringify contSym}) {"
1955
+ lines.push " _pvals_.push(expect(#{JSON.stringify contSym}));"
1956
+ for sym in contRule.symbols[contStart + 1..]
1957
+ if @types[sym] then lines.push " _pvals_.push(parse#{sym}());"
1958
+ else lines.push " _pvals_.push(expect(#{JSON.stringify sym}));"
1959
+ lines.push " const $ = _pvals_, $0 = _pvals_.length - 1;"
1960
+ lines.push " const _r = ruleActions(#{contRule.id}, _pvals_, [], {});"
1961
+ lines.push " left = _r != null ? withLoc(_r, l) : left; continue;"
1962
+ lines.push " }"
1963
+
1964
+ lines.push " const $ = _pvals_, $0 = _pvals_.length - 1;"
1965
+ lines.push " const _r = ruleActions(#{baseRule.id}, _pvals_, [], {});"
1966
+ lines.push " left = _r != null ? withLoc(_r, l) : left;"
1967
+ lines.push " continue;"
1968
+ lines.push " }"
1969
+
1970
+ # Pattern: Expression KEYWORD — postfix comprehension/loop (terminal second symbol)
1971
+ if syms[0] is @_exprNT and not @types[syms[1]] and @_prefixStarters.some((p) -> p.token is syms[1])
1972
+ postToken = syms[1]
1973
+ continue if postfixTokensEmitted.has "postfix:#{postToken}"
1974
+ postfixTokensEmitted.add "postfix:#{postToken}"
1975
+ lines.push ""
1976
+ lines.push " // Postfix #{postToken} (derived from #{altName} rules)"
1977
+ lines.push " if (token === #{JSON.stringify postToken} && minBP === 0) {"
1978
+ lines.push " left = parsePostfixFor(left, l);"
1979
+ lines.push " continue;"
1980
+ lines.push " }"
1981
+
1982
+ # Pattern: Expression NONTERMINAL — postfix with nonterminal (e.g., Expression WhileSource)
1983
+ if syms.length is 2 and syms[0] is @_exprNT and @types[syms[1]]
1984
+ ntSym = syms[1]
1985
+ ntFirsts = @firsts[ntSym]
1986
+ continue unless ntFirsts?.size
1987
+ ntFirsts.forEach (postToken) =>
1988
+ return if postfixTokensEmitted.has "postfix:#{postToken}"
1989
+ postfixTokensEmitted.add "postfix:#{postToken}"
1990
+ lines.push ""
1991
+ lines.push " // Postfix #{postToken} (derived from #{altName} via #{ntSym})"
1992
+ lines.push " if (token === #{JSON.stringify postToken} && minBP === 0) {"
1993
+ lines.push " const _ws = parse#{ntSym}();"
1994
+ lines.push " const _vals_ = [left, _ws];"
1995
+ lines.push " const $ = _vals_, $0 = 1;"
1996
+ lines.push " const _r = ruleActions(#{rule.id}, _vals_, [], {});"
1997
+ lines.push " left = _r != null ? withLoc(_r, l) : left;"
1998
+ lines.push " continue;"
1999
+ lines.push " }"
2000
+
2001
+ lines.push ""
2002
+ lines.push " break;"
2003
+ lines.push " }"
2004
+ lines.push " return left;"
2005
+ lines.push "}"
2006
+ lines.join '\n'
2007
+
2008
+ # --- Specialized nonterminal parsers ---
2009
+ Generator::_generateRDSpecialized = (name) ->
2010
+ switch name
2011
+ when 'For' then @_generateRDFor()
2012
+ when 'Object' then @_generateRDObject()
2013
+ when 'AssignObj' then @_generateRDAssignObj()
2014
+ else throw new Error "No specialized generator for #{name}"
2015
+
2016
+ Generator::_generateRDFor = ->
2017
+ """
2018
+ function parseFor() {
2019
+ const l = loc();
2020
+ expect('FOR');
2021
+ // Check for range: FOR Range Block (use speculation — [a,b] could be destructuring)
2022
+ if (token === '[') {
2023
+ const range = speculate(() => parseRange());
2024
+ if (range !== null) {
2025
+ if (token === 'BY') {
2026
+ advance();
2027
+ const step = parseExpression(0);
2028
+ const block = parseBlock();
2029
+ return withLoc(["for-in", [], range, step, null, block], l);
2030
+ }
2031
+ const block = parseBlock();
2032
+ return withLoc(["for-in", [], range, null, null, block], l);
2033
+ }
2034
+ }
2035
+ // Check for AWAIT (async for-as)
2036
+ if (token === 'AWAIT') {
2037
+ advance();
2038
+ const vars = parseForVariables();
2039
+ expect('FORAS');
2040
+ const iter = parseExpression(0);
2041
+ let guard = null;
2042
+ if (token === 'WHEN') { advance(); guard = parseExpression(0); }
2043
+ const block = parseBlock();
2044
+ return withLoc(["for-as", vars, iter, true, guard, block], l);
2045
+ }
2046
+ // Check for OWN (for own k of obj)
2047
+ if (token === 'OWN') {
2048
+ advance();
2049
+ const vars = parseForVariables();
2050
+ expect('FOROF');
2051
+ const obj = parseExpression(0);
2052
+ let guard = null;
2053
+ if (token === 'WHEN') { advance(); guard = parseExpression(0); }
2054
+ const block = parseBlock();
2055
+ return withLoc(["for-of", vars, obj, true, guard, block], l);
2056
+ }
2057
+ // Regular for: parse variables, then dispatch on FORIN/FOROF/FORAS
2058
+ const vars = parseForVariables();
2059
+ if (token === 'FORIN') {
2060
+ advance();
2061
+ const arr = parseExpression(0);
2062
+ let step = null, guard = null;
2063
+ if (token === 'WHEN') { advance(); guard = parseExpression(0); }
2064
+ if (token === 'BY') { advance(); step = parseExpression(0); }
2065
+ if (!guard && token === 'WHEN') { advance(); guard = parseExpression(0); }
2066
+ const block = parseBlock();
2067
+ return withLoc(["for-in", vars, arr, step, guard, block], l);
2068
+ }
2069
+ if (token === 'FOROF') {
2070
+ advance();
2071
+ const obj = parseExpression(0);
2072
+ let guard = null;
2073
+ if (token === 'WHEN') { advance(); guard = parseExpression(0); }
2074
+ const block = parseBlock();
2075
+ return withLoc(["for-of", vars, obj, false, guard, block], l);
2076
+ }
2077
+ if (token === 'FORAS' || token === 'FORASAWAIT') {
2078
+ const isAsync = token === 'FORASAWAIT';
2079
+ advance();
2080
+ const iter = parseExpression(0);
2081
+ let guard = null;
2082
+ if (token === 'WHEN') { advance(); guard = parseExpression(0); }
2083
+ const block = parseBlock();
2084
+ return withLoc(["for-as", vars, iter, isAsync, guard, block], l);
2085
+ }
2086
+ throw new Error('Parse error in For: expected FORIN/FOROF/FORAS after variables, got ' + token);
2087
+ }
2088
+ """
2089
+
2090
+ Generator::_generateRDAssignObj = ->
2091
+ """
2092
+ function parseAssignObj() {
2093
+ const l = loc();
2094
+ // Rest: {...x}
2095
+ if (token === '...') return parseObjRestValue();
2096
+ // Parse the key — try ObjAssignable first (broader), fall back to SimpleObjAssignable
2097
+ const key = parseObjAssignable();
2098
+ // Property: key: value
2099
+ if (token === ':') {
2100
+ advance();
2101
+ let value;
2102
+ if (token === 'INDENT') { advance(); value = parseExpression(0); expect('OUTDENT'); }
2103
+ else { value = parseExpression(0); }
2104
+ return [key, value, ":"];
2105
+ }
2106
+ // Default: key = value (only valid for SimpleObjAssignable keys)
2107
+ if (token === '=') {
2108
+ advance();
2109
+ let value;
2110
+ if (token === 'INDENT') { advance(); value = parseExpression(0); expect('OUTDENT'); }
2111
+ else { value = parseExpression(0); }
2112
+ return [key, value, "="];
2113
+ }
2114
+ // Shorthand: {x}
2115
+ return [key, key, null];
2116
+ }
2117
+ """
2118
+
2119
+ Generator::_generateRDObject = ->
2120
+ """
2121
+ function parseObject() {
2122
+ const l = loc();
2123
+ expect('{');
2124
+ // Empty object
2125
+ if (token === '}') {
2126
+ advance();
2127
+ return withLoc(["object"], l);
2128
+ }
2129
+ // Parse assign list (handles key: value pairs)
2130
+ const list = parseAssignList();
2131
+ match(',');
2132
+ // Check for comprehension (FOR after the first key:value)
2133
+ if (token === 'FOR') {
2134
+ // Object comprehension: {k: v for ...}
2135
+ // list should have [key, value, ":"] as first entry
2136
+ const first = list[0];
2137
+ const key = first ? first[0] : list[0];
2138
+ const val = first ? first[1] : list[1];
2139
+ advance(); // consume FOR
2140
+ if (token === 'OWN') {
2141
+ advance();
2142
+ const vars = parseForVariables();
2143
+ expect('FOROF');
2144
+ const obj = parseExpression(0);
2145
+ let guard = null;
2146
+ if (token === 'WHEN') { advance(); guard = parseExpression(0); }
2147
+ match(',');
2148
+ expect('}');
2149
+ return withLoc(["object-comprehension", key, val, [["for-of", vars, obj, true]], guard ? [guard] : []], l);
2150
+ }
2151
+ const vars = parseForVariables();
2152
+ if (token === 'FOROF') {
2153
+ advance();
2154
+ const obj = parseExpression(0);
2155
+ let guard = null;
2156
+ if (token === 'WHEN') { advance(); guard = parseExpression(0); }
2157
+ match(',');
2158
+ expect('}');
2159
+ return withLoc(["object-comprehension", key, val, [["for-of", vars, obj, false]], guard ? [guard] : []], l);
2160
+ }
2161
+ throw new Error('Parse error in Object comprehension: unexpected ' + token);
2162
+ }
2163
+ expect('}');
2164
+ return withLoc(["object", ...list], l);
2165
+ }
2166
+ """
2167
+
2168
+ # --- Unary prefix parser (GENERATED from grammar rules) ---
2169
+ Generator::_generateRDUnaryGeneric = ->
2170
+ lines = []
2171
+ lines.push "function parseUnary() {"
2172
+ lines.push " const l = loc();"
2173
+
2174
+ # Prefix operators (from Operation rules)
2175
+ lines.push " // Prefix operators (derived from Operation rules)"
2176
+ seen = new Set
2177
+ for {token: tok, bp, rule} in @_prefixOps
2178
+ continue if seen.has tok
2179
+ seen.add tok
2180
+ syms = rule.symbols
2181
+ isCategoryToken = tok is tok.toUpperCase() and tok.length > 2
2182
+ lines.push " if (token === #{JSON.stringify tok}) {"
2183
+ if isCategoryToken
2184
+ lines.push " const _op = tokenText; advance();"
2185
+ else
2186
+ lines.push " advance();"
2187
+ rightNT = syms[1]
2188
+ bpStr = "#{bp * 2}" # Use the rule's actual binding power (with prec override)
2189
+ # If the right side is Expression, pass binding power
2190
+ if rightNT is @_exprNT or rightNT is @_operationNT
2191
+ lines.push " const expr = parseExpression(#{bpStr});"
2192
+ else
2193
+ lines.push " const expr = parse#{rightNT}();"
2194
+ lines.push " const _vals_ = [#{if isCategoryToken then '_op' else JSON.stringify tok}, expr];"
2195
+ lines.push " const $ = _vals_, $0 = 1;"
2196
+ lines.push " const _r = ruleActions(#{rule.id}, _vals_, [], {});"
2197
+ lines.push " return _r != null ? withLoc(_r, l) : expr;"
2198
+ lines.push " }"
2199
+
2200
+ # Atom tokens (from Value/Literal chain)
2201
+ lines.push " // Atoms (derived from Value/Literal rules)"
2202
+ atomSeen = new Set
2203
+
2204
+ # Detect Range nonterminal for Range/Array speculation
2205
+ rangeNT = null
2206
+ if @_valueNT
2207
+ for rule in @types[@_valueNT].rules when rule.symbols.length is 1
2208
+ ntType = @types[rule.symbols[0]]
2209
+ if ntType?.rules.some((r) => r.symbols[0] is '[' and r.symbols.length >= 5)
2210
+ rangeNT = rule.symbols[0]
2211
+ break
2212
+
2213
+ # Range/Array ambiguity: both start with '['. Use speculation for Range first.
2214
+ if rangeNT
2215
+ atomSeen.add '['
2216
+ lines.push " if (token === '[') {"
2217
+ lines.push " const _range = speculate(() => parse#{rangeNT}());"
2218
+ lines.push " if (_range !== null) return _range;"
2219
+ lines.push " return parseArray();"
2220
+ lines.push " }"
2221
+
2222
+ # Handle tokens that appear in multiple Value alternatives with lookahead
2223
+ # @ → This (bare @) or ThisProperty (@ Property) — check if PROPERTY follows
2224
+ lines.push " if (token === '@') {"
2225
+ lines.push " const _saved = mark(); advance();"
2226
+ lines.push " if (token === 'PROPERTY') { reset(_saved); return parseThisProperty(); }"
2227
+ lines.push " reset(_saved); return parseThis();"
2228
+ lines.push " }"
2229
+ atomSeen.add '@'
2230
+ # SUPER → Super (SUPER . Property | SUPER INDEX_START) or Invocation (SUPER Arguments)
2231
+ lines.push " if (token === 'SUPER') {"
2232
+ lines.push " const _saved = mark(); advance();"
2233
+ lines.push " if (token === '.' || token === 'INDEX_START') { reset(_saved); return parseSuper(); }"
2234
+ lines.push " reset(_saved); return parseInvocation();"
2235
+ lines.push " }"
2236
+ atomSeen.add 'SUPER'
2237
+
2238
+ for {token: tok, rule, nonterminal} in @_atomTokens
2239
+ continue if atomSeen.has tok
2240
+ atomSeen.add tok
2241
+ if rule.symbols.length is 1 and not @types[rule.symbols[0]]
2242
+ lines.push " if (token === #{JSON.stringify tok}) {"
2243
+ lines.push " const _vals_ = [tokenText]; advance();"
2244
+ lines.push " const $ = _vals_, $0 = 0;"
2245
+ lines.push " const _r = ruleActions(#{rule.id}, _vals_, [], {});"
2246
+ lines.push " return _r != null ? _r : _vals_[0];"
2247
+ lines.push " }"
2248
+ else if nonterminal
2249
+ lines.push " if (token === #{JSON.stringify tok}) return parse#{nonterminal}();"
2250
+
2251
+ # Structural atoms — any Value alternative not yet covered
2252
+ if @_valueNT
2253
+ for rule in @types[@_valueNT].rules when rule.symbols.length is 1
2254
+ ntName = rule.symbols[0]
2255
+ continue unless @types[ntName]
2256
+ firsts = @firsts[ntName]
2257
+ continue unless firsts
2258
+ firsts.forEach (tok) =>
2259
+ unless atomSeen.has tok
2260
+ atomSeen.add tok
2261
+ lines.push " if (token === #{JSON.stringify tok}) return parse#{ntName}();"
2262
+ # Code (arrow functions) as atoms
2263
+ if @_codeNT and @firsts[@_codeNT]
2264
+ @firsts[@_codeNT].forEach (tok) =>
2265
+ unless atomSeen.has tok
2266
+ atomSeen.add tok
2267
+ lines.push " if (token === #{JSON.stringify tok}) return parse#{@_codeNT}();"
2268
+
2269
+ lines.push " if (token === '...') return parseSplat();" unless atomSeen.has '...'
2270
+ # Statement tokens as atoms — allows 'break if done', 'return x unless err' patterns
2271
+ lines.push " if (token === 'STATEMENT') { const v = tokenText; advance(); return v; }"
2272
+ lines.push " if (token === 'RETURN') return parseReturn();"
2273
+ # Fire-and-forget effect (~> expr) — prefix form without left-hand side
2274
+ lines.push " if (token === 'REACT_ASSIGN') return parseReactAssign();"
2275
+ lines.push " throw new Error('Parse error: unexpected token ' + token + ' at line ' + ((tokenLoc && tokenLoc.r || 0) + 1));"
2276
+ lines.push "}"
2277
+ lines.join '\n'
2278
+
2279
+ # --- Postfix for (comprehensions) — kept as template, dispatched from grammar ---
2280
+ Generator::_generateRDPostfixForGeneric = ->
2281
+ # Check if any Expression alternative has postfix comprehension rules
2282
+ # (rules where Expression appears first, indicating postfix forms)
2283
+ return '' unless @_exprNT
2284
+ hasPostfix = false
2285
+ for rule in @types[@_exprNT].rules when rule.symbols.length is 1
2286
+ altType = @types[rule.symbols[0]]
2287
+ continue unless altType
2288
+ if altType.rules.some((r) => r.symbols[0] is @_exprNT and r.symbols.length >= 3)
2289
+ hasPostfix = true
2290
+ break
2291
+ return '' unless hasPostfix
2292
+
2293
+ # Generate postfix for by examining the postfix For rules
2294
+ # These follow patterns: Expression FOR ForVariables FORIN/FOROF/FORAS Expression ...
2295
+ """
2296
+ function parsePostfixFor(expr, l) {
2297
+ advance(); // consume FOR
2298
+ if (token === 'AWAIT') {
2299
+ advance();
2300
+ const vars = parseForVariables();
2301
+ if (token === 'FORAS') {
2302
+ advance();
2303
+ const iter = parseExpression(0);
2304
+ const guard = token === 'WHEN' ? (advance(), parseExpression(0)) : null;
2305
+ return withLoc(["comprehension", expr, [["for-as", vars, iter, true, null]], guard ? [guard] : []], l);
2306
+ }
2307
+ }
2308
+ if (token === 'OWN') {
2309
+ advance();
2310
+ const vars = parseForVariables();
2311
+ expect('FOROF');
2312
+ const obj = parseExpression(0);
2313
+ const guard = token === 'WHEN' ? (advance(), parseExpression(0)) : null;
2314
+ return withLoc(["comprehension", expr, [["for-of", vars, obj, true]], guard ? [guard] : []], l);
2315
+ }
2316
+ if (token === '[') {
2317
+ const range = speculate(() => parseRange());
2318
+ if (range !== null) {
2319
+ if (token === 'BY') { advance(); const step = parseExpression(0); return withLoc(["comprehension", expr, [["for-in", [], range, step]], []], l); }
2320
+ return withLoc(["comprehension", expr, [["for-in", [], range, null]], []], l);
2321
+ }
2322
+ }
2323
+ const vars = parseForVariables();
2324
+ if (token === 'FORIN') {
2325
+ advance(); const arr = parseExpression(0);
2326
+ let step = null, guard = null;
2327
+ if (token === 'WHEN') { advance(); guard = parseExpression(0); }
2328
+ if (token === 'BY') { advance(); step = parseExpression(0); }
2329
+ if (!guard && token === 'WHEN') { advance(); guard = parseExpression(0); }
2330
+ return withLoc(["comprehension", expr, [["for-in", vars, arr, step]], guard ? [guard] : []], l);
2331
+ }
2332
+ if (token === 'FOROF') {
2333
+ advance(); const obj = parseExpression(0);
2334
+ const guard = token === 'WHEN' ? (advance(), parseExpression(0)) : null;
2335
+ return withLoc(["comprehension", expr, [["for-of", vars, obj, false]], guard ? [guard] : []], l);
2336
+ }
2337
+ if (token === 'FORAS' || token === 'FORASAWAIT') {
2338
+ const isAsync = token === 'FORASAWAIT'; advance();
2339
+ const iter = parseExpression(0);
2340
+ const guard = token === 'WHEN' ? (advance(), parseExpression(0)) : null;
2341
+ return withLoc(["comprehension", expr, [["for-as", vars, iter, isAsync, null]], guard ? [guard] : []], l);
2342
+ }
2343
+ throw new Error('Parse error in postfix for: unexpected ' + token);
2344
+ }
2345
+ """
2346
+
2347
+ # --- ExpressionLine parser (generic) ---
2348
+ Generator::_generateRDExpressionLine = ->
2349
+ return '' unless @_exprLineNT
2350
+ type = @types[@_exprLineNT]
2351
+ return '' unless type
2352
+ # Generate as a choice dispatch
2353
+ @_generateRDChoice @_exprLineNT
2354
+
2355
+ # --- Operation parser (just delegates to expression with BP) ---
2356
+ Generator::_generateRDOperation = ->
2357
+ """
2358
+ function parseOperation() {
2359
+ return parseExpression(0);
2360
+ }
2361
+ """
2362
+
2363
+ # --- OperationLine parser (generic) ---
2364
+ Generator::_generateRDOperationLine = ->
2365
+ return '' unless @_operationLineNT
2366
+ # Generate using the generic keyword/sequence generator
2367
+ @_generateRDGeneric @_operationLineNT
2368
+
2369
+ # --- Generic fallback nonterminal parser ---
2370
+ Generator::_generateRDGeneric = (name) ->
2371
+ fnName = "parse#{name}"
2372
+ type = @types[name]
2373
+ rules = type.rules
2374
+
2375
+ return @_generateRDToken name if rules.length is 1 and rules[0].symbols.length is 1 and not @types[rules[0].symbols[0]]
2376
+ return @_generateRDChoice name if rules.every (r) -> r.symbols.length is 1
2377
+
2378
+ lines = []
2379
+ lines.push "function #{fnName}() {"
2380
+ lines.push " const l = loc();"
2381
+
2382
+ if rules.length is 1
2383
+ lines.push @_generateRDRuleBody rules[0], ' '
2384
+ else
2385
+ # Try to dispatch on first token
2386
+ firstKey = true
2387
+ defaultRule = null
2388
+ for rule in rules
2389
+ first = rule.symbols[0]
2390
+ if first is '' or (first and @types[first])
2391
+ defaultRule ?= rule
2392
+ continue
2393
+ prefix = if firstKey then "if" else "else if"
2394
+ firstKey = false
2395
+ lines.push " #{prefix} (token === #{JSON.stringify first}) {"
2396
+ lines.push @_generateRDRuleBody rule, ' '
2397
+ lines.push " }"
2398
+
2399
+ if defaultRule
2400
+ if firstKey
2401
+ lines.push @_generateRDRuleBody defaultRule, ' '
2402
+ else
2403
+ lines.push " else {"
2404
+ lines.push @_generateRDRuleBody defaultRule, ' '
2405
+ lines.push " }"
2406
+ else unless firstKey
2407
+ lines.push " else {"
2408
+ lines.push " throw new Error('Parse error in #{name}: unexpected ' + token);"
2409
+ lines.push " }"
2410
+
2411
+ lines.push "}"
2412
+ lines.join '\n'