grammatik 0.1.0 → 0.1.2
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/README.md +16 -0
- package/bin/grammatik.js +8 -0
- package/dist/cjs/index.js +1637 -139
- package/dist/cjs/runtime.js +642 -34
- package/dist/esm/index.js +1637 -139
- package/dist/esm/runtime.js +642 -34
- package/dist/types/index.d.ts +36 -0
- package/dist/types/runtime.d.ts +4 -3
- package/package.json +3 -2
- package/src/analyze.js +225 -18
- package/src/codegen.js +218 -27
- package/src/definition.js +5 -0
- package/src/emit.js +41 -7
- package/src/lexer.d.ts +43 -2
- package/src/lexer.js +573 -44
- package/src/lexgen.js +451 -0
- package/src/memoplan.js +220 -0
- package/src/methods.js +1 -1
- package/src/parser.js +38 -3
- package/src/plan.js +142 -30
- package/src/runtime.js +8 -6
- package/src/scan.js +422 -0
- package/src/state.js +33 -1
package/dist/types/index.d.ts
CHANGED
|
@@ -70,6 +70,29 @@ export interface ParserOptions {
|
|
|
70
70
|
positions?: PositionMode
|
|
71
71
|
/** Packrat memoization on (rule, position). */
|
|
72
72
|
memo?: boolean
|
|
73
|
+
/**
|
|
74
|
+
* Memoize only the rules that several colliding branches of an `alt` would
|
|
75
|
+
* otherwise each re-parse (default `true`; ignored when `memo` is on). Avoids
|
|
76
|
+
* the exponential blow-up of nested alternatives that share a leading rule,
|
|
77
|
+
* at no cost to a grammar with none. See `memoRules`.
|
|
78
|
+
*/
|
|
79
|
+
autoMemo?: boolean
|
|
80
|
+
/**
|
|
81
|
+
* In generated code, write single-caller operator levels (`infixLeft`,
|
|
82
|
+
* `infixRight`, `prefix`, `postfix`) into their caller instead of calling them
|
|
83
|
+
* (default `true` for `createParser`, `false` for `emitModule`). Several levels of an
|
|
84
|
+
* expression grammar become one function: 2-3x faster on a bare precedence chain, about
|
|
85
|
+
* 1.02-1.04x on real grammars, at the cost of a bigger file, which is why an emitted
|
|
86
|
+
* parser leaves it off.
|
|
87
|
+
*/
|
|
88
|
+
inlineRules?: boolean
|
|
89
|
+
/**
|
|
90
|
+
* Recognize common spellings of a grammar (`seq(open, label, close, action)`, a
|
|
91
|
+
* prefix plus one labelled result, a constant action, an `alt` of bare `consume`s,
|
|
92
|
+
* a shared leading run of `alt` branches) and run them as the cheaper combinator
|
|
93
|
+
* they are equivalent to. Default `true`; `false` runs the grammar exactly as written.
|
|
94
|
+
*/
|
|
95
|
+
idioms?: boolean
|
|
73
96
|
/** Require the whole token stream to be consumed. */
|
|
74
97
|
strict?: boolean
|
|
75
98
|
/** FIRST-set dispatch for `alt`. Off forces ordered backtracking. */
|
|
@@ -109,6 +132,8 @@ export interface JLParser {
|
|
|
109
132
|
engine: any
|
|
110
133
|
analysis: Analysis
|
|
111
134
|
execution: ExecutionMode
|
|
135
|
+
/** The rules `autoMemo` chose to memoize, in declaration order. */
|
|
136
|
+
memoRules: string[]
|
|
112
137
|
compiled: boolean
|
|
113
138
|
readonly firsts: Record<string, Set<string>>
|
|
114
139
|
}
|
|
@@ -126,6 +151,17 @@ export declare function analyze (
|
|
|
126
151
|
): Analysis
|
|
127
152
|
|
|
128
153
|
export interface EmitOptions extends ParserOptions {
|
|
154
|
+
/**
|
|
155
|
+
* How the file lexes. `'generated'` (default) writes the lexer out as source, so the
|
|
156
|
+
* file imports only the parse state and error class from the runtime: a smaller
|
|
157
|
+
* bundle (JSON: 24 KB to 10 KB minified), a faster start, and faster lexing.
|
|
158
|
+
* `'runtime'` keeps the token table and builds the lexer from it when the file loads.
|
|
159
|
+
*/
|
|
160
|
+
lexer?: 'generated' | 'runtime'
|
|
161
|
+
/** Scanner substitution for the lexer, as for `createLexer`. Default `true`. */
|
|
162
|
+
scanners?: boolean
|
|
163
|
+
/** Fusing runs of regex tokens in the lexer, as for `createLexer`. Default `true`. */
|
|
164
|
+
fuse?: boolean
|
|
129
165
|
/** Module format of the generated file. */
|
|
130
166
|
format?: 'esm' | 'cjs'
|
|
131
167
|
/** Name recorded in the file header. Defaults to `spec.name`. */
|
package/dist/types/runtime.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `grammatik/runtime` — the surface a generated parser imports.
|
|
3
3
|
*
|
|
4
|
-
* Deliberately small: a generated file needs the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Deliberately small: a generated file needs the parse state and error
|
|
5
|
+
* construction, and nothing else. The analyzer, planner, code generator and
|
|
6
|
+
* (by default) the lexer were already run when the file was written. `createLexer`
|
|
7
|
+
* is still here for files emitted with `lexer: 'runtime'`.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import type { Token, TokenDef, PositionMode, Lexer, Parser } from './index.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grammatik",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A parser toolkit whose grammars are data: tokens, rules and semantic actions all serializable, compiled ahead of time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"bench:compare": "node bench/compare-codex.js",
|
|
39
39
|
"bench:formula": "node bench/peggy/build.js && node bench/formula.js",
|
|
40
40
|
"bench:generated": "node bench/peggy/build.js && node bench/generate-formula.js && node bench/generated.js",
|
|
41
|
-
"bench:json-parsers": "node bench/jsonParsers/run.js"
|
|
41
|
+
"bench:json-parsers": "node bench/jsonParsers/run.js",
|
|
42
|
+
"build:site": "node site/build.js"
|
|
42
43
|
},
|
|
43
44
|
"keywords": [
|
|
44
45
|
"parser",
|
package/src/analyze.js
CHANGED
|
@@ -60,9 +60,9 @@ export function opOf (node) {
|
|
|
60
60
|
|
|
61
61
|
/** The set of node shapes this module understands as *parsers*. */
|
|
62
62
|
export const PARSER_OPS = new Set([
|
|
63
|
-
'consume', 'seq', 'alt', 'many', 'many1', 'option',
|
|
63
|
+
'consume', 'seq', 'alt', 'many', 'many1', 'option', 'flag',
|
|
64
64
|
'manySep', 'many1Sep', 'infixLeft', 'infixRight', 'postfix', 'prefix',
|
|
65
|
-
'between', 'as', 'oneOf', 'eof',
|
|
65
|
+
'between', 'pick', 'yields', 'as', 'oneOf', 'eof',
|
|
66
66
|
'subrule', 'label', 'action',
|
|
67
67
|
'lookahead', 'negLookahead', 'text', 'epsilon'
|
|
68
68
|
])
|
|
@@ -122,7 +122,8 @@ function firstOf (node, ruleFirsts) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
case 'many':
|
|
125
|
-
case 'option':
|
|
125
|
+
case 'option':
|
|
126
|
+
case 'flag': {
|
|
126
127
|
const inner = firstOf(arg, ruleFirsts)
|
|
127
128
|
return { tokens: inner.tokens, nullable: true, unknown: inner.unknown }
|
|
128
129
|
}
|
|
@@ -153,6 +154,11 @@ function firstOf (node, ruleFirsts) {
|
|
|
153
154
|
// Exactly a sequence of open, body, close.
|
|
154
155
|
return firstOf({ seq: [arg && arg.open, arg && arg.body, arg && arg.close] }, ruleFirsts)
|
|
155
156
|
|
|
157
|
+
case 'pick':
|
|
158
|
+
case 'yields':
|
|
159
|
+
// Exactly a sequence of its parsers.
|
|
160
|
+
return firstOf({ seq: arg && Array.isArray(arg.parsers) ? arg.parsers : [] }, ruleFirsts)
|
|
161
|
+
|
|
156
162
|
case 'prefix': {
|
|
157
163
|
// Zero operators are allowed, so the operand's first set counts too.
|
|
158
164
|
const ops = firstOf(arg && arg.operator, ruleFirsts)
|
|
@@ -254,15 +260,169 @@ export function recognizeSeqIdiom (children) {
|
|
|
254
260
|
}
|
|
255
261
|
}
|
|
256
262
|
|
|
263
|
+
// The two idioms below drop the `seq`'s bindings object, so they decline when a
|
|
264
|
+
// child could reach into an enclosing scope: without that object an outward hop
|
|
265
|
+
// would land one scope further out than it did.
|
|
266
|
+
if (children.length >= 2) {
|
|
267
|
+
const last = children[children.length - 1]
|
|
268
|
+
const head = children.slice(0, -1)
|
|
269
|
+
|
|
270
|
+
// yields: parsers, then an action that is a constant — `seq(consume(True), action(true))`
|
|
271
|
+
if (opOf(last) === 'action' && isConstant(last.action) && head.every(isPlainParser) && !head.some(usesOuterScope)) {
|
|
272
|
+
return { yields: { parsers: head, value: last.action } }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// pick: any number of unlabelled parsers around exactly one labelled one, then
|
|
276
|
+
// an action that returns just that label — `seq(consume(Where), label(e, P), action({val: e}))`.
|
|
277
|
+
// `between` is the special case with one on each side.
|
|
278
|
+
if (opOf(last) === 'action' && opOf(last.action) === 'val' && typeof last.action.val === 'string') {
|
|
279
|
+
const name = last.action.val
|
|
280
|
+
const labelled = head.map((child, i) => (opOf(child) === 'label' ? i : -1)).filter((i) => i !== -1)
|
|
281
|
+
if (labelled.length === 1 && head[labelled[0]].label[0] === name &&
|
|
282
|
+
head.every((child, i) => i === labelled[0] || isPlainParser(child)) &&
|
|
283
|
+
!head.some(usesOuterScope)) {
|
|
284
|
+
return { pick: { parsers: head.map((child, i) => (i === labelled[0] ? child.label[1] : child)), index: labelled[0] } }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
257
289
|
return null
|
|
258
290
|
}
|
|
259
291
|
|
|
292
|
+
/** A value that cannot be mutated or aliased: safe to hand out from every parse. */
|
|
293
|
+
const isConstant = (value) =>
|
|
294
|
+
value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* True when the logic reads from an outer scope (`{val: [[-1], ...]}` or
|
|
298
|
+
* `{var: "../x"}`). The engine's compiler does not thread `above` into built
|
|
299
|
+
* functions, so such actions must stay interpreted — and a rewrite that removes
|
|
300
|
+
* a `seq`'s bindings object would change what such a hop reaches.
|
|
301
|
+
*/
|
|
302
|
+
export function usesOuterScope (logic) {
|
|
303
|
+
if (Array.isArray(logic)) return logic.some(usesOuterScope)
|
|
304
|
+
if (!logic || typeof logic !== 'object') return false
|
|
305
|
+
|
|
306
|
+
for (const key of Object.keys(logic)) {
|
|
307
|
+
const arg = logic[key]
|
|
308
|
+
if (key === 'val' && Array.isArray(arg) && Array.isArray(arg[0])) return true
|
|
309
|
+
if (key === 'var') {
|
|
310
|
+
if (typeof arg === 'string' && arg.includes('../')) return true
|
|
311
|
+
if (Array.isArray(arg) && typeof arg[0] === 'string' && arg[0].includes('../')) return true
|
|
312
|
+
}
|
|
313
|
+
if (usesOuterScope(arg)) return true
|
|
314
|
+
}
|
|
315
|
+
return false
|
|
316
|
+
}
|
|
317
|
+
|
|
260
318
|
/** A node that is a parser and not one of `seq`'s own interpreted shapes. */
|
|
261
319
|
function isPlainParser (node) {
|
|
262
320
|
const op = opOf(node)
|
|
263
321
|
return op !== null && op !== 'label' && op !== 'action'
|
|
264
322
|
}
|
|
265
323
|
|
|
324
|
+
/** Structural equality over plain JSON — every shape a grammar node can be. */
|
|
325
|
+
function deepEqual (a, b) {
|
|
326
|
+
if (a === b) return true
|
|
327
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
328
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false
|
|
329
|
+
for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false
|
|
330
|
+
return true
|
|
331
|
+
}
|
|
332
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false
|
|
333
|
+
const keysA = Object.keys(a)
|
|
334
|
+
const keysB = Object.keys(b)
|
|
335
|
+
if (keysA.length !== keysB.length) return false
|
|
336
|
+
for (const key of keysA) {
|
|
337
|
+
if (!Object.prototype.hasOwnProperty.call(b, key) || !deepEqual(a[key], b[key])) return false
|
|
338
|
+
}
|
|
339
|
+
return true
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Left-factors an `alt` whose branches are all `seq` and share a literal
|
|
344
|
+
* leading run of nodes — `alt(seq(P, X, actionX), seq(P, Y, actionY))`
|
|
345
|
+
* becomes `seq(P, alt(seq(X, actionX), seq(Y, actionY)))`, so a shared,
|
|
346
|
+
* possibly multi-token prefix is matched once per attempt instead of once per
|
|
347
|
+
* colliding branch.
|
|
348
|
+
*
|
|
349
|
+
* Two things have to hold for the rewrite to be safe, not just structurally
|
|
350
|
+
* possible:
|
|
351
|
+
*
|
|
352
|
+
* - The shared prefix cannot contain a `label` or an `action`. A `seq`'s
|
|
353
|
+
* bindings are scoped to itself; hoisting a labelled node out from under
|
|
354
|
+
* the branch that reads it would strand the binding in a different scope
|
|
355
|
+
* than the action that expects to see it.
|
|
356
|
+
* - Every branch must already end its own decision with an `action`
|
|
357
|
+
* somewhere in its remainder (`seq` yields that action's value instead of
|
|
358
|
+
* the flat array of its children). That is what lets the rewritten form
|
|
359
|
+
* reproduce the original value exactly, by wrapping the factored `alt` in
|
|
360
|
+
* one more `label`/`action` pair rather than redesigning what the branch
|
|
361
|
+
* returns.
|
|
362
|
+
*
|
|
363
|
+
* A branch that is a strict prefix of another (nothing left after the shared
|
|
364
|
+
* run) fails the second check on its own — its remainder is empty, so it has
|
|
365
|
+
* no action left to find — and the whole rewrite backs off rather than guess
|
|
366
|
+
* what an author meant by writing one alternative as a prefix of another.
|
|
367
|
+
*
|
|
368
|
+
* @param {any[]} branches
|
|
369
|
+
* @returns {any|null}
|
|
370
|
+
*/
|
|
371
|
+
export function recognizeAltPrefixIdiom (branches) {
|
|
372
|
+
if (!Array.isArray(branches) || branches.length < 2) return null
|
|
373
|
+
if (!branches.every((b) => opOf(b) === 'seq' && Array.isArray(b.seq))) return null
|
|
374
|
+
|
|
375
|
+
const seqs = branches.map((b) => b.seq)
|
|
376
|
+
const minLen = Math.min(...seqs.map((s) => s.length))
|
|
377
|
+
|
|
378
|
+
let n = 0
|
|
379
|
+
while (n < minLen) {
|
|
380
|
+
const candidate = seqs[0][n]
|
|
381
|
+
const op = opOf(candidate)
|
|
382
|
+
if (op === 'label' || op === 'action') break
|
|
383
|
+
if (!seqs.every((s) => deepEqual(s[n], candidate))) break
|
|
384
|
+
n++
|
|
385
|
+
}
|
|
386
|
+
if (n === 0) return null
|
|
387
|
+
if (!seqs.every((s) => s.slice(n).some((child) => opOf(child) === 'action'))) return null
|
|
388
|
+
|
|
389
|
+
const prefix = seqs[0].slice(0, n)
|
|
390
|
+
const remainders = seqs.map((s) => ({ seq: s.slice(n) }))
|
|
391
|
+
return {
|
|
392
|
+
seq: [
|
|
393
|
+
...prefix,
|
|
394
|
+
{ label: ['__grammatik_rest', { alt: remainders }] },
|
|
395
|
+
{ action: { val: '__grammatik_rest' } }
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Rewrites an `alt` whose every branch is a bare `consume` into `oneOf`:
|
|
402
|
+
* `alt(consume(Plus), consume(Minus))` is `oneOf(Plus, Minus)`.
|
|
403
|
+
*
|
|
404
|
+
* Same language, same value (the matched token), same expected-token list on
|
|
405
|
+
* failure — a token has one type, so "first branch that matches" and "any of
|
|
406
|
+
* these" cannot differ. What differs is cost: `alt` builds a branch per token,
|
|
407
|
+
* `oneOf` is a single test. Measured at 1.3–1.8x for the 2–7 token operator sets
|
|
408
|
+
* grammars actually have, and still 1.2x at 24.
|
|
409
|
+
*
|
|
410
|
+
* @param {any[]} branches
|
|
411
|
+
* @returns {{ oneOf: string[] } | null}
|
|
412
|
+
*/
|
|
413
|
+
export function recognizeAltConsumeIdiom (branches) {
|
|
414
|
+
if (!Array.isArray(branches) || branches.length < 2) return null
|
|
415
|
+
const names = []
|
|
416
|
+
for (const branch of branches) {
|
|
417
|
+
if (opOf(branch) !== 'consume') return null
|
|
418
|
+
const arg = branch.consume
|
|
419
|
+
const name = typeof arg === 'string' ? arg : (Array.isArray(arg) && arg.length === 1 ? arg[0] : null)
|
|
420
|
+
if (typeof name !== 'string') return null
|
|
421
|
+
names.push(name)
|
|
422
|
+
}
|
|
423
|
+
return { oneOf: names }
|
|
424
|
+
}
|
|
425
|
+
|
|
266
426
|
const BAIL = Symbol('grammatik.bail')
|
|
267
427
|
|
|
268
428
|
/**
|
|
@@ -346,7 +506,7 @@ function computeFirsts (grammar) {
|
|
|
346
506
|
* @param {Map<string, First>} ruleFirsts
|
|
347
507
|
* @param {Set<string>} out
|
|
348
508
|
*/
|
|
349
|
-
function leftmostCalls (node, ruleFirsts, out) {
|
|
509
|
+
export function leftmostCalls (node, ruleFirsts, out) {
|
|
350
510
|
const op = opOf(node)
|
|
351
511
|
if (op === null) return
|
|
352
512
|
const arg = node[op]
|
|
@@ -364,6 +524,7 @@ function leftmostCalls (node, ruleFirsts, out) {
|
|
|
364
524
|
case 'many':
|
|
365
525
|
case 'many1':
|
|
366
526
|
case 'option':
|
|
527
|
+
case 'flag':
|
|
367
528
|
case 'lookahead':
|
|
368
529
|
case 'negLookahead':
|
|
369
530
|
leftmostCalls(arg, ruleFirsts, out)
|
|
@@ -374,6 +535,10 @@ function leftmostCalls (node, ruleFirsts, out) {
|
|
|
374
535
|
case 'between':
|
|
375
536
|
if (arg) leftmostCalls({ seq: [arg.open, arg.body, arg.close] }, ruleFirsts, out)
|
|
376
537
|
break
|
|
538
|
+
case 'pick':
|
|
539
|
+
case 'yields':
|
|
540
|
+
if (arg && Array.isArray(arg.parsers)) leftmostCalls({ seq: arg.parsers }, ruleFirsts, out)
|
|
541
|
+
break
|
|
377
542
|
case 'prefix':
|
|
378
543
|
if (arg) {
|
|
379
544
|
leftmostCalls(arg.operator, ruleFirsts, out)
|
|
@@ -451,6 +616,7 @@ function validateNode (node, grammar, tokenNames, errors, path) {
|
|
|
451
616
|
case 'many':
|
|
452
617
|
case 'many1':
|
|
453
618
|
case 'option':
|
|
619
|
+
case 'flag':
|
|
454
620
|
case 'text':
|
|
455
621
|
case 'lookahead':
|
|
456
622
|
case 'negLookahead':
|
|
@@ -472,6 +638,26 @@ function validateNode (node, grammar, tokenNames, errors, path) {
|
|
|
472
638
|
}
|
|
473
639
|
break
|
|
474
640
|
}
|
|
641
|
+
case 'pick': {
|
|
642
|
+
if (!arg || !Array.isArray(arg.parsers) || arg.parsers.length === 0) {
|
|
643
|
+
errors.push(`${path}: 'pick' expects { parsers: [parser, ...], index }`)
|
|
644
|
+
} else if (!Number.isInteger(arg.index) || arg.index < 0 || arg.index >= arg.parsers.length) {
|
|
645
|
+
errors.push(`${path}: 'pick' index ${JSON.stringify(arg.index)} is out of range for ${arg.parsers.length} parser(s)`)
|
|
646
|
+
} else {
|
|
647
|
+
arg.parsers.forEach((c, i) => validateNode(c, grammar, tokenNames, errors, `${path}.pick[${i}]`))
|
|
648
|
+
}
|
|
649
|
+
break
|
|
650
|
+
}
|
|
651
|
+
case 'yields': {
|
|
652
|
+
if (!arg || !Array.isArray(arg.parsers) || arg.parsers.length === 0 || !('value' in arg)) {
|
|
653
|
+
errors.push(`${path}: 'yields' expects { parsers: [parser, ...], value }`)
|
|
654
|
+
} else if (!isConstant(arg.value)) {
|
|
655
|
+
errors.push(`${path}: 'yields' value must be a string, number, boolean or null, got ${JSON.stringify(arg.value)}`)
|
|
656
|
+
} else {
|
|
657
|
+
arg.parsers.forEach((c, i) => validateNode(c, grammar, tokenNames, errors, `${path}.yields[${i}]`))
|
|
658
|
+
}
|
|
659
|
+
break
|
|
660
|
+
}
|
|
475
661
|
case 'prefix': {
|
|
476
662
|
if (!arg || typeof arg !== 'object' || !('operator' in arg) || !('operand' in arg) || !('combine' in arg)) {
|
|
477
663
|
errors.push(`${path}: 'prefix' expects { operator, operand, combine }`)
|
|
@@ -573,30 +759,51 @@ export function analyze (grammar, options = {}) {
|
|
|
573
759
|
}
|
|
574
760
|
|
|
575
761
|
/**
|
|
576
|
-
* Builds an O(1) dispatch table for an `alt`, or null when
|
|
577
|
-
*
|
|
762
|
+
* Builds an O(1) dispatch table for an `alt`, or null when no branch has a
|
|
763
|
+
* known, non-nullable FIRST set (a nullable/opaque branch could match
|
|
764
|
+
* anything, so no table is safe at all).
|
|
765
|
+
*
|
|
766
|
+
* A token whose FIRST set membership is unique to one branch maps straight to
|
|
767
|
+
* that branch. A token shared by more than one branch — the choice needs more
|
|
768
|
+
* than one token of lookahead there — maps to an array of the colliding
|
|
769
|
+
* branches, in their original `alt` order; a caller resolves that case with
|
|
770
|
+
* ordered backtracking over just that subset, rather than over every branch.
|
|
771
|
+
* Equal-content collision groups are the same array instance, so a caller
|
|
772
|
+
* that dedupes on the table's values (e.g. to share generated code between
|
|
773
|
+
* case labels) gets that for free.
|
|
578
774
|
*
|
|
579
775
|
* @param {any[]} branches
|
|
580
776
|
* @param {Map<string, First>} firsts
|
|
581
777
|
* @returns {Map<string, any>|null}
|
|
582
778
|
*/
|
|
583
779
|
export function buildDispatch (branches, firsts) {
|
|
584
|
-
|
|
585
|
-
const
|
|
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.
|
|
780
|
+
const perBranch = branches.map((branch) => firstOf(branch, firsts))
|
|
781
|
+
for (const first of perBranch) {
|
|
590
782
|
if (first.nullable || first.unknown || first.tokens.size === 0) return null
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** @type {Map<string, any[]>} */
|
|
786
|
+
const byToken = new Map()
|
|
787
|
+
for (let i = 0; i < branches.length; i++) {
|
|
788
|
+
for (const token of perBranch[i].tokens) {
|
|
789
|
+
let list = byToken.get(token)
|
|
790
|
+
if (list === undefined) { list = []; byToken.set(token, list) }
|
|
791
|
+
list.push(branches[i])
|
|
596
792
|
}
|
|
597
793
|
}
|
|
794
|
+
if (byToken.size === 0) return null
|
|
795
|
+
|
|
796
|
+
const canonical = new Map()
|
|
797
|
+
const table = new Map()
|
|
798
|
+
for (const [token, list] of byToken) {
|
|
799
|
+
if (list.length === 1) { table.set(token, list[0]); continue }
|
|
800
|
+
const key = list.map((branch) => branches.indexOf(branch)).join(',')
|
|
801
|
+
let group = canonical.get(key)
|
|
802
|
+
if (group === undefined) { group = list; canonical.set(key, group) }
|
|
803
|
+
table.set(token, group)
|
|
804
|
+
}
|
|
598
805
|
|
|
599
|
-
return table
|
|
806
|
+
return table
|
|
600
807
|
}
|
|
601
808
|
|
|
602
809
|
export { firstOf }
|