@jbrowse/jexl 5.0.2 → 6.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 (83) hide show
  1. package/README.md +4 -1
  2. package/dist/Jexl.js +12 -16
  3. package/dist/Jexl.js.map +1 -1
  4. package/dist/Lexer.d.ts +12 -94
  5. package/dist/Lexer.js +41 -142
  6. package/dist/Lexer.js.map +1 -1
  7. package/dist/analyze.d.ts +2 -0
  8. package/dist/analyze.js +7 -7
  9. package/dist/analyze.js.map +1 -1
  10. package/dist/check.js +30 -30
  11. package/dist/check.js.map +1 -1
  12. package/dist/collections.d.ts +7 -0
  13. package/dist/collections.js +9 -2
  14. package/dist/collections.js.map +1 -1
  15. package/dist/conditions.d.ts +1 -1
  16. package/dist/conditions.js +25 -47
  17. package/dist/conditions.js.map +1 -1
  18. package/dist/errors.d.ts +5 -0
  19. package/dist/errors.js +8 -0
  20. package/dist/errors.js.map +1 -1
  21. package/dist/evaluator/compile.js +37 -24
  22. package/dist/evaluator/compile.js.map +1 -1
  23. package/dist/grammar.d.ts +2 -2
  24. package/dist/grammar.js +1 -8
  25. package/dist/grammar.js.map +1 -1
  26. package/dist/operators.d.ts +5 -0
  27. package/dist/operators.js +9 -5
  28. package/dist/operators.js.map +1 -1
  29. package/dist/parser/Parser.d.ts +11 -12
  30. package/dist/parser/Parser.js +4 -10
  31. package/dist/parser/Parser.js.map +1 -1
  32. package/dist/print.d.ts +1 -1
  33. package/dist/print.js +41 -47
  34. package/dist/print.js.map +1 -1
  35. package/dist/types.d.ts +18 -21
  36. package/esm/Jexl.js +12 -16
  37. package/esm/Jexl.js.map +1 -1
  38. package/esm/Lexer.d.ts +12 -94
  39. package/esm/Lexer.js +40 -142
  40. package/esm/Lexer.js.map +1 -1
  41. package/esm/analyze.d.ts +2 -0
  42. package/esm/analyze.js +6 -7
  43. package/esm/analyze.js.map +1 -1
  44. package/esm/check.js +28 -28
  45. package/esm/check.js.map +1 -1
  46. package/esm/collections.d.ts +7 -0
  47. package/esm/collections.js +8 -1
  48. package/esm/collections.js.map +1 -1
  49. package/esm/conditions.d.ts +1 -1
  50. package/esm/conditions.js +26 -48
  51. package/esm/conditions.js.map +1 -1
  52. package/esm/errors.d.ts +5 -0
  53. package/esm/errors.js +7 -0
  54. package/esm/errors.js.map +1 -1
  55. package/esm/evaluator/compile.js +37 -24
  56. package/esm/evaluator/compile.js.map +1 -1
  57. package/esm/grammar.d.ts +2 -2
  58. package/esm/grammar.js +1 -8
  59. package/esm/grammar.js.map +1 -1
  60. package/esm/operators.d.ts +5 -0
  61. package/esm/operators.js +8 -5
  62. package/esm/operators.js.map +1 -1
  63. package/esm/parser/Parser.d.ts +11 -12
  64. package/esm/parser/Parser.js +4 -8
  65. package/esm/parser/Parser.js.map +1 -1
  66. package/esm/print.d.ts +1 -1
  67. package/esm/print.js +41 -47
  68. package/esm/print.js.map +1 -1
  69. package/esm/types.d.ts +18 -21
  70. package/package.json +1 -1
  71. package/src/Jexl.ts +13 -20
  72. package/src/Lexer.ts +42 -144
  73. package/src/analyze.ts +10 -10
  74. package/src/check.ts +47 -53
  75. package/src/collections.ts +11 -1
  76. package/src/conditions.ts +38 -61
  77. package/src/errors.ts +10 -0
  78. package/src/evaluator/compile.ts +49 -36
  79. package/src/grammar.ts +3 -9
  80. package/src/operators.ts +10 -6
  81. package/src/parser/Parser.ts +13 -19
  82. package/src/print.ts +50 -52
  83. package/src/types.ts +19 -23
