xbintsc 0.1.3 → 0.1.6

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.
package/README.md CHANGED
@@ -178,6 +178,11 @@ to the C symbol with the uniform `(argc, argv)` calling convention. Adding a
178
178
  module means dropping a folder under `src/extensions/node/` and its C
179
179
  counterpart under `runtime/ext_node/`; the core compiler never changes.
180
180
 
181
+ Node module coverage:
182
+
183
+ - [Node extension: implemented](./doc/node-implemented.md)
184
+ - [Node extension: unimplemented](./doc/node-unimplemented.md)
185
+
181
186
  ## Tests
182
187
 
183
188
  ```bash
@@ -196,12 +201,5 @@ Tests are organised by module under `tests/` (`lexer`, `parser`, `binder`,
196
201
 
197
202
  ## Language subset
198
203
 
199
- Implemented today: functions, arrow functions and closures, `let`/`const`/`var`,
200
- all common operators, `if`/`while`/`do`/`for`/`for…of`/`for…in`, `break`/
201
- `continue`/`return`/`throw`, objects, arrays, member/element access, assignments,
202
- template literals, `console.log`, extension builtins, and the TypeScript type
203
- syntax is parsed and ignored (types are erased).
204
-
205
- Not yet implemented: classes, enums, `switch`, `try`/`catch`, `new`, `this`,
206
- object spread, `async`/generators, and most standard library methods (`Array`
207
- methods other than `push`, `Math`, etc.).
204
+ - [Implemented features](./doc/implemented.md)
205
+ - [Unimplemented features](./doc/unimplemented.md)
@@ -64,6 +64,23 @@ const RUNTIME_DECLARATIONS = [
64
64
  "declare i64 @xt_object_get_cstr(i64, i8*)",
65
65
  "declare i64 @xt_object_set(i64, i64, i64)",
66
66
  "declare i64 @xt_object_has(i64, i64)",
67
+ "declare i64 @xt_object_keys(i64)",
68
+ "declare i64 @xt_object_values(i64)",
69
+ "declare i64 @xt_object_entries(i64)",
70
+ "declare i64 @xt_object_assign(i32, i64*)",
71
+ "declare i64 @xt_object_spread(i64, i64)",
72
+ "declare i64 @xt_call_method(i64, i64, i32, i64*)",
73
+ "declare i64 @xt_math_call(i64, i32, i64*)",
74
+ "declare i64 @xt_parse_int(i32, i64*)",
75
+ "declare i64 @xt_parse_float(i32, i64*)",
76
+ "declare i64 @xt_is_nan(i32, i64*)",
77
+ "declare i64 @xt_is_finite(i32, i64*)",
78
+ "declare i64 @xt_number_ctor(i32, i64*)",
79
+ "declare i64 @xt_string_ctor(i32, i64*)",
80
+ "declare i64 @xt_boolean_ctor(i32, i64*)",
81
+ "declare i64 @xt_in(i64, i64)",
82
+ "declare i64 @xt_delete(i64, i64)",
83
+ "declare i64 @xt_rest_args(i32, i64*, i32)",
67
84
  "declare i64 @xt_array_new(i32, i64*)",
68
85
  "declare i64 @xt_array_push(i64, i64)",
69
86
  "declare i64 @xt_array_length(i64)",
@@ -75,7 +92,14 @@ const RUNTIME_DECLARATIONS = [
75
92
  "declare i64 @xt_box_set(i64, i64)",
76
93
  "declare i64 @xt_is_nullish(i64)",
77
94
  "declare void @xt_throw(i64)",
95
+ "declare i32 @_setjmp(i8*) returns_twice",
96
+ "declare i8* @xt_try_enter()",
97
+ "declare i64 @xt_try_exception(i8*)",
98
+ "declare void @xt_try_leave(i8*)",
78
99
  "declare void @xt_console_log(i32, i64*)",
100
+ "declare void @xt_console_info(i32, i64*)",
101
+ "declare void @xt_console_warn(i32, i64*)",
102
+ "declare void @xt_console_error(i32, i64*)",
79
103
  ];
80
104
  export function generate(sourceFile, diagnostics, options = {}) {
81
105
  const generator = new Generator(sourceFile, diagnostics, options);
@@ -117,6 +141,9 @@ class Generator {
117
141
  label: 0,
118
142
  terminated: false,
119
143
  loops: [],
144
+ tryFrames: [],
145
+ escapePointers: [],
146
+ usesTry: false,
120
147
  };
121
148
  this.current = state;
122
149
  const header = `define i64 @${name}(i64 %env, i32 %argc, i64* %argv) {`;
@@ -127,11 +154,21 @@ class Generator {
127
154
  this.emit(`%saved.argv = alloca i64*`);
128
155
  this.emit(`store i64* %argv, i64** %saved.argv`);
129
156
  // Parameters.
157
+ const parameterNodes = fn.node.parameters ?? [];
130
158
  for (let index = 0; index < fn.params.length; index++) {
131
159
  const symbol = fn.params[index];
160
+ const parameter = parameterNodes[index];
161
+ if (parameter?.dotDotDotToken) {
162
+ const rest = this.reg();
163
+ this.emit(` ${rest} = call i64 @xt_rest_args(i32 %argc, i64* %argv, i32 ${index})`);
164
+ this.declareSlot(symbol, rest);
165
+ continue;
166
+ }
132
167
  const value = this.reg();
133
168
  this.emit(` ${value} = call i64 @xt_arg(i32 %argc, i64* %argv, i32 ${index})`);
134
169
  this.declareSlot(symbol, value);
170
+ if (parameter?.initializer)
171
+ this.emitDefaultParameter(symbol, value, parameter.initializer);
135
172
  }
136
173
  // Captures threaded through the environment.
137
174
  for (const symbol of fn.captures) {
@@ -154,7 +191,10 @@ class Generator {
154
191
  }
155
192
  if (!this.current.terminated)
156
193
  this.terminate(`ret i64 ${i64(XT_UNDEFINED)}`);
157
- const lines = [...state.allocas.map((a) => ` ${a}`), ...state.buffer];
194
+ const escapes = state.usesTry
195
+ ? state.escapePointers.map((ptr) => ` call void asm sideeffect "", "r"(i64* ${ptr})`)
196
+ : [];
197
+ const lines = [...state.allocas.map((a) => ` ${a}`), ...escapes, ...state.buffer];
158
198
  this.functions.push([header, ...lines, "}", ""].join("\n"));
159
199
  }
160
200
  emitMain() {
@@ -191,6 +231,7 @@ class Generator {
191
231
  alloca() {
192
232
  const ptr = `%slot${this.current.allocas.length}`;
193
233
  this.current.allocas.push(`${ptr} = alloca i64`);
234
+ this.current.escapePointers.push(ptr);
194
235
  return ptr;
195
236
  }
196
237
  const(hex) {
@@ -245,6 +286,22 @@ class Generator {
245
286
  this.emit(` ${box} = load i64, i64* ${slot.ptr}`);
246
287
  this.emit(` call i64 @xt_box_set(i64 ${box}, i64 ${value})`);
247
288
  }
289
+ emitDefaultParameter(symbol, value, initializer) {
290
+ const isUndefined = this.reg();
291
+ this.emit(` ${isUndefined} = call i64 @xt_seq(i64 ${value}, i64 ${i64(XT_UNDEFINED)})`);
292
+ const truthy = this.reg();
293
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${isUndefined})`);
294
+ const condition = this.reg();
295
+ this.emit(` ${condition} = icmp ne i32 ${truthy}, 0`);
296
+ const applyLabel = this.label("param.default");
297
+ const endLabel = this.label("param.end");
298
+ this.terminate(`br i1 ${condition}, label %${applyLabel}, label %${endLabel}`);
299
+ this.startBlock(applyLabel);
300
+ const fallback = this.emitExpression(initializer);
301
+ this.writeSlot(symbol, fallback);
302
+ this.terminate(`br label %${endLabel}`);
303
+ this.startBlock(endLabel);
304
+ }
248
305
  // -- statements ----------------------------------------------------------
