@jbrowse/jexl 4.0.0 → 5.0.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/README.md +163 -20
- package/dist/Expression.d.ts +16 -0
- package/dist/Expression.js +28 -4
- package/dist/Expression.js.map +1 -1
- package/dist/Jexl.d.ts +20 -8
- package/dist/Jexl.js +34 -15
- package/dist/Jexl.js.map +1 -1
- package/dist/Lexer.d.ts +4 -3
- package/dist/Lexer.js +29 -21
- package/dist/Lexer.js.map +1 -1
- package/dist/analyze.d.ts +84 -0
- package/dist/analyze.js +259 -0
- package/dist/analyze.js.map +1 -0
- package/dist/check.d.ts +109 -0
- package/dist/check.js +1267 -0
- package/dist/check.js.map +1 -0
- package/dist/collections.d.ts +3 -0
- package/dist/collections.js +140 -0
- package/dist/collections.js.map +1 -0
- package/dist/conditions.d.ts +62 -0
- package/dist/conditions.js +207 -0
- package/dist/conditions.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluator/compile.d.ts +10 -7
- package/dist/evaluator/compile.js +295 -72
- package/dist/evaluator/compile.js.map +1 -1
- package/dist/grammar.d.ts +19 -9
- package/dist/grammar.js +66 -40
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/operators.d.ts +34 -0
- package/dist/operators.js +131 -0
- package/dist/operators.js.map +1 -0
- package/dist/parser/Parser.d.ts +48 -91
- package/dist/parser/Parser.js +347 -221
- package/dist/parser/Parser.js.map +1 -1
- package/dist/types.d.ts +11 -5
- package/esm/Expression.d.ts +16 -0
- package/esm/Expression.js +28 -4
- package/esm/Expression.js.map +1 -1
- package/esm/Jexl.d.ts +20 -8
- package/esm/Jexl.js +34 -15
- package/esm/Jexl.js.map +1 -1
- package/esm/Lexer.d.ts +4 -3
- package/esm/Lexer.js +29 -21
- package/esm/Lexer.js.map +1 -1
- package/esm/analyze.d.ts +84 -0
- package/esm/analyze.js +256 -0
- package/esm/analyze.js.map +1 -0
- package/esm/check.d.ts +109 -0
- package/esm/check.js +1260 -0
- package/esm/check.js.map +1 -0
- package/esm/collections.d.ts +3 -0
- package/esm/collections.js +137 -0
- package/esm/collections.js.map +1 -0
- package/esm/conditions.d.ts +62 -0
- package/esm/conditions.js +200 -0
- package/esm/conditions.js.map +1 -0
- package/esm/errors.d.ts +5 -0
- package/esm/errors.js +14 -0
- package/esm/errors.js.map +1 -0
- package/esm/evaluator/compile.d.ts +10 -7
- package/esm/evaluator/compile.js +295 -72
- package/esm/evaluator/compile.js.map +1 -1
- package/esm/grammar.d.ts +19 -9
- package/esm/grammar.js +66 -39
- package/esm/grammar.js.map +1 -1
- package/esm/index.d.ts +9 -0
- package/esm/index.js +4 -0
- package/esm/index.js.map +1 -1
- package/esm/operators.d.ts +34 -0
- package/esm/operators.js +123 -0
- package/esm/operators.js.map +1 -0
- package/esm/parser/Parser.d.ts +48 -91
- package/esm/parser/Parser.js +349 -192
- package/esm/parser/Parser.js.map +1 -1
- package/esm/types.d.ts +11 -5
- package/package.json +1 -1
- package/src/Expression.ts +34 -4
- package/src/Jexl.ts +52 -16
- package/src/Lexer.ts +32 -21
- package/src/analyze.ts +378 -0
- package/src/check.ts +1682 -0
- package/src/collections.ts +148 -0
- package/src/conditions.ts +262 -0
- package/src/errors.ts +15 -0
- package/src/evaluator/compile.ts +356 -72
- package/src/grammar.ts +123 -42
- package/src/index.ts +15 -0
- package/src/operators.ts +151 -0
- package/src/parser/Parser.ts +389 -199
- package/src/types.ts +13 -3
- package/dist/parser/handlers.d.ts +0 -115
- package/dist/parser/handlers.js +0 -352
- package/dist/parser/handlers.js.map +0 -1
- package/dist/parser/states.d.ts +0 -47
- package/dist/parser/states.js +0 -176
- package/dist/parser/states.js.map +0 -1
- package/esm/parser/handlers.d.ts +0 -115
- package/esm/parser/handlers.js +0 -328
- package/esm/parser/handlers.js.map +0 -1
- package/esm/parser/states.d.ts +0 -47
- package/esm/parser/states.js +0 -140
- package/esm/parser/states.js.map +0 -1
- package/src/parser/handlers.ts +0 -372
- package/src/parser/states.ts +0 -159
package/src/evaluator/compile.ts
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { Grammar } from '../grammar.ts'
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
AstNode,
|
|
9
|
+
AstNodeUnion,
|
|
10
|
+
JexlFunction,
|
|
11
|
+
JexlValue,
|
|
12
|
+
Literal
|
|
13
|
+
} from '../types.ts'
|
|
8
14
|
|
|
9
15
|
/** The variables an expression is evaluated against. */
|
|
10
16
|
export type Context = Record<string, JexlValue>
|
|
@@ -36,19 +42,236 @@ function assignOwn(target: Context, key: string, value: JexlValue) {
|
|
|
36
42
|
target[key] = value
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
/**
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
/** The key `subject[index]` reads under, or undefined when it reads nothing. */
|
|
46
|
+
function memberKey(index: JexlValue) {
|
|
47
|
+
return typeof index === 'string' || typeof index === 'number'
|
|
48
|
+
? index
|
|
49
|
+
: indexKey(index)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The key a bracket index that is neither a string nor a number reads under:
|
|
54
|
+
* its string form, when it has one worth using.
|
|
55
|
+
*
|
|
56
|
+
* A boolean and an array of primitives do — `['a']` is the key `'a'`, exactly
|
|
57
|
+
* as JS would index by it, and how a `@gmod/vcf` INFO value (always a list,
|
|
58
|
+
* `Number=1` included) indexes a lookup table. A multi-valued list is the key
|
|
59
|
+
* `'a,b'`, and misses rather than guess which value was meant. A plain object
|
|
60
|
+
* does not, since '[object Object]' is not a key anything is stored under, so
|
|
61
|
+
* it and an array holding one answer `undefined` instead. null and undefined
|
|
62
|
+
* are the same case.
|
|
63
|
+
*/
|
|
64
|
+
function indexKey(value: JexlValue): string | undefined {
|
|
65
|
+
if (typeof value === 'boolean') {
|
|
66
|
+
return String(value)
|
|
43
67
|
}
|
|
44
|
-
|
|
68
|
+
return Array.isArray(value) && value.every(isKeyPart)
|
|
69
|
+
? value.join(',')
|
|
70
|
+
: undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isKeyPart(
|
|
74
|
+
value: JexlValue
|
|
75
|
+
): value is string | number | boolean | null | undefined {
|
|
76
|
+
return (
|
|
77
|
+
value == null ||
|
|
45
78
|
typeof value === 'string' ||
|
|
46
79
|
typeof value === 'number' ||
|
|
47
80
|
typeof value === 'boolean'
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The value of an object or array literal holding only primitives, built once.
|
|
86
|
+
* Indexing such a table, as in `{CDS: 'red', exon: 'blue'}[feature.type]`,
|
|
87
|
+
* can only yield a primitive, so the table never escapes and one frozen copy
|
|
88
|
+
* serves every evaluation.
|
|
89
|
+
*/
|
|
90
|
+
function literalTable(node: AstNode) {
|
|
91
|
+
const literal = node as AstNodeUnion
|
|
92
|
+
if (literal.type === 'ArrayLiteral') {
|
|
93
|
+
return literal.value.every((item) => item.type === 'Literal')
|
|
94
|
+
? Object.freeze(literal.value.map((item) => (item as Literal).value))
|
|
95
|
+
: undefined
|
|
96
|
+
}
|
|
97
|
+
if (literal.type === 'ObjectLiteral') {
|
|
98
|
+
const entries = Object.entries(literal.value)
|
|
99
|
+
if (entries.every(([, value]) => value.type === 'Literal')) {
|
|
100
|
+
const table: Context = {}
|
|
101
|
+
for (const [key, value] of entries) {
|
|
102
|
+
defineOwn(table, key, (value as Literal).value)
|
|
103
|
+
}
|
|
104
|
+
return Object.freeze(table)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const unassigned = Symbol('unassigned')
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The slot each name an expression assigns is kept in, and the frame holding
|
|
114
|
+
* those slots for the evaluation in progress.
|
|
115
|
+
*/
|
|
116
|
+
interface Locals {
|
|
117
|
+
slots: ReadonlyMap<string, number>
|
|
118
|
+
frame: { locals: unknown[] }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* One call of a lambda. A name the lambda binds reads from `args`, a name bound
|
|
123
|
+
* by a lambda around it from `outer`, a name the expression assigns from
|
|
124
|
+
* `locals` — the evaluation's own, captured when the lambda was made, so a
|
|
125
|
+
* lambda called after its evaluation ends still sees them — and any other name
|
|
126
|
+
* from `context`.
|
|
127
|
+
*/
|
|
128
|
+
class LambdaFrame {
|
|
129
|
+
args: JexlValue[]
|
|
130
|
+
outer: LambdaFrame | undefined
|
|
131
|
+
context: Context
|
|
132
|
+
locals: unknown[] | undefined
|
|
133
|
+
|
|
134
|
+
constructor(
|
|
135
|
+
args: JexlValue[],
|
|
136
|
+
outer: LambdaFrame | undefined,
|
|
137
|
+
context: Context,
|
|
138
|
+
locals: unknown[] | undefined
|
|
48
139
|
) {
|
|
49
|
-
|
|
140
|
+
this.args = args
|
|
141
|
+
this.outer = outer
|
|
142
|
+
this.context = context
|
|
143
|
+
this.locals = locals
|
|
50
144
|
}
|
|
51
|
-
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The parameters of the lambdas around a node, innermost first. */
|
|
148
|
+
interface Params {
|
|
149
|
+
names: string[]
|
|
150
|
+
outer: Params | undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface Scope {
|
|
154
|
+
locals?: Locals
|
|
155
|
+
params?: Params
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// the closures compiled inside a lambda are handed its LambdaFrame in place of
|
|
159
|
+
// the context, and only the ones that read a name, below, look inside it
|
|
160
|
+
const asFrame = (env: Context) => env as unknown as LambdaFrame
|
|
161
|
+
const asContext = (frame: LambdaFrame) => frame as unknown as Context
|
|
162
|
+
|
|
163
|
+
function compileName(
|
|
164
|
+
name: string,
|
|
165
|
+
grammar: Grammar,
|
|
166
|
+
scope: Scope
|
|
167
|
+
): CompiledNode {
|
|
168
|
+
let hops = 0
|
|
169
|
+
for (let p = scope.params; p; p = p.outer) {
|
|
170
|
+
const index = p.names.indexOf(name)
|
|
171
|
+
if (index !== -1) {
|
|
172
|
+
return hops === 0 ? (env) => asFrame(env).args[index] : climb(hops, index)
|
|
173
|
+
}
|
|
174
|
+
hops++
|
|
175
|
+
}
|
|
176
|
+
const read =
|
|
177
|
+
(grammar.variableReader?.(name) as CompiledNode | undefined) ??
|
|
178
|
+
((ctx) => ctx[name])
|
|
179
|
+
const fromContext: CompiledNode = scope.params
|
|
180
|
+
? (env) => read(asFrame(env).context)
|
|
181
|
+
: read
|
|
182
|
+
const slot = scope.locals?.slots.get(name)
|
|
183
|
+
if (slot === undefined) {
|
|
184
|
+
return fromContext
|
|
185
|
+
}
|
|
186
|
+
if (scope.params) {
|
|
187
|
+
return (env) => {
|
|
188
|
+
const local = asFrame(env).locals![slot]
|
|
189
|
+
return local === unassigned ? fromContext(env) : (local as JexlValue)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const { frame } = scope.locals!
|
|
193
|
+
return (env) => {
|
|
194
|
+
const local = frame.locals[slot]
|
|
195
|
+
return local === unassigned ? fromContext(env) : (local as JexlValue)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function climb(hops: number, index: number): CompiledNode {
|
|
200
|
+
return (env) => {
|
|
201
|
+
let frame = asFrame(env)
|
|
202
|
+
for (let i = 0; i < hops; i++) {
|
|
203
|
+
frame = frame.outer!
|
|
204
|
+
}
|
|
205
|
+
return frame.args[index]
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function assignedNames(ast: AstNode | undefined, names = new Set<string>()) {
|
|
210
|
+
const node = ast as AstNodeUnion | undefined
|
|
211
|
+
switch (node?.type) {
|
|
212
|
+
case 'AssignmentExpression': {
|
|
213
|
+
names.add(node.left.value)
|
|
214
|
+
assignedNames(node.right, names)
|
|
215
|
+
break
|
|
216
|
+
}
|
|
217
|
+
case 'Identifier': {
|
|
218
|
+
assignedNames(node.from, names)
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
case 'BinaryExpression': {
|
|
222
|
+
assignedNames(node.left, names)
|
|
223
|
+
assignedNames(node.right, names)
|
|
224
|
+
break
|
|
225
|
+
}
|
|
226
|
+
case 'UnaryExpression': {
|
|
227
|
+
assignedNames(node.right, names)
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
case 'ConditionalExpression': {
|
|
231
|
+
assignedNames(node.test, names)
|
|
232
|
+
assignedNames(node.consequent, names)
|
|
233
|
+
assignedNames(node.alternate, names)
|
|
234
|
+
break
|
|
235
|
+
}
|
|
236
|
+
case 'FilterExpression': {
|
|
237
|
+
assignedNames(node.subject, names)
|
|
238
|
+
assignedNames(node.expr, names)
|
|
239
|
+
break
|
|
240
|
+
}
|
|
241
|
+
case 'ArrayLiteral':
|
|
242
|
+
case 'FunctionCall':
|
|
243
|
+
case 'SequenceExpression': {
|
|
244
|
+
const children =
|
|
245
|
+
node.type === 'ArrayLiteral'
|
|
246
|
+
? node.value
|
|
247
|
+
: node.type === 'FunctionCall'
|
|
248
|
+
? node.args
|
|
249
|
+
: node.expressions
|
|
250
|
+
for (const child of children) {
|
|
251
|
+
assignedNames(child, names)
|
|
252
|
+
}
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
case 'ObjectLiteral': {
|
|
256
|
+
for (const child of Object.values(node.value)) {
|
|
257
|
+
assignedNames(child, names)
|
|
258
|
+
}
|
|
259
|
+
break
|
|
260
|
+
}
|
|
261
|
+
case 'TemplateLiteral': {
|
|
262
|
+
for (const part of node.parts) {
|
|
263
|
+
if (part.type === 'expression') {
|
|
264
|
+
assignedNames(part.value, names)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
break
|
|
268
|
+
}
|
|
269
|
+
case 'Lambda': {
|
|
270
|
+
assignedNames(node.body, names)
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return names
|
|
52
275
|
}
|
|
53
276
|
|
|
54
277
|
/**
|
|
@@ -60,16 +283,61 @@ function stringify(value: JexlValue) {
|
|
|
60
283
|
* times.
|
|
61
284
|
*
|
|
62
285
|
* Operators are bound here, so a grammar change after compilation requires
|
|
63
|
-
* recompiling
|
|
64
|
-
*
|
|
65
|
-
* an expression has been compiled.
|
|
286
|
+
* recompiling. Functions are looked up per call, since they are commonly
|
|
287
|
+
* registered after an expression has been compiled.
|
|
66
288
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
289
|
+
* Each name the expression assigns gets a slot, numbered at compile time, in a
|
|
290
|
+
* frame that lasts one evaluation, so an assignment never touches the context
|
|
291
|
+
* and a context reused from one evaluation to the next comes back unchanged. A
|
|
292
|
+
* name reads its slot once assigned, and resolves as usual until then. An
|
|
293
|
+
* expression that assigns nothing gets no frame. The frame is not a layer over
|
|
294
|
+
* the context via `Object.create`: V8 gives every object used as a prototype a
|
|
295
|
+
* map of its own, which made evaluating against a context built per evaluation
|
|
296
|
+
* six times slower.
|
|
71
297
|
*/
|
|
72
298
|
export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
299
|
+
const names = [...assignedNames(ast)]
|
|
300
|
+
if (names.length === 0) {
|
|
301
|
+
return compileNode(ast, grammar, {})
|
|
302
|
+
}
|
|
303
|
+
const slots = new Map(names.map((name, i) => [name, i]))
|
|
304
|
+
const empty = names.map(() => unassigned)
|
|
305
|
+
const frame = { locals: empty }
|
|
306
|
+
const fn = compileNode(ast, grammar, { locals: { slots, frame } })
|
|
307
|
+
// saved and restored, so a function that evaluates this same expression
|
|
308
|
+
// again from inside it leaves the outer evaluation's locals intact
|
|
309
|
+
return (ctx) => {
|
|
310
|
+
const outer = frame.locals
|
|
311
|
+
frame.locals = empty.slice()
|
|
312
|
+
try {
|
|
313
|
+
return fn(ctx)
|
|
314
|
+
} finally {
|
|
315
|
+
frame.locals = outer
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Renders an interpolated value for a template literal. */
|
|
321
|
+
function stringify(value: JexlValue) {
|
|
322
|
+
if (value == null) {
|
|
323
|
+
return ''
|
|
324
|
+
}
|
|
325
|
+
if (
|
|
326
|
+
typeof value === 'string' ||
|
|
327
|
+
typeof value === 'number' ||
|
|
328
|
+
typeof value === 'boolean'
|
|
329
|
+
) {
|
|
330
|
+
return String(value)
|
|
331
|
+
}
|
|
332
|
+
return JSON.stringify(value)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function compileNode(
|
|
336
|
+
ast: AstNode,
|
|
337
|
+
grammar: Grammar,
|
|
338
|
+
scope: Scope
|
|
339
|
+
): CompiledNode {
|
|
340
|
+
const compile = (child: AstNode) => compileNode(child, grammar, scope)
|
|
73
341
|
// AstNode types its `type` as a plain string so the Parser can build the tree
|
|
74
342
|
// loosely; narrowing to the union once, here, lets every case below see its
|
|
75
343
|
// own node type instead of repeating the same cast in each branch
|
|
@@ -83,49 +351,44 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
83
351
|
case 'Identifier': {
|
|
84
352
|
const name = node.value
|
|
85
353
|
if (!node.from) {
|
|
86
|
-
return (
|
|
354
|
+
return compileName(name, grammar, scope)
|
|
87
355
|
}
|
|
88
|
-
const from =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
356
|
+
const from = compile(node.from)
|
|
357
|
+
const { getMember } = grammar
|
|
358
|
+
if (getMember) {
|
|
359
|
+
return (ctx) => {
|
|
360
|
+
const target = from(ctx)
|
|
361
|
+
return target == null
|
|
362
|
+
? undefined
|
|
363
|
+
: (getMember(target, name) as JexlValue)
|
|
93
364
|
}
|
|
94
|
-
|
|
95
|
-
|
|
365
|
+
}
|
|
366
|
+
return (ctx) => {
|
|
367
|
+
const target = from(ctx)
|
|
96
368
|
return target == null ? undefined : (target as Context)[name]
|
|
97
369
|
}
|
|
98
370
|
}
|
|
99
371
|
|
|
100
372
|
case 'BinaryExpression': {
|
|
101
373
|
const op = grammar.elements[node.operator]
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
const left = compileAst(node.left, grammar)
|
|
108
|
-
const right = compileAst(node.right!, grammar)
|
|
109
|
-
const { evalOnDemand } = op
|
|
110
|
-
if (evalOnDemand) {
|
|
374
|
+
const left = compile(node.left)
|
|
375
|
+
const right = compile(node.right!)
|
|
376
|
+
if (op?.type === 'binaryOp' && op.evalOnDemand) {
|
|
377
|
+
const { evalOnDemand } = op
|
|
111
378
|
// operands stay unevaluated behind an `eval` thunk, so operators such
|
|
112
379
|
// as && and || can short-circuit
|
|
113
380
|
return (ctx) =>
|
|
114
381
|
evalOnDemand({ eval: () => left(ctx) }, { eval: () => right(ctx) })
|
|
115
382
|
}
|
|
116
|
-
const fn = op.eval
|
|
383
|
+
const fn = op?.type === 'binaryOp' ? op.eval : undefined
|
|
117
384
|
if (!fn) {
|
|
118
|
-
|
|
119
|
-
left(ctx)
|
|
120
|
-
right(ctx)
|
|
121
|
-
return undefined
|
|
122
|
-
}
|
|
385
|
+
throw new Error(`Unknown binary operator '${node.operator}'`)
|
|
123
386
|
}
|
|
124
387
|
return (ctx) => fn(left(ctx), right(ctx))
|
|
125
388
|
}
|
|
126
389
|
|
|
127
390
|
case 'UnaryExpression': {
|
|
128
|
-
const right =
|
|
391
|
+
const right = compile(node.right!)
|
|
129
392
|
const elem = grammar.elements[node.operator]
|
|
130
393
|
const fn =
|
|
131
394
|
elem?.type === 'unaryOp'
|
|
@@ -134,23 +397,15 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
134
397
|
? elem.unaryEval
|
|
135
398
|
: undefined
|
|
136
399
|
if (!fn) {
|
|
137
|
-
|
|
138
|
-
return (ctx) => {
|
|
139
|
-
right(ctx)
|
|
140
|
-
return undefined
|
|
141
|
-
}
|
|
400
|
+
throw new Error(`Unknown unary operator '${node.operator}'`)
|
|
142
401
|
}
|
|
143
402
|
return (ctx) => fn(right(ctx))
|
|
144
403
|
}
|
|
145
404
|
|
|
146
405
|
case 'ConditionalExpression': {
|
|
147
|
-
const test =
|
|
148
|
-
const consequent = node.consequent
|
|
149
|
-
|
|
150
|
-
: undefined
|
|
151
|
-
const alternate = node.alternate
|
|
152
|
-
? compileAst(node.alternate, grammar)
|
|
153
|
-
: undefined
|
|
406
|
+
const test = compile(node.test)
|
|
407
|
+
const consequent = node.consequent ? compile(node.consequent) : undefined
|
|
408
|
+
const alternate = node.alternate ? compile(node.alternate) : undefined
|
|
154
409
|
return (ctx) => {
|
|
155
410
|
const res = test(ctx)
|
|
156
411
|
if (res) {
|
|
@@ -162,22 +417,30 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
162
417
|
}
|
|
163
418
|
|
|
164
419
|
case 'FilterExpression': {
|
|
165
|
-
const
|
|
166
|
-
const
|
|
420
|
+
const table = literalTable(node.subject)
|
|
421
|
+
const subject = table ? () => table : compile(node.subject)
|
|
422
|
+
const index = compile(node.expr)
|
|
423
|
+
const { getMember } = grammar
|
|
424
|
+
if (getMember) {
|
|
425
|
+
return (ctx) => {
|
|
426
|
+
const subjectVal = subject(ctx)
|
|
427
|
+
const key = memberKey(index(ctx))
|
|
428
|
+
return subjectVal == null || key === undefined
|
|
429
|
+
? undefined
|
|
430
|
+
: (getMember(subjectVal, key) as JexlValue)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
167
433
|
return (ctx) => {
|
|
168
434
|
const subjectVal = subject(ctx)
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return typeof indexVal === 'string' || typeof indexVal === 'number'
|
|
174
|
-
? (subjectVal as Record<string | number, JexlValue>)[indexVal]
|
|
175
|
-
: undefined
|
|
435
|
+
const key = memberKey(index(ctx))
|
|
436
|
+
return subjectVal == null || key === undefined
|
|
437
|
+
? undefined
|
|
438
|
+
: (subjectVal as Record<string | number, JexlValue>)[key]
|
|
176
439
|
}
|
|
177
440
|
}
|
|
178
441
|
|
|
179
442
|
case 'ArrayLiteral': {
|
|
180
|
-
const items = node.value.map(
|
|
443
|
+
const items = node.value.map(compile)
|
|
181
444
|
const len = items.length
|
|
182
445
|
return (ctx) => {
|
|
183
446
|
const out: JexlValue[] = new Array(len)
|
|
@@ -191,7 +454,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
191
454
|
case 'ObjectLiteral': {
|
|
192
455
|
const entries = Object.entries(node.value)
|
|
193
456
|
const keys = entries.map(([key]) => key)
|
|
194
|
-
const values = entries.map(([, value]) =>
|
|
457
|
+
const values = entries.map(([, value]) => compile(value))
|
|
195
458
|
const len = keys.length
|
|
196
459
|
// resolved once, here, so the common case keeps its plain store
|
|
197
460
|
const store = keys.includes('__proto__') ? defineOwn : assignOwn
|
|
@@ -211,7 +474,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
211
474
|
const { value } = part
|
|
212
475
|
return () => value
|
|
213
476
|
}
|
|
214
|
-
const expr =
|
|
477
|
+
const expr = compile(part.value)
|
|
215
478
|
return (ctx) => stringify(expr(ctx))
|
|
216
479
|
})
|
|
217
480
|
return (ctx) => {
|
|
@@ -225,7 +488,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
225
488
|
|
|
226
489
|
case 'FunctionCall': {
|
|
227
490
|
const { name } = node
|
|
228
|
-
const args = node.args.map(
|
|
491
|
+
const args = node.args.map(compile)
|
|
229
492
|
// hasOwn, so that inherited Object.prototype members such as `toString`
|
|
230
493
|
// and `constructor` aren't callable as Jexl functions. Resolved per call
|
|
231
494
|
// rather than baked in, since functions are routinely registered after
|
|
@@ -273,7 +536,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
273
536
|
}
|
|
274
537
|
|
|
275
538
|
case 'SequenceExpression': {
|
|
276
|
-
const exprs = node.expressions.map(
|
|
539
|
+
const exprs = node.expressions.map(compile)
|
|
277
540
|
const len = exprs.length
|
|
278
541
|
return (ctx) => {
|
|
279
542
|
let last: JexlValue
|
|
@@ -285,13 +548,34 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
285
548
|
}
|
|
286
549
|
|
|
287
550
|
case 'AssignmentExpression': {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
551
|
+
if (scope.params) {
|
|
552
|
+
throw new Error('Assignment is not supported in a lambda')
|
|
553
|
+
}
|
|
554
|
+
const right = compile(node.right!)
|
|
555
|
+
const { slots, frame } = scope.locals!
|
|
556
|
+
const slot = slots.get(node.left.value)!
|
|
557
|
+
return (ctx) => (frame.locals[slot] = right(ctx))
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
case 'Lambda': {
|
|
561
|
+
const body = compileNode(node.body, grammar, {
|
|
562
|
+
...scope,
|
|
563
|
+
params: { names: node.params, outer: scope.params }
|
|
564
|
+
})
|
|
565
|
+
if (!scope.params) {
|
|
566
|
+
const frame = scope.locals?.frame
|
|
567
|
+
return (ctx): JexlFunction => {
|
|
568
|
+
const locals = frame?.locals
|
|
569
|
+
return (...args) =>
|
|
570
|
+
body(asContext(new LambdaFrame(args, undefined, ctx, locals)))
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return (env): JexlFunction => {
|
|
574
|
+
const outer = asFrame(env)
|
|
575
|
+
return (...args) =>
|
|
576
|
+
body(
|
|
577
|
+
asContext(new LambdaFrame(args, outer, outer.context, outer.locals))
|
|
578
|
+
)
|
|
295
579
|
}
|
|
296
580
|
}
|
|
297
581
|
|