grammatik 0.1.0

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/src/codegen.js ADDED
@@ -0,0 +1,635 @@
1
+ // @ts-check
2
+ 'use strict'
3
+
4
+ import { FAIL, expect } from './state.js'
5
+ import { buildDispatch, opOf, recognizeSeqIdiom } from './analyze.js'
6
+
7
+ /**
8
+ * A real source generator for grammars.
9
+ *
10
+ * The planner removes per-token *decisions*, but it still pays one indirect
11
+ * call per grammar node, through call sites that see many closure shapes and so
12
+ * cannot be inlined. That is the floor for a closure tree, and it is where the
13
+ * remaining gap against hand-written parsers lives.
14
+ *
15
+ * This module emits one JavaScript function per rule with every node inlined as
16
+ * straight-line statements — the shape a hand-written recursive-descent parser
17
+ * has. `alt` becomes a real `switch` on the token type, which V8 compiles to a
18
+ * jump table rather than a Map lookup. Only rule boundaries remain calls.
19
+ *
20
+ * Anything it does not know how to emit falls back to the planner's closure via
21
+ * the helper table, so adding an operator here is optional and incremental.
22
+ */
23
+
24
+ /**
25
+ * @param {{ planner: any, rules: Record<string, any>, firsts: Map<string, any>|null,
26
+ * memo: boolean, maxSteps: number }} config
27
+ */
28
+ export function createCodegen (config) {
29
+ const { planner, rules, firsts, memo, maxSteps } = config
30
+
31
+ /** Runtime values the generated code closes over. */
32
+ const HELPERS = []
33
+ /** What each helper was built from; parallel to HELPERS. */
34
+ const ORIGINS = []
35
+ /** Generated rule functions, indexed; filled in as they are built. */
36
+ const RULES = []
37
+ /** The source each rule was built from, parallel to RULES. */
38
+ const SOURCES = []
39
+ const ruleIndex = new Map()
40
+ const pending = []
41
+
42
+ let counter = 0
43
+ const nextId = () => '_' + (counter++)
44
+
45
+ /**
46
+ * Registers a runtime value and returns its accessor source.
47
+ *
48
+ * `origin` records what the value *is*, so `emitModule` can write an
49
+ * equivalent table into a generated file. A closure cannot be serialized; the
50
+ * JSON Logic it was compiled from can.
51
+ */
52
+ function helper (value, origin) {
53
+ HELPERS.push(value)
54
+ ORIGINS.push(origin)
55
+ return `H[${HELPERS.length - 1}]`
56
+ }
57
+
58
+ /** A semantic action, recorded as the logic it came from. */
59
+ const actionHelper = (logic) => helper(planner.planAction(logic), { kind: 'action', logic })
60
+
61
+ /** A literal value sitting in parser position. */
62
+ const constantHelper = (node) => helper(planner.plan(node), { kind: 'constant', node })
63
+
64
+ function indexOfRule (name) {
65
+ let index = ruleIndex.get(name)
66
+ if (index !== undefined) return index
67
+ index = RULES.length
68
+ ruleIndex.set(name, index)
69
+ RULES.push(null)
70
+ pending.push(name)
71
+ return index
72
+ }
73
+
74
+ const guard = maxSteps > 0
75
+ const tickSrc = guard ? `if (++c.steps > ${maxSteps}) T();` : ''
76
+
77
+ /**
78
+ * Emits statements computing `node` into a fresh variable.
79
+ * @param {*} node
80
+ * @param {string[]} out Statement buffer.
81
+ * @param {string|null} bindingsVar Current sequence's bindings variable, if any.
82
+ * @returns {string} Name of the variable holding the result.
83
+ */
84
+ function gen (node, out, bindingsVar) {
85
+ const op = opOf(node)
86
+ const result = nextId()
87
+
88
+ if (op === null) {
89
+ out.push(`const ${result} = ${constantHelper(node)}(c);`)
90
+ return result
91
+ }
92
+
93
+ const arg = node[op]
94
+
95
+ switch (op) {
96
+ case 'consume': {
97
+ const name = typeof arg === 'string' ? arg : arg[0]
98
+ const token = nextId()
99
+ out.push(
100
+ `let ${result};`,
101
+ `const ${token} = c.tokens[c.idx];`,
102
+ `if (${token} !== undefined && ${token}.type === ${JSON.stringify(name)}) { c.idx++; ${result} = ${token}; }`,
103
+ `else { E(c, ${JSON.stringify(name)}); ${result} = F; }`
104
+ )
105
+ return result
106
+ }
107
+
108
+ case 'epsilon':
109
+ out.push(`const ${result} = null;`)
110
+ return result
111
+
112
+ case 'label': {
113
+ const value = gen(arg[1], out, bindingsVar)
114
+ if (bindingsVar) out.push(`if (${value} !== F) ${bindingsVar}[${JSON.stringify(arg[0])}] = ${value};`)
115
+ else out.push(`if (${value} !== F && c.bindings) c.bindings[${JSON.stringify(arg[0])}] = ${value};`)
116
+ out.push(`const ${result} = ${value};`)
117
+ return result
118
+ }
119
+
120
+ case 'action': {
121
+ const fn = actionHelper(arg)
122
+ out.push(`const ${result} = ${fn}(${bindingsVar || 'c.bindings || {}'}, c);`)
123
+ return result
124
+ }
125
+
126
+ case 'seq':
127
+ return genSeq(arg, out, result)
128
+
129
+ case 'alt':
130
+ return genAlt(arg, out, result, bindingsVar)
131
+
132
+ case 'option': {
133
+ const start = nextId()
134
+ out.push(`let ${result};`, `const ${start} = c.idx;`, '{')
135
+ const value = gen(arg, out, bindingsVar)
136
+ out.push(
137
+ `if (${value} === F) { c.idx = ${start}; ${result} = null; } else ${result} = ${value};`,
138
+ '}'
139
+ )
140
+ return result
141
+ }
142
+
143
+ case 'many':
144
+ case 'many1': {
145
+ const atLeastOne = op === 'many1'
146
+ const before = nextId()
147
+ const label = 'L' + nextId()
148
+ out.push(tickSrc, `let ${result} = [];`, `${label}: {`)
149
+
150
+ if (atLeastOne) {
151
+ const start = nextId()
152
+ out.push(`const ${start} = c.idx;`)
153
+ const first = gen(arg, out, bindingsVar)
154
+ out.push(
155
+ `if (${first} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`,
156
+ `if (c.idx === ${start}) P(${JSON.stringify(op)}, ${start});`,
157
+ `${result}.push(${first});`
158
+ )
159
+ }
160
+
161
+ out.push('for (;;) {', `const ${before} = c.idx;`)
162
+ const value = gen(arg, out, bindingsVar)
163
+ out.push(
164
+ `if (${value} === F) { c.idx = ${before}; break; }`,
165
+ `if (c.idx === ${before}) P(${JSON.stringify(op)}, ${before});`,
166
+ `${result}.push(${value});`,
167
+ '}',
168
+ '}'
169
+ )
170
+ return result
171
+ }
172
+
173
+ case 'infixLeft': {
174
+ // One precedence level, inlined: operand, then a loop that only runs
175
+ // when an operator actually follows. Nothing is allocated for the
176
+ // common case of a bare operand.
177
+ const label = 'L' + nextId()
178
+ const left = nextId()
179
+ const bindings = nextId()
180
+ const mark = nextId()
181
+ const combine = actionHelper(arg.combine)
182
+
183
+ out.push(tickSrc, `let ${result};`, `${label}: {`)
184
+ const first = gen(arg.operand, out, bindingsVar)
185
+ out.push(
186
+ `if (${first} === F) { ${result} = F; break ${label}; }`,
187
+ `let ${left} = ${first};`,
188
+ `let ${bindings} = null;`,
189
+ 'for (;;) {',
190
+ `const ${mark} = c.idx;`
191
+ )
192
+
193
+ let operatorVar = 'null'
194
+ if (arg.operator !== undefined) {
195
+ operatorVar = gen(arg.operator, out, bindingsVar)
196
+ out.push(`if (${operatorVar} === F) { c.idx = ${mark}; break; }`)
197
+ }
198
+
199
+ const right = gen(arg.operand, out, bindingsVar)
200
+ out.push(
201
+ `if (${right} === F) { c.idx = ${mark}; break; }`,
202
+ `if (${bindings} === null) ${bindings} = { left: null, op: null, right: null };`,
203
+ `${bindings}.left = ${left}; ${bindings}.op = ${operatorVar}; ${bindings}.right = ${right};`,
204
+ `${left} = ${combine}(${bindings}, c);`,
205
+ '}',
206
+ `${result} = ${left};`,
207
+ '}'
208
+ )
209
+ return result
210
+ }
211
+
212
+ case 'oneOf': {
213
+ const names = Array.isArray(arg) ? arg : [arg]
214
+ const token = nextId()
215
+ const test = names.map((n) => `${token}.type === ${JSON.stringify(n)}`).join(' || ')
216
+ out.push(
217
+ `let ${result};`,
218
+ `const ${token} = c.tokens[c.idx];`,
219
+ `if (${token} !== undefined && (${test})) { c.idx++; ${result} = ${token}; }`,
220
+ `else { ${names.map((n) => `E(c, ${JSON.stringify(n)});`).join(' ')} ${result} = F; }`
221
+ )
222
+ return result
223
+ }
224
+
225
+ case 'eof':
226
+ out.push(
227
+ `let ${result};`,
228
+ `if (c.idx >= c.tokens.length) ${result} = null;`,
229
+ `else { E(c, "end of input"); ${result} = F; }`
230
+ )
231
+ return result
232
+
233
+ case 'infixRight': {
234
+ const label = 'L' + nextId()
235
+ const left = nextId()
236
+ const items = nextId()
237
+ const mark = nextId()
238
+ const bindings = nextId()
239
+ const value = nextId()
240
+ const combine = actionHelper(arg.combine)
241
+
242
+ out.push(tickSrc, `let ${result};`, `${label}: {`)
243
+ const first = gen(arg.operand, out, bindingsVar)
244
+ out.push(
245
+ `if (${first} === F) { ${result} = F; break ${label}; }`,
246
+ `const ${left} = ${first};`,
247
+ `let ${items} = null;`,
248
+ 'for (;;) {',
249
+ `const ${mark} = c.idx;`
250
+ )
251
+ let operatorVar = 'null'
252
+ if (arg.operator !== undefined) {
253
+ operatorVar = gen(arg.operator, out, bindingsVar)
254
+ out.push(`if (${operatorVar} === F) { c.idx = ${mark}; break; }`)
255
+ }
256
+ const right = gen(arg.operand, out, bindingsVar)
257
+ out.push(
258
+ `if (${right} === F) { c.idx = ${mark}; break; }`,
259
+ `if (${items} === null) ${items} = [];`,
260
+ `${items}.push(${operatorVar}, ${right});`,
261
+ '}',
262
+ `if (${items} === null) { ${result} = ${left}; break ${label}; }`,
263
+ `const ${bindings} = { left: null, op: null, right: null };`,
264
+ `let ${value} = ${items}[${items}.length - 1];`,
265
+ `for (let i = ${items}.length - 2; i >= 0; i -= 2) {`,
266
+ `${bindings}.left = i === 0 ? ${left} : ${items}[i - 1];`,
267
+ `${bindings}.op = ${items}[i]; ${bindings}.right = ${value};`,
268
+ `${value} = ${combine}(${bindings}, c);`,
269
+ '}',
270
+ `${result} = ${value};`,
271
+ '}'
272
+ )
273
+ return result
274
+ }
275
+
276
+ case 'lookahead': {
277
+ const start = nextId()
278
+ out.push(`const ${start} = c.idx;`)
279
+ const value = gen(arg, out, bindingsVar)
280
+ out.push(`c.idx = ${start};`, `const ${result} = ${value};`)
281
+ return result
282
+ }
283
+
284
+ case 'negLookahead': {
285
+ const start = nextId()
286
+ out.push(`const ${start} = c.idx;`)
287
+ const value = gen(arg, out, bindingsVar)
288
+ out.push(`c.idx = ${start};`, `const ${result} = ${value} === F ? null : F;`)
289
+ return result
290
+ }
291
+
292
+ case 'text': {
293
+ const label = 'L' + nextId()
294
+ const start = nextId()
295
+ const first = nextId()
296
+ const last = nextId()
297
+ out.push(`let ${result};`, `${label}: {`, `const ${start} = c.idx;`)
298
+ const value = gen(arg, out, bindingsVar)
299
+ out.push(
300
+ `if (${value} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`,
301
+ `if (c.idx === ${start}) { ${result} = ''; break ${label}; }`,
302
+ `const ${first} = c.tokens[${start}];`,
303
+ `const ${last} = c.tokens[c.idx - 1];`,
304
+ `if (c.source !== undefined) { ${result} = c.source.slice(${first}.start, ${last}.end); break ${label}; }`,
305
+ `let ${result}_s = '';`,
306
+ `for (let i = ${start}; i < c.idx; i++) ${result}_s += c.tokens[i].image;`,
307
+ `${result} = ${result}_s;`,
308
+ '}'
309
+ )
310
+ return result
311
+ }
312
+
313
+ case 'as': {
314
+ const transform = actionHelper(Array.isArray(arg) ? arg[1] : arg.to)
315
+ const value = gen(Array.isArray(arg) ? arg[0] : arg.parser, out, bindingsVar)
316
+ out.push(`const ${result} = ${value} === F ? F : ${transform}(${value}, c);`)
317
+ return result
318
+ }
319
+
320
+ case 'between': {
321
+ const label = 'L' + nextId()
322
+ const start = nextId()
323
+ out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${start} = c.idx;`)
324
+ const open = gen(arg.open, out, bindingsVar)
325
+ out.push(`if (${open} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`)
326
+ const body = gen(arg.body, out, bindingsVar)
327
+ out.push(`if (${body} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`)
328
+ const close = gen(arg.close, out, bindingsVar)
329
+ out.push(
330
+ `if (${close} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`,
331
+ `${result} = ${body};`,
332
+ '}'
333
+ )
334
+ return result
335
+ }
336
+
337
+ case 'prefix': {
338
+ const label = 'L' + nextId()
339
+ const start = nextId()
340
+ const ops = nextId()
341
+ const mark = nextId()
342
+ const bindings = nextId()
343
+ const value = nextId()
344
+ const combine = actionHelper(arg.combine)
345
+
346
+ out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${start} = c.idx;`, `let ${ops} = null;`, 'for (;;) {', `const ${mark} = c.idx;`)
347
+ const op = gen(arg.operator, out, bindingsVar)
348
+ out.push(
349
+ `if (${op} === F) { c.idx = ${mark}; break; }`,
350
+ `if (${ops} === null) ${ops} = [];`,
351
+ `${ops}.push(${op});`,
352
+ '}'
353
+ )
354
+ const operand = gen(arg.operand, out, bindingsVar)
355
+ out.push(
356
+ `if (${operand} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`,
357
+ `let ${value} = ${operand};`,
358
+ `if (${ops} !== null) {`,
359
+ `const ${bindings} = { op: null, operand: null };`,
360
+ `for (let i = ${ops}.length - 1; i >= 0; i--) { ${bindings}.op = ${ops}[i]; ${bindings}.operand = ${value}; ${value} = ${combine}(${bindings}, c); }`,
361
+ '}',
362
+ `${result} = ${value};`,
363
+ '}'
364
+ )
365
+ return result
366
+ }
367
+
368
+ case 'postfix': {
369
+ const label = 'L' + nextId()
370
+ const left = nextId()
371
+ const bindings = nextId()
372
+ const mark = nextId()
373
+ const combine = actionHelper(arg.combine)
374
+
375
+ out.push(tickSrc, `let ${result};`, `${label}: {`)
376
+ const first = gen(arg.operand, out, bindingsVar)
377
+ out.push(
378
+ `if (${first} === F) { ${result} = F; break ${label}; }`,
379
+ `let ${left} = ${first};`,
380
+ `let ${bindings} = null;`,
381
+ 'for (;;) {',
382
+ `const ${mark} = c.idx;`
383
+ )
384
+ const found = gen(arg.suffix, out, bindingsVar)
385
+ out.push(
386
+ `if (${found} === F) { c.idx = ${mark}; break; }`,
387
+ `if (${bindings} === null) ${bindings} = { left: null, suffix: null };`,
388
+ `${bindings}.left = ${left}; ${bindings}.suffix = ${found};`,
389
+ `${left} = ${combine}(${bindings}, c);`,
390
+ '}',
391
+ `${result} = ${left};`,
392
+ '}'
393
+ )
394
+ return result
395
+ }
396
+
397
+ case 'manySep':
398
+ case 'many1Sep': {
399
+ if (!arg || typeof arg !== 'object') throw new Error("'manySep' expects { rule, sep }")
400
+ const atLeastOne = op === 'many1Sep'
401
+ const sepNode = typeof arg.sep === 'string' ? { consume: arg.sep } : arg.sep
402
+ const trailing = arg.trailing === true
403
+ const start = nextId()
404
+ const beforeSep = nextId()
405
+ const beforeItem = nextId()
406
+ const label = 'L' + nextId()
407
+
408
+ out.push(tickSrc, `let ${result} = [];`, `${label}: {`, `const ${start} = c.idx;`)
409
+ const first = gen(arg.rule, out, bindingsVar)
410
+ out.push(
411
+ `if (${first} === F) { c.idx = ${start}; ${result} = ${atLeastOne ? 'F' : '[]'}; break ${label}; }`,
412
+ `${result}.push(${first});`,
413
+ 'for (;;) {',
414
+ `const ${beforeSep} = c.idx;`
415
+ )
416
+ const sep = gen(sepNode, out, bindingsVar)
417
+ out.push(`if (${sep} === F) { c.idx = ${beforeSep}; break; }`, `const ${beforeItem} = c.idx;`)
418
+ const item = gen(arg.rule, out, bindingsVar)
419
+ out.push(
420
+ `if (${item} === F) { c.idx = ${trailing ? beforeItem : beforeSep}; break; }`,
421
+ `${result}.push(${item});`,
422
+ '}',
423
+ '}'
424
+ )
425
+ return result
426
+ }
427
+
428
+ case 'subrule': {
429
+ const name = typeof arg === 'string' ? arg : arg[0]
430
+ if (!(name in rules)) {
431
+ // Preserve the runtime error rather than failing to generate.
432
+ out.push(`const ${result} = ${helper(planner.plan(node), { kind: 'unknownRule', name })}(c);`)
433
+ return result
434
+ }
435
+ const index = indexOfRule(name)
436
+ const parent = nextId()
437
+ out.push(tickSrc)
438
+ if (memo) {
439
+ const key = nextId()
440
+ const hit = nextId()
441
+ const start = nextId()
442
+ out.push(
443
+ `let ${result};`,
444
+ `const ${key} = ${JSON.stringify(name + ':')} + c.idx;`,
445
+ `const ${hit} = c.memo.get(${key});`,
446
+ `if (${hit} !== undefined) { c.idx = ${hit}.end; ${result} = ${hit}.value; }`,
447
+ 'else {',
448
+ `const ${start} = c.idx;`,
449
+ `const ${parent} = c.ruleDepth;`,
450
+ `if (c.track) { c.ruleNames[${parent}] = ${JSON.stringify(name)}; c.ruleDepth = ${parent} + 1; }`,
451
+ `${result} = R[${index}](c);`,
452
+ `if (c.track) c.ruleDepth = ${parent};`,
453
+ `if (${result} === F) c.idx = ${start};`,
454
+ `c.memo.set(${key}, { value: ${result}, end: c.idx });`,
455
+ '}'
456
+ )
457
+ } else {
458
+ out.push(
459
+ `const ${parent} = c.ruleDepth;`,
460
+ `if (c.track) { c.ruleNames[${parent}] = ${JSON.stringify(name)}; c.ruleDepth = ${parent} + 1; }`,
461
+ `const ${result} = R[${index}](c);`,
462
+ `if (c.track) c.ruleDepth = ${parent};`
463
+ )
464
+ }
465
+ return result
466
+ }
467
+
468
+ default:
469
+ // Not generated: defer to the planner's closure.
470
+ out.push(`const ${result} = ${helper(planner.plan(node))}(c);`)
471
+ return result
472
+ }
473
+ }
474
+
475
+ function genSeq (children, out, result) {
476
+ if (!Array.isArray(children)) throw new Error("'seq' expects an array of parsers")
477
+
478
+ // Same idiom recognition as the planner, so both paths agree.
479
+ const idiom = recognizeSeqIdiom(children)
480
+ if (idiom !== null) return gen(idiom, out, null)
481
+
482
+ let hasLabels = false
483
+ let hasAction = false
484
+ for (const child of children) {
485
+ const childOp = opOf(child)
486
+ if (childOp === 'label') hasLabels = true
487
+ else if (childOp === 'action') hasAction = true
488
+ }
489
+ const needsScope = hasLabels || hasAction
490
+
491
+ const start = nextId()
492
+ const label = 'L' + nextId()
493
+ const bindingsVar = needsScope ? nextId() : null
494
+ const saved = nextId()
495
+ const collected = []
496
+
497
+ out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${start} = c.idx;`)
498
+ if (needsScope) {
499
+ out.push(`const ${saved} = c.bindings;`, `const ${bindingsVar} = {};`, `c.bindings = ${bindingsVar};`)
500
+ }
501
+
502
+ let actionResult = null
503
+ for (const child of children) {
504
+ const childOp = opOf(child)
505
+ if (childOp === 'action') {
506
+ actionResult = gen(child, out, bindingsVar)
507
+ continue
508
+ }
509
+ const value = gen(child, out, bindingsVar)
510
+ out.push(
511
+ `if (${value} === F) { c.idx = ${start};` +
512
+ (needsScope ? ` c.bindings = ${saved};` : '') +
513
+ ` ${result} = F; break ${label}; }`
514
+ )
515
+ collected.push(value)
516
+ }
517
+
518
+ if (needsScope) out.push(`c.bindings = ${saved};`)
519
+ out.push(`${result} = ${hasAction ? actionResult : '[' + collected.join(', ') + ']'};`, '}')
520
+ return result
521
+ }
522
+
523
+ function genAlt (branches, out, result, bindingsVar) {
524
+ if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches")
525
+
526
+ const table = firsts ? buildDispatch(branches, firsts) : null
527
+ const label = 'L' + nextId()
528
+
529
+ if (table !== null) {
530
+ // A switch on the token type: V8 turns a dense string switch into a
531
+ // hash lookup with inlined bodies, and every branch is inlined here.
532
+ const expected = [...table.keys()]
533
+ const token = nextId()
534
+ out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${token} = c.tokens[c.idx];`)
535
+ out.push(`if (${token} === undefined) { ${expected.map((t) => `E(c, ${JSON.stringify(t)});`).join(' ')} ${result} = F; break ${label}; }`)
536
+ out.push(`switch (${token}.type) {`)
537
+
538
+ const byBranch = new Map()
539
+ for (const [tokenName, branch] of table) {
540
+ if (!byBranch.has(branch)) byBranch.set(branch, [])
541
+ byBranch.get(branch).push(tokenName)
542
+ }
543
+ for (const [branch, tokenNames] of byBranch) {
544
+ out.push(tokenNames.map((t) => `case ${JSON.stringify(t)}:`).join(' ') + ' {')
545
+ const value = gen(branch, out, bindingsVar)
546
+ out.push(`${result} = ${value}; break ${label};`, '}')
547
+ }
548
+ out.push(
549
+ `default: { ${expected.map((t) => `E(c, ${JSON.stringify(t)});`).join(' ')} ${result} = F; break ${label}; }`,
550
+ '}',
551
+ '}'
552
+ )
553
+ return result
554
+ }
555
+
556
+ // Ordered backtracking, still fully inlined.
557
+ const start = nextId()
558
+ out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${start} = c.idx;`)
559
+ for (const branch of branches) {
560
+ out.push('{')
561
+ const value = gen(branch, out, bindingsVar)
562
+ out.push(`if (${value} !== F) { ${result} = ${value}; break ${label}; }`, `c.idx = ${start};`, '}')
563
+ }
564
+ out.push(`${result} = F;`, '}')
565
+ return result
566
+ }
567
+
568
+ /** Generates and installs the function for one rule. */
569
+ function buildRule (name) {
570
+ const out = []
571
+ const value = gen(rules[name], out, null)
572
+ const body = `(c) => {\n${out.join('\n')}\nreturn ${value};\n}`
573
+ // Kept so `emitModule` can write out exactly what was built here, with the
574
+ // helper indices that go with it. Regenerating later would allocate fresh
575
+ // helpers and the indices would not line up.
576
+ SOURCES[ruleIndex.get(name)] = body
577
+ const source = `return ${body}`
578
+ // eslint-disable-next-line no-new-func
579
+ const factory = new Function('F', 'E', 'P', 'T', 'R', 'H', source)
580
+ return factory(FAIL, expect, progressError, stepsError, RULES, HELPERS)
581
+ }
582
+
583
+ function progressError (op, at) {
584
+ throw new Error(
585
+ `'${op}' matched without consuming any input at token index ${at}; ` +
586
+ 'this would loop forever. Make the repeated parser consume at least one token.'
587
+ )
588
+ }
589
+
590
+ function stepsError () {
591
+ throw new Error(`Parser exceeded maxSteps (${maxSteps}); the grammar is likely looping.`)
592
+ }
593
+
594
+ /**
595
+ * Compiles `start` and everything it reaches.
596
+ * @param {string} start
597
+ * @returns {(state: any) => any}
598
+ */
599
+ function compile (start) {
600
+ const index = indexOfRule(start)
601
+ while (pending.length) {
602
+ const name = pending.shift()
603
+ RULES[ruleIndex.get(name)] = buildRule(name)
604
+ }
605
+ return RULES[index]
606
+ }
607
+
608
+ /** The generated function for a rule, if it was reached from the start rule. */
609
+ function functionFor (name) {
610
+ const index = ruleIndex.get(name)
611
+ return index === undefined ? null : RULES[index]
612
+ }
613
+
614
+ /**
615
+ * Exposed for tests and debugging: the source we actually emit.
616
+ *
617
+ * Generating appends to the helper table and can queue rules, so the state is
618
+ * snapshotted and restored — inspecting a parser must not perturb it.
619
+ */
620
+ function sourceFor (name) {
621
+ const helperCount = HELPERS.length
622
+ const pendingCount = pending.length
623
+ try {
624
+ const out = []
625
+ const value = gen(rules[name], out, null)
626
+ return `(c) => {\n${out.join('\n')}\nreturn ${value};\n}`
627
+ } finally {
628
+ HELPERS.length = helperCount
629
+ ORIGINS.length = helperCount
630
+ pending.length = pendingCount
631
+ }
632
+ }
633
+
634
+ return { compile, functionFor, sourceFor, ruleIndex, RULES, SOURCES, ORIGINS, guard }
635
+ }