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
@@ -0,0 +1,30 @@
1
+ // Topic reference token used in pipe expressions
2
+ export const TOPIC_TOKEN = '%'
3
+
4
+ // Generated variable names used in bootstrap code and visitors
5
+ export const GEN = {
6
+ bool: 'bool',
7
+ bop: 'bop',
8
+ lop: 'lop',
9
+ uop: 'uop',
10
+ call: 'call',
11
+ getIdentifierValue: 'getIdentifierValue',
12
+ prop: 'prop',
13
+ pipe: 'pipe',
14
+ globals: 'globals',
15
+ get: 'get',
16
+ scope: 'scope',
17
+ _get: '_get',
18
+ _scope: '_scope',
19
+ _varNames: '_varNames',
20
+ _varValues: '_varValues',
21
+ ensObj: 'ensObj',
22
+ ensArr: 'ensArr',
23
+ str: 'str',
24
+ nna: 'nna',
25
+ } as const
26
+
27
+ // Semantic indices into the scope array [names, values, parent]
28
+ export const SCOPE_NAMES = 0
29
+ export const SCOPE_VALUES = 1
30
+ export const SCOPE_PARENT = 2
@@ -0,0 +1,112 @@
1
+ import { ExpressionError } from './errors.js'
2
+ import type { Location } from './simplex-tree.js'
3
+ import type { SourceLocation } from './visitors.js'
4
+
5
+ export type { SourceLocation }
6
+
7
+ // --- ErrorMapper Interface ---
8
+
9
+ export interface ErrorMapper {
10
+ /**
11
+ * Map a runtime error from generated code back to source expression location.
12
+ * Returns ExpressionError with source location, or null if unable to map.
13
+ *
14
+ * @param err — the caught runtime error
15
+ * @param expression — original SimplEx expression string
16
+ * @param offsets — source location mapping from code generation
17
+ * @param codeOffset — length of bootstrap code prepended before the expression
18
+ */
19
+ mapError(
20
+ err: unknown,
21
+ expression: string,
22
+ offsets: SourceLocation[],
23
+ codeOffset: number
24
+ ): ExpressionError | null
25
+
26
+ /**
27
+ * Test if this mapper is compatible with the current JS engine.
28
+ * Called once during registration.
29
+ */
30
+ probe(): boolean
31
+ }
32
+
33
+ // --- Helper ---
34
+
35
+ /** Map a code column offset back to an AST Location via the offsets array. */
36
+ export function getExpressionErrorLocation(
37
+ colOffset: number,
38
+ locations: SourceLocation[]
39
+ ): Location | null {
40
+ var curCol = 0
41
+ for (const loc of locations) {
42
+ curCol += loc.len
43
+ if (curCol >= colOffset) return loc.location
44
+ }
45
+ return null
46
+ }
47
+
48
+ // --- V8 ErrorMapper ---
49
+
50
+ var V8_STACK_REGEX = /<anonymous>:(?<row>\d+):(?<col>\d+)/g
51
+
52
+ export var v8ErrorMapper: ErrorMapper = {
53
+ probe() {
54
+ try {
55
+ new Function('throw new Error("__simplex_probe__")')()
56
+ } catch (err) {
57
+ return /<anonymous>:\d+:\d+/.test((err as Error).stack ?? '')
58
+ }
59
+ return false
60
+ },
61
+
62
+ mapError(err, expression, offsets, codeOffset) {
63
+ if (!(err instanceof Error)) return null
64
+
65
+ var evalRow = err.stack
66
+ ?.split('\n')
67
+ .map(r => r.trim())
68
+ .find(r => r.startsWith('at eval '))
69
+ if (!evalRow) return null
70
+
71
+ V8_STACK_REGEX.lastIndex = 0
72
+ var match = V8_STACK_REGEX.exec(evalRow)
73
+ var rowStr = match?.groups?.['row']
74
+ var colStr = match?.groups?.['col']
75
+ if (!rowStr || !colStr) return null
76
+
77
+ var row = Number.parseInt(rowStr)
78
+ if (row !== 3) return null
79
+
80
+ var col = Number.parseInt(colStr)
81
+ var adjustedCol = col - codeOffset
82
+ if (adjustedCol < 0) return null
83
+
84
+ var location = getExpressionErrorLocation(adjustedCol, offsets)
85
+ return new ExpressionError(err.message, expression, location, {
86
+ cause: err
87
+ })
88
+ }
89
+ }
90
+
91
+ // --- Registration ---
92
+
93
+ var activeErrorMapper: ErrorMapper | null = null
94
+
95
+ /**
96
+ * Register an ErrorMapper. If the mapper passes its probe(), it becomes
97
+ * the active mapper. Skips if the mapper is already active.
98
+ */
99
+ export function registerErrorMapper(mapper: ErrorMapper): void {
100
+ if (activeErrorMapper === mapper) return
101
+ if (mapper.probe()) {
102
+ activeErrorMapper = mapper
103
+ }
104
+ }
105
+
106
+ /** Get the currently active ErrorMapper (auto-detected or last registered). */
107
+ export function getActiveErrorMapper(): ErrorMapper | null {
108
+ return activeErrorMapper
109
+ }
110
+
111
+ // Auto-register V8 mapper at module load
112
+ registerErrorMapper(v8ErrorMapper)
package/src/errors.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Location } from './simplex-tree.js'
2
2
  import { typeOf } from './tools/index.js'