package/src/check.ts CHANGED
@@ -3,16 +3,18 @@
3
3
  * Copyright 2020 Tom Shawver
4
4
  */
5
5
 
6
+ import { literalValue } from './analyze.ts'
7
+ import { unknownNode } from './errors.ts'
8
+ import { toRegExp } from './operators.ts'
6
9
  import { isName, key, print, quote } from './print.ts'
7
10
 
8
11
  import type { AnalyzeOptions } from './analyze.ts'
9
12
  import type {
10
13
  AstNode,
11
- AstNodeUnion,
14
+ BinaryExpression,
12
15
  FunctionCall,
13
16
  Identifier,
14
- Lambda,
15
- Literal
17
+ Lambda
16
18
  } from './types.ts'
17
19
 
18
20
  /**
@@ -149,8 +151,6 @@ export interface CheckResult {
149
151
  type: Type
150
152
  }
151
153
 
152
- type Node = AstNodeUnion
153
-
154
154
  export const UNKNOWN: Type = { kind: 'unknown' }
155
155
  const UNDEFINED: Type = { kind: 'undefined' }
156
156
  const BOOLEAN: Type = { kind: 'boolean' }
@@ -291,7 +291,7 @@ function isBoxed(type: Type): type is OfKind<'list'> {
291
291
 
292
292
  function isPattern(pattern: string) {
293
293
  try {
294
- new RegExp(pattern.startsWith('(?i)') ? pattern.slice(4) : pattern)
294
+ toRegExp(pattern)
295
295
  return true
296
296
  } catch {
297
297
  return false
@@ -579,10 +579,9 @@ function negate(type: Type): Type {
579
579
  }
580
580
 
581
581
  function paramName(subject: AstNode, context: AstNode) {
582
- const node = subject as Node
583
582
  const base =
584
- node.type === 'Identifier'
585
- ? node.value.toLowerCase().replaceAll(/\W/g, '')
583
+ subject.type === 'Identifier'
584
+ ? subject.value.toLowerCase().replaceAll(/\W/g, '')
586
585
  : ''
587
586
  const name = isName(base) ? base : 'x'
588
587
  const text = print(context)
@@ -593,10 +592,6 @@ function paramName(subject: AstNode, context: AstNode) {
593
592
  return candidate
594
593
  }
595
594
 
596
- function literalValue(node: AstNode) {
597
- return node.type === 'Literal' ? (node as Literal).value : undefined
598
- }
599
-
600
595
  function describe(type: Type): string {
601
596
  switch (type.kind) {
602
597
  case 'list': {
@@ -836,27 +831,20 @@ export function check(
836
831
  }
837
832
  }
838
833
 
839
- function chain(node: Identifier): { base: AstNode; names: string[] } {
834
+ function dottedKey(node: Identifier): Type | undefined {
840
835
  const names = [node.value]
841
- let base: AstNode = node
842
- while (base.type === 'Identifier' && (base as Identifier).from) {
843
- base = (base as Identifier).from!
844
- if (base.type === 'Identifier') {
845
- names.unshift((base as Identifier).value)
836
+ let base = node
837
+ while (base.from) {
838
+ if (base.from.type !== 'Identifier') {
839
+ return undefined
846
840
  }
841
+ base = base.from
842
+ names.unshift(base.value)
847
843
  }
848
- return { base, names }
849
- }
850
-
851
- function dottedKey(node: Identifier): Type | undefined {
852
- if (!node.from) {
853
- return undefined
854
- }
855
- const { base, names } = chain(node)
856
- if ((base as Identifier).from) {
844
+ if (base === node) {
857
845
  return undefined
858
846
  }
859
- let type = variable(base as Identifier, false)
847
+ let type = variable(base, false)
860
848
  let subjectNode: AstNode = base
861
849
  for (let i = 1; i < names.length; i++) {
862
850
  const record = type.kind === 'record' ? type : undefined
@@ -894,7 +882,7 @@ export function check(
894
882
  type: 'Identifier',
895
883
  value: names[i]!,
896
884
  from: subjectNode
897
- } as Identifier
885
+ }
898
886
  }
899
887
  return undefined
900
888
  }
@@ -938,34 +926,45 @@ export function check(
938
926
  return UNKNOWN
939
927
  }
940
928
 
941
- function hyphenated(node: AstNodeUnion): Type | undefined {
929
+ function hyphenated(node: AstNode): Type | undefined {
942
930
  if (
943
931
  node.type !== 'BinaryExpression' ||
944
932
  node.operator !== '-' ||
945
933
  node.left.type !== 'Identifier' ||
946
- node.right?.type !== 'Identifier' ||
947
- (node.right as Identifier).from
934
+ node.right.type !== 'Identifier' ||
935
+ node.right.from
936
+ ) {
937
+ return undefined
938
+ }
939
+ const left = node.left
940
+ const name = left.value
941
+ if (
942
+ !left.from &&
943
+ (scope.has(name) || name === row || (env && Object.hasOwn(env, name)))
948
944
  ) {
949
945
  return undefined
950
946
  }
951
- const left = node.left as Identifier
952
947
  const reported = diagnostics.length
953
948
  const subject = left.from ? infer(left.from) : root
954
949
  diagnostics.length = reported
955
- if (subject.kind !== 'record' || subject.fields.has(left.value)) {
950
+ if (subject.kind !== 'record' || subject.fields.has(name)) {
956
951
  return undefined
957
952
  }
958
- const whole = `${left.value}-${(node.right as Identifier).value}`
953
+ const whole = `${name}-${node.right.value}`
959
954
  const found = subject.fields.get(whole)
960
955
  if (!found) {
961
956
  return undefined
962
957
  }
958
+ // a bare name can't hold a hyphen, so only a row variable can spell it
959
+ const holder: AstNode | undefined =
960
+ left.from ??
961
+ (row === undefined ? undefined : { type: 'Identifier', value: row })
963
962
  report(
964
963
  'unknown-field',
965
964
  'warning',
966
965
  node,
967
966
  `${print(node)} subtracts; ${whole} is one field`,
968
- [text(left.from, whole)]
967
+ holder ? [text(holder, whole)] : []
969
968
  )
970
969
  return found
971
970
  }
@@ -1048,11 +1047,9 @@ export function check(
1048
1047
  }
1049
1048
  }
1050
1049
 
1051
- function binary(
1052
- node: Extract<AstNodeUnion, { type: 'BinaryExpression' }>
1053
- ): Type {
1050
+ function binary(node: BinaryExpression): Type {
1054
1051
  const { operator } = node
1055
- const rightNode = node.right!
1052
+ const rightNode = node.right
1056
1053
  if (operator === '&&' || operator === '||' || operator === '??') {
1057
1054
  const left = infer(node.left)
1058
1055
  const right = infer(rightNode)
@@ -1147,7 +1144,7 @@ export function check(
1147
1144
  if (EQUALITY.has(operator)) {
1148
1145
  const leftLiteral = literalOf(leftItem)
1149
1146
  const rightLiteral = literalOf(rightItem)
1150
- if (rightLiteral !== undefined && node.right?.type === 'Literal') {
1147
+ if (rightLiteral !== undefined && node.right.type === 'Literal') {
1151
1148
  compareLiteral(leftItem, rightLiteral, node)
1152
1149
  } else if (leftLiteral !== undefined && node.left.type === 'Literal') {
1153
1150
  compareLiteral(rightItem, leftLiteral, node)
@@ -1234,7 +1231,7 @@ export function check(
1234
1231
  type: 'Identifier',
1235
1232
  value: name,
1236
1233
  from: subjectNode
1237
- } as Identifier
1234
+ }
1238
1235
  }
1239
1236
  for (const name of prefix) {
1240
1237
  step(String(name), node)
@@ -1261,9 +1258,7 @@ export function check(
1261
1258
  node,
1262
1259
  `no function ${name}` +
1263
1260
  (near.length ? `; did you mean ${near.join(', ')}?` : ''),
1264
- near.map((candidate) =>
1265
- print({ ...node, name: candidate } as FunctionCall)
1266
- )
1261
+ near.map((candidate) => print({ ...node, name: candidate }))
1267
1262
  )
1268
1263
  }
1269
1264
  return UNKNOWN
@@ -1290,10 +1285,10 @@ export function check(
1290
1285
  }
1291
1286
  const accepted: readonly ParamType[] =
1292
1287
  typeof declared === 'string' ? [declared] : declared
1293
- if ((arg as Node).type === 'Lambda') {
1288
+ if (arg.type === 'Lambda') {
1294
1289
  const item =
1295
1290
  lastCollection?.kind === 'list' ? lastCollection.of : UNKNOWN
1296
- types.push(lambda(arg as Lambda, lambdaParams(name, item)))
1291
+ types.push(lambda(arg, lambdaParams(name, item)))
1297
1292
  continue
1298
1293
  }
1299
1294
  const type = infer(arg)
@@ -1341,8 +1336,7 @@ export function check(
1341
1336
  return returns ?? UNKNOWN
1342
1337
  }
1343
1338
 
1344
- function infer(ast: AstNode): Type {
1345
- const node = ast as Node
1339
+ function infer(node: AstNode): Type {
1346
1340
  switch (node.type) {
1347
1341
  case 'Literal': {
1348
1342
  const { value } = node
@@ -1403,7 +1397,7 @@ export function check(
1403
1397
  return binary(node)
1404
1398
  }
1405
1399
  case 'UnaryExpression': {
1406
- const right = infer(node.right!)
1400
+ const right = infer(node.right)
1407
1401
  if (node.operator === '!') {
1408
1402
  return BOOLEAN
1409
1403
  }
@@ -1468,7 +1462,7 @@ export function check(
1468
1462
  return last
1469
1463
  }
1470
1464
  case 'AssignmentExpression': {
1471
- const value = infer(node.right!)
1465
+ const value = infer(node.right)
1472
1466
  scope.set(node.left.value, value)
1473
1467
  return value
1474
1468
  }
@@ -1476,7 +1470,7 @@ export function check(
1476
1470
  return lambda(node)
1477
1471
  }
1478
1472
  default: {
1479
- throw new Error(`Corrupt AST: unknown node type '${ast.type}'`)
1473
+ return unknownNode(node)
1480
1474
  }
1481
1475
  }
1482
1476
  }
@@ -34,13 +34,23 @@ function isPlainObject(value: JexlValue): value is Record<string, JexlValue> {
34
34
  return proto === Object.prototype || proto === null
35
35
  }
36
36
 
37
+ /**
38
+ * Marks the functions lambdas evaluate to. A collection function calls only
39
+ * these, never a function an expression reads, since reads reach host
40
+ * functions: `reduce(['x', body], ''.constructor.constructor)` built a
41
+ * function from `body`, and `map` then ran it.
42
+ */
43
+ export const LAMBDA = Symbol('lambda')
44
+
45
+ type Lambda = JexlFunction & { [LAMBDA]?: true }
46
+
37
47
  const identity = (value: JexlValue) => value
