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
@@ -1,118 +1,144 @@
1
1
  /* eslint-disable @typescript-eslint/ban-ts-comment */
2
2
  // eslint-disable-next-line n/no-missing-import
3
3
  import { parse } from '../parser/index.js';
4
- import { CompileError, ExpressionError, UnexpectedTypeError } from './errors.js';
5
- import assert from 'node:assert';
6
- import { castToBoolean } from './tools/cast.js';
7
- import { ensureFunction, ensureRelationalComparable, ensureNumber } from './tools/ensure.js';
8
- import { isSimpleValue } from './tools/guards.js';
9
- import { castToString, objToStringAlias, typeOf } from './tools/index.js';
4
+ import { ExpressionError, UnexpectedTypeError } from './errors.js';
5
+ import { getActiveErrorMapper, getExpressionErrorLocation } from './error-mapping.js';
6
+ import { castToBoolean, castToString, ensureFunction, ensureNumber, ensureArray, ensureObject, ensureRelationalComparable, isSimpleValue, objToStringAlias, typeOf } from './tools/index.js';
7
+ import { traverse } from './visitors.js';
8
+ import { GEN, SCOPE_NAMES, SCOPE_VALUES, SCOPE_PARENT } from './constants.js';
9
+ export { traverse, getExpressionErrorLocation };
10
10
  var hasOwn = Object.hasOwn;
