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/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 }
|