@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.
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 +295 -72
  28. package/dist/evaluator/compile.js.map +1 -1
  29. package/dist/grammar.d.ts +19 -9
  30. package/dist/grammar.js +66 -40
  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 +295 -72
  68. package/esm/evaluator/compile.js.map +1 -1
  69. package/esm/grammar.d.ts +19 -9
  70. package/esm/grammar.js +66 -39
  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 +356 -72
  92. package/src/grammar.ts +123 -42
  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/grammar.ts CHANGED
@@ -5,6 +5,16 @@
5
5
 
6
6
  /* eslint eqeqeq:0 */
7
7
 
8
+ import { collectionFunctions } from './collections.ts'
9
+ import {
10
+ addsPairwise,
11
+ anyPair,
12
+ isIn,
13
+ isList,
14
+ matches,
15
+ pairwise
16
+ } from './operators.ts'
17
+
8
18
  import type { JexlValue } from './types.ts'
9
19
 
10
20
  export type BinaryOpEval = (left: JexlValue, right: JexlValue) => JexlValue
@@ -40,6 +50,8 @@ export interface BinaryOp {
40
50
  * which is how `-x` is distinguished from `a - x`.
41
51
  */
42
52
  unaryEval?: (right: JexlValue) => JexlValue
53
+ /** Groups a chain from the right, so `a ^ b ^ c` is `a ^ (b ^ c)`. */
54
+ rightAssociative?: boolean
43
55
  }
44
56
 
45
57
  export type UnaryOpEval = (right: JexlValue) => JexlValue
