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