249
306
  emitStatements(statements) {
250
307
  for (const statement of statements) {
@@ -286,19 +343,29 @@ class Generator {
286
343
  case SyntaxKind.ForInStatement:
287
344
  this.emitForOf(statement);
288
345
  return;
346
+ case SyntaxKind.SwitchStatement:
347
+ this.emitSwitch(statement);
348
+ return;
349
+ case SyntaxKind.TryStatement:
350
+ this.emitTry(statement);
351
+ return;
289
352
  case SyntaxKind.ReturnStatement:
290
353
  this.emitReturn(statement);
291
354
  return;
292
355
  case SyntaxKind.BreakStatement: {
293
356
  const loop = this.current.loops[this.current.loops.length - 1];
294
- if (loop)
357
+ if (loop) {
358
+ this.popTryFramesTo(loop.tryDepth ?? 0);
295
359
  this.terminate(`br label %${loop.breakLabel}`);
360
+ }
296
361
  return;
297
362
  }
298
363
  case SyntaxKind.ContinueStatement: {
299
364
  const loop = this.current.loops[this.current.loops.length - 1];
300
- if (loop)
365
+ if (loop) {
366
+ this.popTryFramesTo(loop.tryDepth ?? 0);
301
367
  this.terminate(`br label %${loop.continueLabel}`);
368
+ }
302
369
  return;
303
370
  }
304
371
  case SyntaxKind.ThrowStatement: {
@@ -365,7 +432,7 @@ class Generator {
365
432
  this.emit(` ${nonzero} = icmp ne i32 ${truthy}, 0`);
366
433
  this.terminate(`br i1 ${nonzero}, label %${bodyLabel}, label %${endLabel}`);
367
434
  this.startBlock(bodyLabel);
368
- this.current.loops.push({ breakLabel: endLabel, continueLabel: condLabel });
435
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: condLabel, tryDepth: this.current.tryFrames.length });
369
436
  this.emitStatement(statement.statement);
370
437
  this.current.loops.pop();
371
438
  if (!this.current.terminated)
@@ -378,7 +445,7 @@ class Generator {
378
445
  const endLabel = this.label("do.end");
379
446
  this.terminate(`br label %${bodyLabel}`);
380
447
  this.startBlock(bodyLabel);
381
- this.current.loops.push({ breakLabel: endLabel, continueLabel: condLabel });
448
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: condLabel, tryDepth: this.current.tryFrames.length });
382
449
  this.emitStatement(statement.statement);
383
450
  this.current.loops.pop();
384
451
  if (!this.current.terminated)
@@ -423,7 +490,7 @@ class Generator {
423
490
  this.terminate(`br label %${bodyLabel}`);
424
491
  }
425
492
  this.startBlock(bodyLabel);
426
- this.current.loops.push({ breakLabel: endLabel, continueLabel: updateLabel });
493
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: updateLabel, tryDepth: this.current.tryFrames.length });
427
494
  this.emitStatement(statement.statement);
