@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.
Files changed (110) hide show
  1. package/README.md +163 -20
  2. package/dist/Expression.d.ts +16 -0
  3. package/dist/Expression.js +28 -4
  4. package/dist/Expression.js.map +1 -1
  5. package/dist/Jexl.d.ts +20 -8
  6. package/dist/Jexl.js +34 -15
  7. package/dist/Jexl.js.map +1 -1
  8. package/dist/Lexer.d.ts +4 -3
  9. package/dist/Lexer.js +29 -21
  10. package/dist/Lexer.js.map +1 -1
  11. package/dist/analyze.d.ts +84 -0
  12. package/dist/analyze.js +259 -0
  13. package/dist/analyze.js.map +1 -0
  14. package/dist/check.d.ts +109 -0
  15. package/dist/check.js +1267 -0
  16. package/dist/check.js.map +1 -0
  17. package/dist/collections.d.ts +3 -0
  18. package/dist/collections.js +140 -0
  19. package/dist/collections.js.map +1 -0
  20. package/dist/conditions.d.ts +62 -0
  21. package/dist/conditions.js +207 -0
  22. package/dist/conditions.js.map +1 -0
  23. package/dist/errors.d.ts +5 -0
  24. package/dist/errors.js +18 -0
  25. package/dist/errors.js.map +1 -0
  26. package/dist/evaluator/compile.d.ts +10 -7
  27. package/dist/evaluator/compile.js +292 -99
  28. package/dist/evaluator/compile.js.map +1 -1
  29. package/dist/grammar.d.ts +22 -9
  30. package/dist/grammar.js +83 -55
  31. package/dist/grammar.js.map +1 -1
  32. package/dist/index.d.ts +9 -0
  33. package/dist/index.js +16 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/operators.d.ts +34 -0
  36. package/dist/operators.js +131 -0
  37. package/dist/operators.js.map +1 -0
  38. package/dist/parser/Parser.d.ts +48 -91
  39. package/dist/parser/Parser.js +347 -221
  40. package/dist/parser/Parser.js.map +1 -1
  41. package/dist/types.d.ts +11 -5
  42. package/esm/Expression.d.ts +16 -0
  43. package/esm/Expression.js +28 -4
  44. package/esm/Expression.js.map +1 -1
  45. package/esm/Jexl.d.ts +20 -8
  46. package/esm/Jexl.js +34 -15
  47. package/esm/Jexl.js.map +1 -1
  48. package/esm/Lexer.d.ts +4 -3
  49. package/esm/Lexer.js +29 -21
  50. package/esm/Lexer.js.map +1 -1
  51. package/esm/analyze.d.ts +84 -0
  52. package/esm/analyze.js +256 -0
  53. package/esm/analyze.js.map +1 -0
  54. package/esm/check.d.ts +109 -0
  55. package/esm/check.js +1260 -0
  56. package/esm/check.js.map +1 -0
  57. package/esm/collections.d.ts +3 -0
  58. package/esm/collections.js +137 -0
  59. package/esm/collections.js.map +1 -0
  60. package/esm/conditions.d.ts +62 -0
  61. package/esm/conditions.js +200 -0
  62. package/esm/conditions.js.map +1 -0
  63. package/esm/errors.d.ts +5 -0
  64. package/esm/errors.js +14 -0
  65. package/esm/errors.js.map +1 -0
  66. package/esm/evaluator/compile.d.ts +10 -7
  67. package/esm/evaluator/compile.js +292 -99
  68. package/esm/evaluator/compile.js.map +1 -1
  69. package/esm/grammar.d.ts +22 -9
  70. package/esm/grammar.js +79 -53
  71. package/esm/grammar.js.map +1 -1
  72. package/esm/index.d.ts +9 -0
  73. package/esm/index.js +4 -0
  74. package/esm/index.js.map +1 -1
  75. package/esm/operators.d.ts +34 -0
  76. package/esm/operators.js +123 -0
  77. package/esm/operators.js.map +1 -0
  78. package/esm/parser/Parser.d.ts +48 -91
  79. package/esm/parser/Parser.js +349 -192
  80. package/esm/parser/Parser.js.map +1 -1
  81. package/esm/types.d.ts +11 -5
  82. package/package.json +1 -1
  83. package/src/Expression.ts +34 -4
  84. package/src/Jexl.ts +52 -16
  85. package/src/Lexer.ts +32 -21
  86. package/src/analyze.ts +378 -0
  87. package/src/check.ts +1682 -0
  88. package/src/collections.ts +148 -0
  89. package/src/conditions.ts +262 -0
  90. package/src/errors.ts +15 -0
  91. package/src/evaluator/compile.ts +351 -102
  92. package/src/grammar.ts +137 -55
  93. package/src/index.ts +15 -0
  94. package/src/operators.ts +151 -0
  95. package/src/parser/Parser.ts +389 -199
  96. package/src/types.ts +13 -3
  97. package/dist/parser/handlers.d.ts +0 -115
  98. package/dist/parser/handlers.js +0 -352
  99. package/dist/parser/handlers.js.map +0 -1
  100. package/dist/parser/states.d.ts +0 -47
  101. package/dist/parser/states.js +0 -176
  102. package/dist/parser/states.js.map +0 -1
  103. package/esm/parser/handlers.d.ts +0 -115
  104. package/esm/parser/handlers.js +0 -328
  105. package/esm/parser/handlers.js.map +0 -1
  106. package/esm/parser/states.d.ts +0 -47
  107. package/esm/parser/states.js +0 -140
  108. package/esm/parser/states.js.map +0 -1
  109. package/src/parser/handlers.ts +0 -372
  110. 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
+ }