38
48
 
39
49
  function callback(name: string, fn: JexlValue): JexlFunction {
40
50
  if (fn === undefined) {
41
51
  return identity
42
52
  }
43
- if (typeof fn !== 'function') {
53
+ if (typeof fn !== 'function' || !(fn as Lambda)[LAMBDA]) {
44
54
  throw new TypeError(`${name}() expects a lambda, such as x => x > 1`)
45
55
  }
46
56
  return fn
package/src/conditions.ts CHANGED
@@ -3,11 +3,11 @@
3
3
  * Copyright 2020 Tom Shawver
4
4
  */
5
5
 
6
- import { analyze } from './analyze.ts'
6
+ import { analyze, literalValue } from './analyze.ts'
7
7
  import { print } from './print.ts'
8
8
 
9
9
  import type { PathKey } from './analyze.ts'
10
- import type { AstNode, AstNodeUnion } from './types.ts'
10
+ import type { AstNode } from './types.ts'
11
11
 
12
12
  export type Scalar = string | number | boolean | null
13
13
 
@@ -50,38 +50,25 @@ const FLIP: Record<string, Condition['op']> = {
50
50
  '>=': '<='
51
51
  }
52
52
 
53
- function scalar(ast: AstNode): { value: Scalar } | undefined {
54
- const node = ast as AstNodeUnion
55
- if (node.type === 'Literal') {
56
- return { value: node.value }
57
- }
58
- return node.type === 'TemplateLiteral' &&
59
- node.parts.every((part) => part.type === 'static')
60
- ? { value: node.parts.map((part) => part.value).join('') }
53
+ function scalars(nodes: AstNode[]) {
54
+ const values = nodes.map(literalValue)
55
+ return values.every((value): value is Scalar => value !== undefined)
56
+ ? values
61
57
  : undefined
62
58
  }
63
59
 
64
- function isRow(ast: AstNode | undefined, row: string) {
65
- const node = ast as AstNodeUnion | undefined
60
+ function isRow(node: AstNode | undefined, row: string) {
66
61
  return node?.type === 'Identifier' && !node.from && node.value === row
67
62
  }
68
63
 
69
- function subjectOf(ast: AstNode, opts: ConditionOptions): Subject | undefined {
70
- const node = ast as AstNodeUnion
64
+ function subjectOf(node: AstNode, opts: ConditionOptions): Subject | undefined {
71
65
  if (
72
66
  node.type === 'FunctionCall' &&
73
67
  opts.calls?.includes(node.name) &&
74
68
  isRow(node.args[0], opts.row)
75
69
  ) {
76
- const rest = node.args.slice(1).map(scalar)
77
- return rest.every(Boolean)
78
- ? {
79
- kind: 'call',
80
- name: node.name,
81
- args: rest.map((arg) => arg!.value),
82
- node
83
- }
84
- : undefined
70
+ const args = scalars(node.args.slice(1))
71
+ return args && { kind: 'call', name: node.name, args, node }
85
72
  }
86
73
  const accessor =
87
74
  node.type === 'FunctionCall' &&
@@ -109,12 +96,11 @@ function subjectOf(ast: AstNode, opts: ConditionOptions): Subject | undefined {
109
96
  }
110
97
 
111
98
  function condition(
112
- ast: AstNode,
99
+ node: AstNode,
113
100
  opts: ConditionOptions
114
101
  ): Condition | undefined {
115
- const node = ast as AstNodeUnion
116
102
  if (node.type === 'UnaryExpression' && node.operator === '!') {
117
- const inner = condition(node.right!, opts)
103
+ const inner = condition(node.right, opts)
118
104
  // only forms whose negation reads as a condition of its own; `!(QUAL > 30)`
119
105
  // holds where QUAL is missing, which `QUAL <= 30` does not
120
106
  return inner?.op === 'in' || inner?.op === 'has' || inner?.op === 'set'
@@ -126,43 +112,36 @@ function condition(
126
112
  return subject && { subject, op: 'set' }
127
113
  }
128
114
  const { operator, left } = node
129
- const right = node.right!
115
+ const right = node.right
130
116
  if (COMPARE.has(operator)) {
131
117
  const subject = subjectOf(left, opts)
132
- const value = scalar(right)
133
- if (subject && value) {
134
- return { subject, op: operator as '==', value: value.value }
118
+ const value = literalValue(right)
119
+ if (subject && value !== undefined) {
120
+ return { subject, op: operator as '==', value }
135
121
  }
136
122
  const flipped = subjectOf(right, opts)
137
- const leftValue = scalar(left)
138
- return flipped && leftValue
139
- ? {
140
- subject: flipped,
141
- op: FLIP[operator] as '==',
142
- value: leftValue.value
143
- }
123
+ const leftValue = literalValue(left)
124
+ return flipped && leftValue !== undefined
125
+ ? { subject: flipped, op: FLIP[operator] as '==', value: leftValue }
144
126
  : undefined
145
127
  }
146
128
  if (operator === '~' || operator === '!~') {
147
129
  const subject = subjectOf(left, opts)
148
- const value = scalar(right)
149
- return subject && typeof value?.value === 'string'
150
- ? { subject, op: operator, value: value.value }
130
+ const value = literalValue(right)
131
+ return subject && typeof value === 'string'
132
+ ? { subject, op: operator, value }
151
133
  : undefined
152
134
  }
153
135
  if (operator === 'in') {
154
136
  const subject = subjectOf(left, opts)
155
- const list = right as AstNodeUnion
156
- if (subject && list.type === 'ArrayLiteral') {
157
- const values = list.value.map(scalar)
158
- return values.length > 0 && values.every(Boolean)
159
- ? { subject, op: 'in', value: values.map((value) => value!.value) }
160
- : undefined
137
+ if (subject && right.type === 'ArrayLiteral') {
138
+ const values = scalars(right.value)
139
+ return values?.length ? { subject, op: 'in', value: values } : undefined
161
140
  }
162
- const key = scalar(left)
141
+ const key = literalValue(left)
163
142
  const container = subjectOf(right, opts)
164
- return container && typeof key?.value === 'string'
165
- ? { subject: container, op: 'has', value: key.value }
143
+ return container && typeof key === 'string'
144
+ ? { subject: container, op: 'has', value: key }
166
145
  : undefined
167
146
  }
168
147
  return undefined
@@ -176,37 +155,35 @@ function condition(
176
155
  * misread.
177
156
  */
178
157
  export function conditions(
179
- ast: AstNode | null,
158
+ node: AstNode | null,
180
159
  opts: ConditionOptions
181
160
  ): Condition[] | undefined {
182
- const node = ast as AstNodeUnion | null
183
161
  if (!node) {
184
162
  return undefined
185
163
  }
186
164
  if (node.type === 'BinaryExpression' && node.operator === '&&') {
187
165
  const left = conditions(node.left, opts)
188
- const right = left && conditions(node.right!, opts)
166
+ const right = left && conditions(node.right, opts)
189
167
  return left && right && [...left, ...right]
190
168
  }
191
169
  const found = condition(node, opts)
192
170
  return found && [found]
193
171
  }
194
172
 
195
- const literal = (value: Scalar) =>
196
- print({ type: 'Literal', value } as AstNodeUnion)
173
+ const literal = (value: Scalar) => print({ type: 'Literal', value })
197
174
 
198
175
  /** A subject reading `path` off the row, for a condition built from a picker. */
199
176
  export function pathSubject(row: string, path: PathKey[]): Subject {
200
- let node: AstNode = { type: 'Identifier', value: row } as AstNodeUnion
177
+ let node: AstNode = { type: 'Identifier', value: row }
201
178
  for (const key of path) {
202
179
  node =
203
180
  typeof key === 'string'
204
- ? ({ type: 'Identifier', value: key, from: node } as AstNodeUnion)
205
- : ({
181
+ ? { type: 'Identifier', value: key, from: node }
182
+ : {
206
183
  type: 'FilterExpression',
207
184
  subject: node,
208
185
  expr: { type: 'Literal', value: key }
209
- } as AstNodeUnion)
186
+ }
210
187
  }
211
188
  return { kind: 'path', path, node }
212
189
  }
@@ -217,14 +194,14 @@ export function callSubject(
217
194
  name: string,
218
195
  args: Scalar[] = []
219
196
  ): Subject {
220
- const node = {
197
+ const node: AstNode = {
221
198
  type: 'FunctionCall',
222
199
  name,
223
200
  args: [
224
201
  { type: 'Identifier', value: row },
225
- ...args.map((value) => ({ type: 'Literal', value }))
202
+ ...args.map((value): AstNode => ({ type: 'Literal', value }))
226
203
  ]
227
- } as AstNodeUnion
204
+ }
228
205
  return { kind: 'call', name, args, node }
229
206
  }
230
207
 
package/src/errors.ts CHANGED
@@ -13,3 +13,13 @@ export class JexlSyntaxError extends Error {
13
13
  this.offset = offset
14
14
  }
15
15
  }
16
+
17
+ /**
18
+ * Throws for a node no case of a walk handled. Typing it `never` makes a new
19
+ * node type a compile error in every walk that doesn't handle it yet.
20
+ */
21
+ export function unknownNode(node: never): never {
22
+ throw new Error(
23
+ `Corrupt AST: unknown node type '${(node as { type: string }).type}'`
24
+ )
25
+ }
@@ -3,16 +3,12 @@
3
3
  * Copyright 2020 Tom Shawver
4
4
  */
5
5
 
6
+ import { LAMBDA } from '../collections.ts'
7
+ import { unknownNode } from '../errors.ts'
6
8
  import { and, nullish, or } from '../grammar.ts'
7
9
 
8
10
  import type { Grammar } from '../grammar.ts'
9
- import type {
10
- AstNode,
11
- AstNodeUnion,
12
- JexlFunction,
13
- JexlValue,
14
- Literal
15
- } from '../types.ts'
11
+ import type { AstNode, JexlFunction, JexlValue } from '../types.ts'
16
12
 
17
13
  /** The variables an expression is evaluated against. */
18
14
  export type Context = Record<string, JexlValue>
@@ -44,6 +40,16 @@ function assignOwn(target: Context, key: string, value: JexlValue) {
44
40
  target[key] = value
45
41
  }
46
42
 
43
+ // inherited, these lead from any value to `Function` and `Object.prototype`
44
+ const isUnsafeKey = (key: string | number) =>
45
+ key === 'constructor' || key === '__proto__'
46
+
47
+ function member(target: Context, key: string | number) {
48
+ return isUnsafeKey(key) && !Object.hasOwn(target, key)
49
+ ? undefined
50
+ : target[key]
51
+ }
52
+
47
53
  /** The key `subject[index]` reads under, or undefined when it reads nothing. */
48
54
  function memberKey(index: JexlValue) {
49
55
  return typeof index === 'string' || typeof index === 'number'
@@ -90,23 +96,27 @@ function isKeyPart(
90
96
  * serves every evaluation.
91
97
  */
92
98
  function literalTable(node: AstNode) {
93
- const literal = node as AstNodeUnion
94
- if (literal.type === 'ArrayLiteral') {
95
- return literal.value.every((item) => item.type === 'Literal')
96
- ? Object.freeze(literal.value.map((item) => (item as Literal).value))
99
+ if (node.type === 'ArrayLiteral') {
100
+ return node.value.every((item) => item.type === 'Literal')
101
+ ? Object.freeze(node.value.map((item) => item.value))
97
102
  : undefined
98
103
  }
99
- if (literal.type === 'ObjectLiteral') {
100
- const entries = Object.entries(literal.value)
101
- if (entries.every(([, value]) => value.type === 'Literal')) {
102
- const table: Context = {}
103
- for (const [key, value] of entries) {
104
- defineOwn(table, key, (value as Literal).value)
105
- }
106
- return Object.freeze(table)
104
+ if (node.type !== 'ObjectLiteral') {
105
+ return undefined
106
+ }
107
+ const table: Context = {}
108
+ for (const [key, value] of Object.entries(node.value)) {
109
+ if (value.type !== 'Literal') {
110
+ return undefined
107
111
  }
112
+ defineOwn(table, key, value.value)
108
113
  }
109
- return undefined
114
+ return Object.freeze(table)
115
+ }
116
+
117
+ function lambda(fn: JexlFunction & { [LAMBDA]?: true }) {
118
+ fn[LAMBDA] = true
119
+ return fn
110
120
  }
111
121
 
112
122
  const unassigned = Symbol('unassigned')
@@ -208,8 +218,7 @@ function climb(hops: number, index: number): CompiledNode {
208
218
  }
209
219
  }
210
220
 
211
- function assignedNames(ast: AstNode | undefined, names = new Set<string>()) {
212
- const node = ast as AstNodeUnion | undefined
221
+ function assignedNames(node: AstNode | undefined, names = new Set<string>()) {
213
222
  switch (node?.type) {
214
223
  case 'AssignmentExpression': {
215
224
  names.add(node.left.value)
@@ -335,15 +344,11 @@ function stringify(value: JexlValue) {
335
344
  }
336
345
 
337
346
  function compileNode(
338
- ast: AstNode,
347
+ node: AstNode,
339
348
  grammar: Grammar,
340
349
  scope: Scope
341
350
  ): CompiledNode {
342
351
  const compile = (child: AstNode) => compileNode(child, grammar, scope)
343
- // AstNode types its `type` as a plain string so the Parser can build the tree
344
- // loosely; narrowing to the union once, here, lets every case below see its
345
- // own node type instead of repeating the same cast in each branch
346
- const node = ast as AstNodeUnion
347
352
  switch (node.type) {
348
353
  case 'Literal': {
349
354
  const { value } = node
@@ -365,6 +370,12 @@ function compileNode(
365
370
  : (getMember(target, name) as JexlValue)
366
371
  }
367
372
  }
373
+ if (isUnsafeKey(name)) {
374
+ return (ctx) => {
375
+ const target = from(ctx)
376
+ return target == null ? undefined : member(target as Context, name)
377
+ }
378
+ }
368
379
  return (ctx) => {
369
380
  const target = from(ctx)
370
381
  return target == null ? undefined : (target as Context)[name]
@@ -374,7 +385,7 @@ function compileNode(
374
385
  case 'BinaryExpression': {
375
386
  const op = grammar.elements[node.operator]
376
387
  const left = compile(node.left)
377
- const right = compile(node.right!)
388
+ const right = compile(node.right)
378
389
  if (op?.type === 'binaryOp' && op.evalOnDemand) {
379
390
  const { evalOnDemand } = op
380
391
  if (evalOnDemand === and) {
@@ -405,7 +416,7 @@ function compileNode(
405
416
  }
406
417
 
407
418
  case 'UnaryExpression': {
408
- const right = compile(node.right!)
419
+ const right = compile(node.right)
409
420
  const elem = grammar.elements[node.operator]
410
421
  const fn =
411
422
  elem?.type === 'unaryOp'
@@ -452,7 +463,7 @@ function compileNode(
452
463
  const key = memberKey(index(ctx))
453
464
  return subjectVal == null || key === undefined
454
465
  ? undefined
455
- : (subjectVal as Record<string | number, JexlValue>)[key]
466
+ : member(subjectVal as Context, key)
456
467
  }
457
468
  }
458
469
 
@@ -568,7 +579,7 @@ function compileNode(
568
579
  if (scope.params) {
569
580
  throw new Error('Assignment is not supported in a lambda')
570
581
  }
571
- const right = compile(node.right!)
582
+ const right = compile(node.right)
572
583
  const { slots, frame } = scope.locals!
573
584
  const slot = slots.get(node.left.value)!
574
585
  return (ctx) => (frame.locals[slot] = right(ctx))
@@ -581,23 +592,25 @@ function compileNode(
581
592
  })
582
593
  if (!scope.params) {
583
594
  const frame = scope.locals?.frame
584
- return (ctx): JexlFunction => {
595
+ return (ctx) => {
585
596
  const locals = frame?.locals
586
- return (...args) =>
597
+ return lambda((...args) =>
587
598
  body(asContext(new LambdaFrame(args, undefined, ctx, locals)))
599
+ )
588
600
  }
589
601
  }
590
- return (env): JexlFunction => {
602
+ return (env) => {
591
603
  const outer = asFrame(env)
592
- return (...args) =>
604
+ return lambda((...args) =>
593
605
  body(
594
606
  asContext(new LambdaFrame(args, outer, outer.context, outer.locals))
595
607
  )
608
+ )
596
609
  }
597
610
  }
598
611
 
599
612
  default: {
600
- throw new Error(`Corrupt AST: unknown node type '${ast.type}'`)
613
+ return unknownNode(node)
601
614
  }
602
615
  }
603
616
  }