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/analyze.js ADDED
@@ -0,0 +1,602 @@
1
+ // @ts-check
2
+ 'use strict'
3
+
4
+ /**
5
+ * Static analysis of a grammar, in the spirit of Chevrotain's grammar recording.
6
+ *
7
+ * Two things come out of this:
8
+ *
9
+ * 1. FIRST sets, which let `alt` dispatch on the next token in O(1) instead of
10
+ * trying each branch and backtracking. This is the difference between an
11
+ * LL(1) parser and a PEG one, and it is most of the performance story.
12
+ * 2. Errors a grammar author actually wants at build time rather than at 3am:
13
+ * left recursion, references to rules or tokens that do not exist.
14
+ */
15
+
16
+ /**
17
+ * @typedef {object} First
18
+ * @property {Set<string>} tokens Token types that can begin this node.
19
+ * @property {boolean} nullable Whether the node can match while consuming nothing.
20
+ * @property {boolean} unknown Whether analysis could not fully determine the set.
21
+ * Any `unknown` disables LL(1) dispatch for the enclosing
22
+ * `alt`, which is a correctness guard, not a bug.
23
+ */
24
+
25
+ /** @returns {First} */
26
+ const emptyFirst = () => ({ tokens: new Set(), nullable: false, unknown: false })
27
+
28
+ /** @returns {First} A node that matches nothing and consumes nothing (an action). */
29
+ const epsilon = () => ({ tokens: new Set(), nullable: true, unknown: false })
30
+
31
+ /** @returns {First} A node whose behaviour we cannot see through. */
32
+ const opaque = () => ({ tokens: new Set(), nullable: true, unknown: true })
33
+
34
+ /**
35
+ * @param {First} target
36
+ * @param {First} source
37
+ * @returns {boolean} whether target changed
38
+ */
39
+ function absorb (target, source) {
40
+ let changed = false
41
+ for (const t of source.tokens) {
42
+ if (!target.tokens.has(t)) {
43
+ target.tokens.add(t)
44
+ changed = true
45
+ }
46
+ }
47
+ if (source.unknown && !target.unknown) {
48
+ target.unknown = true
49
+ changed = true
50
+ }
51
+ return changed
52
+ }
53
+
54
+ /** Returns the single operator key of a JSON Logic node, or null. */
55
+ export function opOf (node) {
56
+ if (!node || typeof node !== 'object' || Array.isArray(node)) return null
57
+ const keys = Object.keys(node)
58
+ return keys.length === 1 ? keys[0] : null
59
+ }
60
+
61
+ /** The set of node shapes this module understands as *parsers*. */
62
+ export const PARSER_OPS = new Set([
63
+ 'consume', 'seq', 'alt', 'many', 'many1', 'option',
64
+ 'manySep', 'many1Sep', 'infixLeft', 'infixRight', 'postfix', 'prefix',
65
+ 'between', 'as', 'oneOf', 'eof',
66
+ 'subrule', 'label', 'action',
67
+ 'lookahead', 'negLookahead', 'text', 'epsilon'
68
+ ])
69
+
70
+ /**
71
+ * Computes the FIRST set of a node given current per-rule estimates.
72
+ * @param {*} node
73
+ * @param {Map<string, First>} ruleFirsts
74
+ * @returns {First}
75
+ */
76
+ function firstOf (node, ruleFirsts) {
77
+ const op = opOf(node)
78
+ if (op === null) return opaque()
79
+ const arg = node[op]
80
+
81
+ switch (op) {
82
+ case 'consume': {
83
+ const result = emptyFirst()
84
+ const name = Array.isArray(arg) ? arg[0] : arg
85
+ if (typeof name === 'string') result.tokens.add(name)
86
+ else result.unknown = true
87
+ return result
88
+ }
89
+
90
+ case 'epsilon':
91
+ case 'action':
92
+ return epsilon()
93
+
94
+ case 'text':
95
+ case 'label':
96
+ // {label: [name, parser]} / {text: parser}
97
+ return firstOf(op === 'label' ? arg[1] : arg, ruleFirsts)
98
+
99
+ case 'seq': {
100
+ const result = epsilon()
101
+ if (!Array.isArray(arg)) return opaque()
102
+ for (const child of arg) {
103
+ const childFirst = firstOf(child, ruleFirsts)
104
+ absorb(result, childFirst)
105
+ if (!childFirst.nullable) {
106
+ result.nullable = false
107
+ break
108
+ }
109
+ }
110
+ return result
111
+ }
112
+
113
+ case 'alt': {
114
+ const result = emptyFirst()
115
+ if (!Array.isArray(arg)) return opaque()
116
+ for (const child of arg) {
117
+ const childFirst = firstOf(child, ruleFirsts)
118
+ absorb(result, childFirst)
119
+ if (childFirst.nullable) result.nullable = true
120
+ }
121
+ return result
122
+ }
123
+
124
+ case 'many':
125
+ case 'option': {
126
+ const inner = firstOf(arg, ruleFirsts)
127
+ return { tokens: inner.tokens, nullable: true, unknown: inner.unknown }
128
+ }
129
+
130
+ case 'many1': {
131
+ const inner = firstOf(arg, ruleFirsts)
132
+ return { tokens: inner.tokens, nullable: inner.nullable, unknown: inner.unknown }
133
+ }
134
+
135
+ case 'oneOf': {
136
+ const result = emptyFirst()
137
+ const names = Array.isArray(arg) ? arg : [arg]
138
+ for (const name of names) {
139
+ if (typeof name === 'string') result.tokens.add(name)
140
+ else result.unknown = true
141
+ }
142
+ return result
143
+ }
144
+
145
+ case 'eof':
146
+ // Zero-width and constraining, like a lookahead: never dispatch past it.
147
+ return opaque()
148
+
149
+ case 'as':
150
+ return firstOf(Array.isArray(arg) ? arg[0] : arg && arg.parser, ruleFirsts)
151
+
152
+ case 'between':
153
+ // Exactly a sequence of open, body, close.
154
+ return firstOf({ seq: [arg && arg.open, arg && arg.body, arg && arg.close] }, ruleFirsts)
155
+
156
+ case 'prefix': {
157
+ // Zero operators are allowed, so the operand's first set counts too.
158
+ const ops = firstOf(arg && arg.operator, ruleFirsts)
159
+ const inner = firstOf(arg && arg.operand, ruleFirsts)
160
+ const result = emptyFirst()
161
+ absorb(result, ops)
162
+ absorb(result, inner)
163
+ result.nullable = inner.nullable
164
+ return result
165
+ }
166
+
167
+ case 'postfix': {
168
+ // Begins and matches exactly where its operand does; suffixes are optional.
169
+ const inner = firstOf(arg && arg.operand, ruleFirsts)
170
+ return { tokens: inner.tokens, nullable: inner.nullable, unknown: inner.unknown }
171
+ }
172
+
173
+ case 'infixRight':
174
+ case 'infixLeft': {
175
+ // The level begins wherever its operand begins, and matches whenever the
176
+ // operand does — the operator part is optional by construction.
177
+ const inner = firstOf(arg && arg.operand, ruleFirsts)
178
+ return { tokens: inner.tokens, nullable: inner.nullable, unknown: inner.unknown }
179
+ }
180
+
181
+ case 'manySep':
182
+ case 'many1Sep': {
183
+ const inner = firstOf(arg && arg.rule, ruleFirsts)
184
+ return {
185
+ tokens: inner.tokens,
186
+ nullable: op === 'manySep' ? true : inner.nullable,
187
+ unknown: inner.unknown
188
+ }
189
+ }
190
+
191
+ case 'subrule': {
192
+ const name = Array.isArray(arg) ? arg[0] : arg
193
+ const known = ruleFirsts.get(name)
194
+ if (!known) return opaque()
195
+ return { tokens: known.tokens, nullable: known.nullable, unknown: known.unknown }
196
+ }
197
+
198
+ // Zero-width assertions constrain the input without consuming it. They are
199
+ // nullable, but treating them as transparent would let `alt` dispatch past a
200
+ // guard that was meant to reject, so they are deliberately opaque.
201
+ case 'lookahead':
202
+ case 'negLookahead':
203
+ return opaque()
204
+
205
+ default:
206
+ // Any other JSON Logic (`+`, `cat`, `var`, ...) is a value expression: it
207
+ // produces a result without touching the token stream.
208
+ return epsilon()
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Rewrites a `seq` into a cheaper equivalent combinator when it matches a known
214
+ * idiom, in the spirit of json-logic-engine's own `checkIdioms`.
215
+ *
216
+ * Grammars are usually written with the general primitives, because that is
217
+ * what a grammar author reaches for. Two of those spellings are both very
218
+ * common and needlessly expensive, and both can be recognized mechanically:
219
+ *
220
+ * seq(open, label(n, body), close, action({val: n})) -> between(open, body, close)
221
+ * seq(label(n, parser), action(A)) -> as(parser, A')
222
+ *
223
+ * Together they were 40% of all `seq` invocations in the formula benchmark.
224
+ * Recognition means an existing grammar gets the faster path without being
225
+ * rewritten by hand.
226
+ *
227
+ * Returns the replacement node, or null when nothing matched.
228
+ *
229
+ * @param {any[]} children
230
+ */
231
+ export function recognizeSeqIdiom (children) {
232
+ if (!Array.isArray(children)) return null
233
+
234
+ // between: open, label(n, body), close, action({val: n})
235
+ if (children.length === 4) {
236
+ const [open, labelled, close, act] = children
237
+ if (opOf(labelled) === 'label' && opOf(act) === 'action' &&
238
+ isPlainParser(open) && isPlainParser(close)) {
239
+ const [name, body] = labelled.label
240
+ const logic = act.action
241
+ if (opOf(logic) === 'val' && logic.val === name) {
242
+ return { between: { open, body, close } }
243
+ }
244
+ }
245
+ }
246
+
247
+ // as: label(n, parser), action(A) — where every reference in A is rooted at n
248
+ if (children.length === 2) {
249
+ const [labelled, act] = children
250
+ if (opOf(labelled) === 'label' && opOf(act) === 'action') {
251
+ const [name, parser] = labelled.label
252
+ const rewritten = rebaseOnLabel(act.action, name)
253
+ if (rewritten !== BAIL) return { as: [parser, rewritten] }
254
+ }
255
+ }
256
+
257
+ return null
258
+ }
259
+
260
+ /** A node that is a parser and not one of `seq`'s own interpreted shapes. */
261
+ function isPlainParser (node) {
262
+ const op = opOf(node)
263
+ return op !== null && op !== 'label' && op !== 'action'
264
+ }
265
+
266
+ const BAIL = Symbol('grammatik.bail')
267
+
268
+ /**
269
+ * Rewrites logic that reads `name` from its enclosing sequence's bindings into
270
+ * logic that reads the value directly, since `as` passes the parsed value as
271
+ * the context itself.
272
+ *
273
+ * {val: name} -> {val: []} (the value)
274
+ * {val: [name, "image"]} -> {val: "image"}
275
+ *
276
+ * Returns BAIL for anything else that touches the context: a different label, a
277
+ * scope hop (`{val: [[-1], ...]}`), a bare `{val: []}` that would have meant the
278
+ * whole bindings object, or any `var` — whose path syntax is not worth
279
+ * re-deriving here.
280
+ */
281
+ function rebaseOnLabel (logic, name) {
282
+ if (Array.isArray(logic)) {
283
+ const out = new Array(logic.length)
284
+ for (let i = 0; i < logic.length; i++) {
285
+ const value = rebaseOnLabel(logic[i], name)
286
+ if (value === BAIL) return BAIL
287
+ out[i] = value
288
+ }
289
+ return out
290
+ }
291
+
292
+ if (!logic || typeof logic !== 'object') return logic
293
+
294
+ const keys = Object.keys(logic)
295
+ if (keys.length === 1 && keys[0] === 'val') {
296
+ const path = logic.val
297
+ if (path === name) return { val: [] }
298
+ if (Array.isArray(path) && path[0] === name) {
299
+ const rest = path.slice(1)
300
+ return { val: rest.length === 1 ? rest[0] : rest }
301
+ }
302
+ return BAIL
303
+ }
304
+ if (keys.length === 1 && keys[0] === 'var') return BAIL
305
+
306
+ const out = {}
307
+ for (const key of keys) {
308
+ const value = rebaseOnLabel(logic[key], name)
309
+ if (value === BAIL) return BAIL
310
+ out[key] = value
311
+ }
312
+ return out
313
+ }
314
+
315
+ /**
316
+ * Runs a fixpoint over the grammar so recursive rules converge.
317
+ * @param {Record<string, any>} grammar
318
+ * @returns {Map<string, First>}
319
+ */
320
+ function computeFirsts (grammar) {
321
+ /** @type {Map<string, First>} */
322
+ const ruleFirsts = new Map()
323
+ for (const name of Object.keys(grammar)) ruleFirsts.set(name, emptyFirst())
324
+
325
+ let changed = true
326
+ while (changed) {
327
+ changed = false
328
+ for (const name of Object.keys(grammar)) {
329
+ const current = ruleFirsts.get(name)
330
+ const computed = firstOf(grammar[name], ruleFirsts)
331
+ if (absorb(current, computed)) changed = true
332
+ if (computed.nullable && !current.nullable) {
333
+ current.nullable = true
334
+ changed = true
335
+ }
336
+ }
337
+ }
338
+
339
+ return ruleFirsts
340
+ }
341
+
342
+ /**
343
+ * Collects the rules a node can invoke without having consumed anything first.
344
+ * Used to find left recursion.
345
+ * @param {*} node
346
+ * @param {Map<string, First>} ruleFirsts
347
+ * @param {Set<string>} out
348
+ */
349
+ function leftmostCalls (node, ruleFirsts, out) {
350
+ const op = opOf(node)
351
+ if (op === null) return
352
+ const arg = node[op]
353
+
354
+ switch (op) {
355
+ case 'subrule': {
356
+ const name = Array.isArray(arg) ? arg[0] : arg
357
+ out.add(name)
358
+ break
359
+ }
360
+ case 'label':
361
+ leftmostCalls(arg[1], ruleFirsts, out)
362
+ break
363
+ case 'text':
364
+ case 'many':
365
+ case 'many1':
366
+ case 'option':
367
+ case 'lookahead':
368
+ case 'negLookahead':
369
+ leftmostCalls(arg, ruleFirsts, out)
370
+ break
371
+ case 'as':
372
+ leftmostCalls(Array.isArray(arg) ? arg[0] : arg && arg.parser, ruleFirsts, out)
373
+ break
374
+ case 'between':
375
+ if (arg) leftmostCalls({ seq: [arg.open, arg.body, arg.close] }, ruleFirsts, out)
376
+ break
377
+ case 'prefix':
378
+ if (arg) {
379
+ leftmostCalls(arg.operator, ruleFirsts, out)
380
+ leftmostCalls(arg.operand, ruleFirsts, out)
381
+ }
382
+ break
383
+ case 'infixLeft':
384
+ case 'infixRight':
385
+ case 'postfix':
386
+ if (arg) leftmostCalls(arg.operand, ruleFirsts, out)
387
+ break
388
+ case 'manySep':
389
+ case 'many1Sep':
390
+ if (arg) leftmostCalls(arg.rule, ruleFirsts, out)
391
+ break
392
+ case 'alt':
393
+ if (Array.isArray(arg)) for (const child of arg) leftmostCalls(child, ruleFirsts, out)
394
+ break
395
+ case 'seq':
396
+ if (Array.isArray(arg)) {
397
+ for (const child of arg) {
398
+ leftmostCalls(child, ruleFirsts, out)
399
+ // Stop at the first element that must consume something.
400
+ if (!firstOf(child, ruleFirsts).nullable) break
401
+ }
402
+ }
403
+ break
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Walks every node, reporting unknown rule/token references.
409
+ * @param {*} node
410
+ * @param {Record<string, any>} grammar
411
+ * @param {Set<string>|null} tokenNames
412
+ * @param {string[]} errors
413
+ * @param {string} path
414
+ */
415
+ function validateNode (node, grammar, tokenNames, errors, path) {
416
+ const op = opOf(node)
417
+ if (op === null) {
418
+ if (Array.isArray(node)) node.forEach((c, i) => validateNode(c, grammar, tokenNames, errors, `${path}[${i}]`))
419
+ return
420
+ }
421
+ const arg = node[op]
422
+
423
+ switch (op) {
424
+ case 'consume': {
425
+ const name = Array.isArray(arg) ? arg[0] : arg
426
+ if (typeof name !== 'string') errors.push(`${path}: 'consume' expects a token name, got ${JSON.stringify(name)}`)
427
+ else if (tokenNames && !tokenNames.has(name)) errors.push(`${path}: 'consume' references unknown token '${name}'`)
428
+ break
429
+ }
430
+ case 'subrule': {
431
+ const name = Array.isArray(arg) ? arg[0] : arg
432
+ if (typeof name !== 'string') errors.push(`${path}: 'subrule' expects a rule name, got ${JSON.stringify(name)}`)
433
+ else if (!(name in grammar)) errors.push(`${path}: 'subrule' references unknown rule '${name}'`)
434
+ break
435
+ }
436
+ case 'label': {
437
+ if (!Array.isArray(arg) || arg.length !== 2 || typeof arg[0] !== 'string') {
438
+ errors.push(`${path}: 'label' expects [name, parser]`)
439
+ } else validateNode(arg[1], grammar, tokenNames, errors, `${path}.label(${arg[0]})`)
440
+ break
441
+ }
442
+ case 'seq':
443
+ case 'alt': {
444
+ if (!Array.isArray(arg)) errors.push(`${path}: '${op}' expects an array`)
445
+ else {
446
+ if (op === 'alt' && arg.length === 0) errors.push(`${path}: 'alt' needs at least one branch`)
447
+ arg.forEach((c, i) => validateNode(c, grammar, tokenNames, errors, `${path}.${op}[${i}]`))
448
+ }
449
+ break
450
+ }
451
+ case 'many':
452
+ case 'many1':
453
+ case 'option':
454
+ case 'text':
455
+ case 'lookahead':
456
+ case 'negLookahead':
457
+ validateNode(arg, grammar, tokenNames, errors, `${path}.${op}`)
458
+ break
459
+ case 'as': {
460
+ const parser = Array.isArray(arg) ? arg[0] : arg && arg.parser
461
+ if (parser === undefined) errors.push(`${path}: 'as' expects [parser, logic]`)
462
+ else validateNode(parser, grammar, tokenNames, errors, `${path}.as`)
463
+ break
464
+ }
465
+ case 'between': {
466
+ if (!arg || typeof arg !== 'object' || !('open' in arg) || !('body' in arg) || !('close' in arg)) {
467
+ errors.push(`${path}: 'between' expects { open, body, close }`)
468
+ } else {
469
+ validateNode(arg.open, grammar, tokenNames, errors, `${path}.between.open`)
470
+ validateNode(arg.body, grammar, tokenNames, errors, `${path}.between.body`)
471
+ validateNode(arg.close, grammar, tokenNames, errors, `${path}.between.close`)
472
+ }
473
+ break
474
+ }
475
+ case 'prefix': {
476
+ if (!arg || typeof arg !== 'object' || !('operator' in arg) || !('operand' in arg) || !('combine' in arg)) {
477
+ errors.push(`${path}: 'prefix' expects { operator, operand, combine }`)
478
+ } else {
479
+ validateNode(arg.operator, grammar, tokenNames, errors, `${path}.prefix.operator`)
480
+ validateNode(arg.operand, grammar, tokenNames, errors, `${path}.prefix.operand`)
481
+ }
482
+ break
483
+ }
484
+ case 'postfix': {
485
+ if (!arg || typeof arg !== 'object' || !('operand' in arg) || !('suffix' in arg) || !('combine' in arg)) {
486
+ errors.push(`${path}: 'postfix' expects { operand, suffix, combine }`)
487
+ } else {
488
+ validateNode(arg.operand, grammar, tokenNames, errors, `${path}.postfix.operand`)
489
+ validateNode(arg.suffix, grammar, tokenNames, errors, `${path}.postfix.suffix`)
490
+ }
491
+ break
492
+ }
493
+ case 'oneOf': {
494
+ const names = Array.isArray(arg) ? arg : [arg]
495
+ for (const name of names) {
496
+ if (typeof name !== 'string') errors.push(`${path}: 'oneOf' expects token names`)
497
+ else if (tokenNames && !tokenNames.has(name)) errors.push(`${path}: 'oneOf' references unknown token '${name}'`)
498
+ }
499
+ break
500
+ }
501
+ case 'eof':
502
+ break
503
+ case 'infixRight':
504
+ case 'infixLeft': {
505
+ if (!arg || typeof arg !== 'object' || !('operand' in arg) || !('combine' in arg)) {
506
+ errors.push(`${path}: '${op}' expects { operand, combine, operator? }`)
507
+ } else {
508
+ validateNode(arg.operand, grammar, tokenNames, errors, `${path}.${op}.operand`)
509
+ if (arg.operator !== undefined) validateNode(arg.operator, grammar, tokenNames, errors, `${path}.${op}.operator`)
510
+ }
511
+ break
512
+ }
513
+ case 'manySep':
514
+ case 'many1Sep': {
515
+ if (!arg || typeof arg !== 'object' || !('rule' in arg) || !('sep' in arg)) {
516
+ errors.push(`${path}: '${op}' expects { rule, sep }`)
517
+ } else {
518
+ validateNode(arg.rule, grammar, tokenNames, errors, `${path}.${op}.rule`)
519
+ if (typeof arg.sep === 'string') {
520
+ if (tokenNames && !tokenNames.has(arg.sep)) errors.push(`${path}: '${op}' separator references unknown token '${arg.sep}'`)
521
+ } else validateNode(arg.sep, grammar, tokenNames, errors, `${path}.${op}.sep`)
522
+ }
523
+ break
524
+ }
525
+ // Everything else is a value expression; the logic engine validates it.
526
+ }
527
+ }
528
+
529
+ /**
530
+ * @param {Record<string, any>} grammar
531
+ * @param {{ tokenNames?: string[] }} [options]
532
+ * @returns {{ firsts: Map<string, First>, errors: string[], leftRecursive: string[] }}
533
+ */
534
+ export function analyze (grammar, options = {}) {
535
+ const errors = []
536
+ const tokenNames = options.tokenNames ? new Set(options.tokenNames) : null
537
+
538
+ for (const name of Object.keys(grammar)) {
539
+ validateNode(grammar[name], grammar, tokenNames, errors, name)
540
+ }
541
+
542
+ const firsts = computeFirsts(grammar)
543
+
544
+ // A rule is left-recursive when it can reach itself without consuming a token.
545
+ const leftEdges = new Map()
546
+ for (const name of Object.keys(grammar)) {
547
+ const out = new Set()
548
+ leftmostCalls(grammar[name], firsts, out)
549
+ leftEdges.set(name, out)
550
+ }
551
+
552
+ const leftRecursive = []
553
+ for (const start of Object.keys(grammar)) {
554
+ const seen = new Set()
555
+ const stack = [...(leftEdges.get(start) || [])]
556
+ while (stack.length) {
557
+ const next = stack.pop()
558
+ if (next === start) {
559
+ leftRecursive.push(start)
560
+ break
561
+ }
562
+ if (seen.has(next)) continue
563
+ seen.add(next)
564
+ for (const edge of leftEdges.get(next) || []) stack.push(edge)
565
+ }
566
+ }
567
+
568
+ for (const name of leftRecursive) {
569
+ errors.push(`rule '${name}' is left-recursive; this parser is LL-style and would not terminate. Rewrite it with 'many'/'many1Sep' (the standard left-recursion elimination).`)
570
+ }
571
+
572
+ return { firsts, errors, leftRecursive }
573
+ }
574
+
575
+ /**
576
+ * Builds an O(1) dispatch table for an `alt`, or null when the branches are not
577
+ * LL(1)-separable and we must fall back to ordered backtracking.
578
+ *
579
+ * @param {any[]} branches
580
+ * @param {Map<string, First>} firsts
581
+ * @returns {Map<string, any>|null}
582
+ */
583
+ export function buildDispatch (branches, firsts) {
584
+ /** @type {Map<string, any>} */
585
+ const table = new Map()
586
+
587
+ for (const branch of branches) {
588
+ const first = firstOf(branch, firsts)
589
+ // A nullable or opaque branch could match anything, so no table is safe.
590
+ if (first.nullable || first.unknown || first.tokens.size === 0) return null
591
+ for (const token of first.tokens) {
592
+ // Overlapping FIRST sets mean the choice needs more than one token of
593
+ // lookahead; ordered backtracking still handles it correctly.
594
+ if (table.has(token)) return null
595
+ table.set(token, branch)
596
+ }
597
+ }
598
+
599
+ return table.size > 0 ? table : null
600
+ }
601
+
602
+ export { firstOf }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * A real source generator for grammars.
3
+ *
4
+ * The planner removes per-token *decisions*, but it still pays one indirect
5
+ * call per grammar node, through call sites that see many closure shapes and so
6
+ * cannot be inlined. That is the floor for a closure tree, and it is where the
7
+ * remaining gap against hand-written parsers lives.
8
+ *
9
+ * This module emits one JavaScript function per rule with every node inlined as
10
+ * straight-line statements — the shape a hand-written recursive-descent parser
11
+ * has. `alt` becomes a real `switch` on the token type, which V8 compiles to a
12
+ * jump table rather than a Map lookup. Only rule boundaries remain calls.
13
+ *
14
+ * Anything it does not know how to emit falls back to the planner's closure via
15
+ * the helper table, so adding an operator here is optional and incremental.
16
+ */
17
+ /**
18
+ * @param {{ planner: any, rules: Record<string, any>, firsts: Map<string, any>|null,
19
+ * memo: boolean, maxSteps: number }} config
20
+ */
21
+ export function createCodegen(config: {
22
+ planner: any;
23
+ rules: Record<string, any>;
24
+ firsts: Map<string, any> | null;
25
+ memo: boolean;
26
+ maxSteps: number;
27
+ }): {
28
+ compile: (start: string) => (state: any) => any;
29
+ functionFor: (name: any) => any;
30
+ sourceFor: (name: any) => string;
31
+ ruleIndex: Map<any, any>;
32
+ RULES: any[];
33
+ SOURCES: any[];
34
+ ORIGINS: any[];
35
+ guard: boolean;
36
+ };