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/src/parser.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createValueMethods } from './values.js'
|
|
|
9
9
|
import { analyze } from './analyze.js'
|
|
10
10
|
import { createPlanner } from './plan.js'
|
|
11
11
|
import { createCodegen } from './codegen.js'
|
|
12
|
+
import { selectMemoRules } from './memoplan.js'
|
|
12
13
|
import { FAIL, createState, buildParseError, ParseError } from './state.js'
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -23,6 +24,21 @@ import { FAIL, createState, buildParseError, ParseError } from './state.js'
|
|
|
23
24
|
* @property {boolean} [memo=false] Enable packrat memoization. Costs a Map
|
|
24
25
|
* write per rule invocation; pays for itself
|
|
25
26
|
* only on grammars that backtrack heavily.
|
|
27
|
+
* @property {boolean} [autoMemo=true] Memoize just the rules that would otherwise be
|
|
28
|
+
* re-parsed by several colliding branches of an
|
|
29
|
+
* `alt` (see `selectMemoRules`). Unlike `memo` it
|
|
30
|
+
* costs nothing on a grammar with no such rule,
|
|
31
|
+
* and it is ignored when `memo` is on.
|
|
32
|
+
* @property {boolean} [inlineRules=true] In generated code, write single-caller operator
|
|
33
|
+
* levels (`infixLeft`, `infixRight`, `prefix`, `postfix`)
|
|
34
|
+
* into their caller instead of calling them. Costs code
|
|
35
|
+
* size, which is why `emitModule` defaults it off.
|
|
36
|
+
* @property {boolean} [idioms=true] Recognize common spellings (`seq(open, label, close, action)`,
|
|
37
|
+
* a prefix plus one labelled result, a constant action, an
|
|
38
|
+
* `alt` of bare `consume`s, a shared leading run of `alt`
|
|
39
|
+
* branches) and run them as the
|
|
40
|
+
* cheaper combinator they are equivalent to. Off runs the
|
|
41
|
+
* grammar exactly as written; for debugging and benchmarking.
|
|
26
42
|
* @property {boolean} [strict=true] Require the whole token stream to be consumed.
|
|
27
43
|
* @property {boolean} [validate=true] Throw on grammar errors (left recursion, bad refs).
|
|
28
44
|
* @property {number} [maxSteps=0] Runaway guard; 0 disables.
|
|
@@ -75,6 +91,9 @@ export function createParser (spec, options = {}) {
|
|
|
75
91
|
const { tokens: tokenDefs, rules, start } = spec
|
|
76
92
|
const {
|
|
77
93
|
memo = false,
|
|
94
|
+
autoMemo = true,
|
|
95
|
+
inlineRules = true,
|
|
96
|
+
idioms = true,
|
|
78
97
|
strict = true,
|
|
79
98
|
validate = true,
|
|
80
99
|
maxSteps = 0,
|
|
@@ -114,9 +133,23 @@ export function createParser (spec, options = {}) {
|
|
|
114
133
|
const startRule = rules[start]
|
|
115
134
|
const firsts = ll1 ? analysis.firsts : null
|
|
116
135
|
|
|
136
|
+
// Declaration index of each token name, the same numbering `createLexer`
|
|
137
|
+
// assigns to the tokens it produces (both derive it from the same array in
|
|
138
|
+
// the same order). `consume` compares this instead of the string `type`
|
|
139
|
+
// where it can — an integer compare instead of a string compare, on the
|
|
140
|
+
// hottest path in the parser.
|
|
141
|
+
const tokenIds = new Map(lexer.tokenNames.map((name, id) => [name, id]))
|
|
142
|
+
|
|
117
143
|
// The planner specializes the grammar into closures; the combinators'
|
|
118
144
|
// `compile` hooks reach it through the engine.
|
|
119
|
-
|
|
145
|
+
// Which rules to remember per position. Global `memo` already remembers all of
|
|
146
|
+
// them, and `validate: false` can hand us a grammar the analysis did not accept.
|
|
147
|
+
const memoRules = autoMemo && !memo && analysis.errors.length === 0
|
|
148
|
+
? selectMemoRules(rules, analysis.firsts, { ll1 })
|
|
149
|
+
: []
|
|
150
|
+
const memoSlots = new Map(memoRules.map((name, slot) => [name, slot]))
|
|
151
|
+
|
|
152
|
+
const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval, tokenIds, memoSlots, idioms })
|
|
120
153
|
engine.__jlGrammar = { planner }
|
|
121
154
|
|
|
122
155
|
// Source generation emits one JS function per rule with every node inlined,
|
|
@@ -125,7 +158,7 @@ export function createParser (spec, options = {}) {
|
|
|
125
158
|
// lands on the generated code rather than a method dispatch.
|
|
126
159
|
// Code generation needs `new Function`, so it is off whenever eval is.
|
|
127
160
|
const codegen = compile && unsafeEval
|
|
128
|
-
? createCodegen({ planner, rules, firsts, memo, maxSteps })
|
|
161
|
+
? createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds, memoSlots, inlineRules, idioms })
|
|
129
162
|
: null
|
|
130
163
|
const compiledStart = codegen && codegen.compile(start)
|
|
131
164
|
|
|
@@ -142,7 +175,7 @@ export function createParser (spec, options = {}) {
|
|
|
142
175
|
* @param {string} [source] Original text, so `text` can slice exact spans.
|
|
143
176
|
*/
|
|
144
177
|
function parseTokens (tokens, source, track = false) {
|
|
145
|
-
const state = createState(tokens, rules, { memo, maxSteps, track })
|
|
178
|
+
const state = createState(tokens, rules, { memo, maxSteps, track, memoSlots: memoRules.length })
|
|
146
179
|
state.firsts = firsts
|
|
147
180
|
state.source = source
|
|
148
181
|
state.positions = lexer.positions
|
|
@@ -185,6 +218,8 @@ export function createParser (spec, options = {}) {
|
|
|
185
218
|
planner,
|
|
186
219
|
codegen,
|
|
187
220
|
execution: mode,
|
|
221
|
+
/** Rules memoized by `autoMemo`, for introspection. */
|
|
222
|
+
memoRules,
|
|
188
223
|
compiled: Boolean(compiledStart),
|
|
189
224
|
/** Rules whose `alt`s resolved to O(1) LL(1) dispatch, for introspection. */
|
|
190
225
|
get firsts () { return analysis.firsts }
|
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, recognizeAltConsumeIdiom, usesOuterScope } from './analyze.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The specializing planner.
|
|
@@ -31,27 +31,6 @@ function declareSync (fn) {
|
|
|
31
31
|
return fn
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
* True when the logic reads from an outer scope (`{val: [[-1], ...]}` or
|
|
36
|
-
* `{var: "../x"}`). The engine's compiler does not thread `above` into built
|
|
37
|
-
* functions, so such actions must stay interpreted.
|
|
38
|
-
*/
|
|
39
|
-
function usesOuterScope (logic) {
|
|
40
|
-
if (Array.isArray(logic)) return logic.some(usesOuterScope)
|
|
41
|
-
if (!logic || typeof logic !== 'object') return false
|
|
42
|
-
|
|
43
|
-
for (const key of Object.keys(logic)) {
|
|
44
|
-
const arg = logic[key]
|
|
45
|
-
if (key === 'val' && Array.isArray(arg) && Array.isArray(arg[0])) return true
|
|
46
|
-
if (key === 'var') {
|
|
47
|
-
if (typeof arg === 'string' && arg.includes('../')) return true
|
|
48
|
-
if (Array.isArray(arg) && typeof arg[0] === 'string' && arg[0].includes('../')) return true
|
|
49
|
-
}
|
|
50
|
-
if (usesOuterScope(arg)) return true
|
|
51
|
-
}
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
|
|
55
34
|
/**
|
|
56
35
|
* True when the logic could return the *context object itself* rather than a
|
|
57
36
|
* value derived from it — `{"var": ""}` and `{"val": []}` both do.
|
|
@@ -76,10 +55,12 @@ function mayReturnContext (logic) {
|
|
|
76
55
|
|
|
77
56
|
/**
|
|
78
57
|
* @param {{ engine: any, rules: Record<string, any>, firsts: Map<string, any>|null,
|
|
79
|
-
* memo: boolean, maxSteps: number, unsafeEval?: boolean
|
|
58
|
+
* memo: boolean, maxSteps: number, unsafeEval?: boolean,
|
|
59
|
+
* tokenIds?: Map<string, number>|null,
|
|
60
|
+
* memoSlots?: Map<string, number>, idioms?: boolean }} config
|
|
80
61
|
*/
|
|
81
62
|
export function createPlanner (config) {
|
|
82
|
-
const { engine, rules, firsts, memo, maxSteps, unsafeEval = true } = config
|
|
63
|
+
const { engine, rules, firsts, memo, maxSteps, unsafeEval = true, tokenIds = null, memoSlots = new Map(), idioms = true } = config
|
|
83
64
|
|
|
84
65
|
/** @type {Map<string, { fn: ((state: any) => any) | null }>} */
|
|
85
66
|
const slots = new Map()
|
|
@@ -227,9 +208,14 @@ export function createPlanner (config) {
|
|
|
227
208
|
/** @returns {(state: any) => any} */
|
|
228
209
|
const buildConsume = (arg) => {
|
|
229
210
|
const name = typeof arg === 'string' ? arg : arg[0]
|
|
211
|
+
const id = tokenIds ? tokenIds.get(name) : undefined
|
|
212
|
+
// `token.id` is undefined only for a hand-built token that bypassed
|
|
213
|
+
// `createLexer` (parseTokens accepts any array shaped like a Token), in
|
|
214
|
+
// which case falling back to the string compare keeps that documented
|
|
215
|
+
// path working; every token this parser's own lexer produces has an id.
|
|
230
216
|
return (state) => {
|
|
231
217
|
const token = state.tokens[state.idx]
|
|
232
|
-
if (token !== undefined && token.type === name) {
|
|
218
|
+
if (token !== undefined && (token.id === id || (token.id === undefined && token.type === name))) {
|
|
233
219
|
state.idx++
|
|
234
220
|
return token
|
|
235
221
|
}
|
|
@@ -297,6 +283,25 @@ export function createPlanner (config) {
|
|
|
297
283
|
}
|
|
298
284
|
}
|
|
299
285
|
|
|
286
|
+
/**
|
|
287
|
+
* `{"flag": p}` — `true` when `p` matched, `false` when it did not; never fails.
|
|
288
|
+
* `option` with the answer already turned into a boolean, for the common
|
|
289
|
+
* `label(negated, option(consume(Not)))` followed by `{"!==": [negated, null]}`.
|
|
290
|
+
*
|
|
291
|
+
* @returns {(state: any) => any}
|
|
292
|
+
*/
|
|
293
|
+
const buildFlag = (arg) => {
|
|
294
|
+
const child = plan(arg)
|
|
295
|
+
return (state) => {
|
|
296
|
+
const start = state.idx
|
|
297
|
+
if (child(state) === FAIL) {
|
|
298
|
+
state.idx = start
|
|
299
|
+
return false
|
|
300
|
+
}
|
|
301
|
+
return true
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
300
305
|
/** @returns {(state: any) => any} */
|
|
301
306
|
const buildInfixLeft = (arg) => {
|
|
302
307
|
// A whole precedence level in one closure.
|
|
@@ -348,10 +353,11 @@ export function createPlanner (config) {
|
|
|
348
353
|
// Terser than an `alt` of `consume`s, and a single set lookup.
|
|
349
354
|
const names = Array.isArray(arg) ? arg : [arg]
|
|
350
355
|
const set = new Set(names)
|
|
356
|
+
const idSet = tokenIds ? new Set(names.map((n) => tokenIds.get(n))) : null
|
|
351
357
|
const count = names.length
|
|
352
358
|
return (state) => {
|
|
353
359
|
const token = state.tokens[state.idx]
|
|
354
|
-
if (token !== undefined && set.has(token.type)) {
|
|
360
|
+
if (token !== undefined && (idSet ? (token.id !== undefined ? idSet.has(token.id) : set.has(token.type)) : set.has(token.type))) {
|
|
355
361
|
state.idx++
|
|
356
362
|
return token
|
|
357
363
|
}
|
|
@@ -460,6 +466,35 @@ export function createPlanner (config) {
|
|
|
460
466
|
}
|
|
461
467
|
}
|
|
462
468
|
|
|
469
|
+
/**
|
|
470
|
+
* `{"pick": {parsers, index}}` and `{"yields": {parsers, value}}` run their parsers
|
|
471
|
+
* in order and give back one of the results or a constant. They are what a `seq`
|
|
472
|
+
* with a single labelled child and `action({val: label})`, or with a constant
|
|
473
|
+
* action, is rewritten to: neither needs the bindings object a `seq` allocates.
|
|
474
|
+
*
|
|
475
|
+
* @returns {(state: any) => any}
|
|
476
|
+
*/
|
|
477
|
+
const buildSequence = (arg, pickIndex, constant) => {
|
|
478
|
+
const fns = arg.parsers.map(plan)
|
|
479
|
+
const count = fns.length
|
|
480
|
+
return (state) => {
|
|
481
|
+
tick(state)
|
|
482
|
+
const start = state.idx
|
|
483
|
+
let picked
|
|
484
|
+
for (let i = 0; i < count; i++) {
|
|
485
|
+
const value = fns[i](state)
|
|
486
|
+
if (value === FAIL) {
|
|
487
|
+
state.idx = start
|
|
488
|
+
return FAIL
|
|
489
|
+
}
|
|
490
|
+
if (i === pickIndex) picked = value
|
|
491
|
+
}
|
|
492
|
+
return pickIndex === -1 ? constant : picked
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const buildPick = (arg) => buildSequence(arg, arg.index, undefined)
|
|
496
|
+
const buildYields = (arg) => buildSequence(arg, -1, arg.value)
|
|
497
|
+
|
|
463
498
|
/** @returns {(state: any) => any} */
|
|
464
499
|
const buildPrefix = (arg) => {
|
|
465
500
|
// Any number of prefix operators, folded right onto the operand:
|
|
@@ -560,6 +595,34 @@ export function createPlanner (config) {
|
|
|
560
595
|
return value
|
|
561
596
|
}
|
|
562
597
|
}
|
|
598
|
+
// A rule chosen by `selectMemoRules` remembers its result per position, so
|
|
599
|
+
// the branches of an `alt` that all start with it parse it once between
|
|
600
|
+
// them. Global `memo` already covers every rule, hence the order here.
|
|
601
|
+
const memoSlot = memoSlots.get(name)
|
|
602
|
+
if (memoSlot !== undefined) {
|
|
603
|
+
return (state) => {
|
|
604
|
+
tick(state)
|
|
605
|
+
const table = state.rm[memoSlot]
|
|
606
|
+
const at = state.idx
|
|
607
|
+
const end = table.ends[at]
|
|
608
|
+
if (end !== 0) {
|
|
609
|
+
state.idx = end - 1
|
|
610
|
+
return table.vals[at]
|
|
611
|
+
}
|
|
612
|
+
let depth = 0
|
|
613
|
+
if (state.track) {
|
|
614
|
+
depth = state.ruleDepth
|
|
615
|
+
state.ruleNames[depth] = name
|
|
616
|
+
state.ruleDepth = depth + 1
|
|
617
|
+
}
|
|
618
|
+
const value = slot.fn(state)
|
|
619
|
+
if (state.track) state.ruleDepth = depth
|
|
620
|
+
if (value === FAIL) state.idx = at
|
|
621
|
+
table.ends[at] = state.idx + 1
|
|
622
|
+
table.vals[at] = value
|
|
623
|
+
return value
|
|
624
|
+
}
|
|
625
|
+
}
|
|
563
626
|
return (state) => {
|
|
564
627
|
tick(state)
|
|
565
628
|
// See above: rule-stack bookkeeping is failure-path only.
|
|
@@ -657,6 +720,9 @@ export function createPlanner (config) {
|
|
|
657
720
|
infixRight: buildInfixRight,
|
|
658
721
|
as: buildAs,
|
|
659
722
|
between: buildBetween,
|
|
723
|
+
flag: buildFlag,
|
|
724
|
+
pick: buildPick,
|
|
725
|
+
yields: buildYields,
|
|
660
726
|
prefix: buildPrefix,
|
|
661
727
|
postfix: buildPostfix,
|
|
662
728
|
manySep: (arg) => planSeparated(arg, false),
|
|
@@ -683,7 +749,7 @@ export function createPlanner (config) {
|
|
|
683
749
|
// A `seq` written in one of the recognized idioms is planned as the cheaper
|
|
684
750
|
// combinator instead, so grammars written with the general primitives get
|
|
685
751
|
// the same treatment as ones written with the specific ones.
|
|
686
|
-
const idiom = recognizeSeqIdiom(children)
|
|
752
|
+
const idiom = idioms ? recognizeSeqIdiom(children) : null
|
|
687
753
|
if (idiom !== null) return planUncached(idiom)
|
|
688
754
|
|
|
689
755
|
// Step kinds. Terminals are inlined rather than dispatched to a closure:
|
|
@@ -700,6 +766,7 @@ export function createPlanner (config) {
|
|
|
700
766
|
const kinds = []
|
|
701
767
|
const names = [] // binding name, for the labelled kinds
|
|
702
768
|
const tokens = [] // token type, for the terminal kinds
|
|
769
|
+
const tokenIdList = [] // token.id equivalent of `tokens`, for the terminal kinds
|
|
703
770
|
const fns = []
|
|
704
771
|
const actionLogic = [] // raw logic of each action child, for the reuse check
|
|
705
772
|
let hasAction = false
|
|
@@ -724,11 +791,13 @@ export function createPlanner (config) {
|
|
|
724
791
|
kinds.push(LABELLED_TERMINAL)
|
|
725
792
|
names.push(name)
|
|
726
793
|
tokens.push(terminal)
|
|
794
|
+
tokenIdList.push(tokenIds ? tokenIds.get(terminal) : undefined)
|
|
727
795
|
fns.push(null)
|
|
728
796
|
} else {
|
|
729
797
|
kinds.push(LABELLED)
|
|
730
798
|
names.push(name)
|
|
731
799
|
tokens.push(null)
|
|
800
|
+
tokenIdList.push(undefined)
|
|
732
801
|
fns.push(plan(inner))
|
|
733
802
|
}
|
|
734
803
|
continue
|
|
@@ -738,6 +807,7 @@ export function createPlanner (config) {
|
|
|
738
807
|
kinds.push(ACTION)
|
|
739
808
|
names.push(null)
|
|
740
809
|
tokens.push(null)
|
|
810
|
+
tokenIdList.push(undefined)
|
|
741
811
|
fns.push(planAction(child.action))
|
|
742
812
|
actionLogic.push(child.action)
|
|
743
813
|
hasAction = true
|
|
@@ -749,11 +819,13 @@ export function createPlanner (config) {
|
|
|
749
819
|
kinds.push(TERMINAL)
|
|
750
820
|
names.push(null)
|
|
751
821
|
tokens.push(terminal)
|
|
822
|
+
tokenIdList.push(tokenIds ? tokenIds.get(terminal) : undefined)
|
|
752
823
|
fns.push(null)
|
|
753
824
|
} else {
|
|
754
825
|
kinds.push(PARSER)
|
|
755
826
|
names.push(null)
|
|
756
827
|
tokens.push(null)
|
|
828
|
+
tokenIdList.push(undefined)
|
|
757
829
|
fns.push(plan(child))
|
|
758
830
|
}
|
|
759
831
|
}
|
|
@@ -803,7 +875,7 @@ export function createPlanner (config) {
|
|
|
803
875
|
|
|
804
876
|
if (kind === TERMINAL || kind === LABELLED_TERMINAL) {
|
|
805
877
|
const token = state.tokens[state.idx]
|
|
806
|
-
if (token !== undefined && token.type === tokens[i]) {
|
|
878
|
+
if (token !== undefined && (token.id === tokenIdList[i] || (token.id === undefined && token.type === tokens[i]))) {
|
|
807
879
|
state.idx++
|
|
808
880
|
value = token
|
|
809
881
|
} else {
|
|
@@ -837,6 +909,15 @@ export function createPlanner (config) {
|
|
|
837
909
|
function planAlt (branches) {
|
|
838
910
|
if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches")
|
|
839
911
|
|
|
912
|
+
// A shared, possibly multi-token prefix across every branch is matched
|
|
913
|
+
// once here rather than once per branch that has it — see the comment on
|
|
914
|
+
// recognizeAltPrefixIdiom for the safety conditions.
|
|
915
|
+
const oneOf = idioms ? recognizeAltConsumeIdiom(branches) : null
|
|
916
|
+
if (oneOf !== null) return planUncached(oneOf)
|
|
917
|
+
|
|
918
|
+
const factored = idioms ? recognizeAltPrefixIdiom(branches) : null
|
|
919
|
+
if (factored !== null) return planUncached(factored)
|
|
920
|
+
|
|
840
921
|
const planned = branches.map(plan)
|
|
841
922
|
const table = firsts ? buildDispatch(branches, firsts) : null
|
|
842
923
|
|
|
@@ -850,8 +931,39 @@ export function createPlanner (config) {
|
|
|
850
931
|
// internalized string is an inline-cached property load instead. The null
|
|
851
932
|
// prototype also means a token type of `toString` or `constructor` cannot
|
|
852
933
|
// find an inherited value.
|
|
934
|
+
//
|
|
935
|
+
// byId is the same table again, indexed by token.id instead of
|
|
936
|
+
// token.type — an array index instead of a property load. Used whenever
|
|
937
|
+
// a token has an id; byToken stays as the fallback for a hand-built
|
|
938
|
+
// token from parseTokens, which has none.
|
|
853
939
|
const byToken = Object.create(null)
|
|
854
|
-
|
|
940
|
+
const byId = tokenIds ? [] : null
|
|
941
|
+
const groupClosures = new Map()
|
|
942
|
+
for (const [token, branch] of table) {
|
|
943
|
+
let closure = groupClosures.get(branch)
|
|
944
|
+
if (closure === undefined) {
|
|
945
|
+
closure = Array.isArray(branch)
|
|
946
|
+
? (() => {
|
|
947
|
+
const group = branch.map((b) => planned[branches.indexOf(b)])
|
|
948
|
+
return (state) => {
|
|
949
|
+
const start = state.idx
|
|
950
|
+
for (let i = 0; i < group.length; i++) {
|
|
951
|
+
const value = group[i](state)
|
|
952
|
+
if (value !== FAIL) return value
|
|
953
|
+
state.idx = start
|
|
954
|
+
}
|
|
955
|
+
return FAIL
|
|
956
|
+
}
|
|
957
|
+
})()
|
|
958
|
+
: planned[branches.indexOf(branch)]
|
|
959
|
+
groupClosures.set(branch, closure)
|
|
960
|
+
}
|
|
961
|
+
byToken[token] = closure
|
|
962
|
+
if (byId) {
|
|
963
|
+
const id = tokenIds.get(token)
|
|
964
|
+
if (id !== undefined) byId[id] = closure
|
|
965
|
+
}
|
|
966
|
+
}
|
|
855
967
|
const expectedTokens = [...table.keys()]
|
|
856
968
|
const expectedCount = expectedTokens.length
|
|
857
969
|
|
|
@@ -859,7 +971,7 @@ export function createPlanner (config) {
|
|
|
859
971
|
tick(state)
|
|
860
972
|
const token = state.tokens[state.idx]
|
|
861
973
|
if (token !== undefined) {
|
|
862
|
-
const branch = byToken[token.type]
|
|
974
|
+
const branch = byId && token.id !== undefined ? byId[token.id] : byToken[token.type]
|
|
863
975
|
if (branch !== undefined) return branch(state)
|
|
864
976
|
}
|
|
865
977
|
for (let i = 0; i < expectedCount; i++) expect(state, expectedTokens[i])
|
package/src/runtime.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* The surface a generated parser module needs at run time.
|
|
6
6
|
*
|
|
7
7
|
* `emitModule` writes files that import from `grammatik/runtime` rather than
|
|
8
|
-
* from the main entry point, so a generated parser pulls in the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* from the main entry point, so a generated parser pulls in the parse state and
|
|
9
|
+
* error construction — and not the analyzer, the planner or the code generator,
|
|
10
|
+
* none of which it can ever use. The grammar was already analyzed when the file
|
|
11
|
+
* was written, and so, by default, was its lexer (see `lexgen.js`): the file
|
|
12
|
+
* carries that as source and needs only `LexError` from here. `createLexer` is
|
|
13
|
+
* exported for files emitted with `lexer: 'runtime'`.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
export { FAIL, expect, createState, buildParseError, ParseError } from './state.js'
|
|
15
|
-
export { createLexer
|
|
16
|
+
export { FAIL, expect, createState, buildParseError, ParseError, LexError } from './state.js'
|
|
17
|
+
export { createLexer } from './lexer.js'
|
|
16
18
|
export { createValueMethods } from './values.js'
|