grammatik 0.1.0 → 0.1.1
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/dist/cjs/index.js +175 -44
- package/dist/cjs/runtime.js +7 -9
- package/dist/esm/index.js +175 -44
- package/dist/esm/runtime.js +7 -9
- package/package.json +3 -2
- package/src/analyze.js +111 -14
- package/src/codegen.js +75 -20
- package/src/emit.js +14 -2
- package/src/lexer.js +38 -16
- package/src/parser.js +9 -2
- package/src/plan.js +58 -8
package/src/codegen.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
4
|
import { FAIL, expect } from './state.js'
|
|
5
|
-
import { buildDispatch, opOf, recognizeSeqIdiom } from './analyze.js'
|
|
5
|
+
import { buildDispatch, opOf, recognizeSeqIdiom, recognizeAltPrefixIdiom } from './analyze.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* A real source generator for grammars.
|
|
@@ -23,10 +23,23 @@ import { buildDispatch, opOf, recognizeSeqIdiom } from './analyze.js'
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* @param {{ planner: any, rules: Record<string, any>, firsts: Map<string, any>|null,
|
|
26
|
-
* memo: boolean, maxSteps: number }} config
|
|
26
|
+
* memo: boolean, maxSteps: number, tokenIds?: Map<string, number>|null }} config
|
|
27
27
|
*/
|
|
28
28
|
export function createCodegen (config) {
|
|
29
|
-
const { planner, rules, firsts, memo, maxSteps } = config
|
|
29
|
+
const { planner, rules, firsts, memo, maxSteps, tokenIds = null } = config
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `token.id === <n>` is an integer compare instead of a string compare, and
|
|
33
|
+
* is what generated code prefers wherever a token's declared type matters.
|
|
34
|
+
* Falls back to `token.type === <name>` only for a hand-built token that
|
|
35
|
+
* bypassed this parser's own lexer (`parseTokens` accepts anything shaped
|
|
36
|
+
* like a Token) — every token the bundled lexer produces has an id.
|
|
37
|
+
*/
|
|
38
|
+
const typeCheck = (tokenVar, name) => {
|
|
39
|
+
const id = tokenIds ? tokenIds.get(name) : undefined
|
|
40
|
+
if (id === undefined) return `${tokenVar}.type === ${JSON.stringify(name)}`
|
|
41
|
+
return `(${tokenVar}.id === ${id} || (${tokenVar}.id === undefined && ${tokenVar}.type === ${JSON.stringify(name)}))`
|
|
42
|
+
}
|
|
30
43
|
|
|
31
44
|
/** Runtime values the generated code closes over. */
|
|
32
45
|
const HELPERS = []
|
|
@@ -99,7 +112,7 @@ export function createCodegen (config) {
|
|
|
99
112
|
out.push(
|
|
100
113
|
`let ${result};`,
|
|
101
114
|
`const ${token} = c.tokens[c.idx];`,
|
|
102
|
-
`if (${token} !== undefined && ${token
|
|
115
|
+
`if (${token} !== undefined && ${typeCheck(token, name)}) { c.idx++; ${result} = ${token}; }`,
|
|
103
116
|
`else { E(c, ${JSON.stringify(name)}); ${result} = F; }`
|
|
104
117
|
)
|
|
105
118
|
return result
|
|
@@ -212,7 +225,7 @@ export function createCodegen (config) {
|
|
|
212
225
|
case 'oneOf': {
|
|
213
226
|
const names = Array.isArray(arg) ? arg : [arg]
|
|
214
227
|
const token = nextId()
|
|
215
|
-
const test = names.map((n) =>
|
|
228
|
+
const test = names.map((n) => typeCheck(token, n)).join(' || ')
|
|
216
229
|
out.push(
|
|
217
230
|
`let ${result};`,
|
|
218
231
|
`const ${token} = c.tokens[c.idx];`,
|
|
@@ -520,29 +533,75 @@ export function createCodegen (config) {
|
|
|
520
533
|
return result
|
|
521
534
|
}
|
|
522
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Emits an inline ordered-backtracking block over `list`, into a fresh
|
|
538
|
+
* variable. Used both for a whole `alt` with no dispatch table, and for the
|
|
539
|
+
* residual subset of branches a dispatch table cannot separate by one
|
|
540
|
+
* token.
|
|
541
|
+
*/
|
|
542
|
+
function genOrderedBacktrack (list, out, bindingsVar) {
|
|
543
|
+
const result = nextId()
|
|
544
|
+
const label = 'L' + nextId()
|
|
545
|
+
const start = nextId()
|
|
546
|
+
out.push(`let ${result};`, `${label}: {`, `const ${start} = c.idx;`)
|
|
547
|
+
for (const branch of list) {
|
|
548
|
+
out.push('{')
|
|
549
|
+
const value = gen(branch, out, bindingsVar)
|
|
550
|
+
out.push(`if (${value} !== F) { ${result} = ${value}; break ${label}; }`, `c.idx = ${start};`, '}')
|
|
551
|
+
}
|
|
552
|
+
out.push(`${result} = F;`, '}')
|
|
553
|
+
return result
|
|
554
|
+
}
|
|
555
|
+
|
|
523
556
|
function genAlt (branches, out, result, bindingsVar) {
|
|
524
557
|
if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches")
|
|
525
558
|
|
|
559
|
+
const factored = recognizeAltPrefixIdiom(branches)
|
|
560
|
+
if (factored !== null) return gen(factored, out, bindingsVar)
|
|
561
|
+
|
|
526
562
|
const table = firsts ? buildDispatch(branches, firsts) : null
|
|
527
563
|
const label = 'L' + nextId()
|
|
528
564
|
|
|
529
565
|
if (table !== null) {
|
|
530
566
|
// A switch on the token type: V8 turns a dense string switch into a
|
|
531
567
|
// hash lookup with inlined bodies, and every branch is inlined here.
|
|
568
|
+
// A table value that is an array is a residual group — tokens shared by
|
|
569
|
+
// more than one branch — resolved by ordered backtracking over just
|
|
570
|
+
// that subset instead of a single inlined branch.
|
|
571
|
+
//
|
|
572
|
+
// Switching on token.id (an integer) instead of token.type (a string)
|
|
573
|
+
// when every expected token resolves to one — a numeric switch is a jump
|
|
574
|
+
// table without even the string-hashing step a dense string switch still
|
|
575
|
+
// does. `I[token.type]` is the same fallback typeCheck uses: a hand-built
|
|
576
|
+
// token from parseTokens has no id, and I is a name -> id table built
|
|
577
|
+
// once from the same tokenIds this parser resolved every other id from.
|
|
532
578
|
const expected = [...table.keys()]
|
|
579
|
+
const expectedIds = tokenIds ? expected.map((t) => tokenIds.get(t)) : null
|
|
580
|
+
const numeric = expectedIds !== null && expectedIds.every((id) => id !== undefined)
|
|
533
581
|
const token = nextId()
|
|
534
582
|
out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${token} = c.tokens[c.idx];`)
|
|
535
583
|
out.push(`if (${token} === undefined) { ${expected.map((t) => `E(c, ${JSON.stringify(t)});`).join(' ')} ${result} = F; break ${label}; }`)
|
|
536
|
-
|
|
584
|
+
|
|
585
|
+
let discriminant
|
|
586
|
+
if (numeric) {
|
|
587
|
+
discriminant = nextId()
|
|
588
|
+
out.push(`const ${discriminant} = ${token}.id !== undefined ? ${token}.id : I[${token}.type];`)
|
|
589
|
+
out.push(`switch (${discriminant}) {`)
|
|
590
|
+
} else {
|
|
591
|
+
discriminant = `${token}.type`
|
|
592
|
+
out.push(`switch (${discriminant}) {`)
|
|
593
|
+
}
|
|
537
594
|
|
|
538
595
|
const byBranch = new Map()
|
|
539
|
-
for (const
|
|
596
|
+
for (const tokenName of expected) {
|
|
597
|
+
const branch = table.get(tokenName)
|
|
540
598
|
if (!byBranch.has(branch)) byBranch.set(branch, [])
|
|
541
599
|
byBranch.get(branch).push(tokenName)
|
|
542
600
|
}
|
|
601
|
+
const caseLabel = (t) => numeric ? tokenIds.get(t) : JSON.stringify(t)
|
|
543
602
|
for (const [branch, tokenNames] of byBranch) {
|
|
544
|
-
out.push(tokenNames.map((t) => `case ${
|
|
545
|
-
const value = gen(branch, out, bindingsVar)
|
|
603
|
+
out.push(tokenNames.map((t) => `case ${caseLabel(t)}:`).join(' ') + ' {')
|
|
604
|
+
const value = Array.isArray(branch) ? genOrderedBacktrack(branch, out, bindingsVar) : gen(branch, out, bindingsVar)
|
|
546
605
|
out.push(`${result} = ${value}; break ${label};`, '}')
|
|
547
606
|
}
|
|
548
607
|
out.push(
|
|
@@ -554,17 +613,13 @@ export function createCodegen (config) {
|
|
|
554
613
|
}
|
|
555
614
|
|
|
556
615
|
// Ordered backtracking, still fully inlined.
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
616
|
+
out.push(tickSrc)
|
|
617
|
+
return genOrderedBacktrack(branches, out, bindingsVar)
|
|
566
618
|
}
|
|
567
619
|
|
|
620
|
+
/** Name -> id, for a generated switch's runtime fallback when token.id is missing. */
|
|
621
|
+
const idByName = tokenIds ? Object.fromEntries(tokenIds) : null
|
|
622
|
+
|
|
568
623
|
/** Generates and installs the function for one rule. */
|
|
569
624
|
function buildRule (name) {
|
|
570
625
|
const out = []
|
|
@@ -576,8 +631,8 @@ export function createCodegen (config) {
|
|
|
576
631
|
SOURCES[ruleIndex.get(name)] = body
|
|
577
632
|
const source = `return ${body}`
|
|
578
633
|
// 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)
|
|
634
|
+
const factory = new Function('F', 'E', 'P', 'T', 'R', 'H', 'I', source)
|
|
635
|
+
return factory(FAIL, expect, progressError, stepsError, RULES, HELPERS, idByName)
|
|
581
636
|
}
|
|
582
637
|
|
|
583
638
|
function progressError (op, at) {
|
package/src/emit.js
CHANGED
|
@@ -139,9 +139,13 @@ export function emitModule (spec, options = {}) {
|
|
|
139
139
|
// The planner is needed only so codegen can record what each helper is; the
|
|
140
140
|
// closures it makes are thrown away.
|
|
141
141
|
const firsts = ll1 ? analysis.firsts : null
|
|
142
|
+
// The generated file's own `createLexer(tokenDefs, ...)` call assigns the same
|
|
143
|
+
// ids, from the same `tokenDefs` array in the same order — see the matching
|
|
144
|
+
// comment in parser.js.
|
|
145
|
+
const tokenIds = new Map(lexer.tokenNames.map((name, id) => [name, id]))
|
|
142
146
|
const engine = { build: () => { throw new Error('not compiled during emit') }, run: () => {} }
|
|
143
|
-
const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval: false })
|
|
144
|
-
const codegen = createCodegen({ planner, rules, firsts, memo, maxSteps })
|
|
147
|
+
const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval: false, tokenIds })
|
|
148
|
+
const codegen = createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds })
|
|
145
149
|
|
|
146
150
|
codegen.compile(start)
|
|
147
151
|
|
|
@@ -335,6 +339,14 @@ const H = [
|
|
|
335
339
|
${helpers.map((source, index) => ` /* ${index} */ ${source}`).join(',\n')}
|
|
336
340
|
]
|
|
337
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Name -> id, the same numbering \`lexer\` assigns. A generated \`alt\`'s switch
|
|
344
|
+
* dispatches on a token's id when it has one; this is only read as a fallback,
|
|
345
|
+
* for a hand-built token from \`parseTokens\` that bypassed this file's own
|
|
346
|
+
* lexer and so has no id of its own.
|
|
347
|
+
*/
|
|
348
|
+
const I = ${literal(Object.fromEntries(tokenIds))}
|
|
349
|
+
|
|
338
350
|
${ruleSources}
|
|
339
351
|
|
|
340
352
|
const RULES = ${literal(Object.fromEntries(reached.map(([name, index]) => [name, index])))}
|
package/src/lexer.js
CHANGED
|
@@ -24,11 +24,19 @@ import { firstCharSet } from './firstchars.js'
|
|
|
24
24
|
/**
|
|
25
25
|
* @typedef {object} Token
|
|
26
26
|
* @property {string} type
|
|
27
|
+
* @property {number} [id] The token definition's declaration index, present
|
|
28
|
+
* in `offset` position mode only — see the note on
|
|
29
|
+
* `positions` below. Not part of the public contract
|
|
30
|
+
* of a token — internal combinators use it as a
|
|
31
|
+
* faster stand-in for `type` (an integer compare and
|
|
32
|
+
* an array index instead of a string compare and a
|
|
33
|
+
* property lookup); `type` is what a grammar
|
|
34
|
+
* author's code should read.
|
|
27
35
|
* @property {string} image
|
|
28
36
|
* @property {number} start
|
|
29
37
|
* @property {number} end
|
|
30
|
-
* @property {number} line
|
|
31
|
-
* @property {number} col
|
|
38
|
+
* @property {number} [line] Present in `full` position mode only.
|
|
39
|
+
* @property {number} [col] Present in `full` position mode only.
|
|
32
40
|
*/
|
|
33
41
|
|
|
34
42
|
export class LexError extends Error {
|
|
@@ -76,9 +84,13 @@ function modesOf (def) {
|
|
|
76
84
|
* @param {TokenDef[]} defs
|
|
77
85
|
* @param {{ positions?: 'full' | 'offset' }} [options]
|
|
78
86
|
* `full` (default) records line and column on every token. `offset` records
|
|
79
|
-
* only `start`/`end`, which skips a scan of the entire input for
|
|
80
|
-
* worth taking when errors do not need to name a line, or when
|
|
81
|
-
* resolved lazily from the offset afterwards.
|
|
87
|
+
* only `start`/`end` plus `id`, which skips a scan of the entire input for
|
|
88
|
+
* newlines — worth taking when errors do not need to name a line, or when
|
|
89
|
+
* positions are resolved lazily from the offset afterwards. The two modes
|
|
90
|
+
* deliberately differ in which extra field they carry rather than both
|
|
91
|
+
* carrying every field: a 7th own property on the token object, in either
|
|
92
|
+
* mode, measured at a real ~30% tokenize regression (see the comment at the
|
|
93
|
+
* push site) that a 6-field object does not pay.
|
|
82
94
|
* @returns {{ tokenize: (text: string) => Token[], tokenNames: string[], positions: string, modeNames: string[] }}
|
|
83
95
|
*/
|
|
84
96
|
export function createLexer (defs, options = {}) {
|
|
@@ -109,7 +121,7 @@ export function createLexer (defs, options = {}) {
|
|
|
109
121
|
}
|
|
110
122
|
for (const def of defs) for (const mode of modesOf(def)) idOf(mode)
|
|
111
123
|
|
|
112
|
-
const compiled = defs.map((def) => {
|
|
124
|
+
const compiled = defs.map((def, id) => {
|
|
113
125
|
if (!def.name) throw new Error('createLexer: every token definition needs a name')
|
|
114
126
|
if (!def.pattern && !def.literal) throw new Error(`createLexer: token '${def.name}' needs a pattern or a literal`)
|
|
115
127
|
if (def.pattern && def.literal) throw new Error(`createLexer: token '${def.name}' has both a pattern and a literal; pick one`)
|
|
@@ -128,6 +140,11 @@ export function createLexer (defs, options = {}) {
|
|
|
128
140
|
|
|
129
141
|
return {
|
|
130
142
|
name: def.name,
|
|
143
|
+
// The declaration index. `createParser` derives the identical mapping
|
|
144
|
+
// from `tokenNames` independently (same array, same order), so a
|
|
145
|
+
// combinator that resolves a `consume` target's id at build time agrees
|
|
146
|
+
// with what a token actually carries without the two ever comparing notes.
|
|
147
|
+
id,
|
|
131
148
|
skip: !!def.skip,
|
|
132
149
|
literal: def.literal,
|
|
133
150
|
longerAlt: def.longerAlt || null,
|
|
@@ -283,16 +300,21 @@ export function createLexer (defs, options = {}) {
|
|
|
283
300
|
const image = winner.literal !== undefined
|
|
284
301
|
? winner.literal
|
|
285
302
|
: (winner === def && matched !== null ? matched : text.slice(offset, end))
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
303
|
+
// Two shapes, not one: a 7th own property on this literal — in
|
|
304
|
+
// either position mode — measured at a genuine ~30% tokenize
|
|
305
|
+
// regression in this exact function, reproducible and isolated to
|
|
306
|
+
// the field count alone (a constant 7th field regresses exactly as
|
|
307
|
+
// much as a computed one), but one that does not reduce to a
|
|
308
|
+
// minimal repro elsewhere. Each mode instead stays at 6 own
|
|
309
|
+
// properties: `id` only in offset mode, where line/col would have
|
|
310
|
+
// been dead weight (always 0) anyway; line/col only in full mode,
|
|
311
|
+
// where `id` is what gets dropped. `token.id === undefined` is
|
|
312
|
+
// already how consume/oneOf/alt's dispatch fall back to comparing
|
|
313
|
+
// `type` for a hand-built token from parseTokens, so a full-mode
|
|
314
|
+
// token needs no special casing there — it takes the same fallback.
|
|
315
|
+
tokens.push(trackLines
|
|
316
|
+
? { type: winner.name, image, start: offset, end, line, col: offset - lineStart + 1 }
|
|
317
|
+
: { type: winner.name, id: winner.id, image, start: offset, end })
|
|
296
318
|
}
|
|
297
319
|
|
|
298
320
|
// A bounded scan over this token's own span, so the whole tokenize costs
|
package/src/parser.js
CHANGED
|
@@ -114,9 +114,16 @@ export function createParser (spec, options = {}) {
|
|
|
114
114
|
const startRule = rules[start]
|
|
115
115
|
const firsts = ll1 ? analysis.firsts : null
|
|
116
116
|
|
|
117
|
+
// Declaration index of each token name, the same numbering `createLexer`
|
|
118
|
+
// assigns to the tokens it produces (both derive it from the same array in
|
|
119
|
+
// the same order). `consume` compares this instead of the string `type`
|
|
120
|
+
// where it can — an integer compare instead of a string compare, on the
|
|
121
|
+
// hottest path in the parser.
|
|
122
|
+
const tokenIds = new Map(lexer.tokenNames.map((name, id) => [name, id]))
|
|
123
|
+
|
|
117
124
|
// The planner specializes the grammar into closures; the combinators'
|
|
118
125
|
// `compile` hooks reach it through the engine.
|
|
119
|
-
const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval })
|
|
126
|
+
const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval, tokenIds })
|
|
120
127
|
engine.__jlGrammar = { planner }
|
|
121
128
|
|
|
122
129
|
// Source generation emits one JS function per rule with every node inlined,
|
|
@@ -125,7 +132,7 @@ export function createParser (spec, options = {}) {
|
|
|
125
132
|
// lands on the generated code rather than a method dispatch.
|
|
126
133
|
// Code generation needs `new Function`, so it is off whenever eval is.
|
|
127
134
|
const codegen = compile && unsafeEval
|
|
128
|
-
? createCodegen({ planner, rules, firsts, memo, maxSteps })
|
|
135
|
+
? createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds })
|
|
129
136
|
: null
|
|
130
137
|
const compiledStart = codegen && codegen.compile(start)
|
|
131
138
|
|
package/src/plan.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
4
|
import { FAIL, expect } from './state.js'
|
|
5
|
-
import { buildDispatch, opOf, recognizeSeqIdiom } from './analyze.js'
|
|
5
|
+
import { buildDispatch, opOf, recognizeSeqIdiom, recognizeAltPrefixIdiom } from './analyze.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The specializing planner.
|
|
@@ -76,10 +76,11 @@ function mayReturnContext (logic) {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* @param {{ engine: any, rules: Record<string, any>, firsts: Map<string, any>|null,
|
|
79
|
-
* memo: boolean, maxSteps: number, unsafeEval?: boolean
|
|
79
|
+
* memo: boolean, maxSteps: number, unsafeEval?: boolean,
|
|
80
|
+
* tokenIds?: Map<string, number>|null }} config
|
|
80
81
|
*/
|
|
81
82
|
export function createPlanner (config) {
|
|
82
|
-
const { engine, rules, firsts, memo, maxSteps, unsafeEval = true } = config
|
|
83
|
+
const { engine, rules, firsts, memo, maxSteps, unsafeEval = true, tokenIds = null } = config
|
|
83
84
|
|
|
84
85
|
/** @type {Map<string, { fn: ((state: any) => any) | null }>} */
|
|
85
86
|
const slots = new Map()
|
|
@@ -227,9 +228,14 @@ export function createPlanner (config) {
|
|
|
227
228
|
/** @returns {(state: any) => any} */
|
|
228
229
|
const buildConsume = (arg) => {
|
|
229
230
|
const name = typeof arg === 'string' ? arg : arg[0]
|
|
231
|
+
const id = tokenIds ? tokenIds.get(name) : undefined
|
|
232
|
+
// `token.id` is undefined only for a hand-built token that bypassed
|
|
233
|
+
// `createLexer` (parseTokens accepts any array shaped like a Token), in
|
|
234
|
+
// which case falling back to the string compare keeps that documented
|
|
235
|
+
// path working; every token this parser's own lexer produces has an id.
|
|
230
236
|
return (state) => {
|
|
231
237
|
const token = state.tokens[state.idx]
|
|
232
|
-
if (token !== undefined && token.type === name) {
|
|
238
|
+
if (token !== undefined && (token.id === id || (token.id === undefined && token.type === name))) {
|
|
233
239
|
state.idx++
|
|
234
240
|
return token
|
|
235
241
|
}
|
|
@@ -348,10 +354,11 @@ export function createPlanner (config) {
|
|
|
348
354
|
// Terser than an `alt` of `consume`s, and a single set lookup.
|
|
349
355
|
const names = Array.isArray(arg) ? arg : [arg]
|
|
350
356
|
const set = new Set(names)
|
|
357
|
+
const idSet = tokenIds ? new Set(names.map((n) => tokenIds.get(n))) : null
|
|
351
358
|
const count = names.length
|
|
352
359
|
return (state) => {
|
|
353
360
|
const token = state.tokens[state.idx]
|
|
354
|
-
if (token !== undefined && set.has(token.type)) {
|
|
361
|
+
if (token !== undefined && (idSet ? (token.id !== undefined ? idSet.has(token.id) : set.has(token.type)) : set.has(token.type))) {
|
|
355
362
|
state.idx++
|
|
356
363
|
return token
|
|
357
364
|
}
|
|
@@ -700,6 +707,7 @@ export function createPlanner (config) {
|
|
|
700
707
|
const kinds = []
|
|
701
708
|
const names = [] // binding name, for the labelled kinds
|
|
702
709
|
const tokens = [] // token type, for the terminal kinds
|
|
710
|
+
const tokenIdList = [] // token.id equivalent of `tokens`, for the terminal kinds
|
|
703
711
|
const fns = []
|
|
704
712
|
const actionLogic = [] // raw logic of each action child, for the reuse check
|
|
705
713
|
let hasAction = false
|
|
@@ -724,11 +732,13 @@ export function createPlanner (config) {
|
|
|
724
732
|
kinds.push(LABELLED_TERMINAL)
|
|
725
733
|
names.push(name)
|
|
726
734
|
tokens.push(terminal)
|
|
735
|
+
tokenIdList.push(tokenIds ? tokenIds.get(terminal) : undefined)
|
|
727
736
|
fns.push(null)
|
|
728
737
|
} else {
|
|
729
738
|
kinds.push(LABELLED)
|
|
730
739
|
names.push(name)
|
|
731
740
|
tokens.push(null)
|
|
741
|
+
tokenIdList.push(undefined)
|
|
732
742
|
fns.push(plan(inner))
|
|
733
743
|
}
|
|
734
744
|
continue
|
|
@@ -738,6 +748,7 @@ export function createPlanner (config) {
|
|
|
738
748
|
kinds.push(ACTION)
|
|
739
749
|
names.push(null)
|
|
740
750
|
tokens.push(null)
|
|
751
|
+
tokenIdList.push(undefined)
|
|
741
752
|
fns.push(planAction(child.action))
|
|
742
753
|
actionLogic.push(child.action)
|
|
743
754
|
hasAction = true
|
|
@@ -749,11 +760,13 @@ export function createPlanner (config) {
|
|
|
749
760
|
kinds.push(TERMINAL)
|
|
750
761
|
names.push(null)
|
|
751
762
|
tokens.push(terminal)
|
|
763
|
+
tokenIdList.push(tokenIds ? tokenIds.get(terminal) : undefined)
|
|
752
764
|
fns.push(null)
|
|
753
765
|
} else {
|
|
754
766
|
kinds.push(PARSER)
|
|
755
767
|
names.push(null)
|
|
756
768
|
tokens.push(null)
|
|
769
|
+
tokenIdList.push(undefined)
|
|
757
770
|
fns.push(plan(child))
|
|
758
771
|
}
|
|
759
772
|
}
|
|
@@ -803,7 +816,7 @@ export function createPlanner (config) {
|
|
|
803
816
|
|
|
804
817
|
if (kind === TERMINAL || kind === LABELLED_TERMINAL) {
|
|
805
818
|
const token = state.tokens[state.idx]
|
|
806
|
-
if (token !== undefined && token.type === tokens[i]) {
|
|
819
|
+
if (token !== undefined && (token.id === tokenIdList[i] || (token.id === undefined && token.type === tokens[i]))) {
|
|
807
820
|
state.idx++
|
|
808
821
|
value = token
|
|
809
822
|
} else {
|
|
@@ -837,6 +850,12 @@ export function createPlanner (config) {
|
|
|
837
850
|
function planAlt (branches) {
|
|
838
851
|
if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches")
|
|
839
852
|
|
|
853
|
+
// A shared, possibly multi-token prefix across every branch is matched
|
|
854
|
+
// once here rather than once per branch that has it — see the comment on
|
|
855
|
+
// recognizeAltPrefixIdiom for the safety conditions.
|
|
856
|
+
const factored = recognizeAltPrefixIdiom(branches)
|
|
857
|
+
if (factored !== null) return planUncached(factored)
|
|
858
|
+
|
|
840
859
|
const planned = branches.map(plan)
|
|
841
860
|
const table = firsts ? buildDispatch(branches, firsts) : null
|
|
842
861
|
|
|
@@ -850,8 +869,39 @@ export function createPlanner (config) {
|
|
|
850
869
|
// internalized string is an inline-cached property load instead. The null
|
|
851
870
|
// prototype also means a token type of `toString` or `constructor` cannot
|
|
852
871
|
// find an inherited value.
|
|
872
|
+
//
|
|
873
|
+
// byId is the same table again, indexed by token.id instead of
|
|
874
|
+
// token.type — an array index instead of a property load. Used whenever
|
|
875
|
+
// a token has an id; byToken stays as the fallback for a hand-built
|
|
876
|
+
// token from parseTokens, which has none.
|
|
853
877
|
const byToken = Object.create(null)
|
|
854
|
-
|
|
878
|
+
const byId = tokenIds ? [] : null
|
|
879
|
+
const groupClosures = new Map()
|
|
880
|
+
for (const [token, branch] of table) {
|
|
881
|
+
let closure = groupClosures.get(branch)
|
|
882
|
+
if (closure === undefined) {
|
|
883
|
+
closure = Array.isArray(branch)
|
|
884
|
+
? (() => {
|
|
885
|
+
const group = branch.map((b) => planned[branches.indexOf(b)])
|
|
886
|
+
return (state) => {
|
|
887
|
+
const start = state.idx
|
|
888
|
+
for (let i = 0; i < group.length; i++) {
|
|
889
|
+
const value = group[i](state)
|
|
890
|
+
if (value !== FAIL) return value
|
|
891
|
+
state.idx = start
|
|
892
|
+
}
|
|
893
|
+
return FAIL
|
|
894
|
+
}
|
|
895
|
+
})()
|
|
896
|
+
: planned[branches.indexOf(branch)]
|
|
897
|
+
groupClosures.set(branch, closure)
|
|
898
|
+
}
|
|
899
|
+
byToken[token] = closure
|
|
900
|
+
if (byId) {
|
|
901
|
+
const id = tokenIds.get(token)
|
|
902
|
+
if (id !== undefined) byId[id] = closure
|
|
903
|
+
}
|
|
904
|
+
}
|
|
855
905
|
const expectedTokens = [...table.keys()]
|
|
856
906
|
const expectedCount = expectedTokens.length
|
|
857
907
|
|
|
@@ -859,7 +909,7 @@ export function createPlanner (config) {
|
|
|
859
909
|
tick(state)
|
|
860
910
|
const token = state.tokens[state.idx]
|
|
861
911
|
if (token !== undefined) {
|
|
862
|
-
const branch = byToken[token.type]
|
|
912
|
+
const branch = byId && token.id !== undefined ? byId[token.id] : byToken[token.type]
|
|
863
913
|
if (branch !== undefined) return branch(state)
|
|
864
914
|
}
|
|
865
915
|
for (let i = 0; i < expectedCount; i++) expect(state, expectedTokens[i])
|