simplex-lang 0.2.2 → 1.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 (51) hide show
  1. package/README.md +530 -13
  2. package/build/parser/index.js +1161 -479
  3. package/build/parser/index.js.map +1 -1
  4. package/build/src/compiler.d.ts +26 -18
  5. package/build/src/compiler.js +211 -457
  6. package/build/src/compiler.js.map +1 -1
  7. package/build/src/constants.d.ts +25 -0
  8. package/build/src/constants.js +29 -0
  9. package/build/src/constants.js.map +1 -0
  10. package/build/src/error-mapping.d.ts +31 -0
  11. package/build/src/error-mapping.js +72 -0
  12. package/build/src/error-mapping.js.map +1 -0
  13. package/build/src/errors.d.ts +8 -5
  14. package/build/src/errors.js +8 -10
  15. package/build/src/errors.js.map +1 -1
  16. package/build/src/index.d.ts +1 -0
  17. package/build/src/index.js +1 -0
  18. package/build/src/index.js.map +1 -1
  19. package/build/src/simplex-tree.d.ts +25 -3
  20. package/build/src/simplex.peggy +120 -3
  21. package/build/src/tools/index.d.ts +25 -6
  22. package/build/src/tools/index.js +91 -19
  23. package/build/src/tools/index.js.map +1 -1
  24. package/build/src/version.js +1 -1
  25. package/build/src/visitors.d.ts +15 -0
  26. package/build/src/visitors.js +330 -0
  27. package/build/src/visitors.js.map +1 -0
  28. package/package.json +10 -8
  29. package/parser/index.js +1161 -479
  30. package/parser/index.js.map +1 -1
  31. package/src/compiler.ts +332 -609
  32. package/src/constants.ts +30 -0
  33. package/src/error-mapping.ts +112 -0
  34. package/src/errors.ts +8 -12
  35. package/src/index.ts +1 -0
  36. package/src/simplex-tree.ts +30 -2
  37. package/src/simplex.peggy +120 -3
  38. package/src/tools/index.ts +117 -24
  39. package/src/visitors.ts +491 -0
  40. package/build/src/tools/cast.d.ts +0 -2
  41. package/build/src/tools/cast.js +0 -20
  42. package/build/src/tools/cast.js.map +0 -1
  43. package/build/src/tools/ensure.d.ts +0 -3
  44. package/build/src/tools/ensure.js +0 -30
  45. package/build/src/tools/ensure.js.map +0 -1
  46. package/build/src/tools/guards.d.ts +0 -2
  47. package/build/src/tools/guards.js +0 -20
  48. package/build/src/tools/guards.js.map +0 -1
  49. package/src/tools/cast.ts +0 -26
  50. package/src/tools/ensure.ts +0 -41
  51. package/src/tools/guards.ts +0 -29
package/src/compiler.ts CHANGED
@@ -2,179 +2,272 @@
2
2
 
3
3
  // eslint-disable-next-line n/no-missing-import
4
4
  import { parse } from '../parser/index.js'
5
- import { CompileError, ExpressionError, UnexpectedTypeError } from './errors.js'
5
+ import { ExpressionError, UnexpectedTypeError } from './errors.js'
6
+ import {
7
+ getActiveErrorMapper,
8
+ getExpressionErrorLocation
9
+ } from './error-mapping.js'
10
+ import type { ErrorMapper } from './error-mapping.js'
6
11
  import {
7
12
  BinaryExpression,
8
- Expression,
9
- ExpressionByType,
10
13
  ExpressionStatement,
11
- Location,
12
14
  LogicalExpression,
13
15
  UnaryExpression
14
16
  } from './simplex-tree.js'
15
- import assert from 'node:assert'
16
- import { castToBoolean } from './tools/cast.js'
17
17
  import {
18
+ castToBoolean,
19
+ castToString,
18
20
  ensureFunction,
21
+ ensureNumber,
22
+ ensureArray,
23
+ ensureObject,
19
24
  ensureRelationalComparable,
20
- ensureNumber
21
- } from './tools/ensure.js'
22
- import { isSimpleValue } from './tools/guards.js'
23
- import { castToString, typeOf } from './tools/index.js'
25
+ isSimpleValue,
26
+ objToStringAlias,
27
+ typeOf
28
+ } from './tools/index.js'
29
+ import { traverse } from './visitors.js'
30
+ import type { SourceLocation, VisitResult } from './visitors.js'
31
+ import { GEN, SCOPE_NAMES, SCOPE_VALUES, SCOPE_PARENT } from './constants.js'
32
+
33
+ export type { SourceLocation, VisitResult, ErrorMapper }
34
+ export { traverse, getExpressionErrorLocation }
24
35
 
