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/src/plan.js ADDED
@@ -0,0 +1,938 @@
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
+ * The specializing planner.
9
+ *
10
+ * Profiling the interpreted parser showed the cost was not in the parsing logic
11
+ * itself but in re-deciding things that never change: `Object.keys` on every
12
+ * child of every `seq` to find its operator, a WeakMap lookup per node to find
13
+ * its cached execution plan, and another to find an `alt`'s dispatch table.
14
+ * All of that is a function of the *grammar*, which is fixed.
15
+ *
16
+ * The planner walks the grammar once and produces a tree of closures with those
17
+ * decisions already baked in: an `alt` closes over its dispatch Map, a `seq`
18
+ * closes over a pre-classified list of steps, a `consume` closes over its token
19
+ * name. Parsing is then just calling closures.
20
+ *
21
+ * These closures are what the combinators' `compile` hooks hand to
22
+ * json-logic-engine's compiler, so `engine.build(rule)` produces a function that
23
+ * calls straight into them.
24
+ */
25
+
26
+ const SYNC = Symbol.for('json_logic_sync')
27
+
28
+ /** Marks a closure as synchronous so the engine's compiler inlines it directly. */
29
+ function declareSync (fn) {
30
+ fn[SYNC] = true
31
+ return fn
32
+ }
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
+ /**
56
+ * True when the logic could return the *context object itself* rather than a
57
+ * value derived from it — `{"var": ""}` and `{"val": []}` both do.
58
+ *
59
+ * That matters for sequences that reuse their binding object: handing the live
60
+ * object to a caller would alias it, and the next invocation would appear to
61
+ * mutate the value they were given. Only `var`/`val` can surface the context;
62
+ * a method receives evaluated arguments, never the context.
63
+ */
64
+ function mayReturnContext (logic) {
65
+ if (Array.isArray(logic)) return logic.some(mayReturnContext)
66
+ if (!logic || typeof logic !== 'object') return false
67
+
68
+ for (const key of Object.keys(logic)) {
69
+ const arg = logic[key]
70
+ if (key === 'val' && Array.isArray(arg) && arg.length === 0) return true
71
+ if (key === 'var' && (arg === '' || arg === null || (Array.isArray(arg) && (arg.length === 0 || arg[0] === '')))) return true
72
+ if (mayReturnContext(arg)) return true
73
+ }
74
+ return false
75
+ }
76
+
77
+ /**
78
+ * @param {{ engine: any, rules: Record<string, any>, firsts: Map<string, any>|null,
79
+ * memo: boolean, maxSteps: number, unsafeEval?: boolean }} config
80
+ */
81
+ export function createPlanner (config) {
82
+ const { engine, rules, firsts, memo, maxSteps, unsafeEval = true } = config
83
+
84
+ /** @type {Map<string, { fn: ((state: any) => any) | null }>} */
85
+ const slots = new Map()
86
+ /** @type {WeakMap<object, (state: any) => any>} */
87
+ const planCache = new WeakMap()
88
+ /** @type {Map<string, {byObject: WeakMap<object, any>, byPrimitive: Map<string, any>}>} */
89
+ const opCaches = new Map()
90
+
91
+ const guard = maxSteps > 0
92
+
93
+ function tick (state) {
94
+ if (guard && ++state.steps > maxSteps) {
95
+ throw new Error(`Parser exceeded maxSteps (${maxSteps}); the grammar is likely looping.`)
96
+ }
97
+ }
98
+
99
+ function assertProgress (state, before, op) {
100
+ if (state.idx === before) {
101
+ throw new Error(
102
+ `'${op}' matched without consuming any input at token index ${before}; ` +
103
+ 'this would loop forever. Make the repeated parser consume at least one token.'
104
+ )
105
+ }
106
+ }
107
+
108
+ /** Resolves a rule to a slot, pre-registering it so recursion terminates. */
109
+ function slotFor (name) {
110
+ let slot = slots.get(name)
111
+ if (slot !== undefined) return slot
112
+
113
+ slot = { fn: null }
114
+ slots.set(name, slot)
115
+
116
+ const body = rules[name]
117
+ if (body === undefined) {
118
+ slot.fn = () => { throw new Error(`Unknown rule '${name}'`) }
119
+ return slot
120
+ }
121
+ // The slot is already in the map, so a recursive reference finds it and
122
+ // reads `slot.fn` at call time rather than at plan time.
123
+ slot.fn = plan(body)
124
+ return slot
125
+ }
126
+
127
+ /**
128
+ * Compiles a semantic action to a `(bindings, state) => value` function.
129
+ *
130
+ * Interpreted rather than compiled when `unsafeEval` is off (the engine's
131
+ * compiler builds through `eval`), when the action reads an outer scope
132
+ * (`above` is not threaded into built functions), or when the compiler simply
133
+ * cannot handle it.
134
+ */
135
+ function planAction (logic) {
136
+ const interpret = (bindings, state) => engine.run(logic, bindings, { above: [state] })
137
+ if (!unsafeEval || usesOuterScope(logic)) return interpret
138
+ try {
139
+ const built = engine.build(logic)
140
+ return typeof built === 'function' ? (bindings) => built(bindings) : () => built
141
+ } catch {
142
+ return interpret
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Plans a node given its operator and raw arguments, memoized on the
148
+ * arguments themselves.
149
+ *
150
+ * This is the entry point the *interpreted* combinators use. A lazy method is
151
+ * handed only its arguments, never the enclosing `{op: args}` object, so
152
+ * rebuilding that object to call `plan()` would allocate a fresh key on every
153
+ * invocation and miss the cache every time. Keying on the argument object —
154
+ * which is stable, it is part of the grammar — gives the interpreted path the
155
+ * same specialize-once behaviour the compiled path gets.
156
+ *
157
+ * @param {string} op
158
+ * @param {*} args
159
+ * @returns {(state: any) => any}
160
+ */
161
+ function planOp (op, args) {
162
+ let cache = opCaches.get(op)
163
+ if (cache === undefined) {
164
+ cache = { byObject: new WeakMap(), byPrimitive: new Map() }
165
+ opCaches.set(op, cache)
166
+ }
167
+
168
+ if (args !== null && typeof args === 'object') {
169
+ let planned = cache.byObject.get(args)
170
+ if (planned === undefined) {
171
+ planned = planUncached({ [op]: args })
172
+ cache.byObject.set(args, planned)
173
+ }
174
+ return planned
175
+ }
176
+
177
+ // Primitive arguments (`{consume: "Int"}`) cannot key a WeakMap.
178
+ const key = typeof args + ':' + String(args)
179
+ let planned = cache.byPrimitive.get(key)
180
+ if (planned === undefined) {
181
+ planned = planUncached({ [op]: args })
182
+ cache.byPrimitive.set(key, planned)
183
+ }
184
+ return planned
185
+ }
186
+
187
+ /**
188
+ * @param {*} node
189
+ * @returns {(state: any) => any}
190
+ */
191
+ function plan (node) {
192
+ if (node === null || typeof node !== 'object') {
193
+ const constant = node
194
+ return () => constant
195
+ }
196
+
197
+ const cached = planCache.get(node)
198
+ if (cached !== undefined) return cached
199
+
200
+ const planned = planUncached(node)
201
+ planCache.set(node, planned)
202
+ publish(node, planned)
203
+ return planned
204
+ }
205
+
206
+ /**
207
+ * Publishes a plan into the engine's own `optimizedMap`.
208
+ *
209
+ * Without this the planner is a shadow cache: the engine would still hold its
210
+ * own entry for the same node — for a lazy method that entry is the slow
211
+ * `(data, abv) => called(rawArgs, ...)` wrapper — and anything reaching the
212
+ * node through `engine.run` or another method's `runOptimizedOrFallback`
213
+ * would take that path instead, then bounce through the combinator to reach
214
+ * the plan anyway.
215
+ *
216
+ * Injecting makes the specialization visible to the engine on its own terms:
217
+ * the node is now genuinely optimized as far as the rest of the engine is
218
+ * concerned, so a grammar embedded inside larger JSON Logic gets the fast path
219
+ * for free. The signature lines up because a plan ignores the extra `above`
220
+ * argument the engine passes.
221
+ */
222
+ function publish (node, planned) {
223
+ if (engine && engine.optimizedMap && !engine.disableInterpretedOptimization) {
224
+ engine.optimizedMap.set(node, planned)
225
+ }
226
+ }
227
+ /** @returns {(state: any) => any} */
228
+ const buildConsume = (arg) => {
229
+ const name = typeof arg === 'string' ? arg : arg[0]
230
+ return (state) => {
231
+ const token = state.tokens[state.idx]
232
+ if (token !== undefined && token.type === name) {
233
+ state.idx++
234
+ return token
235
+ }
236
+ expect(state, name)
237
+ return FAIL
238
+ }
239
+ }
240
+
241
+ /** @returns {(state: any) => any} */
242
+ const buildMany = (arg) => {
243
+ const child = plan(arg)
244
+ return (state) => {
245
+ tick(state)
246
+ const results = []
247
+ for (;;) {
248
+ const before = state.idx
249
+ const value = child(state)
250
+ if (value === FAIL) {
251
+ state.idx = before
252
+ return results
253
+ }
254
+ assertProgress(state, before, 'many')
255
+ results.push(value)
256
+ }
257
+ }
258
+ }
259
+
260
+ /** @returns {(state: any) => any} */
261
+ const buildMany1 = (arg) => {
262
+ const child = plan(arg)
263
+ return (state) => {
264
+ tick(state)
265
+ const start = state.idx
266
+ const first = child(state)
267
+ if (first === FAIL) {
268
+ state.idx = start
269
+ return FAIL
270
+ }
271
+ assertProgress(state, start, 'many1')
272
+ const results = [first]
273
+ for (;;) {
274
+ const before = state.idx
275
+ const value = child(state)
276
+ if (value === FAIL) {
277
+ state.idx = before
278
+ return results
279
+ }
280
+ assertProgress(state, before, 'many1')
281
+ results.push(value)
282
+ }
283
+ }
284
+ }
285
+
286
+ /** @returns {(state: any) => any} */
287
+ const buildOption = (arg) => {
288
+ const child = plan(arg)
289
+ return (state) => {
290
+ const start = state.idx
291
+ const value = child(state)
292
+ if (value === FAIL) {
293
+ state.idx = start
294
+ return null
295
+ }
296
+ return value
297
+ }
298
+ }
299
+
300
+ /** @returns {(state: any) => any} */
301
+ const buildInfixLeft = (arg) => {
302
+ // A whole precedence level in one closure.
303
+ //
304
+ // Spelled out of the primitives this is
305
+ // seq(label(head, operand), label(tail, many(seq(op, operand, action))), action(fold))
306
+ // which pays a bindings object, an empty `tail` array and a fold action
307
+ // on every level *even when there is no operator* — and a precedence
308
+ // chain is six or seven levels deep, so a bare operand pays all of it
309
+ // several times over. Here the no-operator case is one operand parse
310
+ // and one failed operator match, with nothing allocated.
311
+ const operand = plan(arg.operand)
312
+ const operator = arg.operator === undefined ? null : plan(arg.operator)
313
+ const combine = planAction(arg.combine)
314
+ return (state) => {
315
+ tick(state)
316
+ let left = operand(state)
317
+ if (left === FAIL) return FAIL
318
+ // Allocated on the first operator only, then reused for the rest of
319
+ // this fold: `combine` reads it and does not retain it.
320
+ let bindings = null
321
+ for (;;) {
322
+ const mark = state.idx
323
+ let op = null
324
+ if (operator !== null) {
325
+ op = operator(state)
326
+ if (op === FAIL) {
327
+ state.idx = mark
328
+ return left
329
+ }
330
+ }
331
+ const right = operand(state)
332
+ if (right === FAIL) {
333
+ state.idx = mark
334
+ return left
335
+ }
336
+ if (bindings === null) bindings = { left: null, op: null, right: null }
337
+ bindings.left = left
338
+ bindings.op = op
339
+ bindings.right = right
340
+ left = combine(bindings, state)
341
+ }
342
+ }
343
+ }
344
+
345
+ /** @returns {(state: any) => any} */
346
+ const buildOneOf = (arg) => {
347
+ // `{"oneOf": ["Plus", "Minus"]}` — any of a set of token types.
348
+ // Terser than an `alt` of `consume`s, and a single set lookup.
349
+ const names = Array.isArray(arg) ? arg : [arg]
350
+ const set = new Set(names)
351
+ const count = names.length
352
+ return (state) => {
353
+ const token = state.tokens[state.idx]
354
+ if (token !== undefined && set.has(token.type)) {
355
+ state.idx++
356
+ return token
357
+ }
358
+ for (let i = 0; i < count; i++) expect(state, names[i])
359
+ return FAIL
360
+ }
361
+ }
362
+
363
+ /** @returns {(state: any) => any} */
364
+ const buildEof = (arg) => {
365
+ // Matches only at the end of the token stream, consuming nothing.
366
+ return (state) => {
367
+ if (state.idx >= state.tokens.length) return null
368
+ expect(state, 'end of input')
369
+ return FAIL
370
+ }
371
+ }
372
+
373
+ /** @returns {(state: any) => any} */
374
+ const buildInfixRight = (arg) => {
375
+ // The mirror of `infixLeft`: `a ^ b ^ c` is `a ^ (b ^ c)`.
376
+ const operand = plan(arg.operand)
377
+ const operator = arg.operator === undefined ? null : plan(arg.operator)
378
+ const combine = planAction(arg.combine)
379
+ return (state) => {
380
+ tick(state)
381
+ const left = operand(state)
382
+ if (left === FAIL) return FAIL
383
+ // Flat [op, operand, op, operand, ...]; allocated only if an
384
+ // operator is actually present.
385
+ let items = null
386
+ for (;;) {
387
+ const mark = state.idx
388
+ let op = null
389
+ if (operator !== null) {
390
+ op = operator(state)
391
+ if (op === FAIL) {
392
+ state.idx = mark
393
+ break
394
+ }
395
+ }
396
+ const right = operand(state)
397
+ if (right === FAIL) {
398
+ state.idx = mark
399
+ break
400
+ }
401
+ if (items === null) items = []
402
+ items.push(op, right)
403
+ }
404
+ if (items === null) return left
405
+ const bindings = { left: null, op: null, right: null }
406
+ let value = items[items.length - 1]
407
+ for (let i = items.length - 2; i >= 0; i -= 2) {
408
+ bindings.left = i === 0 ? left : items[i - 1]
409
+ bindings.op = items[i]
410
+ bindings.right = value
411
+ value = combine(bindings, state)
412
+ }
413
+ return value
414
+ }
415
+ }
416
+
417
+ /** @returns {(state: any) => any} */
418
+ const buildAs = (arg) => {
419
+ // `{"as": [parser, logic]}` — parse, then transform the result.
420
+ //
421
+ // The parsed value *is* the logic's context, so `{"val": "image"}`
422
+ // reads a token's image directly and nothing is allocated. Spelled out
423
+ // of primitives this is seq(label(x, parser), action(...)), which needs
424
+ // a bindings object purely to give the value a name.
425
+ //
426
+ // Not called `map`: that is an ordinary JSON Logic operator, and
427
+ // shadowing it would break any semantic action that uses it.
428
+ const child = plan(Array.isArray(arg) ? arg[0] : arg.parser)
429
+ const transform = planAction(Array.isArray(arg) ? arg[1] : arg.to)
430
+ return (state) => {
431
+ const value = child(state)
432
+ if (value === FAIL) return FAIL
433
+ return transform(value, state)
434
+ }
435
+ }
436
+
437
+ /** @returns {(state: any) => any} */
438
+ const buildBetween = (arg) => {
439
+ // `{"between": {open, body, close}}` — yields the body.
440
+ const open = plan(arg.open)
441
+ const body = plan(arg.body)
442
+ const close = plan(arg.close)
443
+ return (state) => {
444
+ tick(state)
445
+ const start = state.idx
446
+ if (open(state) === FAIL) {
447
+ state.idx = start
448
+ return FAIL
449
+ }
450
+ const value = body(state)
451
+ if (value === FAIL) {
452
+ state.idx = start
453
+ return FAIL
454
+ }
455
+ if (close(state) === FAIL) {
456
+ state.idx = start
457
+ return FAIL
458
+ }
459
+ return value
460
+ }
461
+ }
462
+
463
+ /** @returns {(state: any) => any} */
464
+ const buildPrefix = (arg) => {
465
+ // Any number of prefix operators, folded right onto the operand:
466
+ // `- - x` is combine(-, combine(-, x)). `combine` sees `op` and `operand`.
467
+ const operator = plan(arg.operator)
468
+ const operand = plan(arg.operand)
469
+ const combine = planAction(arg.combine)
470
+ return (state) => {
471
+ tick(state)
472
+ const start = state.idx
473
+ // Allocated only when a prefix is actually present.
474
+ let ops = null
475
+ for (;;) {
476
+ const mark = state.idx
477
+ const op = operator(state)
478
+ if (op === FAIL) {
479
+ state.idx = mark
480
+ break
481
+ }
482
+ if (ops === null) ops = []
483
+ ops.push(op)
484
+ }
485
+ let value = operand(state)
486
+ if (value === FAIL) {
487
+ state.idx = start
488
+ return FAIL
489
+ }
490
+ if (ops !== null) {
491
+ const bindings = { op: null, operand: null }
492
+ for (let i = ops.length - 1; i >= 0; i--) {
493
+ bindings.op = ops[i]
494
+ bindings.operand = value
495
+ value = combine(bindings, state)
496
+ }
497
+ }
498
+ return value
499
+ }
500
+ }
501
+
502
+ /** @returns {(state: any) => any} */
503
+ const buildPostfix = (arg) => {
504
+ // `operand` followed by any number of suffixes, folded left.
505
+ //
506
+ // Same shape of waste as `infixLeft` replaced: written out of
507
+ // primitives this is seq(label(operand), label(many(suffix)), action),
508
+ // which allocates a bindings object and an array and runs a fold for
509
+ // every operand — while the suffix (`%` here) is rare.
510
+ const operand = plan(arg.operand)
511
+ const suffix = plan(arg.suffix)
512
+ const combine = planAction(arg.combine)
513
+ return (state) => {
514
+ tick(state)
515
+ let left = operand(state)
516
+ if (left === FAIL) return FAIL
517
+ let bindings = null
518
+ for (;;) {
519
+ const mark = state.idx
520
+ const found = suffix(state)
521
+ if (found === FAIL) {
522
+ state.idx = mark
523
+ return left
524
+ }
525
+ if (bindings === null) bindings = { left: null, suffix: null }
526
+ bindings.left = left
527
+ bindings.suffix = found
528
+ left = combine(bindings, state)
529
+ }
530
+ }
531
+ }
532
+
533
+ /** @returns {(state: any) => any} */
534
+ const buildSubrule = (arg) => {
535
+ const name = typeof arg === 'string' ? arg : arg[0]
536
+ const slot = slotFor(name)
537
+ if (memo) {
538
+ return (state) => {
539
+ tick(state)
540
+ const key = name + ':' + state.idx
541
+ const hit = state.memo.get(key)
542
+ if (hit !== undefined) {
543
+ state.idx = hit.end
544
+ return hit.value
545
+ }
546
+ const start = state.idx
547
+ // The rule stack exists only to describe a failure. A successful
548
+ // parse never reads it, so maintaining it is skipped unless this is
549
+ // the tracking re-parse — same reasoning as `expect`.
550
+ let depth = 0
551
+ if (state.track) {
552
+ depth = state.ruleDepth
553
+ state.ruleNames[depth] = name
554
+ state.ruleDepth = depth + 1
555
+ }
556
+ const value = slot.fn(state)
557
+ if (state.track) state.ruleDepth = depth
558
+ if (value === FAIL) state.idx = start
559
+ state.memo.set(key, { value, end: state.idx })
560
+ return value
561
+ }
562
+ }
563
+ return (state) => {
564
+ tick(state)
565
+ // See above: rule-stack bookkeeping is failure-path only.
566
+ if (!state.track) return slot.fn(state)
567
+ const depth = state.ruleDepth
568
+ state.ruleNames[depth] = name
569
+ state.ruleDepth = depth + 1
570
+ const value = slot.fn(state)
571
+ state.ruleDepth = depth
572
+ return value
573
+ }
574
+ }
575
+
576
+ /** @returns {(state: any) => any} */
577
+ const buildLabel = (arg) => {
578
+ const name = arg[0]
579
+ const child = plan(arg[1])
580
+ return (state) => {
581
+ const value = child(state)
582
+ if (value !== FAIL && state.bindings) state.bindings[name] = value
583
+ return value
584
+ }
585
+ }
586
+
587
+ /** @returns {(state: any) => any} */
588
+ const buildAction = (arg) => {
589
+ const action = planAction(arg)
590
+ return (state) => action(state.bindings || {}, state)
591
+ }
592
+
593
+ /** @returns {(state: any) => any} */
594
+ const buildLookahead = (arg) => {
595
+ const child = plan(arg)
596
+ return (state) => {
597
+ const start = state.idx
598
+ const value = child(state)
599
+ state.idx = start
600
+ return value
601
+ }
602
+ }
603
+
604
+ /** @returns {(state: any) => any} */
605
+ const buildNegLookahead = (arg) => {
606
+ const child = plan(arg)
607
+ return (state) => {
608
+ const start = state.idx
609
+ const value = child(state)
610
+ state.idx = start
611
+ return value === FAIL ? null : FAIL
612
+ }
613
+ }
614
+
615
+ /** @returns {(state: any) => any} */
616
+ const buildText = (arg) => {
617
+ const child = plan(arg)
618
+ return (state) => {
619
+ const start = state.idx
620
+ const value = child(state)
621
+ if (value === FAIL) {
622
+ state.idx = start
623
+ return FAIL
624
+ }
625
+ if (state.idx === start) return ''
626
+ const first = state.tokens[start]
627
+ const last = state.tokens[state.idx - 1]
628
+ if (state.source !== undefined) return state.source.slice(first.start, last.end)
629
+ let out = ''
630
+ for (let i = start; i < state.idx; i++) out += state.tokens[i].image
631
+ return out
632
+ }
633
+ }
634
+
635
+ /** @returns {(state: any) => any} */
636
+ const buildValue = (node) => {
637
+ // An ordinary JSON Logic value expression sitting in parser position.
638
+ const built = planAction(node)
639
+ return (state) => built(state.bindings || {}, state)
640
+ }
641
+
642
+ /**
643
+ * One builder per operator. Each takes the operator's raw arguments and
644
+ * returns the closure that parses it; dispatch happens once, at plan time.
645
+ */
646
+ const BUILDERS = {
647
+ consume: buildConsume,
648
+ epsilon: () => () => null,
649
+ seq: planSeq,
650
+ alt: planAlt,
651
+ many: buildMany,
652
+ many1: buildMany1,
653
+ option: buildOption,
654
+ infixLeft: buildInfixLeft,
655
+ oneOf: buildOneOf,
656
+ eof: buildEof,
657
+ infixRight: buildInfixRight,
658
+ as: buildAs,
659
+ between: buildBetween,
660
+ prefix: buildPrefix,
661
+ postfix: buildPostfix,
662
+ manySep: (arg) => planSeparated(arg, false),
663
+ many1Sep: (arg) => planSeparated(arg, true),
664
+ subrule: buildSubrule,
665
+ label: buildLabel,
666
+ action: buildAction,
667
+ lookahead: buildLookahead,
668
+ negLookahead: buildNegLookahead,
669
+ text: buildText
670
+ }
671
+
672
+ function planUncached (node) {
673
+ const op = opOf(node)
674
+ // Not a single-operator node: a literal array or object used as a value.
675
+ if (op === null) return () => node
676
+ const build = BUILDERS[op]
677
+ return build ? build(node[op]) : buildValue(node)
678
+ }
679
+
680
+ function planSeq (children) {
681
+ if (!Array.isArray(children)) throw new Error("'seq' expects an array of parsers")
682
+
683
+ // A `seq` written in one of the recognized idioms is planned as the cheaper
684
+ // combinator instead, so grammars written with the general primitives get
685
+ // the same treatment as ones written with the specific ones.
686
+ const idiom = recognizeSeqIdiom(children)
687
+ if (idiom !== null) return planUncached(idiom)
688
+
689
+ // Step kinds. Terminals are inlined rather than dispatched to a closure:
690
+ // most sequence children are a bare `consume` (or a labelled one), and
691
+ // calling a closure for a two-field comparison is pure overhead — an
692
+ // indirect call per token, through a call site that sees every terminal in
693
+ // the grammar and so cannot be inlined by the JIT.
694
+ const PARSER = 0
695
+ const LABELLED = 1
696
+ const ACTION = 2
697
+ const TERMINAL = 3
698
+ const LABELLED_TERMINAL = 4
699
+
700
+ const kinds = []
701
+ const names = [] // binding name, for the labelled kinds
702
+ const tokens = [] // token type, for the terminal kinds
703
+ const fns = []
704
+ const actionLogic = [] // raw logic of each action child, for the reuse check
705
+ let hasAction = false
706
+ let hasLabels = false
707
+
708
+ /** `{consume: "X"}` -> "X", else null. */
709
+ const terminalOf = (node) => {
710
+ if (opOf(node) !== 'consume') return null
711
+ const arg = node.consume
712
+ const name = typeof arg === 'string' ? arg : (Array.isArray(arg) ? arg[0] : null)
713
+ return typeof name === 'string' ? name : null
714
+ }
715
+
716
+ for (const child of children) {
717
+ const childOp = opOf(child)
718
+
719
+ if (childOp === 'label') {
720
+ const [name, inner] = child.label
721
+ const terminal = terminalOf(inner)
722
+ hasLabels = true
723
+ if (terminal !== null) {
724
+ kinds.push(LABELLED_TERMINAL)
725
+ names.push(name)
726
+ tokens.push(terminal)
727
+ fns.push(null)
728
+ } else {
729
+ kinds.push(LABELLED)
730
+ names.push(name)
731
+ tokens.push(null)
732
+ fns.push(plan(inner))
733
+ }
734
+ continue
735
+ }
736
+
737
+ if (childOp === 'action') {
738
+ kinds.push(ACTION)
739
+ names.push(null)
740
+ tokens.push(null)
741
+ fns.push(planAction(child.action))
742
+ actionLogic.push(child.action)
743
+ hasAction = true
744
+ continue
745
+ }
746
+
747
+ const terminal = terminalOf(child)
748
+ if (terminal !== null) {
749
+ kinds.push(TERMINAL)
750
+ names.push(null)
751
+ tokens.push(terminal)
752
+ fns.push(null)
753
+ } else {
754
+ kinds.push(PARSER)
755
+ names.push(null)
756
+ tokens.push(null)
757
+ fns.push(plan(child))
758
+ }
759
+ }
760
+
761
+ const count = fns.length
762
+ // A sequence opens a binding scope only when it directly contains a
763
+ // `label` or an `action`; otherwise the allocation would be dead weight.
764
+ const needsScope = hasLabels || hasAction
765
+
766
+ // When every child is a terminal or an action, nothing can re-enter this
767
+ // sequence between writing a binding and the action reading it — no
768
+ // `subrule`, `many` or `alt` to recurse through. The binding object can
769
+ // then be created once for the plan and reused on every invocation instead
770
+ // of allocated per parse.
771
+ //
772
+ // `label(consume) + action` — match one token and convert it — is the most
773
+ // common sequence shape in a real grammar (23% of all `seq` invocations in
774
+ // the formula benchmark), and it is exactly this case.
775
+ //
776
+ // The exception is an action that could hand back the binding object
777
+ // itself, which would alias a reused object into the caller's result.
778
+ const reusableScope = needsScope &&
779
+ kinds.every((k) => k === TERMINAL || k === LABELLED_TERMINAL || k === ACTION) &&
780
+ !actionLogic.some(mayReturnContext)
781
+ const scratch = reusableScope ? {} : null
782
+
783
+ // Terminals are matched inline in this loop rather than through a step
784
+ // closure per child. Compiling each child into a uniform
785
+ // `(state, bindings) => value` step was tried, to trade the per-step kind
786
+ // branch for a baked-in one; it was ~12% *slower*, because it adds an
787
+ // indirect call per child through a site that sees every step shape in the
788
+ // grammar. A predictable branch beats a megamorphic call.
789
+ return (state) => {
790
+ tick(state)
791
+ const start = state.idx
792
+ const previousBindings = state.bindings
793
+ const bindings = reusableScope ? scratch : (needsScope ? {} : previousBindings)
794
+ if (needsScope) state.bindings = bindings
795
+
796
+ // With an action present the result array is never observed.
797
+ const results = hasAction ? null : []
798
+ let actionResult
799
+
800
+ for (let i = 0; i < count; i++) {
801
+ const kind = kinds[i]
802
+ let value
803
+
804
+ if (kind === TERMINAL || kind === LABELLED_TERMINAL) {
805
+ const token = state.tokens[state.idx]
806
+ if (token !== undefined && token.type === tokens[i]) {
807
+ state.idx++
808
+ value = token
809
+ } else {
810
+ expect(state, tokens[i])
811
+ state.idx = start
812
+ if (needsScope) state.bindings = previousBindings
813
+ return FAIL
814
+ }
815
+ if (kind === LABELLED_TERMINAL) bindings[names[i]] = value
816
+ } else if (kind === ACTION) {
817
+ actionResult = fns[i](bindings, state)
818
+ continue
819
+ } else {
820
+ value = fns[i](state)
821
+ if (value === FAIL) {
822
+ state.idx = start
823
+ if (needsScope) state.bindings = previousBindings
824
+ return FAIL
825
+ }
826
+ if (kind === LABELLED) bindings[names[i]] = value
827
+ }
828
+
829
+ if (results !== null) results.push(value)
830
+ }
831
+
832
+ if (needsScope) state.bindings = previousBindings
833
+ return hasAction ? actionResult : results
834
+ }
835
+ }
836
+
837
+ function planAlt (branches) {
838
+ if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches")
839
+
840
+ const planned = branches.map(plan)
841
+ const table = firsts ? buildDispatch(branches, firsts) : null
842
+
843
+ if (table !== null) {
844
+ // Resolve the dispatch table to closures once, and precompute the
845
+ // expected-token list used when nothing matches.
846
+ //
847
+ // A null-prototype object rather than a Map: `Map.get` on a string hashes
848
+ // the key on every lookup, and it showed up in profiles as 5% of parse
849
+ // time (`FindOrderedHashMapEntry`). A plain object lookup on an
850
+ // internalized string is an inline-cached property load instead. The null
851
+ // prototype also means a token type of `toString` or `constructor` cannot
852
+ // find an inherited value.
853
+ const byToken = Object.create(null)
854
+ for (const [token, branch] of table) byToken[token] = planned[branches.indexOf(branch)]
855
+ const expectedTokens = [...table.keys()]
856
+ const expectedCount = expectedTokens.length
857
+
858
+ return (state) => {
859
+ tick(state)
860
+ const token = state.tokens[state.idx]
861
+ if (token !== undefined) {
862
+ const branch = byToken[token.type]
863
+ if (branch !== undefined) return branch(state)
864
+ }
865
+ for (let i = 0; i < expectedCount; i++) expect(state, expectedTokens[i])
866
+ return FAIL
867
+ }
868
+ }
869
+
870
+ const count = planned.length
871
+ return (state) => {
872
+ tick(state)
873
+ const start = state.idx
874
+ for (let i = 0; i < count; i++) {
875
+ const value = planned[i](state)
876
+ if (value !== FAIL) return value
877
+ state.idx = start
878
+ }
879
+ return FAIL
880
+ }
881
+ }
882
+
883
+ function planSeparated (arg, atLeastOne) {
884
+ if (!arg || typeof arg !== 'object') throw new Error("'manySep' expects { rule, sep }")
885
+ const { rule, sep, trailing = false } = arg
886
+
887
+ const item = plan(rule)
888
+ // A string separator is by far the common case, so its token check is
889
+ // inlined instead of dispatched through a `consume` closure — that is one
890
+ // indirect call per list element removed.
891
+ const sepToken = typeof sep === 'string' ? sep : null
892
+ const separator = sepToken === null ? plan(sep) : null
893
+
894
+ return (state) => {
895
+ tick(state)
896
+ const start = state.idx
897
+ const first = item(state)
898
+ if (first === FAIL) {
899
+ state.idx = start
900
+ return atLeastOne ? FAIL : []
901
+ }
902
+
903
+ const results = [first]
904
+ for (;;) {
905
+ const beforeSep = state.idx
906
+ if (sepToken !== null) {
907
+ const token = state.tokens[state.idx]
908
+ if (token === undefined || token.type !== sepToken) {
909
+ expect(state, sepToken)
910
+ return results
911
+ }
912
+ state.idx++
913
+ } else if (separator(state) === FAIL) {
914
+ state.idx = beforeSep
915
+ return results
916
+ }
917
+ const beforeItem = state.idx
918
+ const value = item(state)
919
+ if (value === FAIL) {
920
+ state.idx = trailing ? beforeItem : beforeSep
921
+ return results
922
+ }
923
+ results.push(value)
924
+ }
925
+ }
926
+ }
927
+
928
+ return {
929
+ plan,
930
+ planOp,
931
+ planAction,
932
+ planRule: (name) => slotFor(name),
933
+ /** Wraps a plan so json-logic-engine's compiler can embed it. */
934
+ declareSync
935
+ }
936
+ }
937
+
938
+ export { declareSync, usesOuterScope }