@@ -52,9 +64,9 @@ export interface UnaryOp {
52
64
 
53
65
  /**
54
66
  * The punctuation elements of the grammar. Unlike operators these carry no
55
- * behavior of their own; the Parser's state machine gives them meaning. The
56
- * type is a literal union rather than `string` so that `GrammarElement` is a
57
- * discriminated union, letting operator properties be accessed without casts.
67
+ * behavior of their own; the Parser gives them meaning. The type is a literal
68
+ * union rather than `string` so that `GrammarElement` is a discriminated union,
69
+ * letting operator properties be accessed without casts.
58
70
  */
59
71
  export interface SimpleElement {
60
72
  type:
@@ -69,6 +81,7 @@ export interface SimpleElement {
69
81
  | 'closeParen'
70
82
  | 'question'
71
83
  | 'semicolon'
84
+ | 'arrow'
72
85
  }
73
86
 
74
87
  export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
@@ -76,20 +89,58 @@ export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
76
89
  /** A registered function, as jexl calls it once the operands are evaluated. */
77
90
  export type GrammarFn = (...args: JexlValue[]) => JexlValue
78
91
 
92
+ /**
93
+ * Reads `key` off `subject` for `subject.key` and `subject[key]`, in place of
94
+ * a plain property read. Like a registered function, it is handed host values
95
+ * jexl has no type for, so the subject is only promised not to be nullish.
96
+ */
97
+ export type GetMember = (
98
+ subject: NonNullable<unknown>,
99
+ key: string | number
100
+ ) => unknown
101
+
102
+ /**
103
+ * Called once per bare variable name as an expression compiles. A reader it
104
+ * returns replaces `context[name]` for that name; `undefined` keeps it.
105
+ */
106
+ export type VariableReader = (
107
+ name: string
108
+ ) => ((context: Record<string, unknown>) => unknown) | undefined
109
+
79
110
  export interface Grammar {
80
111
  elements: Record<string, GrammarElement>
81
112
  functions: Record<string, GrammarFn>
113
+ getMember?: GetMember
114
+ variableReader?: VariableReader
82
115
  }
83
116
 
84
- /**
85
- * Returns the binding power of a grammar element, or 0 for elements that
86
- * aren't operators and therefore don't participate in precedence.
87
- */
88
- export function precedenceOf(elem: GrammarElement | undefined) {
89
- return elem && (elem.type === 'binaryOp' || elem.type === 'unaryOp')
90
- ? elem.precedence
91
- : 0
92
- }
117
+ const plus = (left: JexlValue, right: JexlValue) =>
118
+ (left as number) + (right as number)
119
+ const minus = (left: JexlValue, right: JexlValue) =>
120
+ (left as number) - (right as number)
121
+ const times = (left: JexlValue, right: JexlValue) =>
122
+ (left as number) * (right as number)
123
+ const divide = (left: JexlValue, right: JexlValue) =>
124
+ (left as number) / (right as number)
125
+ const floorDivide = (left: JexlValue, right: JexlValue) =>
126
+ Math.floor((left as number) / (right as number))
127
+ const modulo = (left: JexlValue, right: JexlValue) =>
128
+ (left as number) % (right as number)
129
+ const power = (left: JexlValue, right: JexlValue) =>
130
+ (left as number) ** (right as number)
131
+ const loose = (left: JexlValue, right: JexlValue) => left == right
132
+ // a missing value orders against nothing, as bcftools skips one; JavaScript
133
+ // alone would read null as 0, making [null, 0.2] < 0.05 true
134
+ const greater = (left: JexlValue, right: JexlValue) =>
135
+ left != null && right != null && (left as number) > (right as number)
136
+ const atLeast = (left: JexlValue, right: JexlValue) =>
137
+ left != null && right != null && (left as number) >= (right as number)
138
+ const less = (left: JexlValue, right: JexlValue) =>
139
+ left != null && right != null && (left as number) < (right as number)
140
+ const atMost = (left: JexlValue, right: JexlValue) =>
141
+ left != null && right != null && (left as number) <= (right as number)
142
+ const equals = (left: JexlValue, right: JexlValue) =>
143
+ isList(left, right) ? anyPair(loose, left, right) : left == right
93
144
 
94
145
  export const getGrammar = (): Grammar => ({
95
146
  /**
@@ -109,75 +160,113 @@ export const getGrammar = (): Grammar => ({
109
160
  ')': { type: 'closeParen' },
110
161
  '?': { type: 'question' },
111
162
  ';': { type: 'semicolon' },
163
+ '=>': { type: 'arrow' },
112
164
  '+': {
113
165
  type: 'binaryOp',
114
166
  precedence: 30,
115
- eval: (left, right) => (left as number) + (right as number)
167
+ eval: (left, right) =>
168
+ addsPairwise(left, right)
169
+ ? pairwise(plus, left, right)
170
+ : plus(left, right)
116
171
  },
117
172
  '-': {
118
173
  type: 'binaryOp',
119
174
  precedence: 30,
120
- eval: (left, right) => (left as number) - (right as number),
121
- unaryEval: (right) => -(right as number)
175
+ eval: (left, right) =>
176
+ isList(left, right) ? pairwise(minus, left, right) : minus(left, right),
177
+ unaryEval: (right) =>
178
+ Array.isArray(right)
179
+ ? right.map((value) => -(value as number))
180
+ : -(right as number)
122
181
  },
123
182
  '*': {
124
183
  type: 'binaryOp',
125
184
  precedence: 40,
126
- eval: (left, right) => (left as number) * (right as number)
185
+ eval: (left, right) =>
186
+ isList(left, right) ? pairwise(times, left, right) : times(left, right)
127
187
  },
128
188
  '/': {
129
189
  type: 'binaryOp',
130
190
  precedence: 40,
131
- eval: (left, right) => (left as number) / (right as number)
191
+ eval: (left, right) =>
192
+ isList(left, right)
193
+ ? pairwise(divide, left, right)
194
+ : divide(left, right)
132
195
  },
133
196
  '//': {
134
197
  type: 'binaryOp',
135
198
  precedence: 40,
136
- eval: (left, right) => Math.floor((left as number) / (right as number))
199
+ eval: (left, right) =>
200
+ isList(left, right)
201
+ ? pairwise(floorDivide, left, right)
202
+ : floorDivide(left, right)
137
203
  },
138
204
  '%': {
139
205
  type: 'binaryOp',
140
206
  precedence: 50,
141
- eval: (left, right) => (left as number) % (right as number)
207
+ eval: (left, right) =>
208
+ isList(left, right)
209
+ ? pairwise(modulo, left, right)
210
+ : modulo(left, right)
142
211
  },
143
212
  '^': {
144
213
  type: 'binaryOp',
145
214
  precedence: 50,
146
- eval: (left, right) => (left as number) ** (right as number)
215
+ rightAssociative: true,
216
+ eval: (left, right) =>
217
+ isList(left, right) ? pairwise(power, left, right) : power(left, right)
147
218
  },
148
219
  '==': {
149
220
  type: 'binaryOp',
150
221
  precedence: 20,
151
- eval: (left, right) => left == right
222
+ eval: equals
152
223
  },
153
224
  '!=': {
154
225
  type: 'binaryOp',
155
226
  precedence: 20,
156
- eval: (left, right) => left != right
227
+ eval: (left, right) => !equals(left, right)
228
+ },
229
+ '~': {
230
+ type: 'binaryOp',
231
+ precedence: 20,
232
+ eval: matches
233
+ },
234
+ '!~': {
235
+ type: 'binaryOp',
236
+ precedence: 20,
237
+ eval: (left, right) => !matches(left, right)
157
238
  },
158
239
  '>': {
159
240
  type: 'binaryOp',
160
241
  precedence: 20,
161
- eval: (left, right) => (left as number) > (right as number)
242
+ eval: (left, right) =>
243
+ isList(left, right)
244
+ ? anyPair(greater, left, right)
245
+ : greater(left, right)
162
246
  },
163
247
  '>=': {
164
248
  type: 'binaryOp',
165
249
  precedence: 20,
166
- eval: (left, right) => (left as number) >= (right as number)
250
+ eval: (left, right) =>
251
+ isList(left, right)
252
+ ? anyPair(atLeast, left, right)
253
+ : atLeast(left, right)
167
254
  },
168
255
  '<': {
169
256
  type: 'binaryOp',
170
257
  precedence: 20,
171
- eval: (left, right) => (left as number) < (right as number)
258
+ eval: (left, right) =>
259
+ isList(left, right) ? anyPair(less, left, right) : less(left, right)
172
260
  },
173
261
  '<=': {
174
262
  type: 'binaryOp',
175
263
  precedence: 20,
176
- eval: (left, right) => (left as number) <= (right as number)
264
+ eval: (left, right) =>
265
+ isList(left, right) ? anyPair(atMost, left, right) : atMost(left, right)
177
266
  },
178
267
  '&&': {
179
268
  type: 'binaryOp',
180
- precedence: 10,
269
+ precedence: 11,
181
270
  evalOnDemand: (left, right) => {
182
271
  const leftVal = left.eval()
183
272
  if (!leftVal) {
@@ -197,23 +286,15 @@ export const getGrammar = (): Grammar => ({
197
286
  return right.eval()
198
287
  }
199
288
  },
289
+ '??': {
290
+ type: 'binaryOp',
291
+ precedence: 10,
292
+ evalOnDemand: (left, right) => left.eval() ?? right.eval()
293
+ },
200
294
  in: {
201
295
  type: 'binaryOp',
202
296
  precedence: 20,
203
- eval: (left, right) => {
204
- if (typeof right === 'string') {
205
- // only a primitive has a meaningful substring form. An absent or
206
- // structured left operand is not "in" a string, and must not be
207
- // coerced to '' — every string contains the empty string, so that
208
- // made `feature.missingAttr in "someString"` true for every feature.
209
- return typeof left === 'string' ||
210
- typeof left === 'number' ||
211
- typeof left === 'boolean'
212
- ? right.includes(String(left))
213
- : false
214
- }
215
- return Array.isArray(right) ? right.includes(left) : false
216
- }
297
+ eval: isIn
217
298
  },
218
299
  '!': {
219
300
  type: 'unaryOp',
@@ -243,5 +324,5 @@ export const getGrammar = (): Grammar => ({
243
324
  * than throw. An error is only appropriate when the function would normally
244
325
  * return a value, but cannot due to some other failure.
245
326
  */
246
- functions: {}
327
+ functions: { ...collectionFunctions }
247
328
  })
package/src/index.ts CHANGED
@@ -4,7 +4,22 @@
4
4
  */
5
5
 
6
6
  export { Jexl, default } from './Jexl.ts'
7
+ export type { JexlOptions } from './Jexl.ts'
8
+ export type { GetMember, VariableReader } from './grammar.ts'
7
9
  export { default as Expression } from './Expression.ts'
10
+ export { analyze } from './analyze.ts'
11
+ export { check, print, recordOf, union } from './check.ts'
12
+ export {
13
+ callSubject,
14
+ conditions,
15
+ fromConditions,
16
+ pathSubject,
17
+ printCondition
18
+ } from './conditions.ts'
8
19
  export { default as Lexer } from './Lexer.ts'
20
+ export { JexlSyntaxError } from './errors.ts'
9
21
  export { getGrammar } from './grammar.ts'
22
+ export type * from './analyze.ts'
23
+ export type * from './check.ts'
24
+ export type * from './conditions.ts'
10
25
  export type * from './types.ts'
@@ -0,0 +1,151 @@
1
+ /*
2
+ * Jexl
3
+ * Copyright 2020 Tom Shawver
4
+ */
5
+
6
+ import type { JexlValue } from './types.ts'
7
+
8
+ type Test = (left: JexlValue, right: JexlValue) => boolean
9
+ type Arithmetic = (left: JexlValue, right: JexlValue) => JexlValue
10
+
11
+ export function isList(left: JexlValue, right: JexlValue) {
12
+ return Array.isArray(left) || Array.isArray(right)
13
+ }
14
+
15
+ /**
16
+ * A comparison over list operands: it holds when it holds for any value, as
17
+ * bcftools compares a multi-valued tag, so `AF > 0.05` is true when any
18
+ * allele's frequency is and `CLNSIG == 'Pathogenic'` when any significance is.
19
+ * Each operator calls this only once {@link isList} says it must, so the
20
+ * comparison of two single values stays a direct call V8 can inline.
21
+ */
22
+ export function anyPair(
23
+ test: Test,
24
+ left: JexlValue,
25
+ right: JexlValue
26
+ ): boolean {
27
+ return Array.isArray(left)
28
+ ? left.some((l) => anyPair(test, l, right))
29
+ : Array.isArray(right)
30
+ ? right.some((r) => anyPair(test, left, r))
31
+ : test(left, right)
32
+ }
33
+
34
+ function hasText(value: JexlValue) {
35
+ return (
36
+ typeof value === 'string' ||
37
+ (Array.isArray(value) && value.some((item) => typeof item === 'string'))
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Whether `+` pairs its operands value by value: only when a list is involved
43
+ * and neither side holds text, so `REF + '>' + ALT` still joins as JavaScript
44
+ * joins, `'A>T,C'`, while `DP + 1` adds.
45
+ */
46
+ export function addsPairwise(left: JexlValue, right: JexlValue) {
47
+ return isList(left, right) && !hasText(left) && !hasText(right)
48
+ }
49
+
50
+ /**
51
+ * Arithmetic over list operands, value by value, as bcftools and R apply it:
52
+ * `AC / AN` divides each count, and a list of one, or a lone value, pairs with
53
+ * every value of the other side. Lists of two other lengths have no pairing
54
+ * and yield undefined.
55
+ */
56
+ export function pairwise(
57
+ fn: Arithmetic,
58
+ left: JexlValue,
59
+ right: JexlValue
60
+ ): JexlValue {
61
+ const ls = Array.isArray(left) ? left : [left]
62
+ const rs = Array.isArray(right) ? right : [right]
63
+ if (ls.length !== rs.length && ls.length !== 1 && rs.length !== 1) {
64
+ return undefined
65
+ }
66
+ const length = ls.length === 1 ? rs.length : ls.length
67
+ const out: JexlValue[] = new Array(length)
68
+ for (let i = 0; i < length; i++) {
69
+ out[i] = fn(
70
+ ls.length === 1 ? ls[0] : ls[i],
71
+ rs.length === 1 ? rs[0] : rs[i]
72
+ )
73
+ }
74
+ return out
75
+ }
76
+
77
+ const patterns = new Map<string, RegExp>()
78
+ const MAX_PATTERNS = 1000
79
+
80
+ /**
81
+ * The regular expression a pattern string stands for. A leading `(?i)`, the
82
+ * inline flag PCRE, Python and Go read, makes it case-insensitive.
83
+ */
84
+ function regex(pattern: string) {
85
+ let re = patterns.get(pattern)
86
+ if (!re) {
87
+ try {
88
+ re = pattern.startsWith('(?i)')
89
+ ? new RegExp(pattern.slice(4), 'i')
90
+ : new RegExp(pattern)
91
+ } catch {
92
+ throw new Error(`Invalid regular expression: ${pattern}`)
93
+ }
94
+ if (patterns.size >= MAX_PATTERNS) {
95
+ patterns.clear()
96
+ }
97
+ patterns.set(pattern, re)
98
+ }
99
+ return re
100
+ }
101
+
102
+ /** Whether a value, or any value of a list, matches a pattern string. */
103
+ export function matches(value: JexlValue, pattern: JexlValue): boolean {
104
+ if (typeof pattern !== 'string') {
105
+ return false
106
+ }
107
+ const re = regex(pattern)
108
+ const test = (v: JexlValue) =>
109
+ (typeof v === 'string' ||
110
+ typeof v === 'number' ||
111
+ typeof v === 'boolean') &&
112
+ re.test(String(v))
113
+ return Array.isArray(value) ? value.some(test) : test(value)
114
+ }
115
+
116
+ /**
117
+ * Whether a value is in a container: a substring of a string, a member of a
118
+ * list or Set, or a key of a Map or plain object. A list on the left is in
119
+ * the container when any of its values is.
120
+ */
121
+ export function isIn(left: JexlValue, right: JexlValue): boolean {
122
+ if (Array.isArray(left)) {
123
+ return left.some((l) => isIn(l, right))
124
+ }
125
+ if (typeof right === 'string') {
126
+ // only a primitive has a meaningful substring form. An absent or
127
+ // structured left operand is not "in" a string, and must not be coerced
128
+ // to '' — every string contains the empty string
129
+ return typeof left === 'string' ||
130
+ typeof left === 'number' ||
131
+ typeof left === 'boolean'
132
+ ? right.includes(String(left))
133
+ : false
134
+ }
135
+ if (Array.isArray(right)) {
136
+ return right.includes(left)
137
+ }
138
+ if (right instanceof Set || right instanceof Map) {
139
+ return right.has(left)
140
+ }
141
+ if (
142
+ typeof right === 'object' &&
143
+ right !== null &&
144
+ (typeof left === 'string' ||
145
+ typeof left === 'number' ||
146
+ typeof left === 'boolean')
147
+ ) {
148
+ return Object.hasOwn(right, String(left))
149
+ }
150
+ return false
151
+ }