@jbrowse/jexl 4.0.1 → 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 +274 -97
- 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 +274 -97
- 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 +332 -100
- 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,15 +42,24 @@ function assignOwn(target: Context, key: string, value: JexlValue) {
|
|
|
36
42
|
target[key] = value
|
|
37
43
|
}
|
|
38
44
|
|
|
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
|
+
|
|
39
52
|
/**
|
|
40
53
|
* The key a bracket index that is neither a string nor a number reads under:
|
|
41
54
|
* its string form, when it has one worth using.
|
|
42
55
|
*
|
|
43
56
|
* A boolean and an array of primitives do — `['a']` is the key `'a'`, exactly
|
|
44
|
-
* as JS would index by it
|
|
45
|
-
*
|
|
46
|
-
* `
|
|
47
|
-
*
|
|
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.
|
|
48
63
|
*/
|
|
49
64
|
function indexKey(value: JexlValue): string | undefined {
|
|
50
65
|
if (typeof value === 'boolean') {
|
|
@@ -66,19 +81,197 @@ function isKeyPart(
|
|
|
66
81
|
)
|
|
67
82
|
}
|
|
68
83
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
73
96
|
}
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
78
139
|
) {
|
|
79
|
-
|
|
140
|
+
this.args = args
|
|
141
|
+
this.outer = outer
|
|
142
|
+
this.context = context
|
|
143
|
+
this.locals = locals
|
|
144
|
+
}
|
|
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]
|
|
80
206
|
}
|
|
81
|
-
|
|
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
|
|
82
275
|
}
|
|
83
276
|
|
|
84
277
|
/**
|
|
@@ -90,16 +283,61 @@ function stringify(value: JexlValue) {
|
|
|
90
283
|
* times.
|
|
91
284
|
*
|
|
92
285
|
* Operators are bound here, so a grammar change after compilation requires
|
|
93
|
-
* recompiling
|
|
94
|
-
*
|
|
95
|
-
* 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.
|
|
96
288
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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.
|
|
101
297
|
*/
|
|
102
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)
|
|
103
341
|
// AstNode types its `type` as a plain string so the Parser can build the tree
|
|
104
342
|
// loosely; narrowing to the union once, here, lets every case below see its
|
|
105
343
|
// own node type instead of repeating the same cast in each branch
|
|
@@ -113,49 +351,44 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
113
351
|
case 'Identifier': {
|
|
114
352
|
const name = node.value
|
|
115
353
|
if (!node.from) {
|
|
116
|
-
return (
|
|
354
|
+
return compileName(name, grammar, scope)
|
|
117
355
|
}
|
|
118
|
-
const from =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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)
|
|
123
364
|
}
|
|
124
|
-
|
|
125
|
-
|
|
365
|
+
}
|
|
366
|
+
return (ctx) => {
|
|
367
|
+
const target = from(ctx)
|
|
126
368
|
return target == null ? undefined : (target as Context)[name]
|
|
127
369
|
}
|
|
128
370
|
}
|
|
129
371
|
|
|
130
372
|
case 'BinaryExpression': {
|
|
131
373
|
const op = grammar.elements[node.operator]
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
const left = compileAst(node.left, grammar)
|
|
138
|
-
const right = compileAst(node.right!, grammar)
|
|
139
|
-
const { evalOnDemand } = op
|
|
140
|
-
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
|
|
141
378
|
// operands stay unevaluated behind an `eval` thunk, so operators such
|
|
142
379
|
// as && and || can short-circuit
|
|
143
380
|
return (ctx) =>
|
|
144
381
|
evalOnDemand({ eval: () => left(ctx) }, { eval: () => right(ctx) })
|
|
145
382
|
}
|
|
146
|
-
const fn = op.eval
|
|
383
|
+
const fn = op?.type === 'binaryOp' ? op.eval : undefined
|
|
147
384
|
if (!fn) {
|
|
148
|
-
|
|
149
|
-
left(ctx)
|
|
150
|
-
right(ctx)
|
|
151
|
-
return undefined
|
|
152
|
-
}
|
|
385
|
+
throw new Error(`Unknown binary operator '${node.operator}'`)
|
|
153
386
|
}
|
|
154
387
|
return (ctx) => fn(left(ctx), right(ctx))
|
|
155
388
|
}
|
|
156
389
|
|
|
157
390
|
case 'UnaryExpression': {
|
|
158
|
-
const right =
|
|
391
|
+
const right = compile(node.right!)
|
|
159
392
|
const elem = grammar.elements[node.operator]
|
|
160
393
|
const fn =
|
|
161
394
|
elem?.type === 'unaryOp'
|
|
@@ -164,23 +397,15 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
164
397
|
? elem.unaryEval
|
|
165
398
|
: undefined
|
|
166
399
|
if (!fn) {
|
|
167
|
-
|
|
168
|
-
return (ctx) => {
|
|
169
|
-
right(ctx)
|
|
170
|
-
return undefined
|
|
171
|
-
}
|
|
400
|
+
throw new Error(`Unknown unary operator '${node.operator}'`)
|
|
172
401
|
}
|
|
173
402
|
return (ctx) => fn(right(ctx))
|
|
174
403
|
}
|
|
175
404
|
|
|
176
405
|
case 'ConditionalExpression': {
|
|
177
|
-
const test =
|
|
178
|
-
const consequent = node.consequent
|
|
179
|
-
|
|
180
|
-
: undefined
|
|
181
|
-
const alternate = node.alternate
|
|
182
|
-
? compileAst(node.alternate, grammar)
|
|
183
|
-
: 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
|
|
184
409
|
return (ctx) => {
|
|
185
410
|
const res = test(ctx)
|
|
186
411
|
if (res) {
|
|
@@ -192,44 +417,30 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
192
417
|
}
|
|
193
418
|
|
|
194
419
|
case 'FilterExpression': {
|
|
195
|
-
const
|
|
196
|
-
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
|
+
}
|
|
197
433
|
return (ctx) => {
|
|
198
434
|
const subjectVal = subject(ctx)
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
return undefined
|
|
202
|
-
}
|
|
203
|
-
if (typeof indexVal === 'string' || typeof indexVal === 'number') {
|
|
204
|
-
return (subjectVal as Record<string | number, JexlValue>)[indexVal]
|
|
205
|
-
}
|
|
206
|
-
// A value with a meaningful string form is indexed by it, which is what
|
|
207
|
-
// JS does and what this did until the evaluator was lowered to closures
|
|
208
|
-
// — the tree-walking version was a bare `subject?.[index]`. Answering
|
|
209
|
-
// `undefined` for everything else reads as a deliberate strictness and
|
|
210
|
-
// is not: nothing wanted it, and the shape it rejects is common.
|
|
211
|
-
//
|
|
212
|
-
// That shape is a one-element array. Every `@gmod/vcf` INFO value is a
|
|
213
|
-
// list — `Number=1` included, so `CLNSIG=Pathogenic` parses to
|
|
214
|
-
// `['Pathogenic']` — and the natural way to colour by one is a lookup
|
|
215
|
-
// table indexed by it. The miss is silent, because such an expression
|
|
216
|
-
// always carries a `|| fallback` for the values it has no entry for,
|
|
217
|
-
// and that fallback then answers for every record.
|
|
218
|
-
//
|
|
219
|
-
// A multi-valued list still misses — `['a','b']` is the key 'a,b' —
|
|
220
|
-
// which is the honest answer rather than a lucky one: reading through
|
|
221
|
-
// to the first element would be the index guessing which value was
|
|
222
|
-
// meant. `Identifier` above does read through a one-element array, but
|
|
223
|
-
// it is navigating rather than selecting, so it has nothing to guess.
|
|
224
|
-
const key = indexKey(indexVal)
|
|
225
|
-
return key === undefined
|
|
435
|
+
const key = memberKey(index(ctx))
|
|
436
|
+
return subjectVal == null || key === undefined
|
|
226
437
|
? undefined
|
|
227
|
-
: (subjectVal as Record<string, JexlValue>)[key]
|
|
438
|
+
: (subjectVal as Record<string | number, JexlValue>)[key]
|
|
228
439
|
}
|
|
229
440
|
}
|
|
230
441
|
|
|
231
442
|
case 'ArrayLiteral': {
|
|
232
|
-
const items = node.value.map(
|
|
443
|
+
const items = node.value.map(compile)
|
|
233
444
|
const len = items.length
|
|
234
445
|
return (ctx) => {
|
|
235
446
|
const out: JexlValue[] = new Array(len)
|
|
@@ -243,7 +454,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
243
454
|
case 'ObjectLiteral': {
|
|
244
455
|
const entries = Object.entries(node.value)
|
|
245
456
|
const keys = entries.map(([key]) => key)
|
|
246
|
-
const values = entries.map(([, value]) =>
|
|
457
|
+
const values = entries.map(([, value]) => compile(value))
|
|
247
458
|
const len = keys.length
|
|
248
459
|
// resolved once, here, so the common case keeps its plain store
|
|
249
460
|
const store = keys.includes('__proto__') ? defineOwn : assignOwn
|
|
@@ -263,7 +474,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
263
474
|
const { value } = part
|
|
264
475
|
return () => value
|
|
265
476
|
}
|
|
266
|
-
const expr =
|
|
477
|
+
const expr = compile(part.value)
|
|
267
478
|
return (ctx) => stringify(expr(ctx))
|
|
268
479
|
})
|
|
269
480
|
return (ctx) => {
|
|
@@ -277,7 +488,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
277
488
|
|
|
278
489
|
case 'FunctionCall': {
|
|
279
490
|
const { name } = node
|
|
280
|
-
const args = node.args.map(
|
|
491
|
+
const args = node.args.map(compile)
|
|
281
492
|
// hasOwn, so that inherited Object.prototype members such as `toString`
|
|
282
493
|
// and `constructor` aren't callable as Jexl functions. Resolved per call
|
|
283
494
|
// rather than baked in, since functions are routinely registered after
|
|
@@ -325,7 +536,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
325
536
|
}
|
|
326
537
|
|
|
327
538
|
case 'SequenceExpression': {
|
|
328
|
-
const exprs = node.expressions.map(
|
|
539
|
+
const exprs = node.expressions.map(compile)
|
|
329
540
|
const len = exprs.length
|
|
330
541
|
return (ctx) => {
|
|
331
542
|
let last: JexlValue
|
|
@@ -337,13 +548,34 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
|
|
|
337
548
|
}
|
|
338
549
|
|
|
339
550
|
case 'AssignmentExpression': {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
+
)
|
|
347
579
|
}
|
|
348
580
|
}
|
|
349
581
|
|