@jbrowse/jexl 3.2.0 → 4.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 (63) hide show
  1. package/dist/Expression.js +1 -2
  2. package/dist/Expression.js.map +1 -1
  3. package/dist/Jexl.d.ts +8 -8
  4. package/dist/Jexl.js +7 -1
  5. package/dist/Jexl.js.map +1 -1
  6. package/dist/Lexer.d.ts +10 -8
  7. package/dist/Lexer.js +46 -35
  8. package/dist/Lexer.js.map +1 -1
  9. package/dist/evaluator/compile.d.ts +6 -4
  10. package/dist/evaluator/compile.js +42 -52
  11. package/dist/evaluator/compile.js.map +1 -1
  12. package/dist/grammar.d.ts +19 -2
  13. package/dist/grammar.js +1 -1
  14. package/dist/grammar.js.map +1 -1
  15. package/dist/parser/Parser.d.ts +2 -9
  16. package/dist/parser/Parser.js +2 -12
  17. package/dist/parser/Parser.js.map +1 -1
  18. package/dist/parser/handlers.d.ts +6 -3
  19. package/dist/parser/handlers.js +45 -28
  20. package/dist/parser/handlers.js.map +1 -1
  21. package/dist/parser/states.js +3 -5
  22. package/dist/parser/states.js.map +1 -1
  23. package/dist/types.d.ts +0 -3
  24. package/esm/Expression.js +1 -2
  25. package/esm/Expression.js.map +1 -1
  26. package/esm/Jexl.d.ts +8 -8
  27. package/esm/Jexl.js +7 -1
  28. package/esm/Jexl.js.map +1 -1
  29. package/esm/Lexer.d.ts +10 -8
  30. package/esm/Lexer.js +46 -35
  31. package/esm/Lexer.js.map +1 -1
  32. package/esm/evaluator/compile.d.ts +6 -4
  33. package/esm/evaluator/compile.js +42 -52
  34. package/esm/evaluator/compile.js.map +1 -1
  35. package/esm/grammar.d.ts +19 -2
  36. package/esm/grammar.js +1 -1
  37. package/esm/grammar.js.map +1 -1
  38. package/esm/parser/Parser.d.ts +2 -9
  39. package/esm/parser/Parser.js +2 -12
  40. package/esm/parser/Parser.js.map +1 -1
  41. package/esm/parser/handlers.d.ts +6 -3
  42. package/esm/parser/handlers.js +45 -28
  43. package/esm/parser/handlers.js.map +1 -1
  44. package/esm/parser/states.js +3 -5
  45. package/esm/parser/states.js.map +1 -1
  46. package/esm/types.d.ts +0 -3
  47. package/package.json +2 -1
  48. package/src/Expression.ts +1 -2
  49. package/src/Jexl.ts +20 -11
  50. package/src/Lexer.ts +47 -36
  51. package/src/evaluator/compile.ts +53 -68
  52. package/src/grammar.ts +23 -3
  53. package/src/parser/Parser.ts +2 -13
  54. package/src/parser/handlers.ts +48 -31
  55. package/src/parser/states.ts +3 -5
  56. package/src/types.ts +0 -3
  57. package/dist/evaluator/Evaluator.d.ts +0 -37
  58. package/dist/evaluator/Evaluator.js +0 -49
  59. package/dist/evaluator/Evaluator.js.map +0 -1
  60. package/esm/evaluator/Evaluator.d.ts +0 -37
  61. package/esm/evaluator/Evaluator.js +0 -47
  62. package/esm/evaluator/Evaluator.js.map +0 -1
  63. package/src/evaluator/Evaluator.ts +0 -58
@@ -5,13 +5,16 @@
5
5
 
6
6
  import type { Grammar } from '../grammar.ts'
7
7
  import type { AstNode, AstNodeUnion, JexlValue } from '../types.ts'
8
- import type Evaluator from './Evaluator.ts'
8
+
9
+ /** The variables an expression is evaluated against. */
10
+ export type Context = Record<string, JexlValue>
9
11
 
