@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
@@ -3,253 +3,443 @@
3
3
  * Copyright 2020 Tom Shawver
4
4
  */
5
5
 
6
- import * as handlers from './handlers.ts'
7
- import { states } from './states.ts'
6
+ import { JexlSyntaxError } from '../errors.ts'
8
7
 
9
8
  import type Lexer from '../Lexer.ts'
10
9
  import type { Grammar } from '../grammar.ts'
11
- import type { AstNode, SequenceExpression, Token } from '../types.ts'
10
+ import type {
11
+ AstNode,
12
+ AstNodeUnion,
13
+ ConditionalExpression,
14
+ Lambda,
15
+ Literal,
16
+ ObjectLiteral,
17
+ TemplateLiteral,
18
+ TemplatePart,
19
+ Token
20
+ } from '../types.ts'
12
21
 
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
30
- }
22
+ const logical = new Set(['&&', '||'])
23
+
24
+ const omittedAlternateBefore = new Set([
25
+ 'closeParen',
26
+ 'closeBracket',
27
+ 'closeCurl',
28
+ 'comma',
29
+ 'semicolon'
30
+ ])
31
+
32
+ export { JexlSyntaxError } from '../errors.ts'
31
33
 
32
34
  /**
33
- * The Parser is a state machine that converts tokens from the {@link Lexer}
34
- * into an Abstract Syntax Tree (AST), capable of being evaluated in any
35
- * context by {@link compileAst}. The Parser expects that all tokens
36
- * provided to it are legal and typed properly according to the grammar, but
37
- * accepts that the tokens may still be in an invalid order or in some other
38
- * unparsable configuration that requires it to throw an Error.
39
- * @param {{}} grammar The grammar object to use to parse Jexl strings
40
- * @param {string} [prefix] A string prefix to prepend to the expression string
41
- * for error messaging purposes. This is useful for when a new Parser is
42
- * instantiated to parse an subexpression, as the parent Parser's
43
- * expression string thus far can be passed for a more user-friendly
44
- * error message.
45
- * @param {{}} [stopMap] A mapping of token types to any truthy value. When the
46
- * token type is encountered, the parser will return the mapped value
47
- * instead of boolean false.
35
+ * Converts the tokens from the {@link Lexer} into an Abstract Syntax Tree, for
36
+ * {@link compileAst} to lower to closures. A Pratt parser: each level of the
37
+ * grammar is a method, from `_sequence` (loosest) down to `_primary`, and
38
+ * binary operators are climbed by the precedence the grammar gives them, so
39
+ * operators a host registers slot in without the parser knowing them.
48
40
  */
