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