428
495
  this.current.loops.pop();
429
496
  if (!this.current.terminated)
@@ -434,9 +501,13 @@ class Generator {
434
501
  this.terminate(`br label %${condLabel}`);
435
502
  this.startBlock(endLabel);
436
503
  }
437
- /** `for (const x of xs)` / `for (const k in obj)` over arrays and objects. */
504
+ /** `for (const x of xs)` / `for (const k in obj)` over arrays, strings and objects. */
438
505
  emitForOf(statement) {
439
- const iterable = this.emitExpression(statement.expression);
506
+ const source = this.emitExpression(statement.expression);
507
+ const isForIn = statement.kind === SyntaxKind.ForInStatement;
508
+ // `for...in` iterates the enumerable keys (indices become strings);
509
+ // `for...of` iterates the values at each index.
510
+ const iterable = isForIn ? this.runtimeCall("xt_object_keys", [source]) : source;
440
511
  const indexPtr = this.alloca();
441
512
  this.emit(` store i64 ${numberLiteral(0)}, i64* ${indexPtr}`);
442
513
  const lengthValue = this.reg();
@@ -460,7 +531,7 @@ class Generator {
460
531
  const element = this.reg();
461
532
  this.emit(` ${element} = call i64 @xt_get(i64 ${iterable}, i64 ${index})`);
462
533
  this.bindLoopVariable(statement.initializer, element);
463
- this.current.loops.push({ breakLabel: endLabel, continueLabel: updateLabel });
534
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: updateLabel, tryDepth: this.current.tryFrames.length });
464
535
  this.emitStatement(statement.statement);
465
536
  this.current.loops.pop();
466
537
  if (!this.current.terminated)
@@ -488,10 +559,167 @@ class Generator {
488
559
  }
489
560
  this.emitAssignmentTarget(initializer, value);
490
561
  }