11
- var ERROR_STACK_REGEX = /<anonymous>:(?<row>\d+):(?<col>\d+)/g;
12
- var TOPIC_TOKEN = '%';
13
- const defaultContextHelpers = {
14
- castToBoolean,
15
- ensureFunction,
16
- getIdentifierValue: (identifierName, globals, data) => {
17
- // TODO Should test on parse time?
18
- if (identifierName === TOPIC_TOKEN) {
19
- throw new Error(`Topic reference "${TOPIC_TOKEN}" is unbound; it must be inside a pipe body.`);
20
- }
21
- if (identifierName === 'undefined')
22
- return undefined;
23
- if (globals != null && Object.hasOwn(globals, identifierName)) {
24
- return globals[identifierName];
25
- }
26
- if (data != null && Object.hasOwn(data, identifierName)) {
27
- return data[identifierName];
28
- }
29
- throw new Error(`Unknown identifier - ${identifierName}`);
30
- },
31
- getProperty(obj, key) {
32
- if (obj == null)
33
- return undefined;
34
- const typeofObj = typeof obj;
35
- if (typeofObj === 'string' && typeof key === 'number') {
36
- return obj[key];
37
- }
38
- if (typeofObj !== 'object') {
39
- throw new UnexpectedTypeError(['object'], obj);
40
- }
41
- if (isSimpleValue(key) === false) {
42
- throw new UnexpectedTypeError(['simple type object key'], key);
43
- }
44
- if (hasOwn(obj, key)) {
45
- // @ts-expect-error Type cannot be used as an index type
46
- return obj[key];
47
- }
48
- if (obj instanceof Map) {
49
- return obj.get(key);
11
+ /** Look up an identifier in globals first, then data; throw on miss. */
12
+ function defaultGetIdentifierValue(identifierName, globals, data) {
13
+ if (identifierName === 'undefined')
14
+ return undefined;
15
+ if (globals != null && Object.hasOwn(globals, identifierName)) {
16
+ return globals[identifierName];
17
+ }
18
+ if (data != null && Object.hasOwn(data, identifierName)) {
19
+ return data[identifierName];
20
+ }
21
+ throw new Error(`Unknown identifier - ${identifierName}`);
22
+ }
23
+ /** Look up an extension method for the given object type and bind obj as first argument. */
24
+ function getExtensionMethod(obj, key, extensionMap, classesKeys, classesValues) {
25
+ var typeofObj = typeof obj;
26
+ var methods;
27
+ if (typeofObj === 'object') {
28
+ for (var i = 0; i < classesKeys.length; i++) {
29
+ // @ts-expect-error supports objects with Symbol.hasInstance
30
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
31
+ if (obj instanceof classesKeys[i]) {
32
+ methods = classesValues[i];
33
+ break;
34
+ }
50
35
  }
36
+ }
37
+ else {
38
+ methods = extensionMap.get(typeofObj);
39
+ }
40
+ if (methods === undefined) {
41
+ throw new TypeError(`No extension methods defined for type "${typeofObj}"`);
42
+ }
43
+ var method = methods[key];
44
+ if (method === undefined) {
45
+ throw new TypeError(`Extension method "${String(key)}" is not defined for type "${typeofObj}"`);
46
+ }
47
+ return method.bind(null, obj);
48
+ }
49
+ /** Resolve property access on an object, Map, or string (null-safe). */
50
+ function defaultGetProperty(obj, key, extension) {
51
+ if (obj == null)
51
52
  return undefined;
52
- },
53
- callFunction(fn, args) {
54
- return fn == null
55
- ? undefined
56
- : (args === null
57
- ? ensureFunction(fn)()
58
- : ensureFunction(fn).apply(null, args));
59
- },
60
- pipe(head, tail) {
61
- var result = head;
62
- for (const it of tail) {
63
- if (it.opt && result == null)
64
- return result;
65
- result = it.next(result);
53
+ if (extension) {
54
+ throw new ExpressionError('Extension member expression (::) is reserved and not implemented', '', null);
55
+ }
56
+ var typeofObj = typeof obj;
57
+ if (typeofObj === 'string' && typeof key === 'number') {
58
+ return obj[key];
59
+ }
60
+ if (typeofObj !== 'object') {
61
+ throw new UnexpectedTypeError(['object'], obj);
62
+ }
63
+ if (isSimpleValue(key) === false) {
64
+ throw new UnexpectedTypeError(['simple type object key'], key);
65
+ }
66
+ if (hasOwn(obj, key)) {
67
+ // @ts-expect-error Type cannot be used as an index type
68
+ return obj[key];
69
+ }
70
+ if (obj instanceof Map) {
71
+ return obj.get(key);
72
+ }
73
+ return undefined;
74
+ }
75
+ /** Call a function value; null/undefined silently returns undefined. */
76
+ function defaultCallFunction(fn, args) {
77
+ return fn == null
78
+ ? undefined
79
+ : (args === null
80
+ ? ensureFunction(fn)()
81
+ : ensureFunction(fn).apply(null, args));
82
+ }
83
+ /** Assert that a value is not null or undefined; throw on null/undefined. */
84
+ function defaultNonNullAssert(val) {
85
+ if (val == null) {
86
+ throw new ExpressionError('Non-null assertion failed: value is ' +
87
+ (val === null ? 'null' : 'undefined'), '', null);
88
+ }
89
+ return val;
90
+ }
91
+ /** Execute a pipe sequence, threading each result through the next step. */
92
+ function defaultPipe(head, tail) {
93
+ var result = head;
94
+ for (const it of tail) {
95
+ if (it.fwd) {
96
+ throw new ExpressionError('Pipe forward operator (|>) is reserved and not implemented', '', null);
66
97
  }
67
- return result;
98
+ if (it.opt && result == null)
99
+ return result;
100
+ result = it.next(result);
68
101
  }
102
+ return result;
103
+ }
104
+ const defaultContextHelpers = {
105
+ castToBoolean,
106
+ castToString,
107
+ ensureFunction,
108
+ ensureObject,
109
+ ensureArray,
110
+ nonNullAssert: defaultNonNullAssert,
111
+ getIdentifierValue: defaultGetIdentifierValue,
112
+ getProperty: defaultGetProperty,
113
+ callFunction: defaultCallFunction,
114
+ pipe: defaultPipe
69
115
  };
70
- export const defaultUnaryOperators = {
71
- '+': val => ensureNumber(val),
72
- '-': val => -ensureNumber(val),
73
- 'not': val => !castToBoolean(val),
74
- 'typeof': val => typeof val
75
- };
116
+ /** Create the default unary operator map (+, -, not, typeof). */
117
+ export function createDefaultUnaryOperators(bool) {
118
+ return {
119
+ '+': val => ensureNumber(val),
120
+ '-': val => -ensureNumber(val),
121
+ 'not': val => !bool(val),
122
+ 'typeof': val => typeof val
123
+ };
124
+ }
125
+ export const defaultUnaryOperators = createDefaultUnaryOperators(castToBoolean);
126
+ const numericOp = (fn) => (a, b) => fn(ensureNumber(a), ensureNumber(b));
76
127
  export const defaultBinaryOperators = {
77
128
  '!=': (a, b) => a !== b,
78
129
  '==': (a, b) => a === b,
79
- // TIPS give the opportunity to get a base js error
80
- '*': (a, b) => {
81
- // @ts-expect-error
82
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
83
- return ensureNumber(a) * ensureNumber(b);
84
- },
85
- '+': (a, b) => {
86
- // @ts-expect-error
87
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/restrict-plus-operands
88
- return ensureNumber(a) + ensureNumber(b);
89
- },
90
- '-': (a, b) => {
91
- // @ts-expect-error
92
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
93
- return ensureNumber(a) - ensureNumber(b);
94
- },
95
- '/': (a, b) => {
96
- // @ts-expect-error
97
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
98
- return ensureNumber(a) / ensureNumber(b);
99
- },
100
- 'mod': (a, b) => {
101
- // @ts-expect-error
102
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
103
- return ensureNumber(a) % ensureNumber(b);
104
- },
105
- '^': (a, b) => {
106
- // @ts-expect-error
107
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
108
- return ensureNumber(a) ** ensureNumber(b);
109
- },
130
+ '*': numericOp((a, b) => a * b),
131
+ '+': numericOp((a, b) => a + b),
132
+ '-': numericOp((a, b) => a - b),
133
+ '/': numericOp((a, b) => a / b),
134
+ 'mod': numericOp((a, b) => a % b),
135
+ '^': numericOp((a, b) => a ** b),
110
136
  '&': (a, b) => castToString(a) + castToString(b),
111
137
  '<': (a, b) => ensureRelationalComparable(a) < ensureRelationalComparable(b),
112
138
  '<=': (a, b) => ensureRelationalComparable(a) <= ensureRelationalComparable(b),
113
139
  '>': (a, b) => ensureRelationalComparable(a) > ensureRelationalComparable(b),
114
140
  '>=': (a, b) => ensureRelationalComparable(a) >= ensureRelationalComparable(b),
115
- // Is some container has specified key
141
+ // Check if key exists in container (Object/Array/Map)
116
142
  'in': (a, b) => {
117
143
  const bType = objToStringAlias.call(b);
118
144
  switch (bType) {
@@ -125,7 +151,7 @@ export const defaultBinaryOperators = {
125
151
  return a in b;
126
152
  }
127
153
  else {
128
- throw new TypeError(`Wrong "in" operator usage - key value should to be safe integer`);
154
+ throw new TypeError(`Wrong "in" operator usage - key value must be a safe integer`);
129
155
  }
130
156
  }
131
157
  case '[object Map]':
@@ -136,391 +162,99 @@ export const defaultBinaryOperators = {
136
162
  }
137
163
  }
138
164
  };
139
- const logicalAndOperatorFn = (a, b) => castToBoolean(a()) && castToBoolean(b());
140
- const logicalOrOperatorFn = (a, b) => castToBoolean(a()) || castToBoolean(b());
141
- export const defaultLogicalOperators = {
142
- // TODO Use castToBoolean from compile options?
143
- 'and': logicalAndOperatorFn,
144
- '&&': logicalAndOperatorFn,
145
- 'or': logicalOrOperatorFn,
146
- '||': logicalOrOperatorFn
147
- };
148
- const codePart = (codePart, ownerNode) => ({
149
- code: codePart,
150
- offsets: [{ len: codePart.length, location: ownerNode.location }]
151
- });
152
- const combineVisitResults = (parts) => {
153
- return parts.reduce((res, it) => {
154
- return {
155
- code: res.code + it.code,
156
- offsets: res.offsets.concat(it.offsets)
157
- };
158
- });
159
- };
160
- const visitors = {
161
- Literal: node => {
162
- const parts = [codePart(JSON.stringify(node.value), node)];
163
- return parts;
164
- },
165
- Identifier: node => {
166
- const parts = [
167
- codePart(`get(scope,${JSON.stringify(node.name)})`, node)
168
- ];
169
- return parts;
170
- },
171
- UnaryExpression: (node, visit) => {
172
- const parts = [
173
- codePart(`uop["${node.operator}"](`, node),
174
- ...visit(node.argument),
175
- codePart(')', node)
176
- ];
177
- return parts;
178
- },
179
- BinaryExpression: (node, visit) => {
180
- const parts = [
181
- codePart(`bop["${node.operator}"](`, node),
182
- ...visit(node.left),
183
- codePart(',', node),
184
- ...visit(node.right),
185
- codePart(')', node)
186
- ];
187
- return parts;
188
- },
189
- LogicalExpression: (node, visit) => {
190
- const parts = [
191
- codePart(`lop["${node.operator}"](()=>(`, node),
192
- ...visit(node.left),
193
- codePart('),()=>(', node),
194
- ...visit(node.right),
195
- codePart('))', node)
196
- ];
197
- return parts;
198
- },
199
- ConditionalExpression: (node, visit) => {
200
- const parts = [
201
- codePart('(bool(', node),
202
- ...visit(node.test),
203
- codePart(')?', node),
204
- ...visit(node.consequent),
205
- codePart(':', node),
206
- ...(node.alternate !== null
207
- ? visit(node.alternate)
208
- : [codePart('undefined', node)]),
209
- codePart(')', node)
210
- ];
211
- return parts;
212
- },
213
- ObjectExpression: (node, visit) => {
214
- const innerObj = node.properties
215
- .map((p) => {
216
- if (p.key.type === 'Identifier') {
217
- return [codePart(p.key.name, p), visit(p.value)];
218
- }
219
- //
220
- else if (p.key.type === 'Literal') {
221
- // TODO look for ECMA spec
222
- return [codePart(JSON.stringify(p.key.value), p), visit(p.value)];
223
- }
224
- //
225
- else {
226
- // TODO Restrict on parse step
227
- // TODO Error with locations
228
- throw new TypeError(`Incorrect object key type ${p.key.type}`);
229
- }
230
- })
231
- .flatMap(([k, v]) => {
232
- return [k, codePart(':', node), ...v, codePart(',', node)];
233
- });
234
- // remove last comma
235
- if (innerObj.length > 1) {
236
- innerObj.pop();
237
- }
238
- const parts = [
239
- codePart('{', node),
240
- ...innerObj,
241
- codePart('}', node)
242
- ];
243
- return parts;
244
- },
245
- ArrayExpression: (node, visit) => {
246
- const innerArrParts = node.elements.flatMap(el => {
247
- return el === null
248
- ? [codePart(',', node)]
249
- : [...visit(el), codePart(',', node)];
250
- });
251
- // remove last comma
252
- if (innerArrParts.length > 1) {
253
- innerArrParts.pop();
254
- }
255
- const parts = [
256
- codePart('[', node),
257
- ...innerArrParts,
258
- codePart(']', node)
259
- ];
260
- return parts;
261
- },
262
- MemberExpression: (node, visit) => {
263
- const { computed, object, property } = node;
264
- // TODO Pass computed to prop?
265
- const parts = [
266
- codePart('prop(', node),
267
- ...visit(object),
268
- codePart(',', node),
269
- ...(computed
270
- ? visit(property)
271
- : [codePart(JSON.stringify(property.name), property)]),
272
- codePart(')', node)
273
- ];
274
- return parts;
275
- },
276
- CallExpression: (node, visit) => {
277
- if (node.arguments.length > 0) {
278
- const innerArgs = node.arguments.flatMap((arg, index) => [
279
- ...(arg.type === 'CurryPlaceholder'
280
- ? [codePart(`a${index}`, arg)]
281
- : visit(arg)),
282
- codePart(',', node)
283
- ]);
284
- const curriedArgs = node.arguments.flatMap((arg, index) => arg.type === 'CurryPlaceholder' ? [`a${index}`] : []);
285
- // remove last comma
286
- innerArgs?.pop();
287
- // call({{callee}},[{{arguments}}])
288
- let parts = [
289
- codePart('call(', node),
290
- ...visit(node.callee),
291
- codePart(',[', node),
292
- ...innerArgs,
293
- codePart('])', node)
294
- ];
295
- if (curriedArgs.length > 0) {
296
- parts = [
297
- codePart(`(scope=>(${curriedArgs.join()})=>`, node),
298
- ...parts,
299
- codePart(')(scope)', node)
300
- ];
301
- }
302
- return parts;
303
- }
304
- //
305
- else {
306
- const parts = [
307
- codePart('call(', node),
308
- ...visit(node.callee),
309
- codePart(',null)', node)
310
- ];
311
- return parts;
312
- }
313
- },
314
- NullishCoalescingExpression: (node, visit) => {
315
- const parts = [
316
- codePart('(', node),
317
- ...visit(node.left),
318
- codePart('??', node),
319
- ...visit(node.right),
320
- codePart(')', node)
321
- ];
322
- return parts;
323
- },
324
- PipeSequence: (node, visit) => {
325
- const headCode = visit(node.head);
326
- const tailsCodeArrInner = node.tail.flatMap(t => {
327
- const opt = t.operator === '|?';
328
- const tailParts = [
329
- codePart(`{opt:${opt},next:(scope=>topic=>{scope=[["%"],[topic],scope];return `, t.expression),
330
- ...visit(t.expression),
331
- codePart(`})(scope)}`, t.expression),
332
- codePart(`,`, t.expression)
333
- ];
334
- return tailParts;
335
- });
336
- // remove last comma
337
- tailsCodeArrInner.pop();
338
- const parts = [
339
- codePart('pipe(', node),
340
- ...headCode,
341
- codePart(',[', node),
342
- ...tailsCodeArrInner,
343
- codePart('])', node)
344
- ];
345
- return parts;
346
- },
347
- TopicReference: node => {
348
- const parts = [codePart(`get(scope,"${TOPIC_TOKEN}")`, node)];
349
- return parts;
350
- },
351
- LambdaExpression: (node, visit) => {
352
- // Lambda with parameters
353
- if (node.params.length > 0) {
354
- const paramsNames = node.params.map(p => p.name);
355
- const fnParams = Array.from({ length: paramsNames.length }, (_, index) => `p${index}`);
356
- const fnParamsList = fnParams.join();
357
- const fnParamsNamesList = paramsNames.map(p => JSON.stringify(p)).join();
358
- // TODO Is "...args" more performant?
359
- // (params => function (p0, p1) {
360
- // var scope = [params, [p0, p1], scope]
361
- // return {{code}}
362
- // })(["a", "b"])
363
- const parts = [
364
- codePart(`((scope,params)=>function(${fnParamsList}){scope=[params,[${fnParamsList}],scope];return `, node),
365
- ...visit(node.expression),
366
- codePart(`})(scope,[${fnParamsNamesList}])`, node)
367
- ];
368
- return parts;
369
- }
370
- // Lambda without parameters
371
- else {
372
- // (() => {{code}})
373
- const parts = [
374
- codePart(`(()=>`, node),
375
- ...visit(node.expression),
376
- codePart(`)`, node)
377
- ];
378
- return parts;
379
- }
380
- },
381
- LetExpression: (node, visit) => {
382
- const declarationsNamesSet = new Set();
383
- for (const d of node.declarations) {
384
- if (declarationsNamesSet.has(d.id.name)) {
385
- throw new CompileError(`"${d.id.name}" name defined inside let expression was repeated`, '', d.id.location);
386
- }
387
- declarationsNamesSet.add(d.id.name);
388
- }
389
- // (scope=> {
390
- // var _varNames = [];
391
- // var _varValues = [];
392
- // scope = [_varNames, _varValues, scope];
393
- // // a = {{init}}
394
- // _varNames.push("a");
395
- // _varValues.push({{init}});
396
- // // {{expression}}
397
- // return {{expression}}
398
- // })(scope)
399
- const parts = [
400
- codePart(`(scope=>{var _varNames=[];var _varValues=[];scope=[_varNames,_varValues,scope];`, node),
401
- ...node.declarations.flatMap(d => [
402
- codePart(`_varValues.push(`, d),
403
- ...visit(d.init),
404
- codePart(`);`, d),
405
- codePart(`_varNames.push(`, d),
406
- codePart(JSON.stringify(d.id.name), d.id),
407
- codePart(`);`, d)
408
- ]),
409
- codePart(`return `, node),
410
- ...visit(node.expression),
411
- codePart(`})(scope)`, node)
412
- ];
413
- return parts;
414
- }
415
- };
416
- const visit = node => {
417
- const nodeTypeVisitor = visitors[node.type];
418
- if (nodeTypeVisitor === undefined) {
419
- throw new Error(`No handler for node type - ${node.type}`);
420
- }
421
- const innerVisit = (childNode) => {
422
- return visit(childNode, node);
423
- };
424
- // @ts-expect-error skip node is never
425
- return nodeTypeVisitor(node, innerVisit);
426
- };
427
- export function traverse(tree) {
428
- return combineVisitResults(visit(tree.expression, null));
165
+ /** Create the default logical operator map (and/&&, or/||). */
166
+ export function createDefaultLogicalOperators(bool) {
167
+ const and = (a, b) => bool(a()) && bool(b());
168
+ const or = (a, b) => bool(a()) || bool(b());
169
+ return { 'and': and, '&&': and, 'or': or, '||': or };
429
170
  }
430
- function getExpressionErrorLocation(colOffset, locations) {
431
- var curCol = 0;
432
- for (const loc of locations) {
433
- curCol += loc.len;
434
- if (curCol >= colOffset)
435
- return loc.location;
436
- }
437
- return null;
438
- }
439
- export function compile(expression, options) {
440
- const tree = parse(expression);
441
- let traverseResult;
442
- try {
443
- traverseResult = traverse(tree);
444
- }
445
- catch (err) {
446
- // TODO Use class to access expression from visitors?
447
- if (err instanceof CompileError) {
448
- err.expression = expression;
449
- }
450
- throw err;
451
- }
452
- const { code: expressionCode, offsets } = traverseResult;
453
- const bootstrapCodeHead = `
454
- var bool=ctx.castToBoolean;
455
- var bop=ctx.binaryOperators;
456
- var lop=ctx.logicalOperators;
457
- var uop=ctx.unaryOperators;
458
- var call=ctx.callFunction;
459
- var getIdentifierValue=ctx.getIdentifierValue;
460
- var prop=ctx.getProperty;
461
- var pipe=ctx.pipe;
462
- var globals=ctx.globals??null;
171
+ export const defaultLogicalOperators = createDefaultLogicalOperators(castToBoolean);
172
+ // --- Bootstrap Code ---
173
+ const bootstrapCodeHead = `
174
+ var ${GEN.bool}=ctx.castToBoolean;
175
+ var ${GEN.str}=ctx.castToString;
176
+ var ${GEN.bop}=ctx.binaryOperators;
177
+ var ${GEN.lop}=ctx.logicalOperators;
178
+ var ${GEN.uop}=ctx.unaryOperators;
179
+ var ${GEN.call}=ctx.callFunction;
180
+ var ${GEN.ensObj}=ctx.ensureObject;
181
+ var ${GEN.ensArr}=ctx.ensureArray;
182
+ var ${GEN.getIdentifierValue}=ctx.getIdentifierValue;
183
+ var ${GEN.prop}=ctx.getProperty;
184
+ var ${GEN.pipe}=ctx.pipe;
185
+ var ${GEN.nna}=ctx.nonNullAssert;
186
+ var ${GEN.globals}=ctx.globals??null;
463
187
 
464
- function _get(_scope,name){
465
- if(_scope===null)return getIdentifierValue(name,globals,this);
466
- var paramIndex=_scope[0].findIndex(it=>it===name);
467
- if(paramIndex===-1)return _get.call(this,_scope[2],name);
468
- return _scope[1][paramIndex]
188
+ function ${GEN._get}(${GEN._scope},name){
189
+ if(${GEN._scope}===null)return ${GEN.getIdentifierValue}(name,${GEN.globals},this);
190
+ var paramIndex=${GEN._scope}[${SCOPE_NAMES}].findIndex(it=>it===name);
191
+ if(paramIndex===-1)return ${GEN._get}.call(this,${GEN._scope}[${SCOPE_PARENT}],name);
192
+ return ${GEN._scope}[${SCOPE_VALUES}][paramIndex]
469
193
  };
470
194
 
471
195
  return data=>{
472
- var scope=null;
473
- var get=_get.bind(data);
196
+ var ${GEN.scope}=null;
197
+ var ${GEN.get}=${GEN._get}.bind(data);
474
198
  return
475
199
  `
476
- .split('\n')
477
- .map(it => it.trim())
478
- .filter(it => it !== '')
479
- .join('') + ' ';
480
- const bootstrapCodeHeadLen = bootstrapCodeHead.length;
200
+ .split('\n')
201
+ .map(it => it.trim())
202
+ .filter(it => it !== '')
203
+ .join('') + ' ';
204
+ const bootstrapCodeHeadLen = bootstrapCodeHead.length;
205
+ /** Compile a SimplEx expression string into an executable function. */
206
+ export function compile(expression, options) {
207
+ const tree = parse(expression);
208
+ const traverseResult = traverse(tree, expression);
209
+ const { code: expressionCode, offsets } = traverseResult;
481
210
  const functionCode = bootstrapCodeHead + expressionCode + '}';
482
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
211
+ const resolvedBool = options?.castToBoolean ?? castToBoolean;
483
212
  const defaultOptions = {
484
213
  ...defaultContextHelpers,
214
+ // Recreate operators with custom castToBoolean so compile options
215
+ // are honored by logical (and/or) and unary (not) operators
485
216
  ...{
486
- unaryOperators: defaultUnaryOperators,
217
+ unaryOperators: options?.castToBoolean
218
+ ? createDefaultUnaryOperators(resolvedBool)
219
+ : defaultUnaryOperators,
487
220
  binaryOperators: defaultBinaryOperators,
488
- logicalOperators: defaultLogicalOperators
221
+ logicalOperators: options?.castToBoolean
222
+ ? createDefaultLogicalOperators(resolvedBool)
223
+ : defaultLogicalOperators
489
224
  },
490
225
  ...options
491
226
  };
227
+ if (options?.extensions && options.extensions.size > 0 && !options.getProperty) {
228
+ const extensionMap = options.extensions;
229
+ const classesKeys = [];
230
+ const classesValues = [];
231
+ for (const [key, methods] of extensionMap) {
232
+ if (typeof key !== 'string') {
233
+ classesKeys.push(key);
234
+ classesValues.push(methods);
235
+ }
236
+ }
237
+ defaultOptions.getProperty = (obj, key, extension) => {
238
+ if (obj == null)
239
+ return undefined;
240
+ if (extension)
241
+ return getExtensionMethod(obj, key, extensionMap, classesKeys, classesValues);
242
+ return defaultGetProperty(obj, key, false);
243
+ };
244
+ }
492
245
  const func = new Function('ctx', functionCode)(defaultOptions);
246
+ const errorMapper = options?.errorMapper !== undefined
247
+ ? options.errorMapper
248
+ : getActiveErrorMapper();
249
+ if (errorMapper === null)
250
+ return func;
493
251
  return function (data) {
494
252
  try {
495
253
  return func(data);
496
254
  }
497
255
  catch (err) {
498
- if (err instanceof Error === false)
499
- throw err;
500
- const stackRows = err.stack?.split('\n').map(row => row.trim());
501
- const evalRow = stackRows?.find(row => row.startsWith('at eval '));
502
- if (evalRow === undefined) {
503
- throw err;
504
- }
505
- ERROR_STACK_REGEX.lastIndex = 0;
506
- const match = ERROR_STACK_REGEX.exec(evalRow);
507
- if (match == null) {
508
- throw err;
509
- }
510
- const rowOffsetStr = match.groups?.['row'];
511
- const colOffsetStr = match.groups?.['col'];
512
- if (rowOffsetStr === undefined || colOffsetStr === undefined) {
513
- throw err;
514
- }
515
- const rowOffset = Number.parseInt(rowOffsetStr);
516
- assert.equal(rowOffset, 3);
517
- const colOffset = Number.parseInt(colOffsetStr);
518
- const adjustedColOffset = colOffset - bootstrapCodeHeadLen;
519
- assert.ok(adjustedColOffset >= 0);
520
- const errorLocation = getExpressionErrorLocation(adjustedColOffset, offsets);
521
- throw new ExpressionError(err.message, expression, errorLocation, {
522
- cause: err
523
- });
256
+ throw (errorMapper.mapError(err, expression, offsets, bootstrapCodeHeadLen) ??
257
+ err);
524
258
  }
525
259
  };
526
260
  }