25
- interface ContextHelpers<Data, Globals> {
36
+ // --- Context Helpers ---
37
+
38
+ export interface ContextHelpers<Data, Globals> {
26
39
  castToBoolean(this: void, val: unknown): boolean
40
+ castToString(this: void, val: unknown): string
27
41
  ensureFunction(this: void, val: unknown): Function
42
+ ensureObject(this: void, val: unknown): object
43
+ ensureArray(this: void, val: unknown): unknown[]
44
+ nonNullAssert(this: void, val: unknown): unknown
28
45
  getIdentifierValue(
29
46
  this: void,
30
47
  identifierName: string,
31
48
  globals: Globals,
32
49
  data: Data
33
50
  ): unknown
34
- getProperty(this: void, obj: unknown, key: unknown): unknown
51
+ getProperty(
52
+ this: void,
53
+ obj: unknown,
54
+ key: unknown,
55
+ extension: boolean
56
+ ): unknown
35
57
  callFunction(this: void, fn: unknown, args: unknown[] | null): unknown
36
58
  pipe(
37
59
  this: void,
38
60
  head: unknown,
39
- tail: { opt: boolean; next: (topic: unknown) => unknown }[]
61
+ tail: { opt: boolean; fwd: boolean; next: (topic: unknown) => unknown }[]
40
62
  ): unknown
41
63
  }
42
64
 
43
65
  var hasOwn = Object.hasOwn
44
- var ERROR_STACK_REGEX = /<anonymous>:(?<row>\d+):(?<col>\d+)/g
45
- var TOPIC_TOKEN = '%'
46
66
 
47
- const defaultContextHelpers: ContextHelpers<
48
- Record<string, unknown>,
49
- Record<string, unknown>
50
- > = {
51
- castToBoolean,
67
+ /** Look up an identifier in globals first, then data; throw on miss. */
68
+ function defaultGetIdentifierValue(
69
+ identifierName: string,
70
+ globals: Record<string, unknown>,
71
+ data: Record<string, unknown>
72
+ ): unknown {
73
+ if (identifierName === 'undefined') return undefined
52
74
 
53
- ensureFunction,
75
+ if (globals != null && Object.hasOwn(globals, identifierName)) {
76
+ return globals[identifierName]
77
+ }
54
78
 
55
- getIdentifierValue: (identifierName, globals, data) => {
56
- // TODO Should test on parse time?
57
- if (identifierName === TOPIC_TOKEN) {
58
- throw new Error(
59
- `Topic reference "${TOPIC_TOKEN}" is unbound; it must be inside a pipe body.`
60
- )
61
- }
79
+ if (data != null && Object.hasOwn(data, identifierName)) {
80
+ return data[identifierName]
81
+ }
62
82
 
63
- if (identifierName === 'undefined') return undefined
83
+ throw new Error(`Unknown identifier - ${identifierName}`)
84
+ }
64
85
 
65
- if (globals != null && Object.hasOwn(globals, identifierName)) {
66
- return globals[identifierName]
86
+ /** Look up an extension method for the given object type and bind obj as first argument. */
87
+ function getExtensionMethod(
88
+ obj: unknown,
89
+ key: unknown,
90
+ extensionMap: Map<string | object | Function, Record<string, Function>>,
91
+ classesKeys: (object | Function)[],
92
+ classesValues: Record<string, Function>[]
93
+ ): Function {
94
+ var typeofObj = typeof obj
95
+ var methods: Record<string, Function> | undefined
96
+
97
+ if (typeofObj === 'object') {
98
+ for (var i = 0; i < classesKeys.length; i++) {
99
+ // @ts-expect-error supports objects with Symbol.hasInstance
100
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
101
+ if (obj instanceof classesKeys[i]!) {
102
+ methods = classesValues[i]
103
+ break
104
+ }
67
105
  }
106
+ } else {
107
+ methods = extensionMap.get(typeofObj)
108
+ }
68
109
 
69
- if (data != null && Object.hasOwn(data, identifierName)) {
70
- return data[identifierName]
71
- }
110
+ if (methods === undefined) {
111
+ throw new TypeError(
112
+ `No extension methods defined for type "${typeofObj}"`
113
+ )
114
+ }
72
115
 
73
- throw new Error(`Unknown identifier - ${identifierName}`)
74
- },
116
+ var method = methods[key as string]
117
+ if (method === undefined) {
118
+ throw new TypeError(
119
+ `Extension method "${String(key)}" is not defined for type "${typeofObj}"`
120
+ )
121
+ }
75
122
 
76
- getProperty(obj, key) {
77
- if (obj == null) return obj
123
+ return method.bind(null, obj) as Function
124
+ }
78
125
 
79
- const typeofObj = typeof obj
126
+ /** Resolve property access on an object, Map, or string (null-safe). */
127
+ function defaultGetProperty(
128
+ obj: unknown,
129
+ key: unknown,
130
+ extension: boolean
131
+ ): unknown {
132
+ if (obj == null) return undefined
133
+
134
+ if (extension) {
135
+ throw new ExpressionError(
136
+ 'Extension member expression (::) is reserved and not implemented',
137
+ '',
138
+ null
139
+ )
140
+ }
80
141
 
81
- if (typeofObj === 'string' && typeof key === 'number') {
82
- return (obj as string)[key]
83
- }
142
+ var typeofObj = typeof obj
84
143
 
85
- if (typeofObj !== 'object') {
86
- throw new UnexpectedTypeError(['object'], obj)
87
- }
144
+ if (typeofObj === 'string' && typeof key === 'number') {
145
+ return (obj as string)[key]
146
+ }
88
147
 
89
- if (isSimpleValue(key) === false) {
90
- throw new UnexpectedTypeError(['simple type object key'], key)
91
- }
148
+ if (typeofObj !== 'object') {
149
+ throw new UnexpectedTypeError(['object'], obj)
150
+ }
92
151
 
93
- if (hasOwn(obj, key as any)) {
94
- // @ts-expect-error Type cannot be used as an index type
95
- return obj[key] as unknown
96
- }
152
+ if (isSimpleValue(key) === false) {
153
+ throw new UnexpectedTypeError(['simple type object key'], key)
154
+ }
97
155
 
98
- return undefined
99
- },
156
+ if (hasOwn(obj, key as any)) {
157
+ // @ts-expect-error Type cannot be used as an index type
158
+ return obj[key] as unknown
159
+ }
160
+
161
+ if (obj instanceof Map) {
162
+ return obj.get(key) as unknown
163
+ }
100
164
 
101
- callFunction(fn, args) {
102
- return (
103
- args === null
165
+ return undefined
166
+ }
167
+
168
+ /** Call a function value; null/undefined silently returns undefined. */
169
+ function defaultCallFunction(fn: unknown, args: unknown[] | null): unknown {
170
+ return fn == null
171
+ ? undefined
172
+ : ((args === null
104
173
  ? ensureFunction(fn)()
105
- : ensureFunction(fn).apply(null, args)
106
- ) as unknown
107
- },
108
-
109
- pipe(head, tail) {
110
- var result = head
111
- for (const it of tail) {
112
- if (it.opt && result == null) return result
113
- result = it.next(result)
174
+ : ensureFunction(fn).apply(null, args)) as unknown)
175
+ }
176
+
177
+ /** Assert that a value is not null or undefined; throw on null/undefined. */
178
+ function defaultNonNullAssert(val: unknown): unknown {
179
+ if (val == null) {
180
+ throw new ExpressionError(
181
+ 'Non-null assertion failed: value is ' +
182
+ (val === null ? 'null' : 'undefined'),
183
+ '',
184
+ null
185
+ )
186
+ }
187
+ return val
188
+ }
189
+
190
+ /** Execute a pipe sequence, threading each result through the next step. */
191
+ function defaultPipe(
192
+ head: unknown,
193
+ tail: { opt: boolean; fwd: boolean; next: (topic: unknown) => unknown }[]
194
+ ): unknown {
195
+ var result = head
196
+ for (const it of tail) {
197
+ if (it.fwd) {
198
+ throw new ExpressionError(
199
+ 'Pipe forward operator (|>) is reserved and not implemented',
200
+ '',
201
+ null
202
+ )
114
203
  }
115
- return result
204
+ if (it.opt && result == null) return result
205
+ result = it.next(result)
116
206
  }
207
+ return result
117
208
  }
118
209
 
119
- type ExpressionUnaryOperators = Record<
210
+ const defaultContextHelpers: ContextHelpers<
211
+ Record<string, unknown>,
212
+ Record<string, unknown>
213
+ > = {
214
+ castToBoolean,
215
+ castToString,
216
+ ensureFunction,
217
+ ensureObject,
218
+ ensureArray,
219
+ nonNullAssert: defaultNonNullAssert,
220
+ getIdentifierValue: defaultGetIdentifierValue,
221
+ getProperty: defaultGetProperty,
222
+ callFunction: defaultCallFunction,
223
+ pipe: defaultPipe
224
+ }
225
+
226
+ // --- Operators ---
227
+
228
+ export type ExpressionUnaryOperators = Record<
120
229
  UnaryExpression['operator'],
121
230
  (val: unknown) => unknown
122
231
  >
123
232
 
124
- export const defaultUnaryOperators: ExpressionUnaryOperators = {
125
- '+': val => ensureNumber(val),
126
- '-': val => -ensureNumber(val),
127
- 'not': val => !castToBoolean(val),
128
- 'typeof': val => typeof val
233
+ /** Create the default unary operator map (+, -, not, typeof). */
234
+ export function createDefaultUnaryOperators(
235
+ bool: (val: unknown) => boolean
236
+ ): ExpressionUnaryOperators {
237
+ return {
238
+ '+': val => ensureNumber(val),
239
+ '-': val => -ensureNumber(val),
240
+ 'not': val => !bool(val),
241
+ 'typeof': val => typeof val
242
+ }
129
243
  }
130
244
 
131
- type ExpressionBinaryOperators = Record<
245
+ export const defaultUnaryOperators: ExpressionUnaryOperators =
246
+ createDefaultUnaryOperators(castToBoolean)
247
+
248
+ export type ExpressionBinaryOperators = Record<
132
249
  BinaryExpression['operator'],
133
250
  (left: unknown, right: unknown) => unknown
134
251
  >
135
252
 
253
+ const numericOp =
254
+ (
255
+ fn: (a: number, b: number) => number
256
+ ): ((a: unknown, b: unknown) => unknown) =>
257
+ (a, b) =>
258
+ fn(ensureNumber(a) as number, ensureNumber(b) as number)
259
+
136
260
  export const defaultBinaryOperators: ExpressionBinaryOperators = {
137
261
  '!=': (a, b) => a !== b,
138
262
 
139
263
  '==': (a, b) => a === b,
140
264
 
141
- // TIPS give the opportunity to get a base js error
142
-
143
- '*': (a, b) => {
144
- // @ts-expect-error
145
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
146
- return ensureNumber(a) * ensureNumber(b)
147
- },
148
-
149
- '+': (a, b) => {
150
- // @ts-expect-error
151
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/restrict-plus-operands
152
- return ensureNumber(a) + ensureNumber(b)
153
- },
154
-
155
- '-': (a, b) => {
156
- // @ts-expect-error
157
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
158
- return ensureNumber(a) - ensureNumber(b)
159
- },
160
-
161
- '/': (a, b) => {
162
- // @ts-expect-error
163
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
164
- return ensureNumber(a) / ensureNumber(b)
165
- },
166
-
167
- 'mod': (a, b) => {
168
- // @ts-expect-error
169
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
170
- return ensureNumber(a) % ensureNumber(b)
171
- },
172
-
173
- '^': (a, b) => {
174
- // @ts-expect-error
175
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
176
- return ensureNumber(a) ** ensureNumber(b)
177
- },
265
+ '*': numericOp((a, b) => a * b),
266
+ '+': numericOp((a, b) => a + b),
267
+ '-': numericOp((a, b) => a - b),
268
+ '/': numericOp((a, b) => a / b),
269
+ 'mod': numericOp((a, b) => a % b),
270
+ '^': numericOp((a, b) => a ** b),
178
271
 
179
272
  '&': (a, b) => castToString(a) + castToString(b),
180
273
 
@@ -188,42 +281,61 @@ export const defaultBinaryOperators: ExpressionBinaryOperators = {
188
281
  '>=': (a, b) =>
189
282
  ensureRelationalComparable(a) >= ensureRelationalComparable(b),
190
283
 
284
+ // Check if key exists in container (Object/Array/Map)
191
285
  'in': (a, b) => {
192
- if (isSimpleValue(a) && b != null && typeof b === 'object') {
193
- return Object.hasOwn(b, a as any)
194
- } else {
195
- throw new TypeError(
196
- `Cannot use "in" operator to search for ${typeOf(a)} key in ${typeOf(b)}`
197
- )
286
+ const bType = objToStringAlias.call(b)
287
+
288
+ switch (bType) {
289
+ case '[object Object]': {
290
+ return Object.hasOwn(b as object, a as any)
291
+ }
292
+
293
+ case '[object Array]': {
294
+ if (Number.isSafeInteger(a)) {
295
+ // @ts-ignore
296
+ return a in b
297
+ } else {
298
+ throw new TypeError(
299
+ `Wrong "in" operator usage - key value must be a safe integer`
300
+ )
301
+ }
302
+ }
303
+
304
+ case '[object Map]':
305
+ // @ts-ignore
306
+ return b.has(a) as boolean
307
+
308
+ default:
309
+ throw new TypeError(
310
+ `Cannot use "in" operator to ensure ${typeOf(a)} key in ${typeOf(b)}`
311
+ )
198
312
  }
199
313
  }
200
314
  }
201
315
 
202
- type LogicalOperatorFunction = (
316
+ export type LogicalOperatorFunction = (
203
317
  left: () => unknown,
204
318
  right: () => unknown
205
319
  ) => unknown
206
320
 
207
- type ExpressionLogicalOperators = Record<
321
+ export type ExpressionLogicalOperators = Record<
208
322
  LogicalExpression['operator'],
209
323
  LogicalOperatorFunction
210
324
  >
211
325
 
212
- const logicalAndOperatorFn: LogicalOperatorFunction = (a, b) =>
213
- castToBoolean(a()) && castToBoolean(b())
214
-
215
- const logicalOrOperatorFn: LogicalOperatorFunction = (a, b) =>
216
- castToBoolean(a()) || castToBoolean(b())
217
-
218
- export const defaultLogicalOperators: ExpressionLogicalOperators = {
219
- // TODO Use castToBoolean from compile options?
220
- 'and': logicalAndOperatorFn,
221
- '&&': logicalAndOperatorFn,
222
- 'or': logicalOrOperatorFn,
223
- '||': logicalOrOperatorFn
326
+ /** Create the default logical operator map (and/&&, or/||). */
327
+ export function createDefaultLogicalOperators(
328
+ bool: (val: unknown) => boolean
329
+ ): ExpressionLogicalOperators {
330
+ const and: LogicalOperatorFunction = (a, b) => bool(a()) && bool(b())
331
+ const or: LogicalOperatorFunction = (a, b) => bool(a()) || bool(b())
332
+ return { 'and': and, '&&': and, 'or': or, '||': or }
224
333
  }
225
334
 
226
- interface ExpressionOperators {
335
+ export const defaultLogicalOperators: ExpressionLogicalOperators =
336
+ createDefaultLogicalOperators(castToBoolean)
337
+
338
+ export interface ExpressionOperators {
227
339
  unaryOperators: Record<UnaryExpression['operator'], (val: unknown) => unknown>
228
340
  binaryOperators: Record<
229
341
  BinaryExpression['operator'],
@@ -235,408 +347,55 @@ interface ExpressionOperators {
235
347
  >
236
348
  }
237
349
 
238
- export interface SourceLocation {
239
- len: number
240
- location: Location
241
- }
242
-
243
- export interface VisitResult {
244
- code: string
245
- offsets: SourceLocation[]
246
- }
247
-
248
- type Visit = (node: Expression) => VisitResult[]
249
-
250
- const codePart = (
251
- codePart: string,
252
- ownerNode: { location: Location }
253
- ): VisitResult => ({
254
- code: codePart,
255
- offsets: [{ len: codePart.length, location: ownerNode.location }]
256
- })
257
-
258
- const combineVisitResults = (parts: VisitResult[]) => {
259
- return parts.reduce((res, it) => {
260
- return {
261
- code: res.code + it.code,
262
- offsets: res.offsets.concat(it.offsets)
263
- } as VisitResult
264
- })
265
- }
350
+ // --- Bootstrap Code ---
266
351
 
267
- const visitors: {
268
- [P in keyof ExpressionByType]: (
269
- node: ExpressionByType[P],
270
- visit: Visit
271
- ) => VisitResult[]
272
- } = {
273
- Literal: node => {
274
- const parts: VisitResult[] = [codePart(JSON.stringify(node.value), node)]
275
-
276
- return parts
277
- },
278
-
279
- Identifier: node => {
280
- const parts: VisitResult[] = [
281
- codePart(`get(scope,${JSON.stringify(node.name)})`, node)
282
- ]
283
-
284
- return parts
285
- },
286
-
287
- UnaryExpression: (node, visit) => {
288
- const parts: VisitResult[] = [
289
- codePart(`uop["${node.operator}"](`, node),
290
- ...visit(node.argument),
291
- codePart(')', node)
292
- ]
293
-
294
- return parts
295
- },
296
-
297
- BinaryExpression: (node, visit) => {
298
- const parts: VisitResult[] = [
299
- codePart(`bop["${node.operator}"](`, node),
300
- ...visit(node.left),
301
- codePart(',', node),
302
- ...visit(node.right),
303
- codePart(')', node)
304
- ]
305
-
306
- return parts
307
- },
308
-
309
- LogicalExpression: (node, visit) => {
310
- const parts: VisitResult[] = [
311
- codePart(`lop["${node.operator}"](()=>(`, node),
312
- ...visit(node.left),
313
- codePart('),()=>(', node),
314
- ...visit(node.right),
315
- codePart('))', node)
316
- ]
317
-
318
- return parts
319
- },
320
-
321
- ConditionalExpression: (node, visit) => {
322
- const parts: VisitResult[] = [
323
- codePart('(bool(', node),
324
- ...visit(node.test),
325
- codePart(')?', node),
326
- ...visit(node.consequent),
327
- codePart(':', node),
328
- ...(node.alternate !== null
329
- ? visit(node.alternate)
330
- : [codePart('undefined', node)]),
331
- codePart(')', node)
332
- ]
333
-
334
- return parts
335
- },
336
-
337
- ObjectExpression: (node, visit) => {
338
- const innerObj = node.properties
339
- .map((p): [VisitResult, VisitResult[]] => {
340
- if (p.key.type === 'Identifier') {
341
- return [codePart(p.key.name, p), visit(p.value)]
342
- }
343
- //
344
- else if (p.key.type === 'Literal') {
345
- // TODO look for ECMA spec
346
- return [codePart(JSON.stringify(p.key.value), p), visit(p.value)]
347
- }
348
- //
349
- else {
350
- // TODO Restrict on parse step
351
- // TODO Error with locations
352
- throw new TypeError(`Incorrect object key type ${p.key.type}`)
353
- }
354
- })
355
- .flatMap(([k, v]) => {
356
- return [k, codePart(':', node), ...v, codePart(',', node)]
357
- })
358
-
359
- // remove last comma
360
- if (innerObj.length > 1) {
361
- innerObj.pop()
362
- }
363
-
364
- const parts: VisitResult[] = [
365
- codePart('{', node),
366
- ...innerObj,
367
- codePart('}', node)
368
- ]
369
-
370
- return parts
371
- },
372
-
373
- ArrayExpression: (node, visit) => {
374
- const innerArrParts = node.elements.flatMap(el => {
375
- return el === null
376
- ? [codePart(',', node)]
377
- : [...visit(el), codePart(',', node)]
378
- })
379
-
380
- // remove last comma
381
- if (innerArrParts.length > 1) {
382
- innerArrParts.pop()
383
- }
384
-
385
- const parts: VisitResult[] = [
386
- codePart('[', node),
387
- ...innerArrParts,
388
- codePart(']', node)
389
- ]
390
-
391
- return parts
392
- },
393
-
394
- MemberExpression: (node, visit) => {
395
- const { computed, object, property } = node
396
-
397
- // TODO Pass computed to prop?
398
-
399
- const parts: VisitResult[] = [
400
- codePart('prop(', node),
401
- ...visit(object),
402
- codePart(',', node),
403
- ...(computed
404
- ? visit(property)
405
- : [codePart(JSON.stringify(property.name), property)]),
406
- codePart(')', node)
407
- ]
408
-
409
- return parts
410
- },
411
-
412
- CallExpression: (node, visit) => {
413
- if (node.arguments.length > 0) {
414
- const innerArgs = node.arguments.flatMap((arg, index) => [
415
- ...(arg.type === 'CurryPlaceholder'
416
- ? [codePart(`a${index}`, arg)]
417
- : visit(arg)),
418
- codePart(',', node)
419
- ])
420
-
421
- const curriedArgs = node.arguments.flatMap((arg, index) =>
422
- arg.type === 'CurryPlaceholder' ? [`a${index}`] : []
423
- )
424
-
425
- // remove last comma
426
- innerArgs?.pop()
427
-
428
- // call({{callee}},[{{arguments}}])
429
- let parts: VisitResult[] = [
430
- codePart('call(', node),
431
- ...visit(node.callee),
432
- codePart(',[', node),
433
- ...innerArgs,
434
- codePart('])', node)
435
- ]
436
-
437
- if (curriedArgs.length > 0) {
438
- parts = [
439
- codePart(`(scope=>(${curriedArgs.join()})=>`, node),
440
- ...parts,
441
- codePart(')(scope)', node)
442
- ]
443
- }
444
-
445
- return parts
446
- }
447
-
448
- //
449
- else {
450
- const parts: VisitResult[] = [
451
- codePart('call(', node),
452
- ...visit(node.callee),
453
- codePart(',null)', node)
454
- ]
455
-
456
- return parts
457
- }
458
- },
459
-
460
- NullishCoalescingExpression: (node, visit) => {
461
- const parts: VisitResult[] = [
462
- codePart('(', node),
463
- ...visit(node.left),
464
- codePart('??', node),
465
- ...visit(node.right),
466
- codePart(')', node)
467
- ]
468
-
469
- return parts
470
- },
471
-
472
- PipeSequence: (node, visit) => {
473
- const headCode = visit(node.head)
474
-
475
- const tailsCodeArrInner = node.tail.flatMap(t => {
476
- const opt = t.operator === '|?'
477
-
478
- const tailParts: VisitResult[] = [
479
- codePart(
480
- `{opt:${opt},next:(scope=>topic=>{scope=[["%"],[topic],scope];return `,
481
- t.expression
482
- ),
483
- ...visit(t.expression),
484
- codePart(`})(scope)}`, t.expression),
485
- codePart(`,`, t.expression)
486
- ]
487
-
488
- return tailParts
489
- })
490
-
491
- // remove last comma
492
- tailsCodeArrInner.pop()
493
-
494
- const parts: VisitResult[] = [
495
- codePart('pipe(', node),
496
- ...headCode,
497
- codePart(',[', node),
498
- ...tailsCodeArrInner,
499
- codePart('])', node)
500
- ]
501
-
502
- return parts
503
- },
504
-
505
- TopicReference: node => {
506
- const parts: VisitResult[] = [codePart(`get(scope,"${TOPIC_TOKEN}")`, node)]
507
- return parts
508
- },
509
-
510
- LambdaExpression: (node, visit) => {
511
- // Lambda with parameters
512
- if (node.params.length > 0) {
513
- const paramsNames = node.params.map(p => p.name)
514
-
515
- const fnParams = Array.from(
516
- { length: paramsNames.length },
517
- (_, index) => `p${index}`
518
- )
519
-
520
- const fnParamsList = fnParams.join()
521
- const fnParamsNamesList = paramsNames.map(p => JSON.stringify(p)).join()
522
-
523
- // TODO Is "...args" more performant?
524
- // (params => function (p0, p1) {
525
- // var scope = [params, [p0, p1], scope]
526
- // return {{code}}
527
- // })(["a", "b"])
528
- const parts: VisitResult[] = [
529
- codePart(
530
- `((scope,params)=>function(${fnParamsList}){scope=[params,[${fnParamsList}],scope];return `,
531
- node
532
- ),
533
- ...visit(node.expression),
534
- codePart(`})(scope,[${fnParamsNamesList}])`, node)
535
- ]
536
-
537
- return parts
538
- }
539
-
540
- // Lambda without parameters
541
- else {
542
- // (() => {{code}})
543
- const parts: VisitResult[] = [
544
- codePart(`(()=>`, node),
545
- ...visit(node.expression),
546
- codePart(`)`, node)
547
- ]
548
-
549
- return parts
550
- }
551
- },
552
-
553
- LetExpression: (node, visit) => {
554
- const declarationsNamesSet = new Set()
555
-
556
- for (const d of node.declarations) {
557
- if (declarationsNamesSet.has(d.id.name)) {
558
- throw new CompileError(
559
- `"${d.id.name}" name defined inside let expression was repeated`,
560
- '',
561
- d.id.location
562
- )
563
- }
564
- declarationsNamesSet.add(d.id.name)
565
- }
566
-
567
- // (scope=> {
568
- // var _varNames = [];
569
- // var _varValues = [];
570
- // scope = [_varNames, _varValues, scope];
571
-
572
- // // a = {{init}}
573
- // _varNames.push("a");
574
- // _varValues.push({{init}});
575
-
576
- // // {{expression}}
577
- // return {{expression}}
578
- // })(scope)
579
-
580
- const parts: VisitResult[] = [
581
- codePart(
582
- `(scope=>{var _varNames=[];var _varValues=[];scope=[_varNames,_varValues,scope];`,
583
- node
584
- ),
585
- ...node.declarations.flatMap(d => [
586
- codePart(`_varValues.push(`, d),
587
- ...visit(d.init),
588
- codePart(`);`, d),
589
- codePart(`_varNames.push(`, d),
590
- codePart(JSON.stringify(d.id.name), d.id),
591
- codePart(`);`, d)
592
- ]),
593
- codePart(`return `, node),
594
- ...visit(node.expression),
595
- codePart(`})(scope)`, node)
596
- ]
597
-
598
- return parts
599
- }
600
- }
601
-
602
- const visit: (
603
- node: Expression,
604
- parentNode: Expression | null
605
- ) => VisitResult[] = node => {
606
- const nodeTypeVisitor = visitors[node.type]
607
-
608
- if (nodeTypeVisitor === undefined) {
609
- throw new Error(`No handler for node type - ${node.type}`)
610
- }
611
-
612
- const innerVisit: Visit = (childNode: Expression) => {
613
- return visit(childNode, node)
614
- }
352
+ const bootstrapCodeHead =
353
+ `
354
+ var ${GEN.bool}=ctx.castToBoolean;
355
+ var ${GEN.str}=ctx.castToString;
356
+ var ${GEN.bop}=ctx.binaryOperators;
357
+ var ${GEN.lop}=ctx.logicalOperators;
358
+ var ${GEN.uop}=ctx.unaryOperators;
359
+ var ${GEN.call}=ctx.callFunction;
360
+ var ${GEN.ensObj}=ctx.ensureObject;
361
+ var ${GEN.ensArr}=ctx.ensureArray;
362
+ var ${GEN.getIdentifierValue}=ctx.getIdentifierValue;
363
+ var ${GEN.prop}=ctx.getProperty;
364
+ var ${GEN.pipe}=ctx.pipe;
365
+ var ${GEN.nna}=ctx.nonNullAssert;
366
+ var ${GEN.globals}=ctx.globals??null;
367
+
368
+ function ${GEN._get}(${GEN._scope},name){
369
+ if(${GEN._scope}===null)return ${GEN.getIdentifierValue}(name,${GEN.globals},this);
370
+ var paramIndex=${GEN._scope}[${SCOPE_NAMES}].findIndex(it=>it===name);
371
+ if(paramIndex===-1)return ${GEN._get}.call(this,${GEN._scope}[${SCOPE_PARENT}],name);
372
+ return ${GEN._scope}[${SCOPE_VALUES}][paramIndex]
373
+ };
615
374
 
616
- // @ts-expect-error skip node is never
617
- return nodeTypeVisitor(node, innerVisit)
618
- }
375
+ return data=>{
376
+ var ${GEN.scope}=null;
377
+ var ${GEN.get}=${GEN._get}.bind(data);
378
+ return
379
+ `
380
+ .split('\n')
381
+ .map(it => it.trim())
382
+ .filter(it => it !== '')
383
+ .join('') + ' '
619
384
 
620
- export function traverse(tree: ExpressionStatement): VisitResult {
621
- return combineVisitResults(visit(tree.expression, null))
622
- }
385
+ const bootstrapCodeHeadLen = bootstrapCodeHead.length
623
386
 
624
- function getExpressionErrorLocation(
625
- colOffset: number,
626
- locations: SourceLocation[]
627
- ): Location | null {
628
- var curCol = 0
629
- for (const loc of locations) {
630
- curCol += loc.len
631
- if (curCol >= colOffset) return loc.location
632
- }
633
- return null
634
- }
387
+ // --- Compile ---
635
388
 
636
389
  export type CompileOptions<Data, Globals> = Partial<
637
- ContextHelpers<Data, Globals> & ExpressionOperators & { globals: Globals }
390
+ ContextHelpers<Data, Globals> &
391
+ ExpressionOperators & {
392
+ globals: Globals
393
+ extensions: Map<string | object | Function, Record<string, Function>>
394
+ errorMapper: ErrorMapper | null
395
+ }
638
396
  >
639
397
 
398
+ /** Compile a SimplEx expression string into an executable function. */
640
399
  export function compile<
641
400
  Data = Record<string, unknown>,
642
401
  Globals = Record<string, unknown>
@@ -645,111 +404,75 @@ export function compile<
645
404
  options?: CompileOptions<Data, Globals>
646
405
  ): (data?: Data) => unknown {
647
406
  const tree = parse(expression) as ExpressionStatement
648
- let traverseResult
649
-
650
- try {
651
- traverseResult = traverse(tree)
652
- } catch (err) {
653
- // TODO Use class to access expression from visitors?
654
- if (err instanceof CompileError) {
655
- err.expression = expression
656
- }
657
- throw err
658
- }
407
+ const traverseResult = traverse(tree, expression)
659
408
 
660
409
  const { code: expressionCode, offsets } = traverseResult
661
410
 
662
- const bootstrapCodeHead =
663
- `
664
- var bool=ctx.castToBoolean;
665
- var bop=ctx.binaryOperators;
666
- var lop=ctx.logicalOperators;
667
- var uop=ctx.unaryOperators;
668
- var call=ctx.callFunction;
669
- var getIdentifierValue=ctx.getIdentifierValue;
670
- var prop=ctx.getProperty;
671
- var pipe=ctx.pipe;
672
- var globals=ctx.globals??null;
673
-
674
- function _get(_scope,name){
675
- if(_scope===null)return getIdentifierValue(name,globals,this);
676
- var paramIndex=_scope[0].findIndex(it=>it===name);
677
- if(paramIndex===-1)return _get.call(this,_scope[2],name);
678
- return _scope[1][paramIndex]
679
- };
680
-
681
- return data=>{
682
- var scope=null;
683
- var get=_get.bind(data);
684
- return
685
- `
686
- .split('\n')
687
- .map(it => it.trim())
688
- .filter(it => it !== '')
689
- .join('') + ' '
690
-
691
- const bootstrapCodeHeadLen = bootstrapCodeHead.length
692
-
693
411
  const functionCode = bootstrapCodeHead + expressionCode + '}'
694
412
 
695
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
413
+ const resolvedBool = options?.castToBoolean ?? castToBoolean
414
+
696
415
  const defaultOptions: CompileOptions<Data, Globals> = {
697
416
  ...defaultContextHelpers,
417
+ // Recreate operators with custom castToBoolean so compile options
418
+ // are honored by logical (and/or) and unary (not) operators
698
419
  ...{
699
- unaryOperators: defaultUnaryOperators,
420
+ unaryOperators: options?.castToBoolean
421
+ ? createDefaultUnaryOperators(resolvedBool)
422
+ : defaultUnaryOperators,
700
423
  binaryOperators: defaultBinaryOperators,
701
- logicalOperators: defaultLogicalOperators
424
+ logicalOperators: options?.castToBoolean
425
+ ? createDefaultLogicalOperators(resolvedBool)
426
+ : defaultLogicalOperators
702
427
  },
703
428
  ...(options as any)
704
429
  }
705
430
 
431
+ if (options?.extensions && options.extensions.size > 0 && !options.getProperty) {
432
+ const extensionMap = options.extensions
433
+ const classesKeys: (object | Function)[] = []
434
+ const classesValues: Record<string, Function>[] = []
435
+
436
+ for (const [key, methods] of extensionMap) {
437
+ if (typeof key !== 'string') {
438
+ classesKeys.push(key)
439
+ classesValues.push(methods)
440
+ }
441
+ }
442
+
443
+ defaultOptions.getProperty = (obj, key, extension) => {
444
+ if (obj == null) return undefined
445
+ if (extension)
446
+ return getExtensionMethod(
447
+ obj,
448
+ key,
449
+ extensionMap,
450
+ classesKeys,
451
+ classesValues
452
+ )
453
+ return defaultGetProperty(obj, key, false)
454
+ }
455
+ }
456
+
706
457
  const func = new Function('ctx', functionCode)(defaultOptions) as (
707
458
  data?: Data
708
459
  ) => unknown
709
460
 
461
+ const errorMapper =
462
+ options?.errorMapper !== undefined
463
+ ? options.errorMapper
464
+ : getActiveErrorMapper()
465
+
466
+ if (errorMapper === null) return func
467
+
710
468
  return function (data?: Data) {
711
469
  try {
712
470
  return func(data)
713
471
  } catch (err) {
714
- if (err instanceof Error === false) throw err
715
-
716
- const stackRows = err.stack?.split('\n').map(row => row.trim())
717
-
718
- const evalRow = stackRows?.find(row => row.startsWith('at eval '))
719
-
720
- if (evalRow === undefined) {
721
- throw err
722
- }
723
-
724
- ERROR_STACK_REGEX.lastIndex = 0
725
- const match = ERROR_STACK_REGEX.exec(evalRow)
726
-
727
- if (match == null) {
728
- throw err
729
- }
730
-
731
- const rowOffsetStr = match.groups?.['row']
732
- const colOffsetStr = match.groups?.['col']
733
-
734
- if (rowOffsetStr === undefined || colOffsetStr === undefined) {
735
- throw err
736
- }
737
-
738
- const rowOffset = Number.parseInt(rowOffsetStr)
739
- assert.equal(rowOffset, 3)
740
-
741
- const colOffset = Number.parseInt(colOffsetStr)
742
- const adjustedColOffset = colOffset - bootstrapCodeHeadLen
743
- assert.ok(adjustedColOffset >= 0)
744
-
745
- const errorLocation = getExpressionErrorLocation(
746
- adjustedColOffset,
747
- offsets
472
+ throw (
473
+ errorMapper.mapError(err, expression, offsets, bootstrapCodeHeadLen) ??
474
+ err
748
475
  )
749
-
750
- throw new ExpressionError(err.message, expression, errorLocation, {
751
- cause: err
752
- })
753
476
  }
754
477
  }
755
478
  }