562
+ /**
563
+ * JavaScript `switch`: test each `case` with strict equality, then run the
564
+ * matched clause and fall through into the following clauses until `break`.
565
+ */
566
+ emitSwitch(statement) {
567
+ const discriminant = this.emitExpression(statement.expression);
568
+ const endLabel = this.label("switch.end");
569
+ const clauses = statement.clauses;
570
+ const labels = clauses.map(() => this.label("switch.case"));
571
+ const testLabels = clauses.map((clause) => clause.kind === SyntaxKind.CaseClause ? this.label("switch.test") : undefined);
572
+ const defaultIndex = clauses.findIndex((clause) => clause.kind === SyntaxKind.DefaultClause);
573
+ const defaultLabel = defaultIndex >= 0 ? labels[defaultIndex] : endLabel;
574
+ const caseIndexes = clauses
575
+ .map((clause, index) => ({ clause, index }))
576
+ .filter((entry) => entry.clause.kind === SyntaxKind.CaseClause);
577
+ this.terminate(`br label %${caseIndexes.length > 0 ? testLabels[caseIndexes[0].index] : defaultLabel}`);
578
+ for (let test = 0; test < caseIndexes.length; test++) {
579
+ const entry = caseIndexes[test];
580
+ this.startBlock(testLabels[entry.index]);
581
+ const caseValue = this.emitExpression(entry.clause.expression);
582
+ const equals = this.reg();
583
+ this.emit(` ${equals} = call i64 @xt_seq(i64 ${discriminant}, i64 ${caseValue})`);
584
+ const truthy = this.reg();
585
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${equals})`);
586
+ const condition = this.reg();
587
+ this.emit(` ${condition} = icmp ne i32 ${truthy}, 0`);
588
+ const next = test + 1 < caseIndexes.length ? testLabels[caseIndexes[test + 1].index] : defaultLabel;
589
+ this.terminate(`br i1 ${condition}, label %${labels[entry.index]}, label %${next}`);
590
+ }
591
+ for (let index = 0; index < clauses.length; index++) {
592
+ this.startBlock(labels[index]);
593
+ const enclosing = this.current.loops[this.current.loops.length - 1];
594
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: enclosing?.continueLabel ?? endLabel, tryDepth: this.current.tryFrames.length });
595
+ for (const child of clauses[index].statements) {
596
+ if (this.current.terminated)
597
+ break;
598
+ this.emitStatement(child);
599
+ }
600
+ this.current.loops.pop();
601
+ if (!this.current.terminated) {
602
+ const next = index + 1 < clauses.length ? labels[index + 1] : endLabel;
603
+ this.terminate(`br label %${next}`);
604
+ }
605
+ }
606
+ this.startBlock(endLabel);
607
+ }
608
+ /**
609
+ * `try`/`catch`/`finally` via a runtime setjmp frame. The runtime keeps a
610
+ * stack of frames; `xt_throw` longjmps into the innermost one. A `return`
611
+ * that exits the protected region skips `finally` (a known limitation).
612
+ */
613
+ emitTry(statement) {
614
+ this.current.usesTry = true;
615
+ const frameSlot = this.alloca();
616
+ const exceptionSlot = this.alloca();
617
+ const flagSlot = this.alloca();
618
+ const tryLabel = this.label("try.body");
619
+ const catchLabel = this.label("try.catch");
620
+ const exceptionLabel = this.label("try.exception");
621
+ const rethrowLabel = this.label("try.rethrow");
622
+ const finallyLabel = statement.finallyBlock ? this.label("try.finally") : undefined;
623
+ const endLabel = this.label("try.end");
624
+ const hasCatch = !!statement.catchClause;
625
+ const frame = this.reg();
626
+ this.emit(` ${frame} = call i8* @xt_try_enter()`);
627
+ this.emit(` store i8* ${frame}, i8** ${frameSlot}`);
628
+ this.emit(` store i64 0, i64* ${flagSlot}`);
629
+ const jump = this.reg();
630
+ this.emit(` ${jump} = call i32 @_setjmp(i8* ${frame})`);
631
+ const isThrow = this.reg();
632
+ this.emit(` ${isThrow} = icmp ne i32 ${jump}, 0`);
633
+ const exceptionTarget = hasCatch ? catchLabel : exceptionLabel;
634
+ this.terminate(`br i1 ${isThrow}, label %${exceptionTarget}, label %${tryLabel}`);
635
+ // Normal completion of the try block.
636
+ this.startBlock(tryLabel);
637
+ this.current.tryFrames.push(frameSlot);
638
+ this.emitStatements(statement.tryBlock.statements);
639
+ this.current.tryFrames.pop();
640
+ if (!this.current.terminated) {
641
+ const currentFrame = this.reg();
642
+ this.emit(` ${currentFrame} = load i8*, i8** ${frameSlot}`);
643
+ this.emit(` call void @xt_try_leave(i8* ${currentFrame})`);
644
+ this.terminate(`br label %${finallyLabel ?? endLabel}`);
645
+ }
646
+ if (hasCatch) {
647
+ this.startBlock(catchLabel);
648
+ const currentFrame = this.reg();
649
+ this.emit(` ${currentFrame} = load i8*, i8** ${frameSlot}`);
650
+ const exception = this.reg();
651
+ this.emit(` ${exception} = call i64 @xt_try_exception(i8* ${currentFrame})`);
652
+ this.emit(` call void @xt_try_leave(i8* ${currentFrame})`);
653
+ this.bindCatchVariable(statement.catchClause.variable, exception);
654
+ this.emitStatements(statement.catchClause.block.statements);
655
+ if (!this.current.terminated)
656
+ this.terminate(`br label %${finallyLabel ?? endLabel}`);
657
+ }
658
+ else {
659
+ // Without a catch clause the exception is remembered, then rethrown
660
+ // after `finally` runs.
661
+ this.startBlock(exceptionLabel);
662
+ const currentFrame = this.reg();
663
+ this.emit(` ${currentFrame} = load i8*, i8** ${frameSlot}`);
664
+ const exception = this.reg();
665
+ this.emit(` ${exception} = call i64 @xt_try_exception(i8* ${currentFrame})`);
666
+ this.emit(` call void @xt_try_leave(i8* ${currentFrame})`);
667
+ this.emit(` store i64 ${exception}, i64* ${exceptionSlot}`);
668
+ this.emit(` store i64 1, i64* ${flagSlot}`);
669
+ this.terminate(`br label %${finallyLabel ?? rethrowLabel}`);
670
+ }
671
+ if (finallyLabel) {
672
+ this.startBlock(finallyLabel);
673
+ this.emitStatements(statement.finallyBlock.statements);
674
+ if (!this.current.terminated) {
675
+ if (hasCatch) {
676
+ this.terminate(`br label %${endLabel}`);
677
+ }
678
+ else {
679
+ const flag = this.reg();
680
+ this.emit(` ${flag} = load i64, i64* ${flagSlot}`);
681
+ const truthy = this.reg();
682
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${flag})`);
683
+ const shouldRethrow = this.reg();
684
+ this.emit(` ${shouldRethrow} = icmp ne i32 ${truthy}, 0`);
685
+ this.terminate(`br i1 ${shouldRethrow}, label %${rethrowLabel}, label %${endLabel}`);
686
+ }
687
+ }
688
+ }
689
+ if (!hasCatch) {
690
+ this.startBlock(rethrowLabel);
691
+ const exception = this.reg();
692
+ this.emit(` ${exception} = load i64, i64* ${exceptionSlot}`);
693
+ this.emit(` call void @xt_throw(i64 ${exception})`);
694
+ this.terminate("unreachable");
695
+ }
696
+ this.startBlock(endLabel);
697
+ }
698
+ bindCatchVariable(variable, value) {
699
+ if (!variable)
700
+ return;
701
+ const symbol = this.binding.symbolOfDeclaration.get(variable);
702
+ if (!symbol)
703
+ return;
704
+ if (this.current.slots.has(symbol.id))
705
+ this.writeSlot(symbol, value);
706
+ else
707
+ this.declareSlot(symbol, value);
708
+ }
491
709
  emitReturn(statement) {
492
710
  const value = statement.expression ? this.emitExpression(statement.expression) : i64(XT_UNDEFINED);
711
+ this.popTryFramesTo(0);
493
712
  this.terminate(`ret i64 ${value}`);
494
713
  }