49
41
  class Parser {
50
42
  _grammar: Grammar
51
43
  _lexer: Lexer
52
- _state: string
53
- _tree: AstNode | null
54
- _exprStr: string
55
- _stopMap: Record<string, string>
56
- _cursor?: AstNode | null
57
- _subParser?: Parser
58
- _parentStop?: boolean
59
- _nextIdentEncapsulate?: boolean
60
- _curObjKey?: string
61
- _sequenceExpressions?: AstNode[]
44
+ _tokens: Token[] = []
45
+ _pos = 0
46
+ _offset: number
47
+ _grouped?: WeakSet<AstNode>
48
+ _lambdaDepth = 0
62
49
 
63
- constructor(
64
- grammar: Grammar,
65
- lexer: Lexer,
66
- prefix?: string,
67
- stopMap?: Record<string, string>
68
- ) {
50
+ /**
51
+ * @param offset where the tokens start in the source, so that an error
52
+ * inside a template interpolation reports its place in the whole
53
+ */
54
+ constructor(grammar: Grammar, lexer: Lexer, offset = 0) {
69
55
  this._grammar = grammar
70
56
  this._lexer = lexer
71
- this._state = 'expectOperand'
72
- this._tree = null
73
- this._exprStr = prefix || ''
74
- this._stopMap = stopMap || {}
57
+ this._offset = offset
58
+ }
59
+
60
+ parse(source: string) {
61
+ const start = this._offset
62
+ this._offset += source.length - source.trimStart().length
63
+ try {
64
+ this.addTokens(this._lexer.tokenize(source))
65
+ } catch (error) {
66
+ if (error instanceof JexlSyntaxError) {
67
+ error.offset += start
68
+ }
69
+ throw error
70
+ }
71
+ return this.complete()
72
+ }
73
+
74
+ addTokens(tokens: Token[]) {
75
+ this._tokens = this._tokens.concat(tokens)
75
76
  }
76
77
 
77
78
  /**
78
- * Processes a new token into the AST and manages the transitions of the state
79
- * machine.
80
- * @param {{type: <string>}} token A token object, as provided by the
81
- * {@link Lexer#tokenize} function.
82
- * @throws {Error} if a token is added when the Parser has been marked as
83
- * complete by {@link #complete}, or if an unexpected token type is added.
84
- * @returns {boolean|*} the stopState value if this parser encountered a token
85
- * in the stopState mapb false if tokens can continue.
79
+ * @returns the expression tree, or null for an expression with no tokens
80
+ * @throws {JexlSyntaxError} if the tokens do not form exactly one expression
86
81
  */
87
- addToken(token: Token): string | false {
88
- if (this._state === 'complete') {
89
- throw new Error('Cannot add a new token to a completed Parser')
82
+ complete(): AstNodeUnion | null {
83
+ if (this._tokens.length === 0) {
84
+ return null
90
85
  }
91
- const state = states[this._state]!
92
- const startExpr = this._exprStr
93
- this._exprStr += token.raw
94
- if (state.subHandler) {
95
- const subParser = this._subParser ?? this._startSubExpression(startExpr)
96
- const stopState = subParser.addToken(token)
97
- if (stopState) {
98
- this._endSubExpression()
99
- if (stopState === '_semicolon') {
100
- handlers.semicolon.call(this)
101
- } else if (this._parentStop) {
102
- return stopState
103
- } else {
104
- this._state = stopState
105
- }
86
+ const ast = this._sequence()
87
+ if (this._pos < this._tokens.length) {
88
+ throw this._unexpected(this._pos)
89
+ }
90
+ return ast
91
+ }
92
+
93
+ _sequence(): AstNodeUnion {
94
+ const first = this._assignment()
95
+ if (this._peek()?.type !== 'semicolon') {
96
+ return first
97
+ }
98
+ const expressions: AstNode[] = [first]
99
+ while (this._eat('semicolon')) {
100
+ const next = this._peek()
101
+ if (!next || next.type === 'closeParen') {
102
+ break
103
+ }
104
+ expressions.push(this._assignment())
105
+ }
106
+ return { type: 'SequenceExpression', expressions }
107
+ }
108
+
109
+ _assignment(): AstNodeUnion {
110
+ if (this._atLambda()) {
111
+ return this._lambda()
112
+ }
113
+ const left = this._ternary()
114
+ const token = this._peek()
115
+ if (token?.type !== 'binaryOp' || token.value !== '=') {
116
+ return left
117
+ }
118
+ if (left.type !== 'Identifier' || left.from) {
119
+ throw this._error(
120
+ 'Left side of assignment must be a variable name',
121
+ this._pos
122
+ )
123
+ }
124
+ if (this._lambdaDepth > 0) {
125
+ throw this._error('Assignment is not supported in a lambda', this._pos)
126
+ }
127
+ this._pos++
128
+ return {
129
+ type: 'AssignmentExpression',
130
+ operator: '=',
131
+ left,
132
+ right: this._assignment()
133
+ }
134
+ }
135
+
136
+ _atLambda() {
137
+ let i = this._pos
138
+ if (this._tokens[i]?.type === 'identifier') {
139
+ return this._tokens[i + 1]?.type === 'arrow'
140
+ }
141
+ if (this._tokens[i]?.type !== 'openParen') {
142
+ return false
143
+ }
144
+ i++
145
+ while (this._tokens[i]?.type === 'identifier') {
146
+ i++
147
+ if (this._tokens[i]?.type !== 'comma') {
148
+ break
106
149
  }
107
- } else if (token.type === 'semicolon' && this._stopMap[token.type]) {
108
- return this._stopMap[token.type]!
109
- } else if (state.tokenTypes?.[token.type]) {
110
- const typeOpts = state.tokenTypes[token.type]!
111
- // the state's own handler wins over the token type's default one, and a
112
- // token type with neither only drives the transition below
113
- const handleFunc = typeOpts.handler ?? tokenHandlers[token.type]
114
- handleFunc?.call(this, token)
115
- if (typeOpts.toState) {
116
- this._state = typeOpts.toState
150
+ i++
151
+ }
152
+ return (
153
+ this._tokens[i]?.type === 'closeParen' &&
154
+ this._tokens[i + 1]?.type === 'arrow'
155
+ )
156
+ }
157
+
158
+ /** Parses the lambda {@link _atLambda} found, whose shape it has checked. */
159
+ _lambda(): Lambda {
160
+ const params: string[] = []
161
+ if (this._eat('openParen')) {
162
+ while (!this._eat('closeParen')) {
163
+ params.push(this._next().value as string)
164
+ this._eat('comma')
117
165
  }
118
- } else if (this._stopMap[token.type]) {
119
- return this._stopMap[token.type]!
120
166
  } else {
121
- throw new Error(
122
- `Token ${token.raw} (${token.type}) unexpected in expression: ${this._exprStr}`
123
- )
167
+ params.push(this._next().value as string)
168
+ }
169
+ if (new Set(params).size < params.length) {
170
+ throw this._error('Duplicate lambda parameter name', this._pos)
124
171
  }
125
- return false
172
+ this._pos++
173
+ this._lambdaDepth++
174
+ const body = this._assignment()
175
+ this._lambdaDepth--
176
+ return { type: 'Lambda', params, body }
126
177
  }
127
178
 
128
- /**
129
- * Processes an array of tokens iteratively through the {@link #addToken}
130
- * function.
131
- * @param {Array<{type: <string>}>} tokens An array of tokens, as provided by
132
- * the {@link Lexer#tokenize} function.
133
- */
134
- addTokens(tokens: Token[]) {
135
- for (const token of tokens) {
136
- this.addToken(token)
179
+ _ternary(): AstNodeUnion {
180
+ const test = this._binary(-Infinity)
181
+ if (!this._eat('question')) {
182
+ return test
137
183
  }
184
+ const node: ConditionalExpression = { type: 'ConditionalExpression', test }
185
+ if (!this._eat('colon')) {
186
+ node.consequent = this._assignment()
187
+ this._expect('colon')
188
+ }
189
+ const next = this._peek()
190
+ if (next && !omittedAlternateBefore.has(next.type)) {
191
+ node.alternate = this._assignment()
192
+ }
193
+ return node
138
194
  }
139
195
 
140
196
  /**
141
- * Marks this Parser instance as completed and retrieves the full AST.
142
- * @returns {{}|null} a full expression tree, ready for evaluation by the
143
- * {@link compileAst} function, or null if no tokens were passed to
144
- * the parser before complete was called
145
- * @throws {Error} if the parser is not in a state where it's legal to end
146
- * the expression, indicating that the expression is incomplete
197
+ * Parses operands joined by binary operators that bind tighter than `floor`.
198
+ * One exactly at `floor` binds only when it continues a right-associative
199
+ * chain, which is how `a ^ b ^ c` groups from the right.
147
200
  */
148
- complete() {
149
- // `_subParser` counts as unfinished business alongside `_cursor`: a group
150
- // that opened before anything was placed in the tree, as in "(1", leaves
151
- // the cursor null, and testing the cursor alone let the missing ")" pass
152
- // silently and evaluate as if it were closed.
153
- if (
154
- (this._cursor || this._subParser) &&
155
- !states[this._state]!.completable
156
- ) {
157
- throw new Error(`Unexpected end of expression: ${this._exprStr}`)
201
+ _binary(floor: number, rightAssociative = false): AstNodeUnion {
202
+ let left = this._unary()
203
+ for (;;) {
204
+ const token = this._peek()
205
+ if (token?.type !== 'binaryOp' || token.value === '=') {
206
+ return left
207
+ }
208
+ const operator = token.value as string
209
+ const op = this._grammar.elements[operator]!
210
+ if (
211
+ op.type !== 'binaryOp' ||
212
+ op.precedence < floor ||
213
+ (op.precedence === floor && !(rightAssociative && op.rightAssociative))
214
+ ) {
215
+ return left
216
+ }
217
+ const at = this._pos++
218
+ const right = this._binary(op.precedence, op.rightAssociative)
219
+ if (this._mixesNullish(operator, left, right)) {
220
+ throw this._error('Parenthesize ?? when mixing it with && or ||', at)
221
+ }
222
+ left = { type: 'BinaryExpression', operator, left, right }
223
+ }
224
+ }
225
+
226
+ /** `a ?? b || c` has no grouping a reader can guess; JS refuses it too. */
227
+ _mixesNullish(operator: string, ...operands: AstNodeUnion[]) {
228
+ return operands.some(
229
+ (operand) =>
230
+ operand.type === 'BinaryExpression' &&
231
+ !this._grouped?.has(operand) &&
232
+ (operator === '??'
233
+ ? logical.has(operand.operator)
234
+ : logical.has(operator) && operand.operator === '??')
235
+ )
236
+ }
237
+
238
+ _unary(): AstNodeUnion {
239
+ const token = this._peek()
240
+ if (token?.type !== 'unaryOp') {
241
+ return this._postfix(this._primary())
158
242
  }
159
- if (this._subParser) {
160
- this._endSubExpression()
243
+ this._pos++
244
+ const operator = token.value as string
245
+ const op = this._grammar.elements[operator]
246
+ return {
247
+ type: 'UnaryExpression',
248
+ operator,
249
+ right: this._binary(op?.type === 'unaryOp' ? op.precedence : Infinity)
161
250
  }
251
+ }
162
252
 
163
- if (this._sequenceExpressions) {
164
- if (this._tree) {
165
- this._sequenceExpressions.push(this._tree)
166
- }
167
- const sequence: SequenceExpression = {
168
- type: 'SequenceExpression',
169
- expressions: this._sequenceExpressions
253
+ _postfix(subject: AstNodeUnion): AstNodeUnion {
254
+ let node = subject
255
+ for (;;) {
256
+ switch (this._peek()?.type) {
257
+ case 'dot': {
258
+ this._pos++
259
+ node = {
260
+ type: 'Identifier',
261
+ value: this._expect('identifier').value as string,
262
+ from: node
263
+ }
264
+ break
265
+ }
266
+ case 'openBracket': {
267
+ this._pos++
268
+ const expr = this._assignment()
269
+ this._expect('closeBracket')
270
+ node = { type: 'FilterExpression', expr, subject: node }
271
+ break
272
+ }
273
+ case 'openParen': {
274
+ if (
275
+ node.type !== 'Identifier' ||
276
+ this._tokens[this._pos - 1]!.type !== 'identifier'
277
+ ) {
278
+ throw this._error('Functions must be called by name', this._pos)
279
+ }
280
+ this._pos++
281
+ const { from, value } = node
282
+ const args = this._list('closeParen')
283
+ node = {
284
+ type: 'FunctionCall',
285
+ name: value,
286
+ args: from ? [from, ...args] : args
287
+ }
288
+ break
289
+ }
290
+ default: {
291
+ return node
292
+ }
170
293
  }
171
- this._state = 'complete'
172
- return sequence
173
294
  }
174
-
175
- this._state = 'complete'
176
- return this._cursor ? this._tree : null
177
295
  }
178
296
 
179
- /**
180
- * Ends a subexpression by completing the subParser and passing its result
181
- * to the subHandler configured in the current state.
182
- * @private
183
- */
184
- _endSubExpression() {
185
- states[this._state]!.subHandler!.call(this, this._subParser!.complete())
186
- this._subParser = undefined
297
+ _primary(): AstNodeUnion {
298
+ const token = this._next()
299
+ switch (token.type) {
300
+ case 'literal': {
301
+ return {
302
+ type: 'Literal',
303
+ value: token.value as string | number | boolean
304
+ }
305
+ }
306
+ case 'identifier': {
307
+ return { type: 'Identifier', value: token.value as string }
308
+ }
309
+ case 'templateString': {
310
+ return this._template(token.value as TemplatePart[], this._pos - 1)
311
+ }
312
+ case 'openParen': {
313
+ const node = this._sequence()
314
+ this._expect('closeParen')
315
+ this._grouped ??= new WeakSet()
316
+ this._grouped.add(node)
317
+ return node
318
+ }
319
+ case 'openBracket': {
320
+ return { type: 'ArrayLiteral', value: this._list('closeBracket') }
321
+ }
322
+ case 'openCurl': {
323
+ return this._object()
324
+ }
325
+ case 'dot': {
326
+ throw this._error('Relative paths are not supported', this._pos - 1)
327
+ }
328
+ default: {
329
+ throw this._unexpected(this._pos - 1)
330
+ }
331
+ }
187
332
  }
188
333
 
189
- /**
190
- * Places a new tree node at the current position of the cursor (to the 'right'
191
- * property) and then advances the cursor to the new node. This function also
192
- * handles setting the parent of the new node.
193
- * @param {{type: <string>}} node A node to be added to the AST
194
- * @private
195
- */
196
- _placeAtCursor(node: AstNode) {
197
- if (!this._cursor) {
198
- this._tree = node
199
- } else {
200
- this._cursor.right = node
201
- this._setParent(node, this._cursor)
334
+ _list(close: string): AstNode[] {
335
+ const items: AstNode[] = []
336
+ while (!this._eat(close)) {
337
+ items.push(this._assignment())
338
+ if (!this._eat('comma')) {
339
+ this._expect(close)
340
+ break
341
+ }
202
342
  }
203
- this._cursor = node
343
+ return items
204
344
  }
205
345
 
206
- /**
207
- * Places a tree node before the current position of the cursor, replacing
208
- * the node that the cursor currently points to. This should only be called in
209
- * cases where the cursor is known to exist, and the provided node already
210
- * contains a pointer to what's at the cursor currently.
211
- * @param {{type: <string>}} node A node to be added to the AST
212
- * @private
213
- */
214
- _placeBeforeCursor(node: AstNode) {
215
- this._cursor = this._cursor?._parent
216
- this._placeAtCursor(node)
346
+ _object(): ObjectLiteral {
347
+ const node: ObjectLiteral = { type: 'ObjectLiteral', value: {} }
348
+ while (!this._eat('closeCurl')) {
349
+ const key = this._next()
350
+ if (key.type !== 'identifier' && key.type !== 'literal') {
351
+ throw this._unexpected(this._pos - 1)
352
+ }
353
+ this._expect('colon')
354
+ // defined rather than assigned, so that "__proto__" is an ordinary key
355
+ Object.defineProperty(node.value, String(key.value as Literal['value']), {
356
+ value: this._assignment(),
357
+ writable: true,
358
+ enumerable: true,
359
+ configurable: true
360
+ })
361
+ if (!this._eat('comma')) {
362
+ this._expect('closeCurl')
363
+ break
364
+ }
365
+ }
366
+ return node
217
367
  }
218
368
 
219
- /**
220
- * Sets the parent of a node by creating a non-enumerable _parent property
221
- * that points to the supplied parent argument.
222
- * @param {{type: <string>}} node A node of the AST on which to set a new
223
- * parent
224
- * @param {{type: <string>}} parent An existing node of the AST to serve as the
225
- * parent of the new node
226
- * @private
227
- */
228
- _setParent(node: AstNode, parent: AstNode) {
229
- Object.defineProperty(node, '_parent', {
230
- value: parent,
231
- writable: true
369
+ _template(tokenParts: TemplatePart[], index: number): TemplateLiteral {
370
+ let offset = this._offsetOf(index) + '`'.length
371
+ const parts: TemplateLiteral['parts'] = tokenParts.map((part) => {
372
+ if (part.type === 'static') {
373
+ offset += part.value.length
374
+ return {
375
+ type: 'static',
376
+ value: this._lexer._unescapeTemplateString(part.value)
377
+ }
378
+ }
379
+ const start = offset + '${'.length
380
+ offset = start + part.value.length + '}'.length
381
+ const sub = new Parser(this._grammar, this._lexer, start)
382
+ sub._lambdaDepth = this._lambdaDepth
383
+ const value = sub.parse(part.value)
384
+ if (!value) {
385
+ throw new JexlSyntaxError(
386
+ 'Empty interpolation in template string',
387
+ start
388
+ )
389
+ }
390
+ return { type: 'expression', value }
232
391
  })
392
+ return { type: 'TemplateLiteral', parts }
233
393
  }
234
394
 
235
- /**
236
- * Prepares the Parser to accept a subexpression by (re)instantiating the
237
- * subParser.
238
- * @param {string} [exprStr] The expression string to prefix to the new Parser
239
- * @returns {Parser} the new subParser, also stored on this instance
240
- * @private
241
- */
242
- _startSubExpression(exprStr?: string) {
243
- let endStates = states[this._state]!.endStates
244
- if (!endStates) {
245
- this._parentStop = true
246
- endStates = this._stopMap
395
+ _peek(): Token | undefined {
396
+ return this._tokens[this._pos]
397
+ }
398
+
399
+ _next(): Token {
400
+ const token = this._tokens[this._pos]
401
+ if (!token) {
402
+ throw this._error('Unexpected end of expression', this._pos)
247
403
  }
248
- if (states[this._state]!.completable && !endStates.semicolon) {
249
- endStates = { ...endStates, semicolon: '_semicolon' }
404
+ this._pos++
405
+ return token
406
+ }
407
+
408
+ _eat(type: string) {
409
+ if (this._tokens[this._pos]?.type !== type) {
410
+ return false
411
+ }
412
+ this._pos++
413
+ return true
414
+ }
415
+
416
+ _expect(type: string): Token {
417
+ const token = this._next()
418
+ if (token.type !== type) {
419
+ throw this._unexpected(this._pos - 1)
420
+ }
421
+ return token
422
+ }
423
+
424
+ _unexpected(index: number) {
425
+ const { raw, type } = this._tokens[index]!
426
+ return this._error(`Token ${raw} (${type}) unexpected in expression`, index)
427
+ }
428
+
429
+ _error(message: string, index: number) {
430
+ const source = this._tokens
431
+ .slice(0, index + 1)
432
+ .map((token) => token.raw)
433
+ .join('')
434
+ return new JexlSyntaxError(`${message}: ${source}`, this._offsetOf(index))
435
+ }
436
+
437
+ _offsetOf(index: number) {
438
+ let offset = this._offset
439
+ for (let i = 0; i < index; i++) {
440
+ offset += this._tokens[i]!.raw.length
250
441
  }
251
- this._subParser = new Parser(this._grammar, this._lexer, exprStr, endStates)
252
- return this._subParser
442
+ return offset
253
443
  }
254
444
  }
255
445