grammatik 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +443 -0
- package/bin/grammatik.js +115 -0
- package/dist/cjs/index.js +3010 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/runtime.js +639 -0
- package/dist/esm/index.js +2959 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/runtime.js +605 -0
- package/dist/types/index.d.ts +214 -0
- package/dist/types/runtime.d.ts +40 -0
- package/package.json +65 -0
- package/src/analyze.d.ts +78 -0
- package/src/analyze.js +602 -0
- package/src/codegen.d.ts +36 -0
- package/src/codegen.js +635 -0
- package/src/definition.d.ts +133 -0
- package/src/definition.js +291 -0
- package/src/emit.d.ts +23 -0
- package/src/emit.js +374 -0
- package/src/firstchars.d.ts +6 -0
- package/src/firstchars.js +353 -0
- package/src/index.d.ts +7 -0
- package/src/index.js +15 -0
- package/src/lexer.d.ts +96 -0
- package/src/lexer.js +358 -0
- package/src/methods.d.ts +20 -0
- package/src/methods.js +61 -0
- package/src/methodsblock.js +197 -0
- package/src/parser.d.ts +130 -0
- package/src/parser.js +194 -0
- package/src/plan.d.ts +29 -0
- package/src/plan.js +938 -0
- package/src/runtime.js +16 -0
- package/src/state.d.ts +98 -0
- package/src/state.js +121 -0
- package/src/values.js +67 -0
package/src/codegen.js
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { FAIL, expect } from './state.js'
|
|
5
|
+
import { buildDispatch, opOf, recognizeSeqIdiom } from './analyze.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A real source generator for grammars.
|
|
9
|
+
*
|
|
10
|
+
* The planner removes per-token *decisions*, but it still pays one indirect
|
|
11
|
+
* call per grammar node, through call sites that see many closure shapes and so
|
|
12
|
+
* cannot be inlined. That is the floor for a closure tree, and it is where the
|
|
13
|
+
* remaining gap against hand-written parsers lives.
|
|
14
|
+
*
|
|
15
|
+
* This module emits one JavaScript function per rule with every node inlined as
|
|
16
|
+
* straight-line statements — the shape a hand-written recursive-descent parser
|
|
17
|
+
* has. `alt` becomes a real `switch` on the token type, which V8 compiles to a
|
|
18
|
+
* jump table rather than a Map lookup. Only rule boundaries remain calls.
|
|
19
|
+
*
|
|
20
|
+
* Anything it does not know how to emit falls back to the planner's closure via
|
|
21
|
+
* the helper table, so adding an operator here is optional and incremental.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{ planner: any, rules: Record<string, any>, firsts: Map<string, any>|null,
|
|
26
|
+
* memo: boolean, maxSteps: number }} config
|
|
27
|
+
*/
|
|
28
|
+
export function createCodegen (config) {
|
|
29
|
+
const { planner, rules, firsts, memo, maxSteps } = config
|
|
30
|
+
|
|
31
|
+
/** Runtime values the generated code closes over. */
|
|
32
|
+
const HELPERS = []
|
|
33
|
+
/** What each helper was built from; parallel to HELPERS. */
|
|
34
|
+
const ORIGINS = []
|
|
35
|
+
/** Generated rule functions, indexed; filled in as they are built. */
|
|
36
|
+
const RULES = []
|
|
37
|
+
/** The source each rule was built from, parallel to RULES. */
|
|
38
|
+
const SOURCES = []
|
|
39
|
+
const ruleIndex = new Map()
|
|
40
|
+
const pending = []
|
|
41
|
+
|
|
42
|
+
let counter = 0
|
|
43
|
+
const nextId = () => '_' + (counter++)
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Registers a runtime value and returns its accessor source.
|
|
47
|
+
*
|
|
48
|
+
* `origin` records what the value *is*, so `emitModule` can write an
|
|
49
|
+
* equivalent table into a generated file. A closure cannot be serialized; the
|
|
50
|
+
* JSON Logic it was compiled from can.
|
|
51
|
+
*/
|
|
52
|
+
function helper (value, origin) {
|
|
53
|
+
HELPERS.push(value)
|
|
54
|
+
ORIGINS.push(origin)
|
|
55
|
+
return `H[${HELPERS.length - 1}]`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A semantic action, recorded as the logic it came from. */
|
|
59
|
+
const actionHelper = (logic) => helper(planner.planAction(logic), { kind: 'action', logic })
|
|
60
|
+
|
|
61
|
+
/** A literal value sitting in parser position. */
|
|
62
|
+
const constantHelper = (node) => helper(planner.plan(node), { kind: 'constant', node })
|
|
63
|
+
|
|
64
|
+
function indexOfRule (name) {
|
|
65
|
+
let index = ruleIndex.get(name)
|
|
66
|
+
if (index !== undefined) return index
|
|
67
|
+
index = RULES.length
|
|
68
|
+
ruleIndex.set(name, index)
|
|
69
|
+
RULES.push(null)
|
|
70
|
+
pending.push(name)
|
|
71
|
+
return index
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const guard = maxSteps > 0
|
|
75
|
+
const tickSrc = guard ? `if (++c.steps > ${maxSteps}) T();` : ''
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Emits statements computing `node` into a fresh variable.
|
|
79
|
+
* @param {*} node
|
|
80
|
+
* @param {string[]} out Statement buffer.
|
|
81
|
+
* @param {string|null} bindingsVar Current sequence's bindings variable, if any.
|
|
82
|
+
* @returns {string} Name of the variable holding the result.
|
|
83
|
+
*/
|
|
84
|
+
function gen (node, out, bindingsVar) {
|
|
85
|
+
const op = opOf(node)
|
|
86
|
+
const result = nextId()
|
|
87
|
+
|
|
88
|
+
if (op === null) {
|
|
89
|
+
out.push(`const ${result} = ${constantHelper(node)}(c);`)
|
|
90
|
+
return result
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const arg = node[op]
|
|
94
|
+
|
|
95
|
+
switch (op) {
|
|
96
|
+
case 'consume': {
|
|
97
|
+
const name = typeof arg === 'string' ? arg : arg[0]
|
|
98
|
+
const token = nextId()
|
|
99
|
+
out.push(
|
|
100
|
+
`let ${result};`,
|
|
101
|
+
`const ${token} = c.tokens[c.idx];`,
|
|
102
|
+
`if (${token} !== undefined && ${token}.type === ${JSON.stringify(name)}) { c.idx++; ${result} = ${token}; }`,
|
|
103
|
+
`else { E(c, ${JSON.stringify(name)}); ${result} = F; }`
|
|
104
|
+
)
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'epsilon':
|
|
109
|
+
out.push(`const ${result} = null;`)
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
case 'label': {
|
|
113
|
+
const value = gen(arg[1], out, bindingsVar)
|
|
114
|
+
if (bindingsVar) out.push(`if (${value} !== F) ${bindingsVar}[${JSON.stringify(arg[0])}] = ${value};`)
|
|
115
|
+
else out.push(`if (${value} !== F && c.bindings) c.bindings[${JSON.stringify(arg[0])}] = ${value};`)
|
|
116
|
+
out.push(`const ${result} = ${value};`)
|
|
117
|
+
return result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case 'action': {
|
|
121
|
+
const fn = actionHelper(arg)
|
|
122
|
+
out.push(`const ${result} = ${fn}(${bindingsVar || 'c.bindings || {}'}, c);`)
|
|
123
|
+
return result
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case 'seq':
|
|
127
|
+
return genSeq(arg, out, result)
|
|
128
|
+
|
|
129
|
+
case 'alt':
|
|
130
|
+
return genAlt(arg, out, result, bindingsVar)
|
|
131
|
+
|
|
132
|
+
case 'option': {
|
|
133
|
+
const start = nextId()
|
|
134
|
+
out.push(`let ${result};`, `const ${start} = c.idx;`, '{')
|
|
135
|
+
const value = gen(arg, out, bindingsVar)
|
|
136
|
+
out.push(
|
|
137
|
+
`if (${value} === F) { c.idx = ${start}; ${result} = null; } else ${result} = ${value};`,
|
|
138
|
+
'}'
|
|
139
|
+
)
|
|
140
|
+
return result
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case 'many':
|
|
144
|
+
case 'many1': {
|
|
145
|
+
const atLeastOne = op === 'many1'
|
|
146
|
+
const before = nextId()
|
|
147
|
+
const label = 'L' + nextId()
|
|
148
|
+
out.push(tickSrc, `let ${result} = [];`, `${label}: {`)
|
|
149
|
+
|
|
150
|
+
if (atLeastOne) {
|
|
151
|
+
const start = nextId()
|
|
152
|
+
out.push(`const ${start} = c.idx;`)
|
|
153
|
+
const first = gen(arg, out, bindingsVar)
|
|
154
|
+
out.push(
|
|
155
|
+
`if (${first} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`,
|
|
156
|
+
`if (c.idx === ${start}) P(${JSON.stringify(op)}, ${start});`,
|
|
157
|
+
`${result}.push(${first});`
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
out.push('for (;;) {', `const ${before} = c.idx;`)
|
|
162
|
+
const value = gen(arg, out, bindingsVar)
|
|
163
|
+
out.push(
|
|
164
|
+
`if (${value} === F) { c.idx = ${before}; break; }`,
|
|
165
|
+
`if (c.idx === ${before}) P(${JSON.stringify(op)}, ${before});`,
|
|
166
|
+
`${result}.push(${value});`,
|
|
167
|
+
'}',
|
|
168
|
+
'}'
|
|
169
|
+
)
|
|
170
|
+
return result
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'infixLeft': {
|
|
174
|
+
// One precedence level, inlined: operand, then a loop that only runs
|
|
175
|
+
// when an operator actually follows. Nothing is allocated for the
|
|
176
|
+
// common case of a bare operand.
|
|
177
|
+
const label = 'L' + nextId()
|
|
178
|
+
const left = nextId()
|
|
179
|
+
const bindings = nextId()
|
|
180
|
+
const mark = nextId()
|
|
181
|
+
const combine = actionHelper(arg.combine)
|
|
182
|
+
|
|
183
|
+
out.push(tickSrc, `let ${result};`, `${label}: {`)
|
|
184
|
+
const first = gen(arg.operand, out, bindingsVar)
|
|
185
|
+
out.push(
|
|
186
|
+
`if (${first} === F) { ${result} = F; break ${label}; }`,
|
|
187
|
+
`let ${left} = ${first};`,
|
|
188
|
+
`let ${bindings} = null;`,
|
|
189
|
+
'for (;;) {',
|
|
190
|
+
`const ${mark} = c.idx;`
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
let operatorVar = 'null'
|
|
194
|
+
if (arg.operator !== undefined) {
|
|
195
|
+
operatorVar = gen(arg.operator, out, bindingsVar)
|
|
196
|
+
out.push(`if (${operatorVar} === F) { c.idx = ${mark}; break; }`)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const right = gen(arg.operand, out, bindingsVar)
|
|
200
|
+
out.push(
|
|
201
|
+
`if (${right} === F) { c.idx = ${mark}; break; }`,
|
|
202
|
+
`if (${bindings} === null) ${bindings} = { left: null, op: null, right: null };`,
|
|
203
|
+
`${bindings}.left = ${left}; ${bindings}.op = ${operatorVar}; ${bindings}.right = ${right};`,
|
|
204
|
+
`${left} = ${combine}(${bindings}, c);`,
|
|
205
|
+
'}',
|
|
206
|
+
`${result} = ${left};`,
|
|
207
|
+
'}'
|
|
208
|
+
)
|
|
209
|
+
return result
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
case 'oneOf': {
|
|
213
|
+
const names = Array.isArray(arg) ? arg : [arg]
|
|
214
|
+
const token = nextId()
|
|
215
|
+
const test = names.map((n) => `${token}.type === ${JSON.stringify(n)}`).join(' || ')
|
|
216
|
+
out.push(
|
|
217
|
+
`let ${result};`,
|
|
218
|
+
`const ${token} = c.tokens[c.idx];`,
|
|
219
|
+
`if (${token} !== undefined && (${test})) { c.idx++; ${result} = ${token}; }`,
|
|
220
|
+
`else { ${names.map((n) => `E(c, ${JSON.stringify(n)});`).join(' ')} ${result} = F; }`
|
|
221
|
+
)
|
|
222
|
+
return result
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case 'eof':
|
|
226
|
+
out.push(
|
|
227
|
+
`let ${result};`,
|
|
228
|
+
`if (c.idx >= c.tokens.length) ${result} = null;`,
|
|
229
|
+
`else { E(c, "end of input"); ${result} = F; }`
|
|
230
|
+
)
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
case 'infixRight': {
|
|
234
|
+
const label = 'L' + nextId()
|
|
235
|
+
const left = nextId()
|
|
236
|
+
const items = nextId()
|
|
237
|
+
const mark = nextId()
|
|
238
|
+
const bindings = nextId()
|
|
239
|
+
const value = nextId()
|
|
240
|
+
const combine = actionHelper(arg.combine)
|
|
241
|
+
|
|
242
|
+
out.push(tickSrc, `let ${result};`, `${label}: {`)
|
|
243
|
+
const first = gen(arg.operand, out, bindingsVar)
|
|
244
|
+
out.push(
|
|
245
|
+
`if (${first} === F) { ${result} = F; break ${label}; }`,
|
|
246
|
+
`const ${left} = ${first};`,
|
|
247
|
+
`let ${items} = null;`,
|
|
248
|
+
'for (;;) {',
|
|
249
|
+
`const ${mark} = c.idx;`
|
|
250
|
+
)
|
|
251
|
+
let operatorVar = 'null'
|
|
252
|
+
if (arg.operator !== undefined) {
|
|
253
|
+
operatorVar = gen(arg.operator, out, bindingsVar)
|
|
254
|
+
out.push(`if (${operatorVar} === F) { c.idx = ${mark}; break; }`)
|
|
255
|
+
}
|
|
256
|
+
const right = gen(arg.operand, out, bindingsVar)
|
|
257
|
+
out.push(
|
|
258
|
+
`if (${right} === F) { c.idx = ${mark}; break; }`,
|
|
259
|
+
`if (${items} === null) ${items} = [];`,
|
|
260
|
+
`${items}.push(${operatorVar}, ${right});`,
|
|
261
|
+
'}',
|
|
262
|
+
`if (${items} === null) { ${result} = ${left}; break ${label}; }`,
|
|
263
|
+
`const ${bindings} = { left: null, op: null, right: null };`,
|
|
264
|
+
`let ${value} = ${items}[${items}.length - 1];`,
|
|
265
|
+
`for (let i = ${items}.length - 2; i >= 0; i -= 2) {`,
|
|
266
|
+
`${bindings}.left = i === 0 ? ${left} : ${items}[i - 1];`,
|
|
267
|
+
`${bindings}.op = ${items}[i]; ${bindings}.right = ${value};`,
|
|
268
|
+
`${value} = ${combine}(${bindings}, c);`,
|
|
269
|
+
'}',
|
|
270
|
+
`${result} = ${value};`,
|
|
271
|
+
'}'
|
|
272
|
+
)
|
|
273
|
+
return result
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
case 'lookahead': {
|
|
277
|
+
const start = nextId()
|
|
278
|
+
out.push(`const ${start} = c.idx;`)
|
|
279
|
+
const value = gen(arg, out, bindingsVar)
|
|
280
|
+
out.push(`c.idx = ${start};`, `const ${result} = ${value};`)
|
|
281
|
+
return result
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
case 'negLookahead': {
|
|
285
|
+
const start = nextId()
|
|
286
|
+
out.push(`const ${start} = c.idx;`)
|
|
287
|
+
const value = gen(arg, out, bindingsVar)
|
|
288
|
+
out.push(`c.idx = ${start};`, `const ${result} = ${value} === F ? null : F;`)
|
|
289
|
+
return result
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
case 'text': {
|
|
293
|
+
const label = 'L' + nextId()
|
|
294
|
+
const start = nextId()
|
|
295
|
+
const first = nextId()
|
|
296
|
+
const last = nextId()
|
|
297
|
+
out.push(`let ${result};`, `${label}: {`, `const ${start} = c.idx;`)
|
|
298
|
+
const value = gen(arg, out, bindingsVar)
|
|
299
|
+
out.push(
|
|
300
|
+
`if (${value} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`,
|
|
301
|
+
`if (c.idx === ${start}) { ${result} = ''; break ${label}; }`,
|
|
302
|
+
`const ${first} = c.tokens[${start}];`,
|
|
303
|
+
`const ${last} = c.tokens[c.idx - 1];`,
|
|
304
|
+
`if (c.source !== undefined) { ${result} = c.source.slice(${first}.start, ${last}.end); break ${label}; }`,
|
|
305
|
+
`let ${result}_s = '';`,
|
|
306
|
+
`for (let i = ${start}; i < c.idx; i++) ${result}_s += c.tokens[i].image;`,
|
|
307
|
+
`${result} = ${result}_s;`,
|
|
308
|
+
'}'
|
|
309
|
+
)
|
|
310
|
+
return result
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
case 'as': {
|
|
314
|
+
const transform = actionHelper(Array.isArray(arg) ? arg[1] : arg.to)
|
|
315
|
+
const value = gen(Array.isArray(arg) ? arg[0] : arg.parser, out, bindingsVar)
|
|
316
|
+
out.push(`const ${result} = ${value} === F ? F : ${transform}(${value}, c);`)
|
|
317
|
+
return result
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
case 'between': {
|
|
321
|
+
const label = 'L' + nextId()
|
|
322
|
+
const start = nextId()
|
|
323
|
+
out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${start} = c.idx;`)
|
|
324
|
+
const open = gen(arg.open, out, bindingsVar)
|
|
325
|
+
out.push(`if (${open} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`)
|
|
326
|
+
const body = gen(arg.body, out, bindingsVar)
|
|
327
|
+
out.push(`if (${body} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`)
|
|
328
|
+
const close = gen(arg.close, out, bindingsVar)
|
|
329
|
+
out.push(
|
|
330
|
+
`if (${close} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`,
|
|
331
|
+
`${result} = ${body};`,
|
|
332
|
+
'}'
|
|
333
|
+
)
|
|
334
|
+
return result
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
case 'prefix': {
|
|
338
|
+
const label = 'L' + nextId()
|
|
339
|
+
const start = nextId()
|
|
340
|
+
const ops = nextId()
|
|
341
|
+
const mark = nextId()
|
|
342
|
+
const bindings = nextId()
|
|
343
|
+
const value = nextId()
|
|
344
|
+
const combine = actionHelper(arg.combine)
|
|
345
|
+
|
|
346
|
+
out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${start} = c.idx;`, `let ${ops} = null;`, 'for (;;) {', `const ${mark} = c.idx;`)
|
|
347
|
+
const op = gen(arg.operator, out, bindingsVar)
|
|
348
|
+
out.push(
|
|
349
|
+
`if (${op} === F) { c.idx = ${mark}; break; }`,
|
|
350
|
+
`if (${ops} === null) ${ops} = [];`,
|
|
351
|
+
`${ops}.push(${op});`,
|
|
352
|
+
'}'
|
|
353
|
+
)
|
|
354
|
+
const operand = gen(arg.operand, out, bindingsVar)
|
|
355
|
+
out.push(
|
|
356
|
+
`if (${operand} === F) { c.idx = ${start}; ${result} = F; break ${label}; }`,
|
|
357
|
+
`let ${value} = ${operand};`,
|
|
358
|
+
`if (${ops} !== null) {`,
|
|
359
|
+
`const ${bindings} = { op: null, operand: null };`,
|
|
360
|
+
`for (let i = ${ops}.length - 1; i >= 0; i--) { ${bindings}.op = ${ops}[i]; ${bindings}.operand = ${value}; ${value} = ${combine}(${bindings}, c); }`,
|
|
361
|
+
'}',
|
|
362
|
+
`${result} = ${value};`,
|
|
363
|
+
'}'
|
|
364
|
+
)
|
|
365
|
+
return result
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
case 'postfix': {
|
|
369
|
+
const label = 'L' + nextId()
|
|
370
|
+
const left = nextId()
|
|
371
|
+
const bindings = nextId()
|
|
372
|
+
const mark = nextId()
|
|
373
|
+
const combine = actionHelper(arg.combine)
|
|
374
|
+
|
|
375
|
+
out.push(tickSrc, `let ${result};`, `${label}: {`)
|
|
376
|
+
const first = gen(arg.operand, out, bindingsVar)
|
|
377
|
+
out.push(
|
|
378
|
+
`if (${first} === F) { ${result} = F; break ${label}; }`,
|
|
379
|
+
`let ${left} = ${first};`,
|
|
380
|
+
`let ${bindings} = null;`,
|
|
381
|
+
'for (;;) {',
|
|
382
|
+
`const ${mark} = c.idx;`
|
|
383
|
+
)
|
|
384
|
+
const found = gen(arg.suffix, out, bindingsVar)
|
|
385
|
+
out.push(
|
|
386
|
+
`if (${found} === F) { c.idx = ${mark}; break; }`,
|
|
387
|
+
`if (${bindings} === null) ${bindings} = { left: null, suffix: null };`,
|
|
388
|
+
`${bindings}.left = ${left}; ${bindings}.suffix = ${found};`,
|
|
389
|
+
`${left} = ${combine}(${bindings}, c);`,
|
|
390
|
+
'}',
|
|
391
|
+
`${result} = ${left};`,
|
|
392
|
+
'}'
|
|
393
|
+
)
|
|
394
|
+
return result
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
case 'manySep':
|
|
398
|
+
case 'many1Sep': {
|
|
399
|
+
if (!arg || typeof arg !== 'object') throw new Error("'manySep' expects { rule, sep }")
|
|
400
|
+
const atLeastOne = op === 'many1Sep'
|
|
401
|
+
const sepNode = typeof arg.sep === 'string' ? { consume: arg.sep } : arg.sep
|
|
402
|
+
const trailing = arg.trailing === true
|
|
403
|
+
const start = nextId()
|
|
404
|
+
const beforeSep = nextId()
|
|
405
|
+
const beforeItem = nextId()
|
|
406
|
+
const label = 'L' + nextId()
|
|
407
|
+
|
|
408
|
+
out.push(tickSrc, `let ${result} = [];`, `${label}: {`, `const ${start} = c.idx;`)
|
|
409
|
+
const first = gen(arg.rule, out, bindingsVar)
|
|
410
|
+
out.push(
|
|
411
|
+
`if (${first} === F) { c.idx = ${start}; ${result} = ${atLeastOne ? 'F' : '[]'}; break ${label}; }`,
|
|
412
|
+
`${result}.push(${first});`,
|
|
413
|
+
'for (;;) {',
|
|
414
|
+
`const ${beforeSep} = c.idx;`
|
|
415
|
+
)
|
|
416
|
+
const sep = gen(sepNode, out, bindingsVar)
|
|
417
|
+
out.push(`if (${sep} === F) { c.idx = ${beforeSep}; break; }`, `const ${beforeItem} = c.idx;`)
|
|
418
|
+
const item = gen(arg.rule, out, bindingsVar)
|
|
419
|
+
out.push(
|
|
420
|
+
`if (${item} === F) { c.idx = ${trailing ? beforeItem : beforeSep}; break; }`,
|
|
421
|
+
`${result}.push(${item});`,
|
|
422
|
+
'}',
|
|
423
|
+
'}'
|
|
424
|
+
)
|
|
425
|
+
return result
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
case 'subrule': {
|
|
429
|
+
const name = typeof arg === 'string' ? arg : arg[0]
|
|
430
|
+
if (!(name in rules)) {
|
|
431
|
+
// Preserve the runtime error rather than failing to generate.
|
|
432
|
+
out.push(`const ${result} = ${helper(planner.plan(node), { kind: 'unknownRule', name })}(c);`)
|
|
433
|
+
return result
|
|
434
|
+
}
|
|
435
|
+
const index = indexOfRule(name)
|
|
436
|
+
const parent = nextId()
|
|
437
|
+
out.push(tickSrc)
|
|
438
|
+
if (memo) {
|
|
439
|
+
const key = nextId()
|
|
440
|
+
const hit = nextId()
|
|
441
|
+
const start = nextId()
|
|
442
|
+
out.push(
|
|
443
|
+
`let ${result};`,
|
|
444
|
+
`const ${key} = ${JSON.stringify(name + ':')} + c.idx;`,
|
|
445
|
+
`const ${hit} = c.memo.get(${key});`,
|
|
446
|
+
`if (${hit} !== undefined) { c.idx = ${hit}.end; ${result} = ${hit}.value; }`,
|
|
447
|
+
'else {',
|
|
448
|
+
`const ${start} = c.idx;`,
|
|
449
|
+
`const ${parent} = c.ruleDepth;`,
|
|
450
|
+
`if (c.track) { c.ruleNames[${parent}] = ${JSON.stringify(name)}; c.ruleDepth = ${parent} + 1; }`,
|
|
451
|
+
`${result} = R[${index}](c);`,
|
|
452
|
+
`if (c.track) c.ruleDepth = ${parent};`,
|
|
453
|
+
`if (${result} === F) c.idx = ${start};`,
|
|
454
|
+
`c.memo.set(${key}, { value: ${result}, end: c.idx });`,
|
|
455
|
+
'}'
|
|
456
|
+
)
|
|
457
|
+
} else {
|
|
458
|
+
out.push(
|
|
459
|
+
`const ${parent} = c.ruleDepth;`,
|
|
460
|
+
`if (c.track) { c.ruleNames[${parent}] = ${JSON.stringify(name)}; c.ruleDepth = ${parent} + 1; }`,
|
|
461
|
+
`const ${result} = R[${index}](c);`,
|
|
462
|
+
`if (c.track) c.ruleDepth = ${parent};`
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
return result
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
default:
|
|
469
|
+
// Not generated: defer to the planner's closure.
|
|
470
|
+
out.push(`const ${result} = ${helper(planner.plan(node))}(c);`)
|
|
471
|
+
return result
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function genSeq (children, out, result) {
|
|
476
|
+
if (!Array.isArray(children)) throw new Error("'seq' expects an array of parsers")
|
|
477
|
+
|
|
478
|
+
// Same idiom recognition as the planner, so both paths agree.
|
|
479
|
+
const idiom = recognizeSeqIdiom(children)
|
|
480
|
+
if (idiom !== null) return gen(idiom, out, null)
|
|
481
|
+
|
|
482
|
+
let hasLabels = false
|
|
483
|
+
let hasAction = false
|
|
484
|
+
for (const child of children) {
|
|
485
|
+
const childOp = opOf(child)
|
|
486
|
+
if (childOp === 'label') hasLabels = true
|
|
487
|
+
else if (childOp === 'action') hasAction = true
|
|
488
|
+
}
|
|
489
|
+
const needsScope = hasLabels || hasAction
|
|
490
|
+
|
|
491
|
+
const start = nextId()
|
|
492
|
+
const label = 'L' + nextId()
|
|
493
|
+
const bindingsVar = needsScope ? nextId() : null
|
|
494
|
+
const saved = nextId()
|
|
495
|
+
const collected = []
|
|
496
|
+
|
|
497
|
+
out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${start} = c.idx;`)
|
|
498
|
+
if (needsScope) {
|
|
499
|
+
out.push(`const ${saved} = c.bindings;`, `const ${bindingsVar} = {};`, `c.bindings = ${bindingsVar};`)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
let actionResult = null
|
|
503
|
+
for (const child of children) {
|
|
504
|
+
const childOp = opOf(child)
|
|
505
|
+
if (childOp === 'action') {
|
|
506
|
+
actionResult = gen(child, out, bindingsVar)
|
|
507
|
+
continue
|
|
508
|
+
}
|
|
509
|
+
const value = gen(child, out, bindingsVar)
|
|
510
|
+
out.push(
|
|
511
|
+
`if (${value} === F) { c.idx = ${start};` +
|
|
512
|
+
(needsScope ? ` c.bindings = ${saved};` : '') +
|
|
513
|
+
` ${result} = F; break ${label}; }`
|
|
514
|
+
)
|
|
515
|
+
collected.push(value)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (needsScope) out.push(`c.bindings = ${saved};`)
|
|
519
|
+
out.push(`${result} = ${hasAction ? actionResult : '[' + collected.join(', ') + ']'};`, '}')
|
|
520
|
+
return result
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function genAlt (branches, out, result, bindingsVar) {
|
|
524
|
+
if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches")
|
|
525
|
+
|
|
526
|
+
const table = firsts ? buildDispatch(branches, firsts) : null
|
|
527
|
+
const label = 'L' + nextId()
|
|
528
|
+
|
|
529
|
+
if (table !== null) {
|
|
530
|
+
// A switch on the token type: V8 turns a dense string switch into a
|
|
531
|
+
// hash lookup with inlined bodies, and every branch is inlined here.
|
|
532
|
+
const expected = [...table.keys()]
|
|
533
|
+
const token = nextId()
|
|
534
|
+
out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${token} = c.tokens[c.idx];`)
|
|
535
|
+
out.push(`if (${token} === undefined) { ${expected.map((t) => `E(c, ${JSON.stringify(t)});`).join(' ')} ${result} = F; break ${label}; }`)
|
|
536
|
+
out.push(`switch (${token}.type) {`)
|
|
537
|
+
|
|
538
|
+
const byBranch = new Map()
|
|
539
|
+
for (const [tokenName, branch] of table) {
|
|
540
|
+
if (!byBranch.has(branch)) byBranch.set(branch, [])
|
|
541
|
+
byBranch.get(branch).push(tokenName)
|
|
542
|
+
}
|
|
543
|
+
for (const [branch, tokenNames] of byBranch) {
|
|
544
|
+
out.push(tokenNames.map((t) => `case ${JSON.stringify(t)}:`).join(' ') + ' {')
|
|
545
|
+
const value = gen(branch, out, bindingsVar)
|
|
546
|
+
out.push(`${result} = ${value}; break ${label};`, '}')
|
|
547
|
+
}
|
|
548
|
+
out.push(
|
|
549
|
+
`default: { ${expected.map((t) => `E(c, ${JSON.stringify(t)});`).join(' ')} ${result} = F; break ${label}; }`,
|
|
550
|
+
'}',
|
|
551
|
+
'}'
|
|
552
|
+
)
|
|
553
|
+
return result
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Ordered backtracking, still fully inlined.
|
|
557
|
+
const start = nextId()
|
|
558
|
+
out.push(tickSrc, `let ${result};`, `${label}: {`, `const ${start} = c.idx;`)
|
|
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
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Generates and installs the function for one rule. */
|
|
569
|
+
function buildRule (name) {
|
|
570
|
+
const out = []
|
|
571
|
+
const value = gen(rules[name], out, null)
|
|
572
|
+
const body = `(c) => {\n${out.join('\n')}\nreturn ${value};\n}`
|
|
573
|
+
// Kept so `emitModule` can write out exactly what was built here, with the
|
|
574
|
+
// helper indices that go with it. Regenerating later would allocate fresh
|
|
575
|
+
// helpers and the indices would not line up.
|
|
576
|
+
SOURCES[ruleIndex.get(name)] = body
|
|
577
|
+
const source = `return ${body}`
|
|
578
|
+
// 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)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function progressError (op, at) {
|
|
584
|
+
throw new Error(
|
|
585
|
+
`'${op}' matched without consuming any input at token index ${at}; ` +
|
|
586
|
+
'this would loop forever. Make the repeated parser consume at least one token.'
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function stepsError () {
|
|
591
|
+
throw new Error(`Parser exceeded maxSteps (${maxSteps}); the grammar is likely looping.`)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Compiles `start` and everything it reaches.
|
|
596
|
+
* @param {string} start
|
|
597
|
+
* @returns {(state: any) => any}
|
|
598
|
+
*/
|
|
599
|
+
function compile (start) {
|
|
600
|
+
const index = indexOfRule(start)
|
|
601
|
+
while (pending.length) {
|
|
602
|
+
const name = pending.shift()
|
|
603
|
+
RULES[ruleIndex.get(name)] = buildRule(name)
|
|
604
|
+
}
|
|
605
|
+
return RULES[index]
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** The generated function for a rule, if it was reached from the start rule. */
|
|
609
|
+
function functionFor (name) {
|
|
610
|
+
const index = ruleIndex.get(name)
|
|
611
|
+
return index === undefined ? null : RULES[index]
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Exposed for tests and debugging: the source we actually emit.
|
|
616
|
+
*
|
|
617
|
+
* Generating appends to the helper table and can queue rules, so the state is
|
|
618
|
+
* snapshotted and restored — inspecting a parser must not perturb it.
|
|
619
|
+
*/
|
|
620
|
+
function sourceFor (name) {
|
|
621
|
+
const helperCount = HELPERS.length
|
|
622
|
+
const pendingCount = pending.length
|
|
623
|
+
try {
|
|
624
|
+
const out = []
|
|
625
|
+
const value = gen(rules[name], out, null)
|
|
626
|
+
return `(c) => {\n${out.join('\n')}\nreturn ${value};\n}`
|
|
627
|
+
} finally {
|
|
628
|
+
HELPERS.length = helperCount
|
|
629
|
+
ORIGINS.length = helperCount
|
|
630
|
+
pending.length = pendingCount
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return { compile, functionFor, sourceFor, ruleIndex, RULES, SOURCES, ORIGINS, guard }
|
|
635
|
+
}
|