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/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, recognizeAltConsumeIdiom } from './analyze.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* A real source generator for grammars.
|
|
@@ -23,10 +23,24 @@ 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
|
|
26
|
+
* memo: boolean, maxSteps: number, tokenIds?: Map<string, number>|null,
|
|
27
|
+
* memoSlots?: Map<string, number>, inlineRules?: boolean, idioms?: boolean }} config
|
|
27
28
|
*/
|
|
28
29
|
export function createCodegen (config) {
|
|
29
|
-
const { planner, rules, firsts, memo, maxSteps } = config
|
|
30
|
+
const { planner, rules, firsts, memo, maxSteps, tokenIds = null, memoSlots = new Map(), inlineRules = true, idioms = true } = config
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `token.id === <n>` is an integer compare instead of a string compare, and
|
|
34
|
+
* is what generated code prefers wherever a token's declared type matters.
|
|
35
|
+
* Falls back to `token.type === <name>` only for a hand-built token that
|
|
36
|
+
* bypassed this parser's own lexer (`parseTokens` accepts anything shaped
|
|
37
|
+
* like a Token) — every token the bundled lexer produces has an id.
|
|
38
|
+
*/
|
|
39
|
+
const typeCheck = (tokenVar, name) => {
|
|
40
|
+
const id = tokenIds ? tokenIds.get(name) : undefined
|
|
41
|
+
if (id === undefined) return `${tokenVar}.type === ${JSON.stringify(name)}`
|
|
42
|
+
return `(${tokenVar}.id === ${id} || (${tokenVar}.id === undefined && ${tokenVar}.type === ${JSON.stringify(name)}))`
|
|
43
|
+
}
|
|
30
44
|
|
|
31
45
|
/** Runtime values the generated code closes over. */
|
|
32
46
|
const HELPERS = []
|
|
@@ -61,6 +75,56 @@ export function createCodegen (config) {
|
|
|
61
75
|
/** A literal value sitting in parser position. */
|
|
62
76
|
const constantHelper = (node) => helper(planner.plan(node), { kind: 'constant', node })
|
|
63
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Operator-precedence levels (`Additive = infixLeft(subrule(Multiplicative), ...)`)
|
|
80
|
+
* are written as a chain of rules, one per level, and each level costs a call, a
|
|
81
|
+
* loop and a token check even when no operator of that level follows: measured at
|
|
82
|
+
* ~10 ns per level per atom, so an 8-level chain costs several times the atom
|
|
83
|
+
* itself. A level with exactly one caller can be written into that caller's body
|
|
84
|
+
* instead, which keeps the loop and drops the call.
|
|
85
|
+
*
|
|
86
|
+
* Only these four shapes, and only a single call site: nothing is duplicated, and
|
|
87
|
+
* the function that results stays small enough for V8 to optimize (a rule like
|
|
88
|
+
* `Primary`, with an `alt` of many branches, is not a level and is left a call).
|
|
89
|
+
*/
|
|
90
|
+
// How many levels go into one function. Unbounded, a long chain makes a function
|
|
91
|
+
// larger than V8 will optimize at all (its `max-optimized-bytecode-size`), and the
|
|
92
|
+
// gain turns back into a loss: measured on a synthetic chain, 8 levels in one
|
|
93
|
+
// function ran at 66 ns per atom against 33 ns for 7. Six per function was the best
|
|
94
|
+
// of the caps tried (2, 3, 4, 5, 6, 8) across chains of 3 to 16 levels.
|
|
95
|
+
const MAX_INLINE_DEPTH = 6
|
|
96
|
+
/** `oneOf` with more names than this is generated as a `switch` on the token id. */
|
|
97
|
+
const ONE_OF_SWITCH_AT = 12
|
|
98
|
+
const INLINE_OPS = new Set(['infixLeft', 'infixRight', 'prefix', 'postfix'])
|
|
99
|
+
const callSites = new Map()
|
|
100
|
+
;(function count (node) {
|
|
101
|
+
if (Array.isArray(node)) { for (const child of node) count(child); return }
|
|
102
|
+
if (!node || typeof node !== 'object') return
|
|
103
|
+
if ('subrule' in node) {
|
|
104
|
+
const target = Array.isArray(node.subrule) ? node.subrule[0] : node.subrule
|
|
105
|
+
if (typeof target === 'string') callSites.set(target, (callSites.get(target) || 0) + 1)
|
|
106
|
+
}
|
|
107
|
+
for (const key of Object.keys(node)) count(node[key])
|
|
108
|
+
})(rules)
|
|
109
|
+
/** Rules being written into a caller right now, so a cycle cannot recurse forever. */
|
|
110
|
+
const inlining = new Set()
|
|
111
|
+
/**
|
|
112
|
+
* Some constructs write a child twice: `infixLeft` and `infixRight` their operand (once for the
|
|
113
|
+
* first, once inside the loop), `many1` and `many1Sep` their item. Inline a rule into *both*
|
|
114
|
+
* copies and a chain of such rules multiplies out — one copy per level at the first, two at the
|
|
115
|
+
* next, four at the next: measured, the formula grammar's module nearly doubled and a six-level
|
|
116
|
+
* chain grew fivefold. The second copy is the loop body, which only runs when more input follows,
|
|
117
|
+
* so it stays a call; only the first copy, the one every parse goes through, is inlined.
|
|
118
|
+
*/
|
|
119
|
+
let coldDepth = 0
|
|
120
|
+
const cold = (write) => {
|
|
121
|
+
coldDepth++
|
|
122
|
+
try { return write() } finally { coldDepth-- }
|
|
123
|
+
}
|
|
124
|
+
const canInline = (name) =>
|
|
125
|
+
inlineRules && coldDepth === 0 && !memo && !memoSlots.has(name) && !inlining.has(name) && inlining.size < MAX_INLINE_DEPTH &&
|
|
126
|
+
callSites.get(name) === 1 && INLINE_OPS.has(opOf(rules[name]))
|
|
127
|
+
|
|
64
128
|
function indexOfRule (name) {
|
|
65
129
|
let index = ruleIndex.get(name)
|
|
66
130
|
if (index !== undefined) return index
|
|
@@ -99,7 +163,7 @@ export function createCodegen (config) {
|
|
|
99
163
|
out.push(
|
|
100
164
|
`let ${result};`,
|
|
101
165
|
`const ${token} = c.tokens[c.idx];`,
|
|
102
|
-
`if (${token} !== undefined && ${token
|
|
166
|
+
`if (${token} !== undefined && ${typeCheck(token, name)}) { c.idx++; ${result} = ${token}; }`,
|
|
103
167
|
`else { E(c, ${JSON.stringify(name)}); ${result} = F; }`
|
|
104
168
|
)
|
|
105
169
|
return result
|
|
@@ -140,6 +204,17 @@ export function createCodegen (config) {
|
|
|
140
204
|
return result
|
|
141
205
|
}
|
|
142
206
|
|
|
207
|
+
case 'flag': {
|
|
208
|
+
const start = nextId()
|
|
209
|
+
out.push(`let ${result};`, `const ${start} = c.idx;`, '{')
|
|
210
|
+
const value = gen(arg, out, bindingsVar)
|
|
211
|
+
out.push(
|
|
212
|
+
`if (${value} === F) { c.idx = ${start}; ${result} = false; } else ${result} = true;`,
|
|
213
|
+
'}'
|
|
214
|
+
)
|
|
215
|
+
return result
|
|
216
|
+
}
|
|
217
|
+
|
|
143
218
|
case 'many':
|
|
144
219
|
case 'many1': {
|
|
145
220
|
const atLeastOne = op === 'many1'
|
|
@@ -159,7 +234,7 @@ export function createCodegen (config) {
|
|
|
159
234
|
}
|
|
160
235
|
|
|
161
236
|
out.push('for (;;) {', `const ${before} = c.idx;`)
|
|
162
|
-
const value = gen(arg, out, bindingsVar)
|
|
237
|
+
const value = atLeastOne ? cold(() => gen(arg, out, bindingsVar)) : gen(arg, out, bindingsVar)
|
|
163
238
|
out.push(
|
|
164
239
|
`if (${value} === F) { c.idx = ${before}; break; }`,
|
|
165
240
|
`if (c.idx === ${before}) P(${JSON.stringify(op)}, ${before});`,
|
|
@@ -196,7 +271,7 @@ export function createCodegen (config) {
|
|
|
196
271
|
out.push(`if (${operatorVar} === F) { c.idx = ${mark}; break; }`)
|
|
197
272
|
}
|
|
198
273
|
|
|
199
|
-
const right = gen(arg.operand, out, bindingsVar)
|
|
274
|
+
const right = cold(() => gen(arg.operand, out, bindingsVar))
|
|
200
275
|
out.push(
|
|
201
276
|
`if (${right} === F) { c.idx = ${mark}; break; }`,
|
|
202
277
|
`if (${bindings} === null) ${bindings} = { left: null, op: null, right: null };`,
|
|
@@ -212,12 +287,34 @@ export function createCodegen (config) {
|
|
|
212
287
|
case 'oneOf': {
|
|
213
288
|
const names = Array.isArray(arg) ? arg : [arg]
|
|
214
289
|
const token = nextId()
|
|
215
|
-
const
|
|
290
|
+
const ids = tokenIds ? names.map((n) => tokenIds.get(n)) : []
|
|
291
|
+
const failed = `else { ${names.map((n) => `E(c, ${JSON.stringify(n)});`).join(' ')} ${result} = F; }`
|
|
292
|
+
|
|
293
|
+
if (names.length > ONE_OF_SWITCH_AT && ids.every((id) => id !== undefined)) {
|
|
294
|
+
// Past a handful of names a chain of comparisons costs more than a jump
|
|
295
|
+
// table: measured, the chain matched `alt`'s switch at 12 names and lost
|
|
296
|
+
// beyond. Integer cases only, as `alt` does it — a `case undefined` for a
|
|
297
|
+
// hand-built token would stop V8 building the table — so the id is
|
|
298
|
+
// normalised first, from the type name when the token has none.
|
|
299
|
+
const hit = nextId()
|
|
300
|
+
const cases = [...new Set(ids)].map((id) => `case ${id}:`).join(' ')
|
|
301
|
+
out.push(
|
|
302
|
+
`let ${result};`,
|
|
303
|
+
`const ${token} = c.tokens[c.idx];`,
|
|
304
|
+
`let ${hit} = false;`,
|
|
305
|
+
`if (${token} !== undefined) { switch (${token}.id !== undefined ? ${token}.id : I[${token}.type]) { ${cases} ${hit} = true; } }`,
|
|
306
|
+
`if (${hit}) { c.idx++; ${result} = ${token}; }`,
|
|
307
|
+
failed
|
|
308
|
+
)
|
|
309
|
+
return result
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const test = names.map((n) => typeCheck(token, n)).join(' || ')
|
|
216
313
|
out.push(
|
|
217
314
|
`let ${result};`,
|
|
218
315
|
`const ${token} = c.tokens[c.idx];`,
|
|
219
316
|
`if (${token} !== undefined && (${test})) { c.idx++; ${result} = ${token}; }`,
|
|
220
|
-
|
|
317
|
+
failed
|
|
221
318
|
)
|
|
222
319
|
return result
|
|
223
320
|
}
|
|
@@ -253,7 +350,7 @@ export function createCodegen (config) {
|
|
|
253
350
|
operatorVar = gen(arg.operator, out, bindingsVar)
|
|
254
351
|
out.push(`if (${operatorVar} === F) { c.idx = ${mark}; break; }`)
|
|
255
352
|
}
|
|
256
|
-
const right = gen(arg.operand, out, bindingsVar)
|
|
353
|
+
const right = cold(() => gen(arg.operand, out, bindingsVar))
|
|
257
354
|
out.push(
|
|
258
355
|
`if (${right} === F) { c.idx = ${mark}; break; }`,
|
|
259
356
|
`if (${items} === null) ${items} = [];`,
|
|
@@ -334,6 +431,22 @@ export function createCodegen (config) {
|
|
|
334
431
|
return result
|
|
335
432
|
}
|
|
336
433
|
|
|
434
|
+
case 'pick':
|
|
435
|
+
case 'yields': {
|
|
436
|
+
// The parsers in order, then one of their results or a constant; no
|
|
437
|
+
// bindings object, which is the point (see `recognizeSeqIdiom`).
|
|
438
|
+
const label = 'L' + nextId()
|
|
439
|
+
const start = nextId()
|
|
440
|
+
out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${start} = c.idx;`)
|
|
441
|
+
const values = arg.parsers.map((parser) => {
|
|
442
|
+
const value = gen(parser, out, bindingsVar)
|
|
443
|
+
out.push(`if (${value} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`)
|
|
444
|
+
return value
|
|
445
|
+
})
|
|
446
|
+
out.push(`${result} = ${op === 'pick' ? values[arg.index] : JSON.stringify(arg.value)};`, '}')
|
|
447
|
+
return result
|
|
448
|
+
}
|
|
449
|
+
|
|
337
450
|
case 'prefix': {
|
|
338
451
|
const label = 'L' + nextId()
|
|
339
452
|
const start = nextId()
|
|
@@ -415,7 +528,7 @@ export function createCodegen (config) {
|
|
|
415
528
|
)
|
|
416
529
|
const sep = gen(sepNode, out, bindingsVar)
|
|
417
530
|
out.push(`if (${sep} === F) { c.idx = ${beforeSep}; break; }`, `const ${beforeItem} = c.idx;`)
|
|
418
|
-
const item = gen(arg.rule, out, bindingsVar)
|
|
531
|
+
const item = cold(() => gen(arg.rule, out, bindingsVar))
|
|
419
532
|
out.push(
|
|
420
533
|
`if (${item} === F) { c.idx = ${trailing ? beforeItem : beforeSep}; break; }`,
|
|
421
534
|
`${result}.push(${item});`,
|
|
@@ -432,8 +545,20 @@ export function createCodegen (config) {
|
|
|
432
545
|
out.push(`const ${result} = ${helper(planner.plan(node), { kind: 'unknownRule', name })}(c);`)
|
|
433
546
|
return result
|
|
434
547
|
}
|
|
435
|
-
const index = indexOfRule(name)
|
|
436
548
|
const parent = nextId()
|
|
549
|
+
if (canInline(name)) {
|
|
550
|
+
out.push(tickSrc)
|
|
551
|
+
out.push(
|
|
552
|
+
`const ${parent} = c.ruleDepth;`,
|
|
553
|
+
`if (c.track) { c.ruleNames[${parent}] = ${JSON.stringify(name)}; c.ruleDepth = ${parent} + 1; }`
|
|
554
|
+
)
|
|
555
|
+
inlining.add(name)
|
|
556
|
+
const inner = gen(rules[name], out, null)
|
|
557
|
+
inlining.delete(name)
|
|
558
|
+
out.push(`if (c.track) c.ruleDepth = ${parent};`)
|
|
559
|
+
return inner
|
|
560
|
+
}
|
|
561
|
+
const index = indexOfRule(name)
|
|
437
562
|
out.push(tickSrc)
|
|
438
563
|
if (memo) {
|
|
439
564
|
const key = nextId()
|
|
@@ -454,6 +579,27 @@ export function createCodegen (config) {
|
|
|
454
579
|
`c.memo.set(${key}, { value: ${result}, end: c.idx });`,
|
|
455
580
|
'}'
|
|
456
581
|
)
|
|
582
|
+
} else if (memoSlots.has(name)) {
|
|
583
|
+
// Chosen by `selectMemoRules`: remember the result per position.
|
|
584
|
+
const table = nextId()
|
|
585
|
+
const at = nextId()
|
|
586
|
+
const end = nextId()
|
|
587
|
+
out.push(
|
|
588
|
+
`let ${result};`,
|
|
589
|
+
`const ${table} = c.rm[${memoSlots.get(name)}];`,
|
|
590
|
+
`const ${at} = c.idx;`,
|
|
591
|
+
`const ${end} = ${table}.ends[${at}];`,
|
|
592
|
+
`if (${end} !== 0) { c.idx = ${end} - 1; ${result} = ${table}.vals[${at}]; }`,
|
|
593
|
+
'else {',
|
|
594
|
+
`const ${parent} = c.ruleDepth;`,
|
|
595
|
+
`if (c.track) { c.ruleNames[${parent}] = ${JSON.stringify(name)}; c.ruleDepth = ${parent} + 1; }`,
|
|
596
|
+
`${result} = R[${index}](c);`,
|
|
597
|
+
`if (c.track) c.ruleDepth = ${parent};`,
|
|
598
|
+
`if (${result} === F) c.idx = ${at};`,
|
|
599
|
+
`${table}.ends[${at}] = c.idx + 1;`,
|
|
600
|
+
`${table}.vals[${at}] = ${result};`,
|
|
601
|
+
'}'
|
|
602
|
+
)
|
|
457
603
|
} else {
|
|
458
604
|
out.push(
|
|
459
605
|
`const ${parent} = c.ruleDepth;`,
|
|
@@ -476,7 +622,7 @@ export function createCodegen (config) {
|
|
|
476
622
|
if (!Array.isArray(children)) throw new Error("'seq' expects an array of parsers")
|
|
477
623
|
|
|
478
624
|
// Same idiom recognition as the planner, so both paths agree.
|
|
479
|
-
const idiom = recognizeSeqIdiom(children)
|
|
625
|
+
const idiom = idioms ? recognizeSeqIdiom(children) : null
|
|
480
626
|
if (idiom !== null) return gen(idiom, out, null)
|
|
481
627
|
|
|
482
628
|
let hasLabels = false
|
|
@@ -520,29 +666,78 @@ export function createCodegen (config) {
|
|
|
520
666
|
return result
|
|
521
667
|
}
|
|
522
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Emits an inline ordered-backtracking block over `list`, into a fresh
|
|
671
|
+
* variable. Used both for a whole `alt` with no dispatch table, and for the
|
|
672
|
+
* residual subset of branches a dispatch table cannot separate by one
|
|
673
|
+
* token.
|
|
674
|
+
*/
|
|
675
|
+
function genOrderedBacktrack (list, out, bindingsVar) {
|
|
676
|
+
const result = nextId()
|
|
677
|
+
const label = 'L' + nextId()
|
|
678
|
+
const start = nextId()
|
|
679
|
+
out.push(`let ${result};`, `${label}: {`, `const ${start} = c.idx;`)
|
|
680
|
+
for (const branch of list) {
|
|
681
|
+
out.push('{')
|
|
682
|
+
const value = gen(branch, out, bindingsVar)
|
|
683
|
+
out.push(`if (${value} !== F) { ${result} = ${value}; break ${label}; }`, `c.idx = ${start};`, '}')
|
|
684
|
+
}
|
|
685
|
+
out.push(`${result} = F;`, '}')
|
|
686
|
+
return result
|
|
687
|
+
}
|
|
688
|
+
|
|
523
689
|
function genAlt (branches, out, result, bindingsVar) {
|
|
524
690
|
if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches")
|
|
525
691
|
|
|
692
|
+
const oneOf = idioms ? recognizeAltConsumeIdiom(branches) : null
|
|
693
|
+
if (oneOf !== null) return gen(oneOf, out, bindingsVar)
|
|
694
|
+
|
|
695
|
+
const factored = idioms ? recognizeAltPrefixIdiom(branches) : null
|
|
696
|
+
if (factored !== null) return gen(factored, out, bindingsVar)
|
|
697
|
+
|
|
526
698
|
const table = firsts ? buildDispatch(branches, firsts) : null
|
|
527
699
|
const label = 'L' + nextId()
|
|
528
700
|
|
|
529
701
|
if (table !== null) {
|
|
530
702
|
// A switch on the token type: V8 turns a dense string switch into a
|
|
531
703
|
// hash lookup with inlined bodies, and every branch is inlined here.
|
|
704
|
+
// A table value that is an array is a residual group — tokens shared by
|
|
705
|
+
// more than one branch — resolved by ordered backtracking over just
|
|
706
|
+
// that subset instead of a single inlined branch.
|
|
707
|
+
//
|
|
708
|
+
// Switching on token.id (an integer) instead of token.type (a string)
|
|
709
|
+
// when every expected token resolves to one — a numeric switch is a jump
|
|
710
|
+
// table without even the string-hashing step a dense string switch still
|
|
711
|
+
// does. `I[token.type]` is the same fallback typeCheck uses: a hand-built
|
|
712
|
+
// token from parseTokens has no id, and I is a name -> id table built
|
|
713
|
+
// once from the same tokenIds this parser resolved every other id from.
|
|
532
714
|
const expected = [...table.keys()]
|
|
715
|
+
const expectedIds = tokenIds ? expected.map((t) => tokenIds.get(t)) : null
|
|
716
|
+
const numeric = expectedIds !== null && expectedIds.every((id) => id !== undefined)
|
|
533
717
|
const token = nextId()
|
|
534
718
|
out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${token} = c.tokens[c.idx];`)
|
|
535
719
|
out.push(`if (${token} === undefined) { ${expected.map((t) => `E(c, ${JSON.stringify(t)});`).join(' ')} ${result} = F; break ${label}; }`)
|
|
536
|
-
|
|
720
|
+
|
|
721
|
+
let discriminant
|
|
722
|
+
if (numeric) {
|
|
723
|
+
discriminant = nextId()
|
|
724
|
+
out.push(`const ${discriminant} = ${token}.id !== undefined ? ${token}.id : I[${token}.type];`)
|
|
725
|
+
out.push(`switch (${discriminant}) {`)
|
|
726
|
+
} else {
|
|
727
|
+
discriminant = `${token}.type`
|
|
728
|
+
out.push(`switch (${discriminant}) {`)
|
|
729
|
+
}
|
|
537
730
|
|
|
538
731
|
const byBranch = new Map()
|
|
539
|
-
for (const
|
|
732
|
+
for (const tokenName of expected) {
|
|
733
|
+
const branch = table.get(tokenName)
|
|
540
734
|
if (!byBranch.has(branch)) byBranch.set(branch, [])
|
|
541
735
|
byBranch.get(branch).push(tokenName)
|
|
542
736
|
}
|
|
737
|
+
const caseLabel = (t) => numeric ? tokenIds.get(t) : JSON.stringify(t)
|
|
543
738
|
for (const [branch, tokenNames] of byBranch) {
|
|
544
|
-
out.push(tokenNames.map((t) => `case ${
|
|
545
|
-
const value = gen(branch, out, bindingsVar)
|
|
739
|
+
out.push(tokenNames.map((t) => `case ${caseLabel(t)}:`).join(' ') + ' {')
|
|
740
|
+
const value = Array.isArray(branch) ? genOrderedBacktrack(branch, out, bindingsVar) : gen(branch, out, bindingsVar)
|
|
546
741
|
out.push(`${result} = ${value}; break ${label};`, '}')
|
|
547
742
|
}
|
|
548
743
|
out.push(
|
|
@@ -554,17 +749,13 @@ export function createCodegen (config) {
|
|
|
554
749
|
}
|
|
555
750
|
|
|
556
751
|
// 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
|
|
752
|
+
out.push(tickSrc)
|
|
753
|
+
return genOrderedBacktrack(branches, out, bindingsVar)
|
|
566
754
|
}
|
|
567
755
|
|
|
756
|
+
/** Name -> id, for a generated switch's runtime fallback when token.id is missing. */
|
|
757
|
+
const idByName = tokenIds ? Object.fromEntries(tokenIds) : null
|
|
758
|
+
|
|
568
759
|
/** Generates and installs the function for one rule. */
|
|
569
760
|
function buildRule (name) {
|
|
570
761
|
const out = []
|
|
@@ -576,8 +767,8 @@ export function createCodegen (config) {
|
|
|
576
767
|
SOURCES[ruleIndex.get(name)] = body
|
|
577
768
|
const source = `return ${body}`
|
|
578
769
|
// 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)
|
|
770
|
+
const factory = new Function('F', 'E', 'P', 'T', 'R', 'H', 'I', source)
|
|
771
|
+
return factory(FAIL, expect, progressError, stepsError, RULES, HELPERS, idByName)
|
|
581
772
|
}
|
|
582
773
|
|
|
583
774
|
function progressError (op, at) {
|
package/src/definition.js
CHANGED
|
@@ -227,6 +227,7 @@ const CONSTRUCTORS = {
|
|
|
227
227
|
many: { arity: [1, 1], of: ([a]) => ({ many: a }) },
|
|
228
228
|
many1: { arity: [1, 1], of: ([a]) => ({ many1: a }) },
|
|
229
229
|
option: { arity: [1, 1], of: ([a]) => ({ option: a }) },
|
|
230
|
+
flag: { arity: [1, 1], of: ([a]) => ({ flag: a }) },
|
|
230
231
|
action: { arity: [1, 1], of: ([a]) => ({ action: a }) },
|
|
231
232
|
lookahead: { arity: [1, 1], of: ([a]) => ({ lookahead: a }) },
|
|
232
233
|
negLookahead: { arity: [1, 1], of: ([a]) => ({ negLookahead: a }) },
|
|
@@ -241,6 +242,10 @@ const CONSTRUCTORS = {
|
|
|
241
242
|
many1Sep: { arity: [2, 2], of: ([a, b]) => ({ many1Sep: { rule: a, sep: b } }) },
|
|
242
243
|
|
|
243
244
|
between: { arity: [3, 3], of: ([a, b, c]) => ({ between: { open: a, body: b, close: c } }) },
|
|
245
|
+
// pick(1, consume(Open), subrule(Body), consume(Close)) — the result at that index.
|
|
246
|
+
pick: { arity: [2, Infinity], of: ([index, ...parsers]) => ({ pick: { parsers, index } }) },
|
|
247
|
+
// yields(consume(True), true) — run the parsers, give back the constant.
|
|
248
|
+
yields: { arity: [2, Infinity], of: (args) => ({ yields: { parsers: args.slice(0, -1), value: args[args.length - 1] } }) },
|
|
244
249
|
infixLeft: { arity: [3, 3], of: ([a, b, c]) => ({ infixLeft: { operand: a, operator: b, combine: logicOf(c) } }) },
|
|
245
250
|
infixRight: { arity: [3, 3], of: ([a, b, c]) => ({ infixRight: { operand: a, operator: b, combine: logicOf(c) } }) },
|
|
246
251
|
prefix: { arity: [3, 3], of: ([a, b, c]) => ({ prefix: { operator: a, operand: b, combine: logicOf(c) } }) },
|
package/src/emit.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
4
|
import { createLexer } from './lexer.js'
|
|
5
|
+
import { generateLexer } from './lexgen.js'
|
|
5
6
|
import { analyze } from './analyze.js'
|
|
6
7
|
import { createPlanner } from './plan.js'
|
|
7
8
|
import { createCodegen } from './codegen.js'
|
|
9
|
+
import { selectMemoRules } from './memoplan.js'
|
|
8
10
|
import { usesOuterScope } from './plan.js'
|
|
9
11
|
import { emitMethodsBlock, evaluateMethodsBlock } from './methodsblock.js'
|
|
10
12
|
import { createValueMethods } from './values.js'
|
|
@@ -100,8 +102,9 @@ const literal = (value, indent = 0) => JSON.stringify(value, null, 2)
|
|
|
100
102
|
|
|
101
103
|
/**
|
|
102
104
|
* @param {import('./parser.js').GrammarSpec} spec
|
|
103
|
-
* @param {{ memo?: boolean, strict?: boolean, ll1?: boolean, maxSteps?: number,
|
|
105
|
+
* @param {{ memo?: boolean, autoMemo?: boolean, inlineRules?: boolean, idioms?: boolean, strict?: boolean, ll1?: boolean, maxSteps?: number,
|
|
104
106
|
* positions?: 'full'|'offset', execution?: 'generated'|'interpreted',
|
|
107
|
+
* lexer?: 'generated'|'runtime', scanners?: boolean, fuse?: boolean,
|
|
105
108
|
* format?: 'esm'|'cjs', validate?: boolean, moduleName?: string,
|
|
106
109
|
* runtimeSpecifier?: string, engineSpecifier?: string,
|
|
107
110
|
* methods?: Record<string, any> }} [options]
|
|
@@ -111,11 +114,21 @@ export function emitModule (spec, options = {}) {
|
|
|
111
114
|
const { tokens: tokenDefs, rules, start } = spec
|
|
112
115
|
const {
|
|
113
116
|
memo = false,
|
|
117
|
+
autoMemo = true,
|
|
118
|
+
// Off here, on in `createParser`. Inlining a precedence chain is worth 2-3x on a bare chain but only
|
|
119
|
+
// 1.02-1.04x on the formula and SQL examples, and it makes the file bigger: the formula module grows
|
|
120
|
+
// 32% minified (38 to 50 KB) even after the second copy of each operand is kept as a call. A parser
|
|
121
|
+
// built at run time pays nothing for that; a file someone ships does.
|
|
122
|
+
inlineRules = false,
|
|
123
|
+
idioms = true,
|
|
114
124
|
strict = true,
|
|
115
125
|
ll1 = true,
|
|
116
126
|
maxSteps = 0,
|
|
117
127
|
positions = 'full',
|
|
118
128
|
execution = 'generated',
|
|
129
|
+
lexer: lexerMode = 'generated',
|
|
130
|
+
scanners = true,
|
|
131
|
+
fuse = true,
|
|
119
132
|
format = 'esm',
|
|
120
133
|
validate = true,
|
|
121
134
|
moduleName = spec.name || 'parser',
|
|
@@ -125,6 +138,7 @@ export function emitModule (spec, options = {}) {
|
|
|
125
138
|
} = options
|
|
126
139
|
|
|
127
140
|
if (format !== 'esm' && format !== 'cjs') throw new Error(`emitModule: format must be 'esm' or 'cjs', got ${quote(format)}`)
|
|
141
|
+
if (lexerMode !== 'generated' && lexerMode !== 'runtime') throw new Error(`emitModule: lexer must be 'generated' or 'runtime', got ${quote(lexerMode)}`)
|
|
128
142
|
|
|
129
143
|
// A `methods { ... }` block travels with the grammar and is written into the
|
|
130
144
|
// output as source. Nothing evaluates it here, so this path stays eval-free.
|
|
@@ -132,6 +146,8 @@ export function emitModule (spec, options = {}) {
|
|
|
132
146
|
const block = methodsBlock ? emitMethodsBlock(methodsBlock, format) : null
|
|
133
147
|
if (!start || !(start in rules)) throw new Error(`emitModule: start rule '${start}' is not defined in rules`)
|
|
134
148
|
|
|
149
|
+
// Built here even when the file will carry its own lexer: constructing it is what validates the token
|
|
150
|
+
// definitions, and it fixes the token ids the rules are compiled against.
|
|
135
151
|
const lexer = createLexer(tokenDefs, { positions })
|
|
136
152
|
const analysis = analyze(rules, { tokenNames: lexer.tokenNames })
|
|
137
153
|
if (validate && analysis.errors.length > 0) throw new GrammarError(analysis.errors)
|
|
@@ -139,9 +155,17 @@ export function emitModule (spec, options = {}) {
|
|
|
139
155
|
// The planner is needed only so codegen can record what each helper is; the
|
|
140
156
|
// closures it makes are thrown away.
|
|
141
157
|
const firsts = ll1 ? analysis.firsts : null
|
|
158
|
+
// The generated file's own `createLexer(tokenDefs, ...)` call assigns the same
|
|
159
|
+
// ids, from the same `tokenDefs` array in the same order — see the matching
|
|
160
|
+
// comment in parser.js.
|
|
161
|
+
const tokenIds = new Map(lexer.tokenNames.map((name, id) => [name, id]))
|
|
142
162
|
const engine = { build: () => { throw new Error('not compiled during emit') }, run: () => {} }
|
|
143
|
-
const
|
|
144
|
-
|
|
163
|
+
const memoRules = autoMemo && !memo && analysis.errors.length === 0
|
|
164
|
+
? selectMemoRules(rules, analysis.firsts, { ll1 })
|
|
165
|
+
: []
|
|
166
|
+
const memoSlots = new Map(memoRules.map((name, slot) => [name, slot]))
|
|
167
|
+
const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval: false, tokenIds, memoSlots, idioms })
|
|
168
|
+
const codegen = createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds, memoSlots, inlineRules, idioms })
|
|
145
169
|
|
|
146
170
|
codegen.compile(start)
|
|
147
171
|
|
|
@@ -210,7 +234,9 @@ export function emitModule (spec, options = {}) {
|
|
|
210
234
|
// actions may use the value methods, so it has to register them the way
|
|
211
235
|
// `createParser` does. They come from the runtime rather than being written
|
|
212
236
|
// out here, so there is one implementation rather than two.
|
|
213
|
-
|
|
237
|
+
// A lexer written out as source needs only the error class from the runtime; the engine stays out of the file.
|
|
238
|
+
const generatedLexer = lexerMode === 'generated'
|
|
239
|
+
const runtimeImports = ['FAIL as F', 'expect as E', 'createState', 'buildParseError', generatedLexer ? 'LexError' : 'createLexer']
|
|
214
240
|
if (needsEngine) runtimeImports.push('createValueMethods')
|
|
215
241
|
|
|
216
242
|
const importRuntime = cjs
|
|
@@ -240,7 +266,7 @@ export function emitModule (spec, options = {}) {
|
|
|
240
266
|
//
|
|
241
267
|
// The grammar was analyzed when this file was written: FIRST sets, left-recursion
|
|
242
268
|
// checks and rule specialization all happened then. Importing it does no grammar
|
|
243
|
-
// work and generates no code.
|
|
269
|
+
// work and generates no code.${generatedLexer ? '\n// The lexer was written out as source too, so there is no lexer engine to load.' : ''}
|
|
244
270
|
${engineFree
|
|
245
271
|
? '// Every semantic action was compiled to source too, so this file has no\n// dependency on json-logic-engine at all.'
|
|
246
272
|
: interpretedActions
|
|
@@ -254,7 +280,7 @@ const tokens = ${literal(tokenDefs)}
|
|
|
254
280
|
|
|
255
281
|
const start = ${quote(start)}
|
|
256
282
|
|
|
257
|
-
const lexer = createLexer(tokens, { positions: ${quote(positions)} })
|
|
283
|
+
const lexer = ${generatedLexer ? generateLexer(tokenDefs, { positions, scanners, fuse }) : `createLexer(tokens, { positions: ${quote(positions)}, scanners: ${scanners}, fuse: ${fuse} })`}
|
|
258
284
|
const tokenize = lexer.tokenize
|
|
259
285
|
|
|
260
286
|
${engineFree
|
|
@@ -335,6 +361,14 @@ const H = [
|
|
|
335
361
|
${helpers.map((source, index) => ` /* ${index} */ ${source}`).join(',\n')}
|
|
336
362
|
]
|
|
337
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Name -> id, the same numbering \`lexer\` assigns. A generated \`alt\`'s switch
|
|
366
|
+
* dispatches on a token's id when it has one; this is only read as a fallback,
|
|
367
|
+
* for a hand-built token from \`parseTokens\` that bypassed this file's own
|
|
368
|
+
* lexer and so has no id of its own.
|
|
369
|
+
*/
|
|
370
|
+
const I = ${literal(Object.fromEntries(tokenIds))}
|
|
371
|
+
|
|
338
372
|
${ruleSources}
|
|
339
373
|
|
|
340
374
|
const RULES = ${literal(Object.fromEntries(reached.map(([name, index]) => [name, index])))}
|
|
@@ -345,7 +379,7 @@ const RULES = ${literal(Object.fromEntries(reached.map(([name, index]) => [name,
|
|
|
345
379
|
* @param {string} [source]
|
|
346
380
|
*/
|
|
347
381
|
function parseTokens (tokenList, source, track = false) {
|
|
348
|
-
const state = createState(tokenList, {}, { memo: ${memo}, maxSteps: ${maxSteps}, track })
|
|
382
|
+
const state = createState(tokenList, {}, { memo: ${memo}, maxSteps: ${maxSteps}, track, memoSlots: ${memoRules.length} })
|
|
349
383
|
state.source = source
|
|
350
384
|
state.positions = ${quote(positions)}
|
|
351
385
|
state.ruleNames[0] = start
|
package/src/lexer.d.ts
CHANGED
|
@@ -5,19 +5,60 @@
|
|
|
5
5
|
* large win for punctuation-heavy grammars (JSON is ~60% punctuation by token count).
|
|
6
6
|
*
|
|
7
7
|
* @param {TokenDef[]} defs
|
|
8
|
-
* @param {{ positions?: 'full' | 'offset' }} [options]
|
|
8
|
+
* @param {{ positions?: 'full' | 'offset', scanners?: boolean, fuse?: boolean, incremental?: boolean, recover?: boolean, errorToken?: string }} [options]
|
|
9
9
|
* `full` (default) records line and column on every token. `offset` records
|
|
10
10
|
* only `start`/`end`, which skips a scan of the entire input for newlines —
|
|
11
11
|
* worth taking when errors do not need to name a line, or when positions are
|
|
12
12
|
* resolved lazily from the offset afterwards.
|
|
13
|
-
*
|
|
13
|
+
*
|
|
14
|
+
* `incremental` (default false) is opt-in: when set, the returned lexer also
|
|
15
|
+
* exposes `tokenizeIncremental` and `relex`, for editors that want to re-lex
|
|
16
|
+
* only the region touched by a single edit instead of the whole document.
|
|
17
|
+
* A lexer created without it is bit-for-bit the plain lexer — no overhead.
|
|
18
|
+
*
|
|
19
|
+
* `scanners` (default true) runs patterns of a few common shapes as
|
|
20
|
+
* table-driven loops instead of regex calls. The tokens are identical;
|
|
21
|
+
* `false` forces every pattern through its regex.
|
|
22
|
+
*
|
|
23
|
+
* `fuse` (default true) joins runs of three or more consecutive regex candidates
|
|
24
|
+
* into one alternation, so a position where several tokens could start costs one
|
|
25
|
+
* regex call rather than one per rejected token. Same tokens; `false` tries them
|
|
26
|
+
* one at a time.
|
|
27
|
+
*
|
|
28
|
+
* `recover` (default false, only meaningful with `incremental`) makes an
|
|
29
|
+
* unmatched character produce a single-character token of type `errorToken`
|
|
30
|
+
* (default `'Error'`) instead of throwing, so highlighting survives invalid
|
|
31
|
+
* or half-typed input.
|
|
32
|
+
* @returns {{ tokenize: (text: string) => Token[], tokenizeIncremental?: (text: string) => IncrementalResult, relex?: (text: string, prev: IncrementalResult, edit: { start: number, oldEnd: number, newEnd: number }) => IncrementalResult, tokenNames: string[], positions: string, modeNames: string[] }}
|
|
14
33
|
*/
|
|
15
34
|
export function createLexer(defs: TokenDef[], options?: {
|
|
16
35
|
positions?: "full" | "offset";
|
|
36
|
+
scanners?: boolean;
|
|
37
|
+
fuse?: boolean;
|
|
38
|
+
incremental?: boolean;
|
|
39
|
+
recover?: boolean;
|
|
40
|
+
errorToken?: string;
|
|
17
41
|
}): {
|
|
18
42
|
tokenize: (text: string) => Token[];
|
|
43
|
+
tokenizeIncremental?: (text: string) => IncrementalResult;
|
|
44
|
+
relex?: (text: string, prev: IncrementalResult, edit: {
|
|
45
|
+
start: number;
|
|
46
|
+
oldEnd: number;
|
|
47
|
+
newEnd: number;
|
|
48
|
+
}) => IncrementalResult;
|
|
19
49
|
tokenNames: string[];
|
|
20
50
|
positions: string;
|
|
51
|
+
modeNames: string[];
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Opaque state returned by `tokenizeIncremental`/`relex` — pass it back into
|
|
55
|
+
* `relex` unmodified; only `.tokens` is meant to be read directly.
|
|
56
|
+
*/
|
|
57
|
+
export type IncrementalResult = {
|
|
58
|
+
text: string;
|
|
59
|
+
tokens: Token[];
|
|
60
|
+
checkpoints: object[];
|
|
61
|
+
eof: object;
|
|
21
62
|
};
|
|
22
63
|
/**
|
|
23
64
|
* A Chevrotain-style lexer. Token definitions are plain JSON, so a whole
|