@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/analyze.ts
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Jexl
|
|
3
|
+
* Copyright 2020 Tom Shawver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AstNode, AstNodeUnion, FunctionCall } from './types.ts'
|
|
7
|
+
|
|
8
|
+
export type PathKey = string | number
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A value the expression reads out of its context: the variable `root`, then
|
|
12
|
+
* each literal key in `path`. `dynamic` means a computed key followed, so the
|
|
13
|
+
* read reaches somewhere under `path` that only evaluation can name.
|
|
14
|
+
*
|
|
15
|
+
* A dot and a bracket both add a key.
|
|
16
|
+
*/
|
|
17
|
+
export interface Read {
|
|
18
|
+
root: string
|
|
19
|
+
path: PathKey[]
|
|
20
|
+
dynamic?: true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type CallArg =
|
|
24
|
+
| { type: 'literal'; value: string | number | boolean | null }
|
|
25
|
+
| { type: 'path'; read: Read }
|
|
26
|
+
| { type: 'dynamic' }
|
|
27
|
+
|
|
28
|
+
/** A path under the row, the record a per-row expression is evaluated for. */
|
|
29
|
+
export interface Field {
|
|
30
|
+
path: PathKey[]
|
|
31
|
+
dynamic?: true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Call {
|
|
35
|
+
name: string
|
|
36
|
+
args: CallArg[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Analysis {
|
|
40
|
+
/** The context variables read, in the order first read. */
|
|
41
|
+
variables: string[]
|
|
42
|
+
/**
|
|
43
|
+
* Every path whose value the expression uses, in the order first used. A
|
|
44
|
+
* path only extended, as `feature` is in `feature.score`, is not a use.
|
|
45
|
+
*/
|
|
46
|
+
reads: Read[]
|
|
47
|
+
/** The reads that are fields of the row, as `row` and `env` say. */
|
|
48
|
+
fields: Field[]
|
|
49
|
+
/** The paths whose value the expression returns as is. */
|
|
50
|
+
returns: Read[]
|
|
51
|
+
/**
|
|
52
|
+
* Whether the expression is only a path: a name, then dots and literal
|
|
53
|
+
* subscripts, as in `score` or `feature.INFO.DP[0]`. Its one read is then
|
|
54
|
+
* its value, which a host can read without jexl.
|
|
55
|
+
*/
|
|
56
|
+
bare: boolean
|
|
57
|
+
/** Every call, outermost first, including accessor calls. */
|
|
58
|
+
calls: Call[]
|
|
59
|
+
/** The names the expression writes into its context. */
|
|
60
|
+
assigned: string[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AnalyzeOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Functions that read a path: `name(subject, ...keys)` reads the subject,
|
|
66
|
+
* then this prefix, then each key. `{ get: [] }` makes `get(feature, 'x')`
|
|
67
|
+
* and `feature.get('x')` both read `feature.x`.
|
|
68
|
+
*/
|
|
69
|
+
accessors?: Record<string, readonly PathKey[]>
|
|
70
|
+
/** The variable holding the row, whose reads `fields` lists: `'feature'`. */
|
|
71
|
+
row?: string
|
|
72
|
+
/**
|
|
73
|
+
* For a host that binds each of the row's fields as a variable, so that
|
|
74
|
+
* `pvalue` means the row's `pvalue`: the variables it binds to anything
|
|
75
|
+
* else. Every other variable the expression reads is then a field.
|
|
76
|
+
*/
|
|
77
|
+
env?: readonly string[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface Binding {
|
|
81
|
+
value: Read[]
|
|
82
|
+
used: boolean
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type Scope = Map<string, Binding>
|
|
86
|
+
|
|
87
|
+
function literalKey(node: AstNode) {
|
|
88
|
+
const value = literalValue(node)
|
|
89
|
+
return typeof value === 'boolean' ? String(value) : (value ?? undefined)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function literalValue(ast: AstNode) {
|
|
93
|
+
const node = ast as AstNodeUnion
|
|
94
|
+
if (node.type === 'Literal') {
|
|
95
|
+
return node.value
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
node.type === 'TemplateLiteral' &&
|
|
99
|
+
node.parts.every((part) => part.type === 'static')
|
|
100
|
+
) {
|
|
101
|
+
return node.parts.map((part) => part.value).join('')
|
|
102
|
+
}
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function extend(read: Read, keys: readonly PathKey[], dynamic = false): Read {
|
|
107
|
+
if (read.dynamic) {
|
|
108
|
+
return read
|
|
109
|
+
}
|
|
110
|
+
const path = [...read.path, ...keys]
|
|
111
|
+
return dynamic
|
|
112
|
+
? { root: read.root, path, dynamic }
|
|
113
|
+
: { root: read.root, path }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isBarePath(ast: AstNode): boolean {
|
|
117
|
+
const node = ast as AstNodeUnion
|
|
118
|
+
if (node.type === 'Identifier') {
|
|
119
|
+
return !node.from || isBarePath(node.from)
|
|
120
|
+
}
|
|
121
|
+
return (
|
|
122
|
+
node.type === 'FilterExpression' &&
|
|
123
|
+
literalKey(node.expr) !== undefined &&
|
|
124
|
+
isBarePath(node.subject)
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function fieldOf(
|
|
129
|
+
{ root, path, dynamic }: Read,
|
|
130
|
+
row: string | undefined,
|
|
131
|
+
env: readonly string[] | undefined
|
|
132
|
+
): Field | undefined {
|
|
133
|
+
const fieldPath =
|
|
134
|
+
root === row
|
|
135
|
+
? path
|
|
136
|
+
: env && !env.includes(root)
|
|
137
|
+
? [root, ...path]
|
|
138
|
+
: undefined
|
|
139
|
+
return (
|
|
140
|
+
fieldPath && (dynamic ? { path: fieldPath, dynamic } : { path: fieldPath })
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function classify(node: AstNode, value: Read[]): CallArg {
|
|
145
|
+
const literal = literalValue(node)
|
|
146
|
+
if (literal !== undefined) {
|
|
147
|
+
return { type: 'literal', value: literal }
|
|
148
|
+
}
|
|
149
|
+
return value.length === 1
|
|
150
|
+
? { type: 'path', read: value[0]! }
|
|
151
|
+
: { type: 'dynamic' }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Lists what an expression reads from its context, without evaluating it or
|
|
156
|
+
* consulting any registered function.
|
|
157
|
+
*
|
|
158
|
+
* A name assigned earlier in the expression is a local: `x = feature.score;
|
|
159
|
+
* x > 1` reads `feature.score` and no `x`. An assignment that may not run — in
|
|
160
|
+
* a conditional's branch, or right of a binary operator, which may short-circuit
|
|
161
|
+
* — counts its value as read and binds nothing past that operand, so a later
|
|
162
|
+
* read of the name is a context read.
|
|
163
|
+
*
|
|
164
|
+
* Every path evaluation reads is reported, or a prefix of it is.
|
|
165
|
+
*/
|
|
166
|
+
export function analyze(
|
|
167
|
+
ast: AstNode | null,
|
|
168
|
+
{ accessors = {}, row, env }: AnalyzeOptions = {}
|
|
169
|
+
): Analysis {
|
|
170
|
+
const reads = new Map<string, Read>()
|
|
171
|
+
const calls: Call[] = []
|
|
172
|
+
const assigned = new Set<string>()
|
|
173
|
+
|
|
174
|
+
function consume(value: Read[]) {
|
|
175
|
+
for (const read of value) {
|
|
176
|
+
const key = JSON.stringify([read.root, read.path, read.dynamic])
|
|
177
|
+
if (!reads.has(key)) {
|
|
178
|
+
reads.set(key, read)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function use(node: AstNode, scope: Scope) {
|
|
184
|
+
consume(walk(node, scope))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function release(scope: Scope, outer?: Scope) {
|
|
188
|
+
for (const [name, binding] of scope) {
|
|
189
|
+
if (!binding.used && outer?.get(name) !== binding) {
|
|
190
|
+
consume(binding.value)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function branch(node: AstNode | undefined, scope: Scope) {
|
|
196
|
+
if (!node) {
|
|
197
|
+
return []
|
|
198
|
+
}
|
|
199
|
+
const inner = new Map(scope)
|
|
200
|
+
const value = walk(node, inner)
|
|
201
|
+
release(inner, scope)
|
|
202
|
+
return value
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function call(node: FunctionCall, scope: Scope): Read[] {
|
|
206
|
+
const record: Call = { name: node.name, args: [] }
|
|
207
|
+
calls.push(record)
|
|
208
|
+
const [subjectNode, ...keyNodes] = node.args
|
|
209
|
+
const prefix = Object.hasOwn(accessors, node.name)
|
|
210
|
+
? accessors[node.name]
|
|
211
|
+
: undefined
|
|
212
|
+
if (!prefix || !subjectNode) {
|
|
213
|
+
for (const arg of node.args) {
|
|
214
|
+
const value = walk(arg, scope)
|
|
215
|
+
record.args.push(classify(arg, value))
|
|
216
|
+
consume(value)
|
|
217
|
+
}
|
|
218
|
+
return []
|
|
219
|
+
}
|
|
220
|
+
const subject = walk(subjectNode, scope)
|
|
221
|
+
record.args.push(classify(subjectNode, subject))
|
|
222
|
+
const keys = [...prefix]
|
|
223
|
+
let dynamic = false
|
|
224
|
+
for (const arg of keyNodes) {
|
|
225
|
+
const key = literalKey(arg)
|
|
226
|
+
if (key === undefined) {
|
|
227
|
+
const value = walk(arg, scope)
|
|
228
|
+
record.args.push(classify(arg, value))
|
|
229
|
+
consume(value)
|
|
230
|
+
dynamic = true
|
|
231
|
+
} else {
|
|
232
|
+
record.args.push({ type: 'literal', value: literalValue(arg)! })
|
|
233
|
+
if (!dynamic) {
|
|
234
|
+
keys.push(key)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return subject.map((read) => extend(read, keys, dynamic))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function walk(ast: AstNode, scope: Scope): Read[] {
|
|
242
|
+
const node = ast as AstNodeUnion
|
|
243
|
+
switch (node.type) {
|
|
244
|
+
case 'Literal': {
|
|
245
|
+
return []
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case 'Identifier': {
|
|
249
|
+
if (node.from) {
|
|
250
|
+
return walk(node.from, scope).map((read) =>
|
|
251
|
+
extend(read, [node.value])
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
const binding = scope.get(node.value)
|
|
255
|
+
if (binding) {
|
|
256
|
+
binding.used = true
|
|
257
|
+
return binding.value
|
|
258
|
+
}
|
|
259
|
+
return [{ root: node.value, path: [] }]
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
case 'FilterExpression': {
|
|
263
|
+
const subject = walk(node.subject, scope)
|
|
264
|
+
const key = literalKey(node.expr)
|
|
265
|
+
if (key !== undefined) {
|
|
266
|
+
return subject.map((read) => extend(read, [key]))
|
|
267
|
+
}
|
|
268
|
+
use(node.expr, scope)
|
|
269
|
+
return subject.map((read) => extend(read, [], true))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
case 'FunctionCall': {
|
|
273
|
+
return call(node, scope)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
case 'ConditionalExpression': {
|
|
277
|
+
const test = walk(node.test, scope)
|
|
278
|
+
consume(test)
|
|
279
|
+
const consequent = node.consequent
|
|
280
|
+
? branch(node.consequent, scope)
|
|
281
|
+
: test
|
|
282
|
+
return [...consequent, ...branch(node.alternate, scope)]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case 'BinaryExpression': {
|
|
286
|
+
use(node.left, scope)
|
|
287
|
+
consume(branch(node.right, scope))
|
|
288
|
+
return []
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
case 'UnaryExpression': {
|
|
292
|
+
use(node.right!, scope)
|
|
293
|
+
return []
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
case 'ArrayLiteral': {
|
|
297
|
+
for (const item of node.value) {
|
|
298
|
+
use(item, scope)
|
|
299
|
+
}
|
|
300
|
+
return []
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
case 'ObjectLiteral': {
|
|
304
|
+
for (const value of Object.values(node.value)) {
|
|
305
|
+
use(value, scope)
|
|
306
|
+
}
|
|
307
|
+
return []
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case 'TemplateLiteral': {
|
|
311
|
+
for (const part of node.parts) {
|
|
312
|
+
if (part.type === 'expression') {
|
|
313
|
+
use(part.value, scope)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return []
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
case 'SequenceExpression': {
|
|
320
|
+
const exprs = node.expressions
|
|
321
|
+
for (const expr of exprs.slice(0, -1)) {
|
|
322
|
+
const value = walk(expr, scope)
|
|
323
|
+
if (expr.type !== 'AssignmentExpression') {
|
|
324
|
+
consume(value)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return walk(exprs.at(-1)!, scope)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
case 'AssignmentExpression': {
|
|
331
|
+
const value = walk(node.right!, scope)
|
|
332
|
+
const name = node.left.value
|
|
333
|
+
const previous = scope.get(name)
|
|
334
|
+
if (previous && !previous.used) {
|
|
335
|
+
consume(previous.value)
|
|
336
|
+
}
|
|
337
|
+
scope.set(name, { value, used: false })
|
|
338
|
+
assigned.add(name)
|
|
339
|
+
return value
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
case 'Lambda': {
|
|
343
|
+
const inner: Scope = new Map(scope)
|
|
344
|
+
for (const param of node.params) {
|
|
345
|
+
inner.set(param, { value: [], used: true })
|
|
346
|
+
}
|
|
347
|
+
use(node.body, inner)
|
|
348
|
+
return []
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
default: {
|
|
352
|
+
throw new Error(`Corrupt AST: unknown node type '${ast.type}'`)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const scope: Scope = new Map()
|
|
358
|
+
const returns = ast ? walk(ast, scope) : []
|
|
359
|
+
consume(returns)
|
|
360
|
+
release(scope)
|
|
361
|
+
const readList = [...reads.values()]
|
|
362
|
+
const fields = new Map<string, Field>()
|
|
363
|
+
for (const read of readList) {
|
|
364
|
+
const field = fieldOf(read, row, env)
|
|
365
|
+
if (field) {
|
|
366
|
+
fields.set(JSON.stringify(field), field)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
variables: [...new Set(readList.map((read) => read.root))],
|
|
371
|
+
reads: readList,
|
|
372
|
+
fields: [...fields.values()],
|
|
373
|
+
returns,
|
|
374
|
+
bare: ast !== null && isBarePath(ast),
|
|
375
|
+
calls,
|
|
376
|
+
assigned: [...assigned]
|
|
377
|
+
}
|
|
378
|
+
}
|