10
12
  /**
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
+ * An AST node lowered to a closure. Calling it with a context produces the
14
+ * node's value. The grammar is captured when the closure is built, so it is not
15
+ * a parameter.
13
16
  */
14
- export type CompiledNode = (ev: Evaluator) => JexlValue
17
+ export type CompiledNode = (context: Context) => JexlValue
15
18
 
16
19
  /**
17
20
  * Writes a key that a plain assignment would mishandle. Storing to
@@ -20,11 +23,7 @@ export type CompiledNode = (ev: Evaluator) => JexlValue
20
23
  * re-pointed the target's prototype). Defining the property instead matches
21
24
  * what `JSON.parse('{"__proto__":1}')` produces: an ordinary own property.
22
25
  */
23
- function defineOwn(
24
- target: Record<string, JexlValue>,
25
- key: string,
26
- value: JexlValue
27
- ) {
26
+ function defineOwn(target: Context, key: string, value: JexlValue) {
28
27
  Object.defineProperty(target, key, {
29
28
  value,
30
29
  writable: true,
@@ -33,11 +32,7 @@ function defineOwn(
33
32
  })
34
33
  }
35
34
 
36
- function assignOwn(
37
- target: Record<string, JexlValue>,
38
- key: string,
39
- value: JexlValue
40
- ) {
35
+ function assignOwn(target: Context, key: string, value: JexlValue) {
41
36
  target[key] = value
42
37
  }
43
38
 
@@ -88,21 +83,17 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
88
83
  case 'Identifier': {
89
84
  const name = node.value
90
85
  if (!node.from) {
91
- return node.relative
92
- ? (ev) => ev._relContext[name]
93
- : (ev) => ev._context[name]
86
+ return (ctx) => ctx[name]
94
87
  }
95
88
  const from = compileAst(node.from, grammar)
96
- return (ev) => {
97
- const context = from(ev)
98
- if (context == null) {
89
+ return (ctx) => {
90
+ const subject = from(ctx)
91
+ if (subject == null) {
99
92
  return undefined
100
93
  }
101
94
  // 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]
95
+ const target = Array.isArray(subject) ? subject[0] : subject
96
+ return target == null ? undefined : (target as Context)[name]
106
97
  }
107
98
  }
108
99
 
@@ -119,18 +110,18 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
119
110
  if (evalOnDemand) {
120
111
  // operands stay unevaluated behind an `eval` thunk, so operators such
121
112
  // as && and || can short-circuit
122
- return (ev) =>
123
- evalOnDemand({ eval: () => left(ev) }, { eval: () => right(ev) })
113
+ return (ctx) =>
114
+ evalOnDemand({ eval: () => left(ctx) }, { eval: () => right(ctx) })
124
115
  }
125
116
  const fn = op.eval
126
117
  if (!fn) {
127
- return (ev) => {
128
- left(ev)
129
- right(ev)
118
+ return (ctx) => {
119
+ left(ctx)
120
+ right(ctx)
130
121
  return undefined
131
122
  }
132
123
  }
133
- return (ev) => fn(left(ev), right(ev))
124
+ return (ctx) => fn(left(ctx), right(ctx))
134
125
  }
135
126
 
136
127
  case 'UnaryExpression': {
@@ -144,12 +135,12 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
144
135
  : undefined
145
136
  if (!fn) {
146
137
  // the operand is still evaluated for an unknown operator, as before
147
- return (ev) => {
148
- right(ev)
138
+ return (ctx) => {
139
+ right(ctx)
149
140
  return undefined
150
141
  }
151
142
  }
152
- return (ev) => fn(right(ev))
143
+ return (ctx) => fn(right(ctx))
153
144
  }
154
145
 
155
146
  case 'ConditionalExpression': {
@@ -160,28 +151,22 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
160
151
  const alternate = node.alternate
161
152
  ? compileAst(node.alternate, grammar)
162
153
  : undefined
163
- return (ev) => {
164
- const res = test(ev)
154
+ return (ctx) => {
155
+ const res = test(ctx)
165
156
  if (res) {
166
157
  // an omitted consequent ("a ?: b") yields the test result
167
- return consequent ? consequent(ev) : res
158
+ return consequent ? consequent(ctx) : res
168
159
  }
169
- return alternate ? alternate(ev) : undefined
160
+ return alternate ? alternate(ctx) : undefined
170
161
  }
171
162
  }
172
163
 
173
164
  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
165
  const subject = compileAst(node.subject, grammar)
181
166
  const index = compileAst(node.expr, grammar)
182
- return (ev) => {
183
- const subjectVal = subject(ev)
184
- const indexVal = index(ev)
167
+ return (ctx) => {
168
+ const subjectVal = subject(ctx)
169
+ const indexVal = index(ctx)
185
170
  if (subjectVal == null) {
186
171
  return undefined
187
172
  }
@@ -194,10 +179,10 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
194
179
  case 'ArrayLiteral': {
195
180
  const items = node.value.map((item) => compileAst(item, grammar))
196
181
  const len = items.length
197
- return (ev) => {
182
+ return (ctx) => {
198
183
  const out: JexlValue[] = new Array(len)
199
184
  for (let i = 0; i < len; i++) {
200
- out[i] = items[i]!(ev)
185
+ out[i] = items[i]!(ctx)
201
186
  }
202
187
  return out
203
188
  }
@@ -210,10 +195,10 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
210
195
  const len = keys.length
211
196
  // resolved once, here, so the common case keeps its plain store
212
197
  const store = keys.includes('__proto__') ? defineOwn : assignOwn
213
- return (ev) => {
198
+ return (ctx) => {
214
199
  const out: Record<string, JexlValue> = {}
215
200
  for (let i = 0; i < len; i++) {
216
- store(out, keys[i]!, values[i]!(ev))
201
+ store(out, keys[i]!, values[i]!(ctx))
217
202
  }
218
203
  return out
219
204
  }
@@ -221,18 +206,18 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
221
206
 
222
207
  case 'TemplateLiteral': {
223
208
  // every part renders to a string, so these are narrower than CompiledNode
224
- const parts = node.parts.map((part): ((ev: Evaluator) => string) => {
209
+ const parts = node.parts.map((part): ((ctx: Context) => string) => {
225
210
  if (part.type === 'static') {
226
211
  const { value } = part
227
212
  return () => value
228
213
  }
229
214
  const expr = compileAst(part.value, grammar)
230
- return (ev) => stringify(expr(ev))
215
+ return (ctx) => stringify(expr(ctx))
231
216
  })
232
- return (ev) => {
217
+ return (ctx) => {
233
218
  let out = ''
234
219
  for (const part of parts) {
235
- out += part(ev)
220
+ out += part(ctx)
236
221
  }
237
222
  return out
238
223
  }
@@ -245,8 +230,8 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
245
230
  // and `constructor` aren't callable as Jexl functions. Resolved per call
246
231
  // rather than baked in, since functions are routinely registered after
247
232
  // an expression has been compiled.
248
- const lookup = (ev: Evaluator) => {
249
- const { functions } = ev._grammar
233
+ const lookup = () => {
234
+ const { functions } = grammar
250
235
  if (!Object.hasOwn(functions, name)) {
251
236
  throw new Error(`Jexl Function ${name} is not defined.`)
252
237
  }
@@ -256,15 +241,15 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
256
241
  // argument array and needs no spread
257
242
  switch (args.length) {
258
243
  case 0: {
259
- return (ev) => lookup(ev)()
244
+ return () => lookup()()
260
245
  }
261
246
  case 1: {
262
247
  const [a0] = args as [CompiledNode]
263
- return (ev) => lookup(ev)(a0(ev))
248
+ return (ctx) => lookup()(a0(ctx))
264
249
  }
265
250
  case 2: {
266
251
  const [a0, a1] = args as [CompiledNode, CompiledNode]
267
- return (ev) => lookup(ev)(a0(ev), a1(ev))
252
+ return (ctx) => lookup()(a0(ctx), a1(ctx))
268
253
  }
269
254
  case 3: {
270
255
  const [a0, a1, a2] = args as [
@@ -272,16 +257,16 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
272
257
  CompiledNode,
273
258
  CompiledNode
274
259
  ]
275
- return (ev) => lookup(ev)(a0(ev), a1(ev), a2(ev))
260
+ return (ctx) => lookup()(a0(ctx), a1(ctx), a2(ctx))
276
261
  }
277
262
  default: {
278
263
  const len = args.length
279
- return (ev) => {
264
+ return (ctx) => {
280
265
  const vals: JexlValue[] = new Array(len)
281
266
  for (let i = 0; i < len; i++) {
282
- vals[i] = args[i]!(ev)
267
+ vals[i] = args[i]!(ctx)
283
268
  }
284
- return lookup(ev)(...vals)
269
+ return lookup()(...vals)
285
270
  }
286
271
  }
287
272
  }
@@ -290,10 +275,10 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
290
275
  case 'SequenceExpression': {
291
276
  const exprs = node.expressions.map((expr) => compileAst(expr, grammar))
292
277
  const len = exprs.length
293
- return (ev) => {
278
+ return (ctx) => {
294
279
  let last: JexlValue
295
280
  for (let i = 0; i < len; i++) {
296
- last = exprs[i]!(ev)
281
+ last = exprs[i]!(ctx)
297
282
  }
298
283
  return last
299
284
  }
@@ -303,9 +288,9 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
303
288
  const name = node.left.value
304
289
  const right = compileAst(node.right!, grammar)
305
290
  const store = name === '__proto__' ? defineOwn : assignOwn
306
- return (ev) => {
307
- const value = right(ev)
308
- store(ev._context, name, value)
291
+ return (ctx) => {
292
+ const value = right(ctx)
293
+ store(ctx, name, value)
309
294
  return value
310
295
  }
311
296
  }
package/src/grammar.ts CHANGED
@@ -9,6 +9,21 @@ import type { JexlValue } from './types.ts'
9
9
 
10
10
  export type BinaryOpEval = (left: JexlValue, right: JexlValue) => JexlValue
11
11
 
12
+ /**
13
+ * A function or operator implementation as its author writes it, for the
14
+ * registration methods on {@link Jexl} to accept.
15
+ *
16
+ * Jexl cannot check what a registered callback will be handed. The arguments
17
+ * are whatever the expression evaluated to, drawn from a context supplied at
18
+ * evaluation time, and that context routinely holds values jexl has no literal
19
+ * for — a host object with methods, a callback, a class instance. Requiring
20
+ * {@link JexlValue} parameters therefore rejected every callback actually
21
+ * written against such a value, since parameters are contravariant. The
22
+ * argument types are the caller's business; jexl only promises to pass along
23
+ * whatever the operands evaluated to.
24
+ */
25
+ export type UncheckedFn = (...args: never[]) => unknown
26
+
12
27
  export type BinaryOpEvalOnDemand = (
13
28
  left: { eval: () => JexlValue },
14
29
  right: { eval: () => JexlValue }
@@ -27,10 +42,12 @@ export interface BinaryOp {
27
42
  unaryEval?: (right: JexlValue) => JexlValue
28
43
  }
29
44
 
45
+ export type UnaryOpEval = (right: JexlValue) => JexlValue
46
+
30
47
  export interface UnaryOp {
31
48
  type: 'unaryOp'
32
49
  precedence: number
33
- eval: (right: JexlValue) => JexlValue
50
+ eval: UnaryOpEval
34
51
  }
35
52
 
36
53
  /**
@@ -56,9 +73,12 @@ export interface SimpleElement {
56
73
 
57
74
  export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
58
75
 
76
+ /** A registered function, as jexl calls it once the operands are evaluated. */
77
+ export type GrammarFn = (...args: JexlValue[]) => JexlValue
78
+
59
79
  export interface Grammar {
60
80
  elements: Record<string, GrammarElement>
61
- functions: Record<string, (...args: JexlValue[]) => JexlValue>
81
+ functions: Record<string, GrammarFn>
62
82
  }
63
83
 
64
84
  /**
@@ -211,7 +231,7 @@ export const getGrammar = (): Grammar => ({
211
231
 
212
232
  /**
213
233
  * A map of function names to javascript functions. A Jexl function
214
- * takes zero ore more arguemnts:
234
+ * takes zero or more arguments:
215
235
  *
216
236
  * - {*} ...args: A variable number of arguments passed to this function.
217
237
  * All of these are pre-evaluated to their actual values before calling
@@ -32,7 +32,7 @@ const tokenHandlers: Record<
32
32
  /**
33
33
  * The Parser is a state machine that converts tokens from the {@link Lexer}
34
34
  * into an Abstract Syntax Tree (AST), capable of being evaluated in any
35
- * context by the {@link Evaluator}. The Parser expects that all tokens
35
+ * context by {@link compileAst}. The Parser expects that all tokens
36
36
  * provided to it are legal and typed properly according to the grammar, but
37
37
  * accepts that the tokens may still be in an invalid order or in some other
38
38
  * unparsable configuration that requires it to throw an Error.
@@ -52,13 +52,11 @@ class Parser {
52
52
  _state: string
53
53
  _tree: AstNode | null
54
54
  _exprStr: string
55
- _relative: boolean
56
55
  _stopMap: Record<string, string>
57
56
  _cursor?: AstNode | null
58
57
  _subParser?: Parser
59
58
  _parentStop?: boolean
60
59
  _nextIdentEncapsulate?: boolean
61
- _nextIdentRelative?: boolean
62
60
  _curObjKey?: string
63
61
  _sequenceExpressions?: AstNode[]
64
62
 
@@ -73,7 +71,6 @@ class Parser {
73
71
  this._state = 'expectOperand'
74
72
  this._tree = null
75
73
  this._exprStr = prefix || ''
76
- this._relative = false
77
74
  this._stopMap = stopMap || {}
78
75
  }
79
76
 
@@ -143,7 +140,7 @@ class Parser {
143
140
  /**
144
141
  * Marks this Parser instance as completed and retrieves the full AST.
145
142
  * @returns {{}|null} a full expression tree, ready for evaluation by the
146
- * {@link Evaluator#eval} function, or null if no tokens were passed to
143
+ * {@link compileAst} function, or null if no tokens were passed to
147
144
  * the parser before complete was called
148
145
  * @throws {Error} if the parser is not in a state where it's legal to end
149
146
  * the expression, indicating that the expression is incomplete
@@ -179,14 +176,6 @@ class Parser {
179
176
  return this._cursor ? this._tree : null
180
177
  }
181
178
 
182
- /**
183
- * Indicates whether the expression tree contains a relative path identifier.
184
- * @returns {boolean} true if a relative identifier exists false otherwise.
185
- */
186
- isRelative() {
187
- return this._relative
188
- }
189
-
190
179
  /**
191
180
  * Ends a subexpression by completing the subParser and passing its result
192
181
  * to the subHandler configured in the current state.
@@ -64,7 +64,7 @@ export function arrayVal(this: Parser, ast: AstNode | null) {
64
64
  export function binaryOp(this: Parser, token: Token) {
65
65
  if (token.value === '=') {
66
66
  // `from` marks a member access such as `a.b`; only a bare name can be
67
- // assigned to, since the Evaluator writes straight into the context
67
+ // assigned to, since the compiled closure writes straight into the context
68
68
  if (
69
69
  this._cursor?.type !== 'Identifier' ||
70
70
  (this._cursor as Identifier).from
@@ -90,9 +90,15 @@ export function binaryOp(this: Parser, token: Token) {
90
90
  if (!parentExpr.operator) {
91
91
  break
92
92
  }
93
- if (
94
- precedenceOf(this._grammar.elements[parentExpr.operator]) < precedence
95
- ) {
93
+ // a prefix operator binds tighter than any binary one, so `-x ^ 2` groups
94
+ // as `(-x) ^ 2` — the same way `-2 ^ 2` already does, since the Lexer folds
95
+ // that sign into the literal. Looking `-` up in the grammar finds its
96
+ // binary precedence instead, which is lower than `^`'s and gave `-(x ^ 2)`
97
+ const parentPrecedence =
98
+ parent.type === 'UnaryExpression'
99
+ ? Infinity
100
+ : precedenceOf(this._grammar.elements[parentExpr.operator])
101
+ if (parentPrecedence < precedence) {
96
102
  break
97
103
  }
98
104
  this._cursor = parent
@@ -115,18 +121,20 @@ export function binaryOp(this: Parser, token: Token) {
115
121
  */
116
122
  export function dot(this: Parser) {
117
123
  const cursor = this._cursor
118
- // the dot continues an identifier chain unless the cursor is an operator
119
- // still waiting on its right-hand side, in which case it opens a relative
120
- // path instead — as in the leading dot of `list[.price > 5]`
121
- this._nextIdentEncapsulate =
122
- !!cursor &&
123
- cursor.type !== 'UnaryExpression' &&
124
- (cursor.type !== 'BinaryExpression' || !!cursor.right)
125
-
126
- this._nextIdentRelative = !this._nextIdentEncapsulate
127
- if (this._nextIdentRelative) {
128
- this._relative = true
124
+ // a dot continues an identifier chain. With nothing to continue — no cursor,
125
+ // or an operator still waiting on its right-hand side — it would instead open
126
+ // a relative path, the leading dot of `list[.price > 5]`. This fork removed
127
+ // array filtering expressions, which are the only construct that gives a
128
+ // relative path a root to resolve against, so it is rejected here rather than
129
+ // parsed into a tree that can only fail once it is evaluated.
130
+ if (
131
+ !cursor ||
132
+ cursor.type === 'UnaryExpression' ||
133
+ (cursor.type === 'BinaryExpression' && !cursor.right)
134
+ ) {
135
+ throw new Error(`Relative paths are not supported: ${this._exprStr}`)
129
136
  }
137
+ this._nextIdentEncapsulate = true
130
138
  }
131
139
 
132
140
  /**
@@ -138,7 +146,6 @@ export function filter(this: Parser, ast: AstNode | null) {
138
146
  const node: FilterExpression = {
139
147
  type: 'FilterExpression',
140
148
  expr: ast!,
141
- relative: this._subParser!.isRelative(),
142
149
  subject: this._cursor!
143
150
  }
144
151
  this._placeBeforeCursor(node)
@@ -162,12 +169,11 @@ export function functionCall(this: Parser) {
162
169
  // there is nothing to look up in the function pool
163
170
  throw new Error(`Functions must be called by name: ${this._exprStr}`)
164
171
  }
165
- const { from } = cursor as Identifier
172
+ const { from, value } = cursor as Identifier
166
173
  const node: FunctionCall = {
167
174
  type: 'FunctionCall',
168
- name: (cursor as Identifier).value,
169
- args: from ? [from] : [],
170
- pool: 'functions'
175
+ name: value,
176
+ args: from ? [from] : []
171
177
  }
172
178
  this._placeBeforeCursor(node)
173
179
  }
@@ -186,10 +192,6 @@ export function identifier(this: Parser, token: Token) {
186
192
  this._placeBeforeCursor(node)
187
193
  this._nextIdentEncapsulate = false
188
194
  } else {
189
- if (this._nextIdentRelative) {
190
- node.relative = true
191
- this._nextIdentRelative = false
192
- }
193
195
  this._placeAtCursor(node)
194
196
  }
195
197
  }
@@ -274,7 +276,7 @@ export function objStart(this: Parser) {
274
276
  export function objVal(this: Parser, ast: AstNode | null) {
275
277
  // defined rather than assigned so that the key "__proto__" becomes an
276
278
  // ordinary entry of the node's key map instead of re-pointing its prototype,
277
- // which dropped the entry before the Evaluator ever saw it
279
+ // which dropped the entry before it could be compiled
278
280
  Object.defineProperty(
279
281
  (this._cursor as ObjectLiteral).value,
280
282
  this._curObjKey!,
@@ -313,17 +315,32 @@ export function ternaryMid(this: Parser, ast: AstNode | null) {
313
315
  }
314
316
 
315
317
  /**
316
- * Handles the start of a new ternary expression by encapsulating the entire
317
- * AST in a ConditionalExpression node, and using the existing tree as the
318
- * test element.
318
+ * Handles the start of a new ternary expression by encapsulating the tree so
319
+ * far in a ConditionalExpression node, and using it as the test element.
320
+ *
321
+ * Every operator binds tighter than the conditional and so belongs in the test
322
+ * — except `=`, which binds looser. `x = a ? b : c` therefore assigns the
323
+ * conditional rather than testing the assignment, which had stored `a` in `x`.
319
324
  */
320
325
  export function ternaryStart(this: Parser) {
326
+ let assignment: AstNode | undefined
327
+ let test = this._tree!
328
+ while (test.type === 'AssignmentExpression' && test.right) {
329
+ assignment = test
330
+ test = test.right
331
+ }
332
+
321
333
  const node: ConditionalExpression = {
322
334
  type: 'ConditionalExpression',
323
- test: this._tree!
335
+ test
336
+ }
337
+ if (assignment) {
338
+ assignment.right = node
339
+ this._setParent(node, assignment)
340
+ } else {
341
+ this._tree = node
324
342
  }
325
- this._tree = node
326
- this._cursor = this._tree
343
+ this._cursor = node
327
344
  }
328
345
 
329
346
  /**
@@ -97,10 +97,6 @@ export const states: States = {
97
97
  colon: { toState: 'objVal' }
98
98
  }
99
99
  },
100
- postArgs: {
101
- tokenTypes: afterOperand,
102
- completable: true
103
- },
104
100
  identifier: {
105
101
  tokenTypes: {
106
102
  ...afterOperand,
@@ -131,7 +127,9 @@ export const states: States = {
131
127
  subHandler: h.argVal,
132
128
  endStates: {
133
129
  comma: 'argVal',
134
- closeParen: 'postArgs'
130
+ // a call's result is an ordinary operand: it can be operated on or
131
+ // drilled into, but not called again, since only a name can be called
132
+ closeParen: 'expectBinOp'
135
133
  }
136
134
  },
137
135
  objVal: {
package/src/types.ts CHANGED
@@ -38,7 +38,6 @@ export interface Identifier extends AstNode {
38
38
  type: 'Identifier'
39
39
  value: string
40
40
  from?: AstNode
41
- relative?: boolean
42
41
  }
43
42
 
44
43
  export interface BinaryExpression extends AstNode {
@@ -77,13 +76,11 @@ export interface FunctionCall extends AstNode {
77
76
  type: 'FunctionCall'
78
77
  name: string
79
78
  args: AstNode[]
80
- pool: 'functions'
81
79
  }
82
80
 
83
81
  export interface FilterExpression extends AstNode {
84
82
  type: 'FilterExpression'
85
83
  expr: AstNode
86
- relative: boolean
87
84
  subject: AstNode
88
85
  }
89
86
 
@@ -1,37 +0,0 @@
1
- import type { Grammar } from '../grammar.ts';
2
- import type { AstNode, JexlValue } from '../types.ts';
3
- /**
4
- * The Evaluator carries everything an expression is evaluated against: the
5
- * grammar, the context, and a relative context to be used as the root for
6
- * relative identifiers. When any of these change, a new instance is required.
7
- * However, a single instance can be used to evaluate many different
8
- * expressions, and does not have to be reinstantiated for each.
9
- *
10
- * The work of evaluation itself lives in {@link compileAst}, which lowers a
11
- * tree to closures. Prefer compiling once and reusing the result — that is what
12
- * {@link Expression} does — over calling {@link #eval}, which compiles the tree
13
- * it is given every time.
14
- *
15
- * @param {{}} grammar A grammar object against which to evaluate the expression
16
- * tree
17
- * @param {{}} [context] A map of variable keys to their values. This will be
18
- * accessed to resolve the value of each non-relative identifier.
19
- * @param {{}|Array<{}|Array>} [relativeContext] A map or array to be accessed
20
- * to resolve the value of a relative identifier.
21
- */
22
- declare class Evaluator {
23
- _grammar: Grammar;
24
- _context: Record<string, JexlValue>;
25
- _relContext: Record<string, JexlValue>;
26
- constructor(grammar: Grammar, context?: Record<string, JexlValue>, relativeContext?: Record<string, JexlValue>);
27
- /**
28
- * Compiles and evaluates an expression tree within the configured context.
29
- * Convenient for a one-shot evaluation; for repeated evaluation of the same
30
- * tree, hold onto the closure from {@link compileAst} instead so the tree is
31
- * only lowered once.
32
- * @param {{}} ast An expression tree object
33
- * @returns {*} the resulting value of the expression.
34
- */
35
- eval(ast: AstNode): JexlValue;
36
- }
37
- export default Evaluator;
@@ -1,49 +0,0 @@
1
- "use strict";
2
- /*
3
- * Jexl
4
- * Copyright 2020 Tom Shawver
5
- */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- const compile_ts_1 = require("./compile.js");
8
- /**
9
- * The Evaluator carries everything an expression is evaluated against: the
10
- * grammar, the context, and a relative context to be used as the root for
11
- * relative identifiers. When any of these change, a new instance is required.
12
- * However, a single instance can be used to evaluate many different
13
- * expressions, and does not have to be reinstantiated for each.
14
- *
15
- * The work of evaluation itself lives in {@link compileAst}, which lowers a
16
- * tree to closures. Prefer compiling once and reusing the result — that is what
17
- * {@link Expression} does — over calling {@link #eval}, which compiles the tree
18
- * it is given every time.
19
- *
20
- * @param {{}} grammar A grammar object against which to evaluate the expression
21
- * tree
22
- * @param {{}} [context] A map of variable keys to their values. This will be
23
- * accessed to resolve the value of each non-relative identifier.
24
- * @param {{}|Array<{}|Array>} [relativeContext] A map or array to be accessed
25
- * to resolve the value of a relative identifier.
26
- */
27
- class Evaluator {
28
- _grammar;
29
- _context;
30
- _relContext;
31
- constructor(grammar, context, relativeContext) {
32
- this._grammar = grammar;
33
- this._context = context || {};
34
- this._relContext = relativeContext || this._context;
35
- }
36
- /**
37
- * Compiles and evaluates an expression tree within the configured context.
38
- * Convenient for a one-shot evaluation; for repeated evaluation of the same
39
- * tree, hold onto the closure from {@link compileAst} instead so the tree is
40
- * only lowered once.
41
- * @param {{}} ast An expression tree object
42
- * @returns {*} the resulting value of the expression.
43
- */
44
- eval(ast) {
45
- return (0, compile_ts_1.compileAst)(ast, this._grammar)(this);
46
- }
47
- }
48
- exports.default = Evaluator;
49
- //# sourceMappingURL=Evaluator.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Evaluator.js","sourceRoot":"","sources":["../../src/evaluator/Evaluator.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAEH,6CAAyC;AAKzC;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,SAAS;IACb,QAAQ,CAAS;IACjB,QAAQ,CAA2B;IACnC,WAAW,CAA2B;IAEtC,YACE,OAAgB,EAChB,OAAmC,EACnC,eAA2C;QAE3C,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;QACvB,IAAI,CAAC,QAAQ,GAAG,OAAO,IAAI,EAAE,CAAA;QAC7B,IAAI,CAAC,WAAW,GAAG,eAAe,IAAI,IAAI,CAAC,QAAQ,CAAA;IACrD,CAAC;IAED;;;;;;;OAOG;IACH,IAAI,CAAC,GAAY;QACf,OAAO,IAAA,uBAAU,EAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAA;IAC7C,CAAC;CACF;AAED,kBAAe,SAAS,CAAA"}