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.
- package/CHANGELOG.md +111 -0
- package/README.md +42 -34
- package/docs/RIP-INTERNALS.md +2 -4
- package/docs/RIP-LANG.md +150 -3
- package/docs/RIP-TYPES.md +1 -2
- package/docs/demo.html +342 -0
- package/docs/dist/rip-ui.min.js +516 -0
- package/docs/dist/rip-ui.min.js.br +0 -0
- package/docs/dist/rip.browser.js +379 -461
- package/docs/dist/rip.browser.min.js +204 -220
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/docs/dist/ui.js +956 -0
- package/docs/dist/ui.min.js +2 -0
- package/docs/dist/ui.min.js.br +0 -0
- package/docs/dist/ui.rip +957 -0
- package/docs/dist/ui.rip.br +0 -0
- package/docs/examples.rip +180 -0
- package/docs/index.html +3 -1599
- package/docs/playground-app.html +1022 -0
- package/docs/playground-js.html +1645 -0
- package/docs/playground-rip-ui.html +1419 -0
- package/docs/playground-rip.html +1450 -0
- package/docs/rip-fav.svg +5 -0
- package/package.json +3 -3
- package/scripts/serve.js +3 -2
- package/src/browser.js +38 -16
- package/src/compiler.js +165 -226
- package/src/components.js +153 -140
- package/src/grammar/README.md +234 -0
- package/src/grammar/lunar.rip +2412 -0
- package/src/grammar/solar.rip +18 -4
- package/src/lexer.js +82 -30
- package/src/parser-rd.js +3242 -0
- package/src/parser.js +6 -5
- package/src/repl.js +24 -5
- package/docs/NOTES.md +0 -93
- package/docs/RIP-GUIDE.md +0 -698
- package/docs/RIP-REACTIVITY.md +0 -311
|
@@ -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'
|