3
3
 
4
- export class ExpressionError extends Error {
4
+ /** Base error with source expression and location info. */
5
+ export class SimplexError extends Error {
5
6
  constructor(
6
7
  message: string,
7
8
  public expression: string,
@@ -13,18 +14,13 @@ export class ExpressionError extends Error {
13
14
  }
14
15
  }
15
16
 
16
- export class CompileError extends Error {
17
- constructor(
18
- message: string,
19
- public expression: string,
20
- public location: Location | null,
21
- options?: ErrorOptions
22
- ) {
23
- super(message, options)
24
- this.name = this.constructor.name
25
- }
26
- }
17
+ /** Runtime error thrown when expression evaluation fails (e.g. unknown identifier). */
18
+ export class ExpressionError extends SimplexError {}
19
+
20
+ /** Error thrown during compilation (e.g. duplicate let bindings, invalid keys). */
21
+ export class CompileError extends SimplexError {}
27
22
 
23
+ /** TypeError with expected-vs-actual type info (e.g. "Expected number, but got string"). */
28
24
  export class UnexpectedTypeError extends TypeError {
29
25
  I18N_STRING = 'UNEXPECTED_TYPE'
30
26
 
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './compiler.js'
2
+ export * from './error-mapping.js'
2
3
  export * from './errors.js'
3
4
  export * from './simplex-tree.js'
4
5
  export * from './tools/index.js'
@@ -7,9 +7,11 @@ export type Expression =
7
7
  | LogicalExpression
8
8
  | LiteralExpression
9
9
  | MemberExpression
10
+ | NonNullAssertExpression
10
11
  | ObjectExpression
11
12
  | NullishCoalescingExpression
12
13
  | PipeSequence
14
+ | TemplateLiteralExpression
13
15
  | TopicReference
14
16
  | UnaryExpression
15
17
  | LambdaExpression
@@ -54,19 +56,26 @@ export interface PropertyAssignment {
54
56
  type: 'Property'
55
57
  key: Expression
56
58
  value: Expression
59
+ computed: boolean
57
60
  kind: 'init'
58
61
  location: Location
59
62
  }
60
63
 
64
+ export interface SpreadElement {
65
+ type: 'SpreadElement'
66
+ argument: Expression
67
+ location: Location
68
+ }
69
+
61
70
  export interface ObjectExpression {
62
71
  type: 'ObjectExpression'
63
- properties: PropertyAssignment[]
72
+ properties: (PropertyAssignment | SpreadElement)[]
64
73
  location: Location
65
74
  }
66
75
 
67
76
  export interface ArrayExpression {
68
77
  type: 'ArrayExpression'
69
- elements: (Expression | null)[]
78
+ elements: (Expression | SpreadElement | null)[]
70
79
  location: Location
71
80
  }
72
81
 
@@ -188,3 +197,22 @@ export interface LetExpression {
188
197
  expression: Expression
189
198
  location: Location
190
199
  }
200
+
201
+ export interface TemplateElement {
202
+ value: string
203
+ location: Location
204
+ }
205
+
206
+ export interface NonNullAssertExpression {
207
+ type: 'NonNullAssertExpression'
208
+ expression: Expression
209
+ location: Location
210
+ }
211
+
212
+ export interface TemplateLiteralExpression {
213
+ type: 'TemplateLiteral'
214
+ tag: Expression | null
215
+ quasis: TemplateElement[]
216
+ expressions: Expression[]
217
+ location: Location
218
+ }
package/src/simplex.peggy CHANGED
@@ -271,6 +271,47 @@ SingleStringCharacter
271
271
  / "\\" sequence:EscapeSequence { return sequence; }
272
272
  / LineContinuation
273
273
 
274
+ TemplateLiteral "template literal"
275
+ = "`" head:TemplateCharacter* parts:TemplatePart* "`" {
276
+ var quasis = [];
277
+ var expressions = [];
278
+
279
+ quasis.push({
280
+ value: head.join(""),
281
+ location: getLocation(location())
282
+ });
283
+
284
+ for (var i = 0; i < parts.length; i++) {
285
+ expressions.push(parts[i].expression);
286
+ quasis.push({
287
+ value: (parts[i].suffix || []).join(""),
288
+ location: getLocation(location())
289
+ });
290
+ }
291
+
292
+ return {
293
+ type: "TemplateLiteral",
294
+ tag: null,
295
+ quasis: quasis,
296
+ expressions: expressions,
297
+ location: getLocation(location())
298
+ };
299
+ }
300
+
301
+ TemplatePart
302
+ = "${" __ expression:Expression __ "}" suffix:TemplateCharacter* {
303
+ return { expression: expression, suffix: suffix };
304
+ }
305
+
306
+ TemplateCharacter
307
+ = "\\" sequence:TemplateEscapeSequence { return sequence; }
308
+ / !("`" / "${" / "\\") SourceCharacter { return text(); }
309
+
310
+ TemplateEscapeSequence
311
+ = "`" { return "`"; }
312
+ / "$" { return "$"; }
313
+ / EscapeSequence
314
+
274
315
  LineContinuation
275
316
  = "\\" LineTerminatorSequence { return ""; }
276
317
 
@@ -381,6 +422,7 @@ PrimaryExpression
381
422
  = Identifier
382
423
  / Literal
383
424
  / TopicReference
425
+ / TemplateLiteral
384
426
  / ArrayLiteral
385
427
  / ObjectLiteral
386
428
  / "(" __ expression:Expression __ ")" { return expression; }
@@ -410,12 +452,12 @@ ArrayLiteral
410
452
 
411
453
  ElementList
412
454
  = head:(
413
- elision:(Elision __)? element:Expression {
455
+ elision:(Elision __)? element:(SpreadElement / Expression) {
414
456
  return optionalList(extractOptional(elision, 0)).concat(element);
415
457
  }
416
458
  )
417
459
  tail:(
418
- __ "," __ elision:(Elision __)? element:Expression {
460
+ __ "," __ elision:(Elision __)? element:(SpreadElement / Expression) {
419
461
  return optionalList(extractOptional(elision, 0)).concat(element);
420
462
  }
421
463
  )*
@@ -452,12 +494,33 @@ PropertyNameAndValueList
452
494
  return buildList(head, tail, 3);
453
495
  }
454
496
 
497
+ SpreadElement
498
+ = "..." __ argument:Expression {
499
+ return {
500
+ type: "SpreadElement",
501
+ argument: argument,
502
+ location: getLocation(location())
503
+ };
504
+ }
505
+
455
506
  PropertyAssignment
456
- = key:PropertyName __ ":" __ value:Expression {
507
+ = SpreadElement
508
+ / key:PropertyName __ ":" __ value:Expression {
509
+ return {
510
+ type: "Property",
511
+ key: key,
512
+ value: value,
513
+ computed: false,
514
+ kind: "init",
515
+ location: getLocation(location())
516
+ };
517
+ }
518
+ / "[" __ key:Expression __ "]" __ ":" __ value:Expression {
457
519
  return {
458
520
  type: "Property",
459
521
  key: key,
460
522
  value: value,
523
+ computed: true,
461
524
  kind: "init",
462
525
  location: getLocation(location())
463
526
  };
@@ -488,9 +551,31 @@ MemberExpression
488
551
  property: property, computed: false, extension: true, location: getLocation(location())
489
552
  };
490
553
  }
554
+ / __ template:TemplateLiteral {
555
+ return {
556
+ tagged: true, template: template, location: getLocation(location())
557
+ };
558
+ }
559
+ / "!" !"=" {
560
+ return {
561
+ nonNullAssert: true, location: getLocation(location())
562
+ };
563
+ }
491
564
  )*
492
565
  {
493
566
  return tail.reduce(function(result, element) {
567
+ if (element.tagged) {
568
+ element.template.tag = result;
569
+ element.template.location = getLocation(location());
570
+ return element.template;
571
+ }
572
+ if (element.nonNullAssert) {
573
+ return {
574
+ type: "NonNullAssertExpression",
575
+ expression: result,
576
+ location: element.location
577
+ };
578
+ }
494
579
  return {
495
580
  type: "MemberExpression",
496
581
  object: result,
@@ -534,12 +619,44 @@ CallExpression
534
619
  type: "MemberExpression",
535
620
  property: property,
536
621
  computed: false,
622
+ extension: false,
623
+ location: getLocation(location())
624
+ };
625
+ }
626
+ / __ "::" __ property:IdentifierName {
627
+ return {
628
+ type: "MemberExpression",
629
+ property: property,
630
+ computed: false,
631
+ extension: true,
537
632
  location: getLocation(location())
538
633
  };
539
634
  }
635
+ / __ template:TemplateLiteral {
636
+ return {
637
+ tagged: true, template: template, location: getLocation(location())
638
+ };
639
+ }
640
+ / "!" !"=" {
641
+ return {
642
+ nonNullAssert: true, location: getLocation(location())
643
+ };
644
+ }
540
645
  )*
541
646
  {
542
647
  return tail.reduce(function(result, element) {
648
+ if (element.tagged) {
649
+ element.template.tag = result;
650
+ element.template.location = getLocation(location());
651
+ return element.template;
652
+ }
653
+ if (element.nonNullAssert) {
654
+ return {
655
+ type: "NonNullAssertExpression",
656
+ expression: result,
657
+ location: element.location
658
+ };
659
+ }
543
660
  element[TYPES_TO_PROPERTY_NAMES[element.type]] = result;
544
661
 
545
662
  return element;
@@ -1,31 +1,18 @@
1
- export * from './cast.js'
2
- export * from './ensure.js'
3
- export * from './guards.js'
4
-
5
- // eslint-disable-next-line @typescript-eslint/unbound-method
6
- const toString = Object.prototype.toString
1
+ import { UnexpectedTypeError } from '../errors.js'
7
2
 
8
3
  /**
9
- * Converts instances of Number, String and Boolean to primitives
4
+ * Alias for `Object.prototype.toString`
10
5
  */
11
- export function unbox(val: unknown) {
12
- if (typeof val !== 'object' || val === null) return val
13
-
14
- const objConstructor = val.constructor
15
-
16
- if (
17
- objConstructor === Number ||
18
- objConstructor === String ||
19
- objConstructor === Boolean
20
- ) {
21
- return val.valueOf()
22
- }
23
-
24
- return val
25
- }
6
+ // eslint-disable-next-line @typescript-eslint/unbound-method
7
+ export const objToStringAlias = Object.prototype.toString
26
8
 
27
9
  /**
28
- * Returns more specific type of a value
10
+ * The method is needed to obtain the most specific readable data type.
11
+ *
12
+ * *Usage note:* Type handling, from a performance perspective, should be done
13
+ * in a targeted manner. It is not possible to replace specific checks like typeof
14
+ * `some === "number"` or `Num.isFinite(some)` with a universal
15
+ * `typeOf(some) === "FiniteNumber"`.
29
16
  */
30
17
  export function typeOf(val: unknown) {
31
18
  const type = typeof val
@@ -38,8 +25,114 @@ export function typeOf(val: unknown) {
38
25
  }
39
26
 
40
27
  if (type === 'object') {
41
- return toString.call(val).slice(8, -1)
28
+ return objToStringAlias.call(val).slice(8, -1)
42
29
  }
43
30
 
44
31
  return type
45
32
  }
33
+
34
+ // --- Guards ---
35
+
36
+ /** Check if value is a plain object (not Array, Map, etc.). */
37
+ export function isObject(val: unknown): val is object {
38
+ return objToStringAlias.call(val) === '[object Object]'
39
+ }
40
+
41
+ // Boxed primitives (new String, etc.) are intentionally not handled — they
42
+ // cannot originate from SimplEx expressions and are not worth the overhead.
43
+ export function isSimpleValue(
44
+ val: unknown
45
+ ): val is number | string | boolean | bigint | null | undefined {
46
+ const type = typeof val
47
+
48
+ if (
49
+ type === 'string' ||
50
+ type === 'number' ||
51
+ type === 'boolean' ||
52
+ type === 'bigint'
53
+ ) {
54
+ return true
55
+ }
56
+
57
+ if (val == null) return true
58
+
59
+ return false
60
+ }
61
+
62
+ // --- Cast ---
63
+
64
+ /** Coerce any value to boolean (standard JS truthiness). */
65
+ export function castToBoolean(val: unknown): boolean {
66
+ // Boxed primitives (new String, etc.) are intentionally not handled — see isSimpleValue comment.
67
+ return Boolean(val)
68
+ }
69
+
70
+ /** Coerce value to string; objects use Object.prototype.toString. */
71
+ export function castToString(val: unknown): string {
72
+ const type = typeof val
73
+
74
+ // Boxed primitives (new String, etc.) are intentionally not handled — see isSimpleValue comment.
75
+ if (type === 'string') return val as string
76
+ if (
77
+ val == null ||
78
+ type === 'number' ||
79
+ type === 'boolean' ||
80
+ type === 'bigint'
81
+ ) {
82
+ return String(val)
83
+ }
84
+
85
+ return objToStringAlias.call(val)
86
+ }
87
+
88
+ // --- Ensure ---
89
+
90
+ /** Validate that value is a finite number or bigint; throw UnexpectedTypeError otherwise. */
91
+ export function ensureNumber(val: unknown): number | bigint {
92
+ if (typeof val === 'number' && Number.isFinite(val)) {
93
+ return val
94
+ }
95
+
96
+ if (typeof val === 'bigint') {
97
+ return val
98
+ }
99
+
100
+ // Boxed primitives (new Number, etc.) are intentionally not handled — see isSimpleValue comment.
101
+ throw new UnexpectedTypeError(['number', 'bigint'], val)
102
+ }
103
+
104
+ /** Validate that value is a function; throw UnexpectedTypeError otherwise. */
105
+ export function ensureFunction(val: unknown): Function {
106
+ if (typeof val === 'function') return val
107
+ throw new UnexpectedTypeError(['function'], val)
108
+ }
109
+
110
+ /** Validate that value is a plain object; throw UnexpectedTypeError otherwise. */
111
+ export function ensureObject(val: unknown): object {
112
+ if (isObject(val)) return val
113
+ throw new UnexpectedTypeError(['object'], val)
114
+ }
115
+
116
+ /** Validate that value is an array; throw UnexpectedTypeError otherwise. */
117
+ export function ensureArray(val: unknown): unknown[] {
118
+ if (Array.isArray(val)) return val
119
+ throw new UnexpectedTypeError(['Array'], val)
120
+ }
121
+
122
+ /** Validate that value is comparable (<, >, <=, >=); must be number, bigint, or string. */
123
+ export function ensureRelationalComparable(
124
+ val: unknown
125
+ ): number | string | bigint {
126
+ // Boxed primitives (new String, etc.) are intentionally not handled — see isSimpleValue comment.
127
+ const type = typeof val
128
+
129
+ if (
130
+ (type === 'number' && Number.isNaN(val) === false) ||
131
+ type === 'string' ||
132
+ type === 'bigint'
133
+ ) {
134
+ return val as number | string | bigint
135
+ }
136
+
137
+ throw new UnexpectedTypeError(['number', 'bigint', 'string'], val)
138
+ }