714
+ /** Emit `xt_try_leave` for every active frame above `depth` (innermost first). */
715
+ popTryFramesTo(depth) {
716
+ for (let index = this.current.tryFrames.length - 1; index >= depth; index--) {
717
+ const slot = this.current.tryFrames[index];
718
+ const frame = this.reg();
719
+ this.emit(` ${frame} = load i8*, i8** ${slot}`);
720
+ this.emit(` call void @xt_try_leave(i8* ${frame})`);
721
+ }
722
+ }
495
723
  // -- expressions ---------------------------------------------------------
496
724
  emitExpression(node) {
497
725
  switch (node.kind) {
@@ -523,6 +751,8 @@ class Generator {
523
751
  case SyntaxKind.SatisfiesExpression:
524
752
  case SyntaxKind.NonNullExpression:
525
753
  return this.emitExpression(node.expression);
754
+ case SyntaxKind.DeleteExpression:
755
+ return this.emitDelete(node);
526
756
  case SyntaxKind.BinaryExpression:
527
757
  return this.emitBinaryOrAssignment(node);
528
758
  case SyntaxKind.PrefixUnaryExpression:
@@ -566,6 +796,15 @@ class Generator {
566
796
  return numberLiteral(Infinity);
567
797
  case "console":
568
798
  return i64(XT_UNDEFINED);
799
+ case "arguments": {
800
+ const argc = this.reg();
801
+ this.emit(` ${argc} = load i32, i32* %saved.argc`);
802
+ const argv = this.reg();
803
+ this.emit(` ${argv} = load i64*, i64** %saved.argv`);
804
+ const rest = this.reg();
805
+ this.emit(` ${rest} = call i64 @xt_rest_args(i32 ${argc}, i64* ${argv}, i32 0)`);
806
+ return rest;
807
+ }
569
808
  default:
570
809
  this.diagnostics.error(DiagnosticCode.CannotFindName, `Cannot find name '${identifier.text}'`, identifier, this.sourceFile.fileName);
571
810
  return i64(XT_UNDEFINED);
@@ -854,6 +1093,32 @@ class Generator {
854
1093
  }
855
1094
  emitCall(node) {
856
1095
  const callee = node.expression;
1096
+ if (node.optional) {
1097
+ const calleeValue = this.emitExpression(callee);
1098
+ return this.emitOptional(calleeValue, () => {
1099
+ const args = this.emitArguments(node.arguments);
1100
+ const result = this.reg();
1101
+ this.emit(` ${result} = call i64 @xt_closure_call(i64 ${calleeValue}, i32 ${args.argc}, i64* ${args.ptr})`);
1102
+ return result;
1103
+ });
1104
+ }
1105
+ if ((callee.kind === SyntaxKind.PropertyAccessExpression || callee.kind === SyntaxKind.ElementAccessExpression) &&
1106
+ callee.optional) {
1107
+ // `obj?.method(args)` / `obj?.[key](args)`: guard the receiver, then
1108
+ // dispatch through the runtime, which understands object methods as well
1109
+ // as the built-in array/string methods.
1110
+ const access = callee;
1111
+ const object = this.emitExpression(access.expression);
1112
+ return this.emitOptional(object, () => {
1113
+ const name = access.kind === SyntaxKind.PropertyAccessExpression
1114
+ ? this.stringValue(access.name.text)
1115
+ : this.emitExpression(access.argumentExpression);
1116
+ const args = this.emitArguments(node.arguments);
1117
+ const result = this.reg();
1118
+ this.emit(` ${result} = call i64 @xt_call_method(i64 ${object}, i64 ${name}, i32 ${args.argc}, i64* ${args.ptr})`);
1119
+ return result;
1120
+ });
1121
+ }
857
1122
  if (callee.kind === SyntaxKind.PropertyAccessExpression) {
858
1123
  const special = this.tryEmitBuiltinCall(node, callee);
859
1124
  if (special)
@@ -871,6 +1136,13 @@ class Generator {
871
1136
  return result;
872
1137
  }
873
1138
  }
1139
+ const globalFunction = !symbol ? GLOBAL_FUNCTIONS[callee.text] : undefined;
1140
+ if (globalFunction) {
1141
+ const args = this.emitArguments(node.arguments);
1142
+ const result = this.reg();
1143
+ this.emit(` ${result} = call i64 @${globalFunction}(i32 ${args.argc}, i64* ${args.ptr})`);
1144
+ return result;
1145
+ }
874
1146
  const builtin = this.builtins[callee.text];
875
1147
  if (!symbol && builtin) {
876
1148
  this.extraDeclarations.add(builtin.returnVoid ? `declare void @${builtin.symbol}(i32, i64*)` : `declare i64 @${builtin.symbol}(i32, i64*)`);
@@ -893,32 +1165,47 @@ class Generator {
893
1165
  tryEmitBuiltinCall(node, callee) {
894
1166
  const target = callee.expression;
895
1167
  const method = callee.name.text;
896
- if (target.kind === SyntaxKind.Identifier && target.text === "console") {
1168
+ const targetIdentifier = target.kind === SyntaxKind.Identifier ? target : undefined;
1169
+ const targetSymbol = targetIdentifier ? this.binding.symbolOfIdentifier.get(targetIdentifier) : undefined;
1170
+ if (targetIdentifier && targetIdentifier.text === "console" && !targetSymbol) {
1171
+ const consoleFn = CONSOLE_METHODS[method];
1172
+ if (consoleFn) {
1173
+ const args = this.emitArguments(node.arguments);
1174
+ this.emit(` call void @${consoleFn}(i32 ${args.argc}, i64* ${args.ptr})`);
1175
+ return i64(XT_UNDEFINED);
1176
+ }
1177
+ return undefined;
1178
+ }
1179
+ if (targetIdentifier && targetIdentifier.text === "Math" && !targetSymbol) {
1180
+ if (!MATH_FUNCTIONS.has(method))
1181
+ return undefined;
1182
+ const name = this.stringValue(method);
897
1183
  const args = this.emitArguments(node.arguments);
898
- this.emit(` call void @xt_console_log(i32 ${args.argc}, i64* ${args.ptr})`);
899
- return i64(XT_UNDEFINED);
1184
+ const result = this.reg();
1185
+ this.emit(` ${result} = call i64 @xt_math_call(i64 ${name}, i32 ${args.argc}, i64* ${args.ptr})`);
1186
+ return result;
900
1187
  }
901
- // Array and string methods.
902
- if (method === "push" || method === "pop" || method === "shift" || method === "unshift" || method === "join" || method === "slice" || method === "indexOf" || method === "includes" || method === "map" || method === "forEach" || method === "filter" || method === "reduce") {
903
- if (method === "push") {
904
- const object = this.emitExpression(target);
1188
+ if (targetIdentifier && targetIdentifier.text === "Object" && !targetSymbol) {
1189
+ if (method === "keys" || method === "values" || method === "entries") {
1190
+ const argument = node.arguments.length > 0 ? this.emitExpression(node.arguments[0]) : i64(XT_UNDEFINED);
1191
+ const fn = method === "keys" ? "xt_object_keys" : method === "values" ? "xt_object_values" : "xt_object_entries";
1192
+ return this.runtimeCall(fn, [argument]);
1193
+ }
1194
+ if (method === "assign") {
905
1195
  const args = this.emitArguments(node.arguments);
906
- let last = object;
907
- if (args.argc === 0)
908
- return this.runtimeCall("xt_array_length", [object]);
909
- for (let index = 0; index < args.argc; index++) {
910
- const ptr = this.reg();
911
- this.emit(` ${ptr} = getelementptr i64, i64* ${args.ptr}, i32 ${index}`);
912
- const element = this.reg();
913
- this.emit(` ${element} = load i64, i64* ${ptr}`);
914
- const previous = last;
915
- last = this.reg();
916
- this.emit(` ${last} = call i64 @xt_array_push(i64 ${previous}, i64 ${element})`);
917
- }
918
- return last;
1196
+ const result = this.reg();
1197
+ this.emit(` ${result} = call i64 @xt_object_assign(i32 ${args.argc}, i64* ${args.ptr})`);
1198
+ return result;
919
1199
  }
920
- this.unsupported(node, `array method '${method}'`);
921
- return i64(XT_UNDEFINED);
1200
+ return undefined;
1201
+ }
1202
+ if (BUILTIN_METHODS.has(method)) {
1203
+ const object = this.emitExpression(target);
1204
+ const name = this.stringValue(method);
1205
+ const args = this.emitArguments(node.arguments);
1206
+ const result = this.reg();
1207
+ this.emit(` ${result} = call i64 @xt_call_method(i64 ${object}, i64 ${name}, i32 ${args.argc}, i64* ${args.ptr})`);
1208
+ return result;
922
1209
  }
923
1210
  return undefined;
924
1211
  }
@@ -940,25 +1227,77 @@ class Generator {
940
1227
  }
941
1228
  return { argc: args.length, ptr };
942
1229
  }
1230
+ emitDelete(node) {
1231
+ const target = node.expression;
1232
+ if (target.kind === SyntaxKind.PropertyAccessExpression) {
1233
+ const access = target;
1234
+ const object = this.emitExpression(access.expression);
1235
+ const key = this.stringValue(access.name.text);
1236
+ return this.runtimeCall("xt_delete", [object, key]);
1237
+ }
1238
+ if (target.kind === SyntaxKind.ElementAccessExpression) {
1239
+ const access = target;
1240
+ const object = this.emitExpression(access.expression);
1241
+ const key = this.emitExpression(access.argumentExpression);
1242
+ return this.runtimeCall("xt_delete", [object, key]);
1243
+ }
1244
+ return i64(XT_TRUE);
1245
+ }
943
1246
  emitPropertyAccess(node) {
944
- if (node.name.text === "length") {
945
- const object = this.emitExpression(node.expression);
946
- const result = this.reg();
947
- this.emit(` ${result} = call i64 @xt_array_length(i64 ${object})`);
948
- return result;
1247
+ if (node.name.text in MATH_CONSTANTS &&
1248
+ node.expression.kind === SyntaxKind.Identifier &&
1249
+ node.expression.text === "Math" &&
1250
+ !this.binding.symbolOfIdentifier.get(node.expression)) {
1251
+ return numberLiteral(MATH_CONSTANTS[node.name.text]);
949
1252
  }
950
1253
  const object = this.emitExpression(node.expression);
951
- const key = this.stringValue(node.name.text);
952
- const result = this.reg();
953
- this.emit(` ${result} = call i64 @xt_get(i64 ${object}, i64 ${key})`);
954
- return result;
1254
+ const access = () => {
1255
+ if (node.name.text === "length") {
1256
+ const result = this.reg();
1257
+ this.emit(` ${result} = call i64 @xt_array_length(i64 ${object})`);
1258
+ return result;
1259
+ }
1260
+ const key = this.stringValue(node.name.text);
1261
+ const result = this.reg();
1262
+ this.emit(` ${result} = call i64 @xt_get(i64 ${object}, i64 ${key})`);
1263
+ return result;
1264
+ };
1265
+ return node.optional ? this.emitOptional(object, access) : access();
955
1266
  }
956
1267
  emitElementAccess(node) {
957
1268
  const object = this.emitExpression(node.expression);
958
- const key = this.emitExpression(node.argumentExpression);
959
- const result = this.reg();
960
- this.emit(` ${result} = call i64 @xt_get(i64 ${object}, i64 ${key})`);
961
- return result;
1269
+ const access = () => {
1270
+ const key = this.emitExpression(node.argumentExpression);
1271
+ const result = this.reg();
1272
+ this.emit(` ${result} = call i64 @xt_get(i64 ${object}, i64 ${key})`);
1273
+ return result;
1274
+ };
1275
+ return node.optional ? this.emitOptional(object, access) : access();
1276
+ }
1277
+ /**
1278
+ * Evaluate `object` once and, when it is neither `null` nor `undefined`, run
1279
+ * `compute` to produce the value; otherwise short-circuit to `undefined`.
1280
+ */
1281
+ emitOptional(objectValue, compute) {
1282
+ const result = this.alloca();
1283
+ this.emit(` store i64 ${i64(XT_UNDEFINED)}, i64* ${result}`);
1284
+ const nullish = this.reg();
1285
+ this.emit(` ${nullish} = call i64 @xt_is_nullish(i64 ${objectValue})`);
1286
+ const truthy = this.reg();
1287
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${nullish})`);
1288
+ const condition = this.reg();
1289
+ this.emit(` ${condition} = icmp ne i32 ${truthy}, 0`);
1290
+ const someLabel = this.label("opt.some");
1291
+ const endLabel = this.label("opt.end");
1292
+ this.terminate(`br i1 ${condition}, label %${endLabel}, label %${someLabel}`);
1293
+ this.startBlock(someLabel);
1294
+ const value = compute();
1295
+ this.emit(` store i64 ${value}, i64* ${result}`);
1296
+ this.terminate(`br label %${endLabel}`);
1297
+ this.startBlock(endLabel);
1298
+ const merged = this.reg();
1299
+ this.emit(` ${merged} = load i64, i64* ${result}`);
1300
+ return merged;
962
1301
  }
963
1302
  emitArrayLiteral(node) {
964
1303
  const hasSpread = node.elements.some((element) => element.kind === SyntaxKind.SpreadElement);
@@ -997,6 +1336,10 @@ class Generator {
997
1336
  const value = this.emitIdentifier(identifier);
998
1337
  this.emit(` call i64 @xt_set(i64 ${object}, i64 ${key}, i64 ${value})`);
999
1338
  }
1339
+ else if (property.kind === SyntaxKind.SpreadElement) {
1340
+ const spread = this.emitExpression(property.expression);
1341
+ this.emit(` call i64 @xt_object_spread(i64 ${object}, i64 ${spread})`);
1342
+ }
1000
1343
  else {
1001
1344
  this.unsupported(property, "object spread");
1002
1345
  }
@@ -1083,7 +1426,42 @@ const BINARY_RUNTIME = {
1083
1426
  [BinaryOperator.LessThanLessThan]: "xt_shl",
1084
1427
  [BinaryOperator.GreaterThanGreaterThan]: "xt_shr",
1085
1428
  [BinaryOperator.GreaterThanGreaterThanGreaterThan]: "xt_ushr",
1429
+ [BinaryOperator.In]: "xt_in",
1430
+ };
1431
+ const CONSOLE_METHODS = {
1432
+ log: "xt_console_log",
1433
+ info: "xt_console_info",
1434
+ warn: "xt_console_warn",
1435
+ error: "xt_console_error",
1436
+ };
1437
+ const MATH_FUNCTIONS = new Set([
1438
+ "abs", "floor", "ceil", "round", "trunc", "sqrt", "cbrt", "pow", "exp", "log", "log2", "log10",
1439
+ "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "hypot", "sign", "random", "min", "max",
1440
+ ]);
1441
+ const MATH_CONSTANTS = {
1442
+ PI: Math.PI,
1443
+ E: Math.E,
1444
+ LN2: Math.LN2,
1445
+ LN10: Math.LN10,
1446
+ LOG2E: Math.LOG2E,
1447
+ LOG10E: Math.LOG10E,
1448
+ SQRT2: Math.SQRT2,
1449
+ SQRT1_2: Math.SQRT1_2,
1450
+ };
1451
+ const GLOBAL_FUNCTIONS = {
1452
+ parseInt: "xt_parse_int",
1453
+ parseFloat: "xt_parse_float",
1454
+ isNaN: "xt_is_nan",
1455
+ isFinite: "xt_is_finite",
1456
+ Number: "xt_number_ctor",
1457
+ String: "xt_string_ctor",
1458
+ Boolean: "xt_boolean_ctor",
1086
1459
  };
1460
+ const BUILTIN_METHODS = new Set([
1461
+ "push", "pop", "shift", "unshift", "join", "slice", "indexOf", "includes", "map", "forEach", "filter",
1462
+ "reduce", "concat", "reverse", "charAt", "charCodeAt", "substring", "substr", "split", "toUpperCase",
1463
+ "toLowerCase", "trim", "replace", "repeat", "startsWith", "endsWith",
1464
+ ]);
1087
1465
  const ASSIGNMENT_OPERATORS = new Set([
1088
1466
  "=",
1089
1467
  "+=",