@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
@@ -4,7 +4,13 @@
4
4
  */
5
5
 
6
6
  import type { Grammar } from '../grammar.ts'
7
- import type { AstNode, AstNodeUnion, JexlValue } from '../types.ts'
7
+ import type {
8
+ AstNode,
9
+ AstNodeUnion,
10
+ JexlFunction,
11
+ JexlValue,
12
+ Literal
13
+ } from '../types.ts'
8
14
 
9
15
  /** The variables an expression is evaluated against. */
10
16
  export type Context = Record<string, JexlValue>
@@ -36,19 +42,236 @@ function assignOwn(target: Context, key: string, value: JexlValue) {
36
42
  target[key] = value
37
43
  }
38
44
 
39
- /** Renders an interpolated value for a template literal. */
40
- function stringify(value: JexlValue) {
41
- if (value == null) {
42
- return ''
45
+ /** The key `subject[index]` reads under, or undefined when it reads nothing. */
46
+ function memberKey(index: JexlValue) {
47
+ return typeof index === 'string' || typeof index === 'number'
48
+ ? index
49
+ : indexKey(index)
50
+ }
51
+
52
+ /**
53
+ * The key a bracket index that is neither a string nor a number reads under:
54
+ * its string form, when it has one worth using.
55
+ *
56
+ * A boolean and an array of primitives do — `['a']` is the key `'a'`, exactly
57
+ * as JS would index by it, and how a `@gmod/vcf` INFO value (always a list,
58
+ * `Number=1` included) indexes a lookup table. A multi-valued list is the key
59
+ * `'a,b'`, and misses rather than guess which value was meant. A plain object
60
+ * does not, since '[object Object]' is not a key anything is stored under, so
61
+ * it and an array holding one answer `undefined` instead. null and undefined
62
+ * are the same case.
63
+ */
64
+ function indexKey(value: JexlValue): string | undefined {
65
+ if (typeof value === 'boolean') {
66
+ return String(value)
43
67
  }
44
- if (
68
+ return Array.isArray(value) && value.every(isKeyPart)
69
+ ? value.join(',')
70
+ : undefined
71
+ }
72
+
73
+ function isKeyPart(
74
+ value: JexlValue
75
+ ): value is string | number | boolean | null | undefined {
76
+ return (
77
+ value == null ||
45
78
  typeof value === 'string' ||
46
79
  typeof value === 'number' ||
47
80
  typeof value === 'boolean'
81
+ )
82
+ }
83
+
84
+ /**
85
+ * The value of an object or array literal holding only primitives, built once.
86
+ * Indexing such a table, as in `{CDS: 'red', exon: 'blue'}[feature.type]`,
87
+ * can only yield a primitive, so the table never escapes and one frozen copy
88
+ * serves every evaluation.
89
+ */
90
+ function literalTable(node: AstNode) {
91
+ const literal = node as AstNodeUnion
92
+ if (literal.type === 'ArrayLiteral') {
93
+ return literal.value.every((item) => item.type === 'Literal')
94
+ ? Object.freeze(literal.value.map((item) => (item as Literal).value))
95
+ : undefined
96
+ }
97
+ if (literal.type === 'ObjectLiteral') {
98
+ const entries = Object.entries(literal.value)
99
+ if (entries.every(([, value]) => value.type === 'Literal')) {
100
+ const table: Context = {}
101
+ for (const [key, value] of entries) {
102
+ defineOwn(table, key, (value as Literal).value)
103
+ }
104
+ return Object.freeze(table)
105
+ }
106
+ }
107
+ return undefined
108
+ }
109
+
110
+ const unassigned = Symbol('unassigned')
111
+
112
+ /**
113
+ * The slot each name an expression assigns is kept in, and the frame holding
114
+ * those slots for the evaluation in progress.
115
+ */
116
+ interface Locals {
117
+ slots: ReadonlyMap<string, number>
118
+ frame: { locals: unknown[] }
119
+ }
120
+
121
+ /**
122
+ * One call of a lambda. A name the lambda binds reads from `args`, a name bound
123
+ * by a lambda around it from `outer`, a name the expression assigns from
124
+ * `locals` — the evaluation's own, captured when the lambda was made, so a
125
+ * lambda called after its evaluation ends still sees them — and any other name
126
+ * from `context`.
127
+ */
128
+ class LambdaFrame {
129
+ args: JexlValue[]
130
+ outer: LambdaFrame | undefined
131
+ context: Context
132
+ locals: unknown[] | undefined
133
+
134
+ constructor(
135
+ args: JexlValue[],
136
+ outer: LambdaFrame | undefined,
137
+ context: Context,
138
+ locals: unknown[] | undefined
48
139
  ) {
49
- return String(value)
140
+ this.args = args
141
+ this.outer = outer
142
+ this.context = context
143
+ this.locals = locals
50
144
  }
51
- return JSON.stringify(value)
145
+ }
146
+
147
+ /** The parameters of the lambdas around a node, innermost first. */
148
+ interface Params {
149
+ names: string[]
150
+ outer: Params | undefined
151
+ }
152
+
153
+ interface Scope {
154
+ locals?: Locals
155
+ params?: Params
156
+ }
157
+
158
+ // the closures compiled inside a lambda are handed its LambdaFrame in place of
159
+ // the context, and only the ones that read a name, below, look inside it
160
+ const asFrame = (env: Context) => env as unknown as LambdaFrame
161
+ const asContext = (frame: LambdaFrame) => frame as unknown as Context
162
+
163
+ function compileName(
164
+ name: string,
165
+ grammar: Grammar,
166
+ scope: Scope
167
+ ): CompiledNode {
168
+ let hops = 0
169
+ for (let p = scope.params; p; p = p.outer) {
170
+ const index = p.names.indexOf(name)
171
+ if (index !== -1) {
172
+ return hops === 0 ? (env) => asFrame(env).args[index] : climb(hops, index)
173
+ }
174
+ hops++
175
+ }
176
+ const read =
177
+ (grammar.variableReader?.(name) as CompiledNode | undefined) ??
178
+ ((ctx) => ctx[name])
179
+ const fromContext: CompiledNode = scope.params
180
+ ? (env) => read(asFrame(env).context)
181
+ : read
182
+ const slot = scope.locals?.slots.get(name)
183
+ if (slot === undefined) {
184
+ return fromContext
185
+ }
186
+ if (scope.params) {
187
+ return (env) => {
188
+ const local = asFrame(env).locals![slot]
189
+ return local === unassigned ? fromContext(env) : (local as JexlValue)
190
+ }
191
+ }
192
+ const { frame } = scope.locals!
193
+ return (env) => {
194
+ const local = frame.locals[slot]
195
+ return local === unassigned ? fromContext(env) : (local as JexlValue)
196
+ }
197
+ }
198
+
199
+ function climb(hops: number, index: number): CompiledNode {
200
+ return (env) => {
201
+ let frame = asFrame(env)
202
+ for (let i = 0; i < hops; i++) {
203
+ frame = frame.outer!
204
+ }
205
+ return frame.args[index]
206
+ }
207
+ }
208
+
209
+ function assignedNames(ast: AstNode | undefined, names = new Set<string>()) {
210
+ const node = ast as AstNodeUnion | undefined
211
+ switch (node?.type) {
212
+ case 'AssignmentExpression': {
213
+ names.add(node.left.value)
214
+ assignedNames(node.right, names)
215
+ break
216
+ }
217
+ case 'Identifier': {
218
+ assignedNames(node.from, names)
219
+ break
220
+ }
221
+ case 'BinaryExpression': {
222
+ assignedNames(node.left, names)
223
+ assignedNames(node.right, names)
224
+ break
225
+ }
226
+ case 'UnaryExpression': {
227
+ assignedNames(node.right, names)
228
+ break
229
+ }
230
+ case 'ConditionalExpression': {
231
+ assignedNames(node.test, names)
232
+ assignedNames(node.consequent, names)
233
+ assignedNames(node.alternate, names)
234
+ break
235
+ }
236
+ case 'FilterExpression': {
237
+ assignedNames(node.subject, names)
238
+ assignedNames(node.expr, names)
239
+ break
240
+ }
241
+ case 'ArrayLiteral':
242
+ case 'FunctionCall':
243
+ case 'SequenceExpression': {
244
+ const children =
245
+ node.type === 'ArrayLiteral'
246
+ ? node.value
247
+ : node.type === 'FunctionCall'
248
+ ? node.args
249
+ : node.expressions
250
+ for (const child of children) {
251
+ assignedNames(child, names)
252
+ }
253
+ break
254
+ }
255
+ case 'ObjectLiteral': {
256
+ for (const child of Object.values(node.value)) {
257
+ assignedNames(child, names)
258
+ }
259
+ break
260
+ }
261
+ case 'TemplateLiteral': {
262
+ for (const part of node.parts) {
263
+ if (part.type === 'expression') {
264
+ assignedNames(part.value, names)
265
+ }
266
+ }
267
+ break
268
+ }
269
+ case 'Lambda': {
270
+ assignedNames(node.body, names)
271
+ break
272
+ }
273
+ }
274
+ return names
52
275
  }
53
276
 
54
277
  /**
@@ -60,16 +283,61 @@ function stringify(value: JexlValue) {
60
283
  * times.
61
284
  *
62
285
  * Operators are bound here, so a grammar change after compilation requires
63
- * recompiling, which is what {@link Expression#compile} already documents.
64
- * Functions are looked up per call, since they are commonly registered after
65
- * an expression has been compiled.
286
+ * recompiling. Functions are looked up per call, since they are commonly
287
+ * registered after an expression has been compiled.
66
288
  *
67
- * @param {{}} ast An expression tree, as produced by the Parser
68
- * @param {{}} grammar The grammar to resolve operators against
69
- * @returns {function} a closure returning the expression's value
70
- * @throws {Error} if the tree contains an unrecognized node type
289
+ * Each name the expression assigns gets a slot, numbered at compile time, in a
290
+ * frame that lasts one evaluation, so an assignment never touches the context
291
+ * and a context reused from one evaluation to the next comes back unchanged. A
292
+ * name reads its slot once assigned, and resolves as usual until then. An
293
+ * expression that assigns nothing gets no frame. The frame is not a layer over
294
+ * the context via `Object.create`: V8 gives every object used as a prototype a
295
+ * map of its own, which made evaluating against a context built per evaluation
296
+ * six times slower.
71
297
  */
72
298
  export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
299
+ const names = [...assignedNames(ast)]
300
+ if (names.length === 0) {
301
+ return compileNode(ast, grammar, {})
302
+ }
303
+ const slots = new Map(names.map((name, i) => [name, i]))
304
+ const empty = names.map(() => unassigned)
305
+ const frame = { locals: empty }
306
+ const fn = compileNode(ast, grammar, { locals: { slots, frame } })
307
+ // saved and restored, so a function that evaluates this same expression
308
+ // again from inside it leaves the outer evaluation's locals intact
309
+ return (ctx) => {
310
+ const outer = frame.locals
311
+ frame.locals = empty.slice()
312
+ try {
313
+ return fn(ctx)
314
+ } finally {
315
+ frame.locals = outer
316
+ }
317
+ }
318
+ }
319
+
320
+ /** Renders an interpolated value for a template literal. */
321
+ function stringify(value: JexlValue) {
322
+ if (value == null) {
323
+ return ''
324
+ }
325
+ if (
326
+ typeof value === 'string' ||
327
+ typeof value === 'number' ||
328
+ typeof value === 'boolean'
329
+ ) {
330
+ return String(value)
331
+ }
332
+ return JSON.stringify(value)
333
+ }
334
+
335
+ function compileNode(
336
+ ast: AstNode,
337
+ grammar: Grammar,
338
+ scope: Scope
339
+ ): CompiledNode {
340
+ const compile = (child: AstNode) => compileNode(child, grammar, scope)
73
341
  // AstNode types its `type` as a plain string so the Parser can build the tree
74
342
  // loosely; narrowing to the union once, here, lets every case below see its
75
343
  // own node type instead of repeating the same cast in each branch
@@ -83,49 +351,44 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
83
351
  case 'Identifier': {
84
352
  const name = node.value
85
353
  if (!node.from) {
86
- return (ctx) => ctx[name]
354
+ return compileName(name, grammar, scope)
87
355
  }
88
- const from = compileAst(node.from, grammar)
89
- return (ctx) => {
90
- const subject = from(ctx)
91
- if (subject == null) {
92
- return undefined
356
+ const from = compile(node.from)
357
+ const { getMember } = grammar
358
+ if (getMember) {
359
+ return (ctx) => {
360
+ const target = from(ctx)
361
+ return target == null
362
+ ? undefined
363
+ : (getMember(target, name) as JexlValue)
93
364
  }
94
- // an identifier chained off an array reads through its first element
95
- const target = Array.isArray(subject) ? subject[0] : subject
365
+ }
366
+ return (ctx) => {
367
+ const target = from(ctx)
96
368
  return target == null ? undefined : (target as Context)[name]
97
369
  }
98
370
  }
99
371
 
100
372
  case 'BinaryExpression': {
101
373
  const op = grammar.elements[node.operator]
102
- if (op?.type !== 'binaryOp') {
103
- // matches the tree-walking behaviour: an unknown operator yields
104
- // undefined without evaluating either operand
105
- return () => undefined
106
- }
107
- const left = compileAst(node.left, grammar)
108
- const right = compileAst(node.right!, grammar)
109
- const { evalOnDemand } = op
110
- if (evalOnDemand) {
374
+ const left = compile(node.left)
375
+ const right = compile(node.right!)
376
+ if (op?.type === 'binaryOp' && op.evalOnDemand) {
377
+ const { evalOnDemand } = op
111
378
  // operands stay unevaluated behind an `eval` thunk, so operators such
112
379
  // as && and || can short-circuit
113
380
  return (ctx) =>
114
381
  evalOnDemand({ eval: () => left(ctx) }, { eval: () => right(ctx) })
115
382
  }
116
- const fn = op.eval
383
+ const fn = op?.type === 'binaryOp' ? op.eval : undefined
117
384
  if (!fn) {
118
- return (ctx) => {
119
- left(ctx)
120
- right(ctx)
121
- return undefined
122
- }
385
+ throw new Error(`Unknown binary operator '${node.operator}'`)
123
386
  }
124
387
  return (ctx) => fn(left(ctx), right(ctx))
125
388
  }
126
389
 
127
390
  case 'UnaryExpression': {
128
- const right = compileAst(node.right!, grammar)
391
+ const right = compile(node.right!)
129
392
  const elem = grammar.elements[node.operator]
130
393
  const fn =
131
394
  elem?.type === 'unaryOp'
@@ -134,23 +397,15 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
134
397
  ? elem.unaryEval
135
398
  : undefined
136
399
  if (!fn) {
137
- // the operand is still evaluated for an unknown operator, as before
138
- return (ctx) => {
139
- right(ctx)
140
- return undefined
141
- }
400
+ throw new Error(`Unknown unary operator '${node.operator}'`)
142
401
  }
143
402
  return (ctx) => fn(right(ctx))
144
403
  }
145
404
 
146
405
  case 'ConditionalExpression': {
147
- const test = compileAst(node.test, grammar)
148
- const consequent = node.consequent
149
- ? compileAst(node.consequent, grammar)
150
- : undefined
151
- const alternate = node.alternate
152
- ? compileAst(node.alternate, grammar)
153
- : undefined
406
+ const test = compile(node.test)
407
+ const consequent = node.consequent ? compile(node.consequent) : undefined
408
+ const alternate = node.alternate ? compile(node.alternate) : undefined
154
409
  return (ctx) => {
155
410
  const res = test(ctx)
156
411
  if (res) {
@@ -162,22 +417,30 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
162
417
  }
163
418
 
164
419
  case 'FilterExpression': {
165
- const subject = compileAst(node.subject, grammar)
166
- const index = compileAst(node.expr, grammar)
420
+ const table = literalTable(node.subject)
421
+ const subject = table ? () => table : compile(node.subject)
422
+ const index = compile(node.expr)
423
+ const { getMember } = grammar
424
+ if (getMember) {
425
+ return (ctx) => {
426
+ const subjectVal = subject(ctx)
427
+ const key = memberKey(index(ctx))
428
+ return subjectVal == null || key === undefined
429
+ ? undefined
430
+ : (getMember(subjectVal, key) as JexlValue)
431
+ }
432
+ }
167
433
  return (ctx) => {
168
434
  const subjectVal = subject(ctx)
169
- const indexVal = index(ctx)
170
- if (subjectVal == null) {
171
- return undefined
172
- }
173
- return typeof indexVal === 'string' || typeof indexVal === 'number'
174
- ? (subjectVal as Record<string | number, JexlValue>)[indexVal]
175
- : undefined
435
+ const key = memberKey(index(ctx))
436
+ return subjectVal == null || key === undefined
437
+ ? undefined
438
+ : (subjectVal as Record<string | number, JexlValue>)[key]
176
439
  }
177
440
  }
178
441
 
179
442
  case 'ArrayLiteral': {
180
- const items = node.value.map((item) => compileAst(item, grammar))
443
+ const items = node.value.map(compile)
181
444
  const len = items.length
182
445
  return (ctx) => {
183
446
  const out: JexlValue[] = new Array(len)
@@ -191,7 +454,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
191
454
  case 'ObjectLiteral': {
192
455
  const entries = Object.entries(node.value)
193
456
  const keys = entries.map(([key]) => key)
194
- const values = entries.map(([, value]) => compileAst(value, grammar))
457
+ const values = entries.map(([, value]) => compile(value))
195
458
  const len = keys.length
196
459
  // resolved once, here, so the common case keeps its plain store
197
460
  const store = keys.includes('__proto__') ? defineOwn : assignOwn
@@ -211,7 +474,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
211
474
  const { value } = part
212
475
  return () => value
213
476
  }
214
- const expr = compileAst(part.value, grammar)
477
+ const expr = compile(part.value)
215
478
  return (ctx) => stringify(expr(ctx))
216
479
  })
217
480
  return (ctx) => {
@@ -225,7 +488,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
225
488
 
226
489
  case 'FunctionCall': {
227
490
  const { name } = node
228
- const args = node.args.map((arg) => compileAst(arg, grammar))
491
+ const args = node.args.map(compile)
229
492
  // hasOwn, so that inherited Object.prototype members such as `toString`
230
493
  // and `constructor` aren't callable as Jexl functions. Resolved per call
231
494
  // rather than baked in, since functions are routinely registered after
@@ -273,7 +536,7 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
273
536
  }
274
537
 
275
538
  case 'SequenceExpression': {
276
- const exprs = node.expressions.map((expr) => compileAst(expr, grammar))
539
+ const exprs = node.expressions.map(compile)
277
540
  const len = exprs.length
278
541
  return (ctx) => {
279
542
  let last: JexlValue
@@ -285,13 +548,34 @@ export function compileAst(ast: AstNode, grammar: Grammar): CompiledNode {
285
548
  }
286
549
 
287
550
  case 'AssignmentExpression': {
288
- const name = node.left.value
289
- const right = compileAst(node.right!, grammar)
290
- const store = name === '__proto__' ? defineOwn : assignOwn
291
- return (ctx) => {
292
- const value = right(ctx)
293
- store(ctx, name, value)
294
- return value
551
+ if (scope.params) {
552
+ throw new Error('Assignment is not supported in a lambda')
553
+ }
554
+ const right = compile(node.right!)
555
+ const { slots, frame } = scope.locals!
556
+ const slot = slots.get(node.left.value)!
557
+ return (ctx) => (frame.locals[slot] = right(ctx))
558
+ }
559
+
560
+ case 'Lambda': {
561
+ const body = compileNode(node.body, grammar, {
562
+ ...scope,
563
+ params: { names: node.params, outer: scope.params }
564
+ })
565
+ if (!scope.params) {
566
+ const frame = scope.locals?.frame
567
+ return (ctx): JexlFunction => {
568
+ const locals = frame?.locals
569
+ return (...args) =>
570
+ body(asContext(new LambdaFrame(args, undefined, ctx, locals)))
571
+ }
572
+ }
573
+ return (env): JexlFunction => {
574
+ const outer = asFrame(env)
575
+ return (...args) =>
576
+ body(
577
+ asContext(new LambdaFrame(args, outer, outer.context, outer.locals))
578
+ )
295
579
  }
296
580
  }
297
581