@jbrowse/jexl 3.0.2 → 3.2.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 (76) hide show
  1. package/README.md +26 -2
  2. package/dist/Expression.d.ts +19 -7
  3. package/dist/Expression.js +32 -11
  4. package/dist/Expression.js.map +1 -1
  5. package/dist/Jexl.d.ts +13 -12
  6. package/dist/Jexl.js +31 -30
  7. package/dist/Jexl.js.map +1 -1
  8. package/dist/Lexer.d.ts +9 -9
  9. package/dist/Lexer.js +61 -24
  10. package/dist/Lexer.js.map +1 -1
  11. package/dist/evaluator/Evaluator.d.ts +20 -34
  12. package/dist/evaluator/Evaluator.js +19 -64
  13. package/dist/evaluator/Evaluator.js.map +1 -1
  14. package/dist/evaluator/compile.d.ts +27 -0
  15. package/dist/evaluator/compile.js +279 -0
  16. package/dist/evaluator/compile.js.map +1 -0
  17. package/dist/grammar.d.ts +29 -9
  18. package/dist/grammar.js +23 -7
  19. package/dist/grammar.js.map +1 -1
  20. package/dist/parser/Parser.d.ts +7 -8
  21. package/dist/parser/Parser.js +46 -14
  22. package/dist/parser/Parser.js.map +1 -1
  23. package/dist/parser/handlers.d.ts +8 -8
  24. package/dist/parser/handlers.js +84 -51
  25. package/dist/parser/handlers.js.map +1 -1
  26. package/dist/parser/states.d.ts +2 -2
  27. package/dist/parser/states.js +19 -40
  28. package/dist/parser/states.js.map +1 -1
  29. package/dist/types.d.ts +18 -7
  30. package/esm/Expression.d.ts +19 -7
  31. package/esm/Expression.js +32 -11
  32. package/esm/Expression.js.map +1 -1
  33. package/esm/Jexl.d.ts +13 -12
  34. package/esm/Jexl.js +31 -30
  35. package/esm/Jexl.js.map +1 -1
  36. package/esm/Lexer.d.ts +9 -9
  37. package/esm/Lexer.js +61 -24
  38. package/esm/Lexer.js.map +1 -1
  39. package/esm/evaluator/Evaluator.d.ts +20 -34
  40. package/esm/evaluator/Evaluator.js +19 -31
  41. package/esm/evaluator/Evaluator.js.map +1 -1
  42. package/esm/evaluator/compile.d.ts +27 -0
  43. package/esm/evaluator/compile.js +276 -0
  44. package/esm/evaluator/compile.js.map +1 -0
  45. package/esm/grammar.d.ts +29 -9
  46. package/esm/grammar.js +22 -7
  47. package/esm/grammar.js.map +1 -1
  48. package/esm/parser/Parser.d.ts +7 -8
  49. package/esm/parser/Parser.js +46 -14
  50. package/esm/parser/Parser.js.map +1 -1
  51. package/esm/parser/handlers.d.ts +8 -8
  52. package/esm/parser/handlers.js +81 -50
  53. package/esm/parser/handlers.js.map +1 -1
  54. package/esm/parser/states.d.ts +2 -2
  55. package/esm/parser/states.js +19 -40
  56. package/esm/parser/states.js.map +1 -1
  57. package/esm/types.d.ts +18 -7
  58. package/package.json +25 -28
  59. package/src/Expression.ts +31 -17
  60. package/src/Jexl.ts +59 -21
  61. package/src/Lexer.ts +63 -33
  62. package/src/evaluator/Evaluator.ts +27 -44
  63. package/src/evaluator/compile.ts +317 -0
  64. package/src/grammar.ts +69 -21
  65. package/src/parser/Parser.ts +50 -28
  66. package/src/parser/handlers.ts +122 -76
  67. package/src/parser/states.ts +22 -42
  68. package/src/types.ts +22 -7
  69. package/CHANGELOG.md +0 -249
  70. package/dist/evaluator/handlers.d.ts +0 -112
  71. package/dist/evaluator/handlers.js +0 -208
  72. package/dist/evaluator/handlers.js.map +0 -1
  73. package/esm/evaluator/handlers.d.ts +0 -112
  74. package/esm/evaluator/handlers.js +0 -194
  75. package/esm/evaluator/handlers.js.map +0 -1
  76. package/src/evaluator/handlers.ts +0 -212
@@ -0,0 +1,317 @@
1
+ /*
2
+ * Jexl
3
+ * Copyright 2020 Tom Shawver
4
+ */
5
+
6
+ import type { Grammar } from '../grammar.ts'
7
+ import type { AstNode, AstNodeUnion, JexlValue } from '../types.ts'
8
+ import type Evaluator from './Evaluator.ts'
9
+
10
+ /**
11
+ * An AST node lowered to a closure. Calling it with an Evaluator (which carries
12
+ * the context and grammar) produces the node's value.
13
+ */
14
+ export type CompiledNode = (ev: Evaluator) => JexlValue
15
+
16
+ /**
17
+ * Writes a key that a plain assignment would mishandle. Storing to
18
+ * "__proto__" invokes the prototype setter rather than creating a property, so
19
+ * an object literal or assignment using that key silently lost its value (and
20
+ * re-pointed the target's prototype). Defining the property instead matches
21
+ * what `JSON.parse('{"__proto__":1}')` produces: an ordinary own property.
22
+ */
23
+ function defineOwn(
24
+ target: Record<string, JexlValue>,
25
+ key: string,
26
+ value: JexlValue
27
+ ) {
28
+ Object.defineProperty(target, key, {
29
+ value,
30
+ writable: true,
31
+ enumerable: true,
32
+ configurable: true
33
+ })
34
+ }
35
+
36
+ function assignOwn(
37
+ target: Record<string, JexlValue>,
38
+ key: string,
39
+ value: JexlValue
40
+ ) {
41
+ target[key] = value
42
+ }
43
+
44
+ /** Renders an interpolated value for a template literal. */
45
+ function stringify(value: JexlValue) {
46
+ if (value == null) {
47
+ return ''
48
+ }
49
+ if (
50
+ typeof value === 'string' ||
51
+ typeof value === 'number' ||
52
+ typeof value === 'boolean'
53
+ ) {
54
+ return String(value)
55
+ }
56
+ return JSON.stringify(value)
57
+ }
58
+
59
+ /**
60
+ * Lowers an expression tree into a tree of closures, resolving each node's type
61
+ * and its operator's implementation once, at compile time, rather than
62
+ * re-dispatching on `node.type` for every node on every evaluation. An
63
+ * expression compiled once and evaluated per-item — the usual shape for
64
+ * per-feature config callbacks — pays the dispatch cost once instead of N
65
+ * times.
66
+ *
67
+ * Operators are bound here, so a grammar change after compilation requires
68
+ * recompiling, which is what {@link Expression#compile} already documents.
69
+ * Functions are looked up per call, since they are commonly registered after
70
+ * an expression has been compiled.
71
+ *
72
+ * @param {{}} ast An expression tree, as produced by the Parser
73
+ * @param {{}} grammar The grammar to resolve operators against
74
+ * @returns {function} a closure returning the expression's value
75
+ * @throws {Error} if the tree contains an unrecognized node type
76
+ */
77
+ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
78
+ // AstNode types its `type` as a plain string so the Parser can build the tree
79
+ // loosely; narrowing to the union once, here, lets every case below see its
80
+ // own node type instead of repeating the same cast in each branch
81
+ const node = ast as AstNodeUnion
82
+ switch (node.type) {
83
+ case 'Literal': {
84
+ const { value } = node
85
+ return () => value
86
+ }
87
+
88
+ case 'Identifier': {
89
+ const name = node.value
90
+ if (!node.from) {
91
+ return node.relative
92
+ ? (ev) => ev._relContext[name]
93
+ : (ev) => ev._context[name]
94
+ }
95
+ const from = compileAst(node.from, grammar)
96
+ return (ev) => {
97
+ const context = from(ev)
98
+ if (context == null) {
99
+ return undefined
100
+ }
101
+ // an identifier chained off an array reads through its first element
102
+ const ctx = Array.isArray(context) ? context[0] : context
103
+ return ctx == null
104
+ ? undefined
105
+ : (ctx as Record<string, JexlValue>)[name]
106
+ }
107
+ }
108
+
109
+ case 'BinaryExpression': {
110
+ const op = grammar.elements[node.operator]
111
+ if (op?.type !== 'binaryOp') {
112
+ // matches the tree-walking behaviour: an unknown operator yields
113
+ // undefined without evaluating either operand
114
+ return () => undefined
115
+ }
116
+ const left = compileAst(node.left, grammar)
117
+ const right = compileAst(node.right!, grammar)
118
+ const { evalOnDemand } = op
119
+ if (evalOnDemand) {
120
+ // operands stay unevaluated behind an `eval` thunk, so operators such
121
+ // as && and || can short-circuit
122
+ return (ev) =>
123
+ evalOnDemand({ eval: () => left(ev) }, { eval: () => right(ev) })
124
+ }
125
+ const fn = op.eval
126
+ if (!fn) {
127
+ return (ev) => {
128
+ left(ev)
129
+ right(ev)
130
+ return undefined
131
+ }
132
+ }
133
+ return (ev) => fn(left(ev), right(ev))
134
+ }
135
+
136
+ case 'UnaryExpression': {
137
+ const right = compileAst(node.right!, grammar)
138
+ const elem = grammar.elements[node.operator]
139
+ const fn =
140
+ elem?.type === 'unaryOp'
141
+ ? elem.eval
142
+ : elem?.type === 'binaryOp'
143
+ ? elem.unaryEval
144
+ : undefined
145
+ if (!fn) {
146
+ // the operand is still evaluated for an unknown operator, as before
147
+ return (ev) => {
148
+ right(ev)
149
+ return undefined
150
+ }
151
+ }
152
+ return (ev) => fn(right(ev))
153
+ }
154
+
155
+ case 'ConditionalExpression': {
156
+ const test = compileAst(node.test, grammar)
157
+ const consequent = node.consequent
158
+ ? compileAst(node.consequent, grammar)
159
+ : undefined
160
+ const alternate = node.alternate
161
+ ? compileAst(node.alternate, grammar)
162
+ : undefined
163
+ return (ev) => {
164
+ const res = test(ev)
165
+ if (res) {
166
+ // an omitted consequent ("a ?: b") yields the test result
167
+ return consequent ? consequent(ev) : res
168
+ }
169
+ return alternate ? alternate(ev) : undefined
170
+ }
171
+ }
172
+
173
+ case 'FilterExpression': {
174
+ if (node.relative) {
175
+ // thrown on evaluation rather than compilation, as before
176
+ return () => {
177
+ throw new Error('Relative filter expressions are not supported')
178
+ }
179
+ }
180
+ const subject = compileAst(node.subject, grammar)
181
+ const index = compileAst(node.expr, grammar)
182
+ return (ev) => {
183
+ const subjectVal = subject(ev)
184
+ const indexVal = index(ev)
185
+ if (subjectVal == null) {
186
+ return undefined
187
+ }
188
+ return typeof indexVal === 'string' || typeof indexVal === 'number'
189
+ ? (subjectVal as Record<string | number, JexlValue>)[indexVal]
190
+ : undefined
191
+ }
192
+ }
193
+
194
+ case 'ArrayLiteral': {
195
+ const items = node.value.map((item) => compileAst(item, grammar))
196
+ const len = items.length
197
+ return (ev) => {
198
+ const out: JexlValue[] = new Array(len)
199
+ for (let i = 0; i < len; i++) {
200
+ out[i] = items[i]!(ev)
201
+ }
202
+ return out
203
+ }
204
+ }
205
+
206
+ case 'ObjectLiteral': {
207
+ const entries = Object.entries(node.value)
208
+ const keys = entries.map(([key]) => key)
209
+ const values = entries.map(([, value]) => compileAst(value, grammar))
210
+ const len = keys.length
211
+ // resolved once, here, so the common case keeps its plain store
212
+ const store = keys.includes('__proto__') ? defineOwn : assignOwn
213
+ return (ev) => {
214
+ const out: Record<string, JexlValue> = {}
215
+ for (let i = 0; i < len; i++) {
216
+ store(out, keys[i]!, values[i]!(ev))
217
+ }
218
+ return out
219
+ }
220
+ }
221
+
222
+ case 'TemplateLiteral': {
223
+ // every part renders to a string, so these are narrower than CompiledNode
224
+ const parts = node.parts.map((part): ((ev: Evaluator) => string) => {
225
+ if (part.type === 'static') {
226
+ const { value } = part
227
+ return () => value
228
+ }
229
+ const expr = compileAst(part.value, grammar)
230
+ return (ev) => stringify(expr(ev))
231
+ })
232
+ return (ev) => {
233
+ let out = ''
234
+ for (const part of parts) {
235
+ out += part(ev)
236
+ }
237
+ return out
238
+ }
239
+ }
240
+
241
+ case 'FunctionCall': {
242
+ const { name } = node
243
+ const args = node.args.map((arg) => compileAst(arg, grammar))
244
+ // hasOwn, so that inherited Object.prototype members such as `toString`
245
+ // and `constructor` aren't callable as Jexl functions. Resolved per call
246
+ // rather than baked in, since functions are routinely registered after
247
+ // an expression has been compiled.
248
+ const lookup = (ev: Evaluator) => {
249
+ const { functions } = ev._grammar
250
+ if (!Object.hasOwn(functions, name)) {
251
+ throw new Error(`Jexl Function ${name} is not defined.`)
252
+ }
253
+ return functions[name]!
254
+ }
255
+ // the small arities are spelled out so an ordinary call allocates no
256
+ // argument array and needs no spread
257
+ switch (args.length) {
258
+ case 0: {
259
+ return (ev) => lookup(ev)()
260
+ }
261
+ case 1: {
262
+ const [a0] = args as [CompiledNode]
263
+ return (ev) => lookup(ev)(a0(ev))
264
+ }
265
+ case 2: {
266
+ const [a0, a1] = args as [CompiledNode, CompiledNode]
267
+ return (ev) => lookup(ev)(a0(ev), a1(ev))
268
+ }
269
+ case 3: {
270
+ const [a0, a1, a2] = args as [
271
+ CompiledNode,
272
+ CompiledNode,
273
+ CompiledNode
274
+ ]
275
+ return (ev) => lookup(ev)(a0(ev), a1(ev), a2(ev))
276
+ }
277
+ default: {
278
+ const len = args.length
279
+ return (ev) => {
280
+ const vals: JexlValue[] = new Array(len)
281
+ for (let i = 0; i < len; i++) {
282
+ vals[i] = args[i]!(ev)
283
+ }
284
+ return lookup(ev)(...vals)
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ case 'SequenceExpression': {
291
+ const exprs = node.expressions.map((expr) => compileAst(expr, grammar))
292
+ const len = exprs.length
293
+ return (ev) => {
294
+ let last: JexlValue
295
+ for (let i = 0; i < len; i++) {
296
+ last = exprs[i]!(ev)
297
+ }
298
+ return last
299
+ }
300
+ }
301
+
302
+ case 'AssignmentExpression': {
303
+ const name = node.left.value
304
+ const right = compileAst(node.right!, grammar)
305
+ const store = name === '__proto__' ? defineOwn : assignOwn
306
+ return (ev) => {
307
+ const value = right(ev)
308
+ store(ev._context, name, value)
309
+ return value
310
+ }
311
+ }
312
+
313
+ default: {
314
+ throw new Error(`Corrupt AST: unknown node type '${ast.type}'`)
315
+ }
316
+ }
317
+ }
package/src/grammar.ts CHANGED
@@ -5,28 +5,70 @@
5
5
 
6
6
  /* eslint eqeqeq:0 */
7
7
 
8
+ import type { JexlValue } from './types.ts'
9
+
10
+ export type BinaryOpEval = (left: JexlValue, right: JexlValue) => JexlValue
11
+
12
+ export type BinaryOpEvalOnDemand = (
13
+ left: { eval: () => JexlValue },
14
+ right: { eval: () => JexlValue }
15
+ ) => JexlValue
16
+
8
17
  export interface BinaryOp {
9
18
  type: 'binaryOp'
10
19
  precedence: number
11
- eval?: (left: any, right: any) => any
12
- evalOnDemand?: (left: { eval: () => any }, right: { eval: () => any }) => any
20
+ eval?: BinaryOpEval
21
+ evalOnDemand?: BinaryOpEvalOnDemand
22
+ /**
23
+ * Evaluator for the operator's prefix form, if it has one. The Lexer emits a
24
+ * unaryOp token when such an operator appears where an operand is expected,
25
+ * which is how `-x` is distinguished from `a - x`.
26
+ */
27
+ unaryEval?: (right: JexlValue) => JexlValue
13
28
  }
14
29
 
15
30
  export interface UnaryOp {
16
31
  type: 'unaryOp'
17
32
  precedence: number
18
- eval: (right: any) => any
33
+ eval: (right: JexlValue) => JexlValue
19
34
  }
20
35
 
36
+ /**
37
+ * The punctuation elements of the grammar. Unlike operators these carry no
38
+ * behavior of their own; the Parser's state machine gives them meaning. The
39
+ * type is a literal union rather than `string` so that `GrammarElement` is a
40
+ * discriminated union, letting operator properties be accessed without casts.
41
+ */
21
42
  export interface SimpleElement {
22
- type: string
43
+ type:
44
+ | 'dot'
45
+ | 'openBracket'
46
+ | 'closeBracket'
47
+ | 'openCurl'
48
+ | 'closeCurl'
49
+ | 'colon'
50
+ | 'comma'
51
+ | 'openParen'
52
+ | 'closeParen'
53
+ | 'question'
54
+ | 'semicolon'
23
55
  }
24
56
 
25
57
  export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
26
58
 
27
59
  export interface Grammar {
28
60
  elements: Record<string, GrammarElement>
29
- functions: Record<string, (...args: any[]) => any>
61
+ functions: Record<string, (...args: JexlValue[]) => JexlValue>
62
+ }
63
+
64
+ /**
65
+ * Returns the binding power of a grammar element, or 0 for elements that
66
+ * aren't operators and therefore don't participate in precedence.
67
+ */
68
+ export function precedenceOf(elem: GrammarElement | undefined) {
69
+ return elem && (elem.type === 'binaryOp' || elem.type === 'unaryOp')
70
+ ? elem.precedence
71
+ : 0
30
72
  }
31
73
 
32
74
  export const getGrammar = (): Grammar => ({
@@ -50,37 +92,38 @@ export const getGrammar = (): Grammar => ({
50
92
  '+': {
51
93
  type: 'binaryOp',
52
94
  precedence: 30,
53
- eval: (left, right) => left + right
95
+ eval: (left, right) => (left as number) + (right as number)
54
96
  },
55
97
  '-': {
56
98
  type: 'binaryOp',
57
99
  precedence: 30,
58
- eval: (left, right) => left - right
100
+ eval: (left, right) => (left as number) - (right as number),
101
+ unaryEval: (right) => -(right as number)
59
102
  },
60
103
  '*': {
61
104
  type: 'binaryOp',
62
105
  precedence: 40,
63
- eval: (left, right) => left * right
106
+ eval: (left, right) => (left as number) * (right as number)
64
107
  },
65
108
  '/': {
66
109
  type: 'binaryOp',
67
110
  precedence: 40,
68
- eval: (left, right) => left / right
111
+ eval: (left, right) => (left as number) / (right as number)
69
112
  },
70
113
  '//': {
71
114
  type: 'binaryOp',
72
115
  precedence: 40,
73
- eval: (left, right) => Math.floor(left / right)
116
+ eval: (left, right) => Math.floor((left as number) / (right as number))
74
117
  },
75
118
  '%': {
76
119
  type: 'binaryOp',
77
120
  precedence: 50,
78
- eval: (left, right) => left % right
121
+ eval: (left, right) => (left as number) % (right as number)
79
122
  },
80
123
  '^': {
81
124
  type: 'binaryOp',
82
125
  precedence: 50,
83
- eval: (left, right) => Math.pow(left, right)
126
+ eval: (left, right) => (left as number) ** (right as number)
84
127
  },
85
128
  '==': {
86
129
  type: 'binaryOp',
@@ -95,22 +138,22 @@ export const getGrammar = (): Grammar => ({
95
138
  '>': {
96
139
  type: 'binaryOp',
97
140
  precedence: 20,
98
- eval: (left, right) => left > right
141
+ eval: (left, right) => (left as number) > (right as number)
99
142
  },
100
143
  '>=': {
101
144
  type: 'binaryOp',
102
145
  precedence: 20,
103
- eval: (left, right) => left >= right
146
+ eval: (left, right) => (left as number) >= (right as number)
104
147
  },
105
148
  '<': {
106
149
  type: 'binaryOp',
107
150
  precedence: 20,
108
- eval: (left, right) => left < right
151
+ eval: (left, right) => (left as number) < (right as number)
109
152
  },
110
153
  '<=': {
111
154
  type: 'binaryOp',
112
155
  precedence: 20,
113
- eval: (left, right) => left <= right
156
+ eval: (left, right) => (left as number) <= (right as number)
114
157
  },
115
158
  '&&': {
116
159
  type: 'binaryOp',
@@ -139,12 +182,17 @@ export const getGrammar = (): Grammar => ({
139
182
  precedence: 20,
140
183
  eval: (left, right) => {
141
184
  if (typeof right === 'string') {
142
- return right.includes(left)
143
- }
144
- if (Array.isArray(right)) {
145
- return right.includes(left)
185
+ // only a primitive has a meaningful substring form. An absent or
186
+ // structured left operand is not "in" a string, and must not be
187
+ // coerced to '' — every string contains the empty string, so that
188
+ // made `feature.missingAttr in "someString"` true for every feature.
189
+ return typeof left === 'string' ||
190
+ typeof left === 'number' ||
191
+ typeof left === 'boolean'
192
+ ? right.includes(String(left))
193
+ : false
146
194
  }
147
- return false
195
+ return Array.isArray(right) ? right.includes(left) : false
148
196
  }
149
197
  },
150
198
  '!': {
@@ -7,10 +7,26 @@ import * as handlers from './handlers.ts'
7
7
  import { states } from './states.ts'
8
8
 
9
9
  import type Lexer from '../Lexer.ts'
10
- import type { AstNode, Token } from '../types.ts'
10
+ import type { Grammar } from '../grammar.ts'
11
+ import type { AstNode, SequenceExpression, Token } from '../types.ts'
11
12
 
12
- interface Grammar {
13
- elements: Record<string, any>
13
+ /**
14
+ * The handler to run for a token type when the state it appears in doesn't
15
+ * name one of its own. Token types absent here — the brackets, parens and
16
+ * separators — carry no behavior beyond the state transition the state machine
17
+ * gives them.
18
+ */
19
+ const tokenHandlers: Record<
20
+ string,
21
+ ((this: Parser, token: Token) => void) | undefined
22
+ > = {
23
+ binaryOp: handlers.binaryOp,
24
+ dot: handlers.dot,
25
+ identifier: handlers.identifier,
26
+ literal: handlers.literal,
27
+ semicolon: handlers.semicolon,
28
+ templateString: handlers.templateString,
29
+ unaryOp: handlers.unaryOp
14
30
  }
15
31
 
16
32
  /**
@@ -37,7 +53,7 @@ class Parser {
37
53
  _tree: AstNode | null
38
54
  _exprStr: string
39
55
  _relative: boolean
40
- _stopMap: Record<string, any>
56
+ _stopMap: Record<string, string>
41
57
  _cursor?: AstNode | null
42
58
  _subParser?: Parser
43
59
  _parentStop?: boolean
@@ -50,7 +66,7 @@ class Parser {
50
66
  grammar: Grammar,
51
67
  lexer: Lexer,
52
68
  prefix?: string,
53
- stopMap?: Record<string, any>
69
+ stopMap?: Record<string, string>
54
70
  ) {
55
71
  this._grammar = grammar
56
72
  this._lexer = lexer
@@ -71,18 +87,16 @@ class Parser {
71
87
  * @returns {boolean|*} the stopState value if this parser encountered a token
72
88
  * in the stopState mapb false if tokens can continue.
73
89
  */
74
- addToken(token: Token): any {
90
+ addToken(token: Token): string | false {
75
91
  if (this._state === 'complete') {
76
92
  throw new Error('Cannot add a new token to a completed Parser')
77
93
  }
78
- const state = states[this._state]
94
+ const state = states[this._state]!
79
95
  const startExpr = this._exprStr
80
96
  this._exprStr += token.raw
81
97
  if (state.subHandler) {
82
- if (!this._subParser) {
83
- this._startSubExpression(startExpr)
84
- }
85
- const stopState = this._subParser!.addToken(token)
98
+ const subParser = this._subParser ?? this._startSubExpression(startExpr)
99
+ const stopState = subParser.addToken(token)
86
100
  if (stopState) {
87
101
  this._endSubExpression()
88
102
  if (stopState === '_semicolon') {
@@ -94,21 +108,18 @@ class Parser {
94
108
  }
95
109
  }
96
110
  } else if (token.type === 'semicolon' && this._stopMap[token.type]) {
97
- return this._stopMap[token.type]
111
+ return this._stopMap[token.type]!
98
112
  } else if (state.tokenTypes?.[token.type]) {
99
- const typeOpts = state.tokenTypes[token.type]
100
- let handleFunc = (handlers as any)[token.type]
101
- if (typeOpts.handler) {
102
- handleFunc = typeOpts.handler
103
- }
104
- if (handleFunc) {
105
- handleFunc.call(this, token)
106
- }
113
+ const typeOpts = state.tokenTypes[token.type]!
114
+ // the state's own handler wins over the token type's default one, and a
115
+ // token type with neither only drives the transition below
116
+ const handleFunc = typeOpts.handler ?? tokenHandlers[token.type]
117
+ handleFunc?.call(this, token)
107
118
  if (typeOpts.toState) {
108
119
  this._state = typeOpts.toState
109
120
  }
110
121
  } else if (this._stopMap[token.type]) {
111
- return this._stopMap[token.type]
122
+ return this._stopMap[token.type]!
112
123
  } else {
113
124
  throw new Error(
114
125
  `Token ${token.raw} (${token.type}) unexpected in expression: ${this._exprStr}`
@@ -124,7 +135,9 @@ class Parser {
124
135
  * the {@link Lexer#tokenize} function.
125
136
  */
126
137
  addTokens(tokens: Token[]) {
127
- tokens.forEach((token) => this.addToken(token))
138
+ for (const token of tokens) {
139
+ this.addToken(token)
140
+ }
128
141
  }
129
142
 
130
143
  /**
@@ -136,7 +149,14 @@ class Parser {
136
149
  * the expression, indicating that the expression is incomplete
137
150
  */
138
151
  complete() {
139
- if (this._cursor && !states[this._state].completable) {
152
+ // `_subParser` counts as unfinished business alongside `_cursor`: a group
153
+ // that opened before anything was placed in the tree, as in "(1", leaves
154
+ // the cursor null, and testing the cursor alone let the missing ")" pass
155
+ // silently and evaluate as if it were closed.
156
+ if (
157
+ (this._cursor || this._subParser) &&
158
+ !states[this._state]!.completable
159
+ ) {
140
160
  throw new Error(`Unexpected end of expression: ${this._exprStr}`)
141
161
  }
142
162
  if (this._subParser) {
@@ -147,7 +167,7 @@ class Parser {
147
167
  if (this._tree) {
148
168
  this._sequenceExpressions.push(this._tree)
149
169
  }
150
- const sequence: any = {
170
+ const sequence: SequenceExpression = {
151
171
  type: 'SequenceExpression',
152
172
  expressions: this._sequenceExpressions
153
173
  }
@@ -173,7 +193,7 @@ class Parser {
173
193
  * @private
174
194
  */
175
195
  _endSubExpression() {
176
- states[this._state].subHandler!.call(this, this._subParser!.complete())
196
+ states[this._state]!.subHandler!.call(this, this._subParser!.complete())
177
197
  this._subParser = undefined
178
198
  }
179
199
 
@@ -188,7 +208,7 @@ class Parser {
188
208
  if (!this._cursor) {
189
209
  this._tree = node
190
210
  } else {
191
- ;(this._cursor as any).right = node
211
+ this._cursor.right = node
192
212
  this._setParent(node, this._cursor)
193
213
  }
194
214
  this._cursor = node
@@ -227,18 +247,20 @@ class Parser {
227
247
  * Prepares the Parser to accept a subexpression by (re)instantiating the
228
248
  * subParser.
229
249
  * @param {string} [exprStr] The expression string to prefix to the new Parser
250
+ * @returns {Parser} the new subParser, also stored on this instance
230
251
  * @private
231
252
  */
232
253
  _startSubExpression(exprStr?: string) {
233
- let endStates = states[this._state].endStates
254
+ let endStates = states[this._state]!.endStates
234
255
  if (!endStates) {
235
256
  this._parentStop = true
236
257
  endStates = this._stopMap
237
258
  }
238
- if (states[this._state].completable && !endStates.semicolon) {
259
+ if (states[this._state]!.completable && !endStates.semicolon) {
239
260
  endStates = { ...endStates, semicolon: '_semicolon' }
240
261
  }
241
262
  this._subParser = new Parser(this._grammar, this._lexer, exprStr, endStates)
263
+ return this._subParser
242
264
  }
243
265
  }
244
266