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.
@@ -41,6 +41,10 @@ import {
41
41
  type ForStatement,
42
42
  type ForOfStatement,
43
43
  type ForInStatement,
44
+ type SwitchStatement,
45
+ type CaseClause,
46
+ type TryStatement,
47
+ type DeleteExpression,
44
48
  type ReturnStatement,
45
49
  type ThrowStatement,
46
50
  type ObjectLiteralExpression,
@@ -77,6 +81,8 @@ interface Slot {
77
81
  interface LoopLabels {
78
82
  readonly breakLabel: string;
79
83
  readonly continueLabel: string;
84
+ /** Number of enclosing `try` frames when the loop was entered. */
85
+ readonly tryDepth?: number;
80
86
  }
81
87
 
82
88
  interface FunctionState {
@@ -88,6 +94,10 @@ interface FunctionState {
88
94
  label: number;
89
95
  terminated: boolean;
90
96
  readonly loops: LoopLabels[];
97
+ readonly tryFrames: string[];
98
+ readonly escapePointers: string[];
99
+ /** Set when the function contains a `try`, forcing locals to live in memory. */
100
+ usesTry: boolean;
91
101
  }
92
102
 
93
103
  const RUNTIME_DECLARATIONS: readonly string[] = [
@@ -133,6 +143,23 @@ const RUNTIME_DECLARATIONS: readonly string[] = [
133
143
  "declare i64 @xt_object_get_cstr(i64, i8*)",
134
144
  "declare i64 @xt_object_set(i64, i64, i64)",
135
145
  "declare i64 @xt_object_has(i64, i64)",
146
+ "declare i64 @xt_object_keys(i64)",
147
+ "declare i64 @xt_object_values(i64)",
148
+ "declare i64 @xt_object_entries(i64)",
149
+ "declare i64 @xt_object_assign(i32, i64*)",
150
+ "declare i64 @xt_object_spread(i64, i64)",
151
+ "declare i64 @xt_call_method(i64, i64, i32, i64*)",
152
+ "declare i64 @xt_math_call(i64, i32, i64*)",
153
+ "declare i64 @xt_parse_int(i32, i64*)",
154
+ "declare i64 @xt_parse_float(i32, i64*)",
155
+ "declare i64 @xt_is_nan(i32, i64*)",
156
+ "declare i64 @xt_is_finite(i32, i64*)",
157
+ "declare i64 @xt_number_ctor(i32, i64*)",
158
+ "declare i64 @xt_string_ctor(i32, i64*)",
159
+ "declare i64 @xt_boolean_ctor(i32, i64*)",
160
+ "declare i64 @xt_in(i64, i64)",
161
+ "declare i64 @xt_delete(i64, i64)",
162
+ "declare i64 @xt_rest_args(i32, i64*, i32)",
136
163
  "declare i64 @xt_array_new(i32, i64*)",
137
164
  "declare i64 @xt_array_push(i64, i64)",
138
165
  "declare i64 @xt_array_length(i64)",
@@ -144,7 +171,14 @@ const RUNTIME_DECLARATIONS: readonly string[] = [
144
171
  "declare i64 @xt_box_set(i64, i64)",
145
172
  "declare i64 @xt_is_nullish(i64)",
146
173
  "declare void @xt_throw(i64)",
174
+ "declare i32 @_setjmp(i8*) returns_twice",
175
+ "declare i8* @xt_try_enter()",
176
+ "declare i64 @xt_try_exception(i8*)",
177
+ "declare void @xt_try_leave(i8*)",
147
178
  "declare void @xt_console_log(i32, i64*)",
179
+ "declare void @xt_console_info(i32, i64*)",
180
+ "declare void @xt_console_warn(i32, i64*)",
181
+ "declare void @xt_console_error(i32, i64*)",
148
182
  ];
149
183
 
150
184
  export interface CodegenResult {
@@ -205,6 +239,9 @@ class Generator {
205
239
  label: 0,
206
240
  terminated: false,
207
241
  loops: [],
242
+ tryFrames: [],
243
+ escapePointers: [],
244
+ usesTry: false,
208
245
  };
209
246
  this.current = state;
210
247
 
@@ -217,11 +254,20 @@ class Generator {
217
254
  this.emit(`store i64* %argv, i64** %saved.argv`);
218
255
 
219
256
  // Parameters.
257
+ const parameterNodes = (fn.node as { parameters?: Parameter[] }).parameters ?? [];
220
258
  for (let index = 0; index < fn.params.length; index++) {
221
259
  const symbol = fn.params[index]!;
260
+ const parameter = parameterNodes[index];
261
+ if (parameter?.dotDotDotToken) {
262
+ const rest = this.reg();
263
+ this.emit(` ${rest} = call i64 @xt_rest_args(i32 %argc, i64* %argv, i32 ${index})`);
264
+ this.declareSlot(symbol, rest);
265
+ continue;
266
+ }
222
267
  const value = this.reg();
223
268
  this.emit(` ${value} = call i64 @xt_arg(i32 %argc, i64* %argv, i32 ${index})`);
224
269
  this.declareSlot(symbol, value);
270
+ if (parameter?.initializer) this.emitDefaultParameter(symbol, value, parameter.initializer);
225
271
  }
226
272
 
227
273
  // Captures threaded through the environment.
@@ -245,7 +291,10 @@ class Generator {
245
291
 
246
292
  if (!this.current.terminated) this.terminate(`ret i64 ${i64(XT_UNDEFINED)}`);
247
293
 
248
- const lines = [...state.allocas.map((a) => ` ${a}`), ...state.buffer];
294
+ const escapes = state.usesTry
295
+ ? state.escapePointers.map((ptr) => ` call void asm sideeffect "", "r"(i64* ${ptr})`)
296
+ : [];
297
+ const lines = [...state.allocas.map((a) => ` ${a}`), ...escapes, ...state.buffer];
249
298
  this.functions.push([header, ...lines, "}", ""].join("\n"));
250
299
  }
251
300
 
@@ -293,6 +342,7 @@ class Generator {
293
342
  private alloca(): string {
294
343
  const ptr = `%slot${this.current.allocas.length}`;
295
344
  this.current.allocas.push(`${ptr} = alloca i64`);
345
+ this.current.escapePointers.push(ptr);
296
346
  return ptr;
297
347
  }
298
348
 
@@ -355,6 +405,23 @@ class Generator {
355
405
  this.emit(` call i64 @xt_box_set(i64 ${box}, i64 ${value})`);
356
406
  }
357
407
 
408
+ private emitDefaultParameter(symbol: SymbolInfo, value: string, initializer: Expression): void {
409
+ const isUndefined = this.reg();
410
+ this.emit(` ${isUndefined} = call i64 @xt_seq(i64 ${value}, i64 ${i64(XT_UNDEFINED)})`);
411
+ const truthy = this.reg();
412
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${isUndefined})`);
413
+ const condition = this.reg();
414
+ this.emit(` ${condition} = icmp ne i32 ${truthy}, 0`);
415
+ const applyLabel = this.label("param.default");
416
+ const endLabel = this.label("param.end");
417
+ this.terminate(`br i1 ${condition}, label %${applyLabel}, label %${endLabel}`);
418
+ this.startBlock(applyLabel);
419
+ const fallback = this.emitExpression(initializer);
420
+ this.writeSlot(symbol, fallback);
421
+ this.terminate(`br label %${endLabel}`);
422
+ this.startBlock(endLabel);
423
+ }
424
+
358
425
  // -- statements ----------------------------------------------------------
359
426
 
360
427
  private emitStatements(statements: readonly Statement[]): void {
@@ -397,17 +464,29 @@ class Generator {
397
464
  case SyntaxKind.ForInStatement:
398
465
  this.emitForOf(statement as ForOfStatement | ForInStatement);
399
466
  return;
467
+ case SyntaxKind.SwitchStatement:
468
+ this.emitSwitch(statement as SwitchStatement);
469
+ return;
470
+ case SyntaxKind.TryStatement:
471
+ this.emitTry(statement as TryStatement);
472
+ return;
400
473
  case SyntaxKind.ReturnStatement:
401
474
  this.emitReturn(statement as ReturnStatement);
402
475
  return;
403
476
  case SyntaxKind.BreakStatement: {
404
477
  const loop = this.current.loops[this.current.loops.length - 1];
405
- if (loop) this.terminate(`br label %${loop.breakLabel}`);
478
+ if (loop) {
479
+ this.popTryFramesTo(loop.tryDepth ?? 0);
480
+ this.terminate(`br label %${loop.breakLabel}`);
481
+ }
406
482
  return;
407
483
  }
408
484
  case SyntaxKind.ContinueStatement: {
409
485
  const loop = this.current.loops[this.current.loops.length - 1];
410
- if (loop) this.terminate(`br label %${loop.continueLabel}`);
486
+ if (loop) {
487
+ this.popTryFramesTo(loop.tryDepth ?? 0);
488
+ this.terminate(`br label %${loop.continueLabel}`);
489
+ }
411
490
  return;
412
491
  }
413
492
  case SyntaxKind.ThrowStatement: {
@@ -476,7 +555,7 @@ class Generator {
476
555
  this.terminate(`br i1 ${nonzero}, label %${bodyLabel}, label %${endLabel}`);
477
556
 
478
557
  this.startBlock(bodyLabel);
479
- this.current.loops.push({ breakLabel: endLabel, continueLabel: condLabel });
558
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: condLabel, tryDepth: this.current.tryFrames.length });
480
559
  this.emitStatement(statement.statement);
481
560
  this.current.loops.pop();
482
561
  if (!this.current.terminated) this.terminate(`br label %${condLabel}`);
@@ -489,7 +568,7 @@ class Generator {
489
568
  const endLabel = this.label("do.end");
490
569
  this.terminate(`br label %${bodyLabel}`);
491
570
  this.startBlock(bodyLabel);
492
- this.current.loops.push({ breakLabel: endLabel, continueLabel: condLabel });
571
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: condLabel, tryDepth: this.current.tryFrames.length });
493
572
  this.emitStatement(statement.statement);
494
573
  this.current.loops.pop();
495
574
  if (!this.current.terminated) this.terminate(`br label %${condLabel}`);
@@ -535,7 +614,7 @@ class Generator {
535
614
  }
536
615
 
537
616
  this.startBlock(bodyLabel);
538
- this.current.loops.push({ breakLabel: endLabel, continueLabel: updateLabel });
617
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: updateLabel, tryDepth: this.current.tryFrames.length });
539
618
  this.emitStatement(statement.statement);
540
619
  this.current.loops.pop();
541
620
  if (!this.current.terminated) this.terminate(`br label %${updateLabel}`);
@@ -546,9 +625,13 @@ class Generator {
546
625
  this.startBlock(endLabel);
547
626
  }
548
627
 
549
- /** `for (const x of xs)` / `for (const k in obj)` over arrays and objects. */
628
+ /** `for (const x of xs)` / `for (const k in obj)` over arrays, strings and objects. */
550
629
  private emitForOf(statement: ForOfStatement | ForInStatement): void {
551
- const iterable = this.emitExpression(statement.expression);
630
+ const source = this.emitExpression(statement.expression);
631
+ const isForIn = statement.kind === SyntaxKind.ForInStatement;
632
+ // `for...in` iterates the enumerable keys (indices become strings);
633
+ // `for...of` iterates the values at each index.
634
+ const iterable = isForIn ? this.runtimeCall("xt_object_keys", [source]) : source;
552
635
  const indexPtr = this.alloca();
553
636
  this.emit(` store i64 ${numberLiteral(0)}, i64* ${indexPtr}`);
554
637
  const lengthValue = this.reg();
@@ -575,7 +658,7 @@ class Generator {
575
658
  const element = this.reg();
576
659
  this.emit(` ${element} = call i64 @xt_get(i64 ${iterable}, i64 ${index})`);
577
660
  this.bindLoopVariable(statement.initializer, element);
578
- this.current.loops.push({ breakLabel: endLabel, continueLabel: updateLabel });
661
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: updateLabel, tryDepth: this.current.tryFrames.length });
579
662
  this.emitStatement(statement.statement);
580
663
  this.current.loops.pop();
581
664
  if (!this.current.terminated) this.terminate(`br label %${updateLabel}`);
@@ -602,11 +685,174 @@ class Generator {
602
685
  this.emitAssignmentTarget(initializer as Expression, value);
603
686
  }
604
687
 
688
+ /**
689
+ * JavaScript `switch`: test each `case` with strict equality, then run the
690
+ * matched clause and fall through into the following clauses until `break`.
691
+ */
692
+ private emitSwitch(statement: SwitchStatement): void {
693
+ const discriminant = this.emitExpression(statement.expression);
694
+ const endLabel = this.label("switch.end");
695
+ const clauses = statement.clauses;
696
+ const labels = clauses.map(() => this.label("switch.case"));
697
+ const testLabels = clauses.map((clause) =>
698
+ clause.kind === SyntaxKind.CaseClause ? this.label("switch.test") : undefined,
699
+ );
700
+ const defaultIndex = clauses.findIndex((clause) => clause.kind === SyntaxKind.DefaultClause);
701
+ const defaultLabel = defaultIndex >= 0 ? labels[defaultIndex]! : endLabel;
702
+ const caseIndexes = clauses
703
+ .map((clause, index) => ({ clause, index }))
704
+ .filter((entry) => entry.clause.kind === SyntaxKind.CaseClause);
705
+
706
+ this.terminate(`br label %${caseIndexes.length > 0 ? testLabels[caseIndexes[0]!.index]! : defaultLabel}`);
707
+ for (let test = 0; test < caseIndexes.length; test++) {
708
+ const entry = caseIndexes[test]!;
709
+ this.startBlock(testLabels[entry.index]!);
710
+ const caseValue = this.emitExpression((entry.clause as CaseClause).expression);
711
+ const equals = this.reg();
712
+ this.emit(` ${equals} = call i64 @xt_seq(i64 ${discriminant}, i64 ${caseValue})`);
713
+ const truthy = this.reg();
714
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${equals})`);
715
+ const condition = this.reg();
716
+ this.emit(` ${condition} = icmp ne i32 ${truthy}, 0`);
717
+ const next = test + 1 < caseIndexes.length ? testLabels[caseIndexes[test + 1]!.index]! : defaultLabel;
718
+ this.terminate(`br i1 ${condition}, label %${labels[entry.index]}, label %${next}`);
719
+ }
720
+
721
+ for (let index = 0; index < clauses.length; index++) {
722
+ this.startBlock(labels[index]!);
723
+ const enclosing = this.current.loops[this.current.loops.length - 1];
724
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: enclosing?.continueLabel ?? endLabel, tryDepth: this.current.tryFrames.length });
725
+ for (const child of clauses[index]!.statements) {
726
+ if (this.current.terminated) break;
727
+ this.emitStatement(child);
728
+ }
729
+ this.current.loops.pop();
730
+ if (!this.current.terminated) {
731
+ const next = index + 1 < clauses.length ? labels[index + 1]! : endLabel;
732
+ this.terminate(`br label %${next}`);
733
+ }
734
+ }
735
+ this.startBlock(endLabel);
736
+ }
737
+
738
+ /**
739
+ * `try`/`catch`/`finally` via a runtime setjmp frame. The runtime keeps a
740
+ * stack of frames; `xt_throw` longjmps into the innermost one. A `return`
741
+ * that exits the protected region skips `finally` (a known limitation).
742
+ */
743
+ private emitTry(statement: TryStatement): void {
744
+ this.current.usesTry = true;
745
+ const frameSlot = this.alloca();
746
+ const exceptionSlot = this.alloca();
747
+ const flagSlot = this.alloca();
748
+ const tryLabel = this.label("try.body");
749
+ const catchLabel = this.label("try.catch");
750
+ const exceptionLabel = this.label("try.exception");
751
+ const rethrowLabel = this.label("try.rethrow");
752
+ const finallyLabel = statement.finallyBlock ? this.label("try.finally") : undefined;
753
+ const endLabel = this.label("try.end");
754
+ const hasCatch = !!statement.catchClause;
755
+
756
+ const frame = this.reg();
757
+ this.emit(` ${frame} = call i8* @xt_try_enter()`);
758
+ this.emit(` store i8* ${frame}, i8** ${frameSlot}`);
759
+ this.emit(` store i64 0, i64* ${flagSlot}`);
760
+ const jump = this.reg();
761
+ this.emit(` ${jump} = call i32 @_setjmp(i8* ${frame})`);
762
+ const isThrow = this.reg();
763
+ this.emit(` ${isThrow} = icmp ne i32 ${jump}, 0`);
764
+ const exceptionTarget = hasCatch ? catchLabel : exceptionLabel;
765
+ this.terminate(`br i1 ${isThrow}, label %${exceptionTarget}, label %${tryLabel}`);
766
+
767
+ // Normal completion of the try block.
768
+ this.startBlock(tryLabel);
769
+ this.current.tryFrames.push(frameSlot);
770
+ this.emitStatements(statement.tryBlock.statements);
771
+ this.current.tryFrames.pop();
772
+ if (!this.current.terminated) {
773
+ const currentFrame = this.reg();
774
+ this.emit(` ${currentFrame} = load i8*, i8** ${frameSlot}`);
775
+ this.emit(` call void @xt_try_leave(i8* ${currentFrame})`);
776
+ this.terminate(`br label %${finallyLabel ?? endLabel}`);
777
+ }
778
+
779
+ if (hasCatch) {
780
+ this.startBlock(catchLabel);
781
+ const currentFrame = this.reg();
782
+ this.emit(` ${currentFrame} = load i8*, i8** ${frameSlot}`);
783
+ const exception = this.reg();
784
+ this.emit(` ${exception} = call i64 @xt_try_exception(i8* ${currentFrame})`);
785
+ this.emit(` call void @xt_try_leave(i8* ${currentFrame})`);
786
+ this.bindCatchVariable(statement.catchClause!.variable, exception);
787
+ this.emitStatements(statement.catchClause!.block.statements);
788
+ if (!this.current.terminated) this.terminate(`br label %${finallyLabel ?? endLabel}`);
789
+ } else {
790
+ // Without a catch clause the exception is remembered, then rethrown
791
+ // after `finally` runs.
792
+ this.startBlock(exceptionLabel);
793
+ const currentFrame = this.reg();
794
+ this.emit(` ${currentFrame} = load i8*, i8** ${frameSlot}`);
795
+ const exception = this.reg();
796
+ this.emit(` ${exception} = call i64 @xt_try_exception(i8* ${currentFrame})`);
797
+ this.emit(` call void @xt_try_leave(i8* ${currentFrame})`);
798
+ this.emit(` store i64 ${exception}, i64* ${exceptionSlot}`);
799
+ this.emit(` store i64 1, i64* ${flagSlot}`);
800
+ this.terminate(`br label %${finallyLabel ?? rethrowLabel}`);
801
+ }
802
+
803
+ if (finallyLabel) {
804
+ this.startBlock(finallyLabel);
805
+ this.emitStatements(statement.finallyBlock!.statements);
806
+ if (!this.current.terminated) {
807
+ if (hasCatch) {
808
+ this.terminate(`br label %${endLabel}`);
809
+ } else {
810
+ const flag = this.reg();
811
+ this.emit(` ${flag} = load i64, i64* ${flagSlot}`);
812
+ const truthy = this.reg();
813
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${flag})`);
814
+ const shouldRethrow = this.reg();
815
+ this.emit(` ${shouldRethrow} = icmp ne i32 ${truthy}, 0`);
816
+ this.terminate(`br i1 ${shouldRethrow}, label %${rethrowLabel}, label %${endLabel}`);
817
+ }
818
+ }
819
+ }
820
+
821
+ if (!hasCatch) {
822
+ this.startBlock(rethrowLabel);
823
+ const exception = this.reg();
824
+ this.emit(` ${exception} = load i64, i64* ${exceptionSlot}`);
825
+ this.emit(` call void @xt_throw(i64 ${exception})`);
826
+ this.terminate("unreachable");
827
+ }
828
+
829
+ this.startBlock(endLabel);
830
+ }
831
+
832
+ private bindCatchVariable(variable: Identifier | undefined, value: string): void {
833
+ if (!variable) return;
834
+ const symbol = this.binding.symbolOfDeclaration.get(variable);
835
+ if (!symbol) return;
836
+ if (this.current.slots.has(symbol.id)) this.writeSlot(symbol, value);
837
+ else this.declareSlot(symbol, value);
838
+ }
839
+
605
840
  private emitReturn(statement: ReturnStatement): void {
606
841
  const value = statement.expression ? this.emitExpression(statement.expression) : i64(XT_UNDEFINED);
842
+ this.popTryFramesTo(0);
607
843
  this.terminate(`ret i64 ${value}`);
608
844
  }
609
845
 
846
+ /** Emit `xt_try_leave` for every active frame above `depth` (innermost first). */
847
+ private popTryFramesTo(depth: number): void {
848
+ for (let index = this.current.tryFrames.length - 1; index >= depth; index--) {
849
+ const slot = this.current.tryFrames[index]!;
850
+ const frame = this.reg();
851
+ this.emit(` ${frame} = load i8*, i8** ${slot}`);
852
+ this.emit(` call void @xt_try_leave(i8* ${frame})`);
853
+ }
854
+ }
855
+
610
856
  // -- expressions ---------------------------------------------------------
611
857
 
612
858
  private emitExpression(node: Expression): string {
@@ -639,6 +885,8 @@ class Generator {
639
885
  case SyntaxKind.SatisfiesExpression:
640
886
  case SyntaxKind.NonNullExpression:
641
887
  return this.emitExpression((node as { expression: Expression }).expression);
888
+ case SyntaxKind.DeleteExpression:
889
+ return this.emitDelete(node as DeleteExpression);
642
890
  case SyntaxKind.BinaryExpression:
643
891
  return this.emitBinaryOrAssignment(node as BinaryExpression);
644
892
  case SyntaxKind.PrefixUnaryExpression:
@@ -683,6 +931,15 @@ class Generator {
683
931
  return numberLiteral(Infinity);
684
932
  case "console":
685
933
  return i64(XT_UNDEFINED);
934
+ case "arguments": {
935
+ const argc = this.reg();
936
+ this.emit(` ${argc} = load i32, i32* %saved.argc`);
937
+ const argv = this.reg();
938
+ this.emit(` ${argv} = load i64*, i64** %saved.argv`);
939
+ const rest = this.reg();
940
+ this.emit(` ${rest} = call i64 @xt_rest_args(i32 ${argc}, i64* ${argv}, i32 0)`);
941
+ return rest;
942
+ }
686
943
  default:
687
944
  this.diagnostics.error(
688
945
  DiagnosticCode.CannotFindName,
@@ -980,6 +1237,37 @@ class Generator {
980
1237
 
981
1238
  private emitCall(node: CallExpression): string {
982
1239
  const callee = node.expression;
1240
+ if (node.optional) {
1241
+ const calleeValue = this.emitExpression(callee);
1242
+ return this.emitOptional(calleeValue, () => {
1243
+ const args = this.emitArguments(node.arguments);
1244
+ const result = this.reg();
1245
+ this.emit(` ${result} = call i64 @xt_closure_call(i64 ${calleeValue}, i32 ${args.argc}, i64* ${args.ptr})`);
1246
+ return result;
1247
+ });
1248
+ }
1249
+ if (
1250
+ (callee.kind === SyntaxKind.PropertyAccessExpression || callee.kind === SyntaxKind.ElementAccessExpression) &&
1251
+ (callee as PropertyAccessExpression | ElementAccessExpression).optional
1252
+ ) {
1253
+ // `obj?.method(args)` / `obj?.[key](args)`: guard the receiver, then
1254
+ // dispatch through the runtime, which understands object methods as well
1255
+ // as the built-in array/string methods.
1256
+ const access = callee as PropertyAccessExpression | ElementAccessExpression;
1257
+ const object = this.emitExpression(access.expression);
1258
+ return this.emitOptional(object, () => {
1259
+ const name =
1260
+ access.kind === SyntaxKind.PropertyAccessExpression
1261
+ ? this.stringValue((access as PropertyAccessExpression).name.text)
1262
+ : this.emitExpression((access as ElementAccessExpression).argumentExpression);
1263
+ const args = this.emitArguments(node.arguments);
1264
+ const result = this.reg();
1265
+ this.emit(
1266
+ ` ${result} = call i64 @xt_call_method(i64 ${object}, i64 ${name}, i32 ${args.argc}, i64* ${args.ptr})`,
1267
+ );
1268
+ return result;
1269
+ });
1270
+ }
983
1271
  if (callee.kind === SyntaxKind.PropertyAccessExpression) {
984
1272
  const special = this.tryEmitBuiltinCall(node, callee as PropertyAccessExpression);
985
1273
  if (special) return special;
@@ -998,6 +1286,13 @@ class Generator {
998
1286
  return result;
999
1287
  }
1000
1288
  }
1289
+ const globalFunction = !symbol ? GLOBAL_FUNCTIONS[(callee as Identifier).text] : undefined;
1290
+ if (globalFunction) {
1291
+ const args = this.emitArguments(node.arguments);
1292
+ const result = this.reg();
1293
+ this.emit(` ${result} = call i64 @${globalFunction}(i32 ${args.argc}, i64* ${args.ptr})`);
1294
+ return result;
1295
+ }
1001
1296
  const builtin = this.builtins[(callee as Identifier).text];
1002
1297
  if (!symbol && builtin) {
1003
1298
  this.extraDeclarations.add(
@@ -1023,31 +1318,52 @@ class Generator {
1023
1318
  private tryEmitBuiltinCall(node: CallExpression, callee: PropertyAccessExpression): string | undefined {
1024
1319
  const target = callee.expression;
1025
1320
  const method = callee.name.text;
1026
- if (target.kind === SyntaxKind.Identifier && (target as Identifier).text === "console") {
1321
+ const targetIdentifier = target.kind === SyntaxKind.Identifier ? (target as Identifier) : undefined;
1322
+ const targetSymbol = targetIdentifier ? this.binding.symbolOfIdentifier.get(targetIdentifier) : undefined;
1323
+
1324
+ if (targetIdentifier && targetIdentifier.text === "console" && !targetSymbol) {
1325
+ const consoleFn = CONSOLE_METHODS[method];
1326
+ if (consoleFn) {
1327
+ const args = this.emitArguments(node.arguments);
1328
+ this.emit(` call void @${consoleFn}(i32 ${args.argc}, i64* ${args.ptr})`);
1329
+ return i64(XT_UNDEFINED);
1330
+ }
1331
+ return undefined;
1332
+ }
1333
+
1334
+ if (targetIdentifier && targetIdentifier.text === "Math" && !targetSymbol) {
1335
+ if (!MATH_FUNCTIONS.has(method)) return undefined;
1336
+ const name = this.stringValue(method);
1027
1337
  const args = this.emitArguments(node.arguments);
1028
- this.emit(` call void @xt_console_log(i32 ${args.argc}, i64* ${args.ptr})`);
1029
- return i64(XT_UNDEFINED);
1338
+ const result = this.reg();
1339
+ this.emit(` ${result} = call i64 @xt_math_call(i64 ${name}, i32 ${args.argc}, i64* ${args.ptr})`);
1340
+ return result;
1030
1341
  }
1031
- // Array and string methods.
1032
- 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") {
1033
- if (method === "push") {
1034
- const object = this.emitExpression(target);
1342
+
1343
+ if (targetIdentifier && targetIdentifier.text === "Object" && !targetSymbol) {
1344
+ if (method === "keys" || method === "values" || method === "entries") {
1345
+ const argument = node.arguments.length > 0 ? this.emitExpression(node.arguments[0]!) : i64(XT_UNDEFINED);
1346
+ const fn = method === "keys" ? "xt_object_keys" : method === "values" ? "xt_object_values" : "xt_object_entries";
1347
+ return this.runtimeCall(fn, [argument]);
1348
+ }
1349
+ if (method === "assign") {
1035
1350
  const args = this.emitArguments(node.arguments);
1036
- let last = object;
1037
- if (args.argc === 0) return this.runtimeCall("xt_array_length", [object]);
1038
- for (let index = 0; index < args.argc; index++) {
1039
- const ptr = this.reg();
1040
- this.emit(` ${ptr} = getelementptr i64, i64* ${args.ptr}, i32 ${index}`);
1041
- const element = this.reg();
1042
- this.emit(` ${element} = load i64, i64* ${ptr}`);
1043
- const previous = last;
1044
- last = this.reg();
1045
- this.emit(` ${last} = call i64 @xt_array_push(i64 ${previous}, i64 ${element})`);
1046
- }
1047
- return last;
1351
+ const result = this.reg();
1352
+ this.emit(` ${result} = call i64 @xt_object_assign(i32 ${args.argc}, i64* ${args.ptr})`);
1353
+ return result;
1048
1354
  }
1049
- this.unsupported(node, `array method '${method}'`);
1050
- return i64(XT_UNDEFINED);
1355
+ return undefined;
1356
+ }
1357
+
1358
+ if (BUILTIN_METHODS.has(method)) {
1359
+ const object = this.emitExpression(target);
1360
+ const name = this.stringValue(method);
1361
+ const args = this.emitArguments(node.arguments);
1362
+ const result = this.reg();
1363
+ this.emit(
1364
+ ` ${result} = call i64 @xt_call_method(i64 ${object}, i64 ${name}, i32 ${args.argc}, i64* ${args.ptr})`,
1365
+ );
1366
+ return result;
1051
1367
  }
1052
1368
  return undefined;
1053
1369
  }
@@ -1071,26 +1387,82 @@ class Generator {
1071
1387
  return { argc: args.length, ptr };
1072
1388
  }
1073
1389
 
1390
+ private emitDelete(node: DeleteExpression): string {
1391
+ const target = node.expression;
1392
+ if (target.kind === SyntaxKind.PropertyAccessExpression) {
1393
+ const access = target as PropertyAccessExpression;
1394
+ const object = this.emitExpression(access.expression);
1395
+ const key = this.stringValue(access.name.text);
1396
+ return this.runtimeCall("xt_delete", [object, key]);
1397
+ }
1398
+ if (target.kind === SyntaxKind.ElementAccessExpression) {
1399
+ const access = target as ElementAccessExpression;
1400
+ const object = this.emitExpression(access.expression);
1401
+ const key = this.emitExpression(access.argumentExpression);
1402
+ return this.runtimeCall("xt_delete", [object, key]);
1403
+ }
1404
+ return i64(XT_TRUE);
1405
+ }
1406
+
1074
1407
  private emitPropertyAccess(node: PropertyAccessExpression): string {
1075
- if (node.name.text === "length") {
1076
- const object = this.emitExpression(node.expression);
1077
- const result = this.reg();
1078
- this.emit(` ${result} = call i64 @xt_array_length(i64 ${object})`);
1079
- return result;
1408
+ if (
1409
+ node.name.text in MATH_CONSTANTS &&
1410
+ node.expression.kind === SyntaxKind.Identifier &&
1411
+ (node.expression as Identifier).text === "Math" &&
1412
+ !this.binding.symbolOfIdentifier.get(node.expression as Identifier)
1413
+ ) {
1414
+ return numberLiteral(MATH_CONSTANTS[node.name.text]!);
1080
1415
  }
1081
1416
  const object = this.emitExpression(node.expression);
1082
- const key = this.stringValue(node.name.text);
1083
- const result = this.reg();
1084
- this.emit(` ${result} = call i64 @xt_get(i64 ${object}, i64 ${key})`);
1085
- return result;
1417
+ const access = (): string => {
1418
+ if (node.name.text === "length") {
1419
+ const result = this.reg();
1420
+ this.emit(` ${result} = call i64 @xt_array_length(i64 ${object})`);
1421
+ return result;
1422
+ }
1423
+ const key = this.stringValue(node.name.text);
1424
+ const result = this.reg();
1425
+ this.emit(` ${result} = call i64 @xt_get(i64 ${object}, i64 ${key})`);
1426
+ return result;
1427
+ };
1428
+ return node.optional ? this.emitOptional(object, access) : access();
1086
1429
  }
1087
1430
 
1088
1431
  private emitElementAccess(node: ElementAccessExpression): string {
1089
1432
  const object = this.emitExpression(node.expression);
1090
- const key = this.emitExpression(node.argumentExpression);
1091
- const result = this.reg();
1092
- this.emit(` ${result} = call i64 @xt_get(i64 ${object}, i64 ${key})`);
1093
- return result;
1433
+ const access = (): string => {
1434
+ const key = this.emitExpression(node.argumentExpression);
1435
+ const result = this.reg();
1436
+ this.emit(` ${result} = call i64 @xt_get(i64 ${object}, i64 ${key})`);
1437
+ return result;
1438
+ };
1439
+ return node.optional ? this.emitOptional(object, access) : access();
1440
+ }
1441
+
1442
+ /**
1443
+ * Evaluate `object` once and, when it is neither `null` nor `undefined`, run
1444
+ * `compute` to produce the value; otherwise short-circuit to `undefined`.
1445
+ */
1446
+ private emitOptional(objectValue: string, compute: () => string): string {
1447
+ const result = this.alloca();
1448
+ this.emit(` store i64 ${i64(XT_UNDEFINED)}, i64* ${result}`);
1449
+ const nullish = this.reg();
1450
+ this.emit(` ${nullish} = call i64 @xt_is_nullish(i64 ${objectValue})`);
1451
+ const truthy = this.reg();
1452
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${nullish})`);
1453
+ const condition = this.reg();
1454
+ this.emit(` ${condition} = icmp ne i32 ${truthy}, 0`);
1455
+ const someLabel = this.label("opt.some");
1456
+ const endLabel = this.label("opt.end");
1457
+ this.terminate(`br i1 ${condition}, label %${endLabel}, label %${someLabel}`);
1458
+ this.startBlock(someLabel);
1459
+ const value = compute();
1460
+ this.emit(` store i64 ${value}, i64* ${result}`);
1461
+ this.terminate(`br label %${endLabel}`);
1462
+ this.startBlock(endLabel);
1463
+ const merged = this.reg();
1464
+ this.emit(` ${merged} = load i64, i64* ${result}`);
1465
+ return merged;
1094
1466
  }
1095
1467
 
1096
1468
  private emitArrayLiteral(node: ArrayLiteralExpression): string {
@@ -1128,6 +1500,9 @@ class Generator {
1128
1500
  const key = this.stringValue(identifier.text);
1129
1501
  const value = this.emitIdentifier(identifier);
1130
1502
  this.emit(` call i64 @xt_set(i64 ${object}, i64 ${key}, i64 ${value})`);
1503
+ } else if (property.kind === SyntaxKind.SpreadElement) {
1504
+ const spread = this.emitExpression((property as { expression: Expression }).expression);
1505
+ this.emit(` call i64 @xt_object_spread(i64 ${object}, i64 ${spread})`);
1131
1506
  } else {
1132
1507
  this.unsupported(property, "object spread");
1133
1508
  }
@@ -1223,8 +1598,48 @@ const BINARY_RUNTIME: Record<string, string | undefined> = {
1223
1598
  [BinaryOperator.LessThanLessThan]: "xt_shl",
1224
1599
  [BinaryOperator.GreaterThanGreaterThan]: "xt_shr",
1225
1600
  [BinaryOperator.GreaterThanGreaterThanGreaterThan]: "xt_ushr",
1601
+ [BinaryOperator.In]: "xt_in",
1602
+ };
1603
+
1604
+ const CONSOLE_METHODS: Record<string, string> = {
1605
+ log: "xt_console_log",
1606
+ info: "xt_console_info",
1607
+ warn: "xt_console_warn",
1608
+ error: "xt_console_error",
1609
+ };
1610
+
1611
+ const MATH_FUNCTIONS = new Set<string>([
1612
+ "abs", "floor", "ceil", "round", "trunc", "sqrt", "cbrt", "pow", "exp", "log", "log2", "log10",
1613
+ "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "hypot", "sign", "random", "min", "max",
1614
+ ]);
1615
+
1616
+ const MATH_CONSTANTS: Record<string, number> = {
1617
+ PI: Math.PI,
1618
+ E: Math.E,
1619
+ LN2: Math.LN2,
1620
+ LN10: Math.LN10,
1621
+ LOG2E: Math.LOG2E,
1622
+ LOG10E: Math.LOG10E,
1623
+ SQRT2: Math.SQRT2,
1624
+ SQRT1_2: Math.SQRT1_2,
1226
1625
  };
1227
1626
 
1627
+ const GLOBAL_FUNCTIONS: Record<string, string> = {
1628
+ parseInt: "xt_parse_int",
1629
+ parseFloat: "xt_parse_float",
1630
+ isNaN: "xt_is_nan",
1631
+ isFinite: "xt_is_finite",
1632
+ Number: "xt_number_ctor",
1633
+ String: "xt_string_ctor",
1634
+ Boolean: "xt_boolean_ctor",
1635
+ };
1636
+
1637
+ const BUILTIN_METHODS = new Set<string>([
1638
+ "push", "pop", "shift", "unshift", "join", "slice", "indexOf", "includes", "map", "forEach", "filter",
1639
+ "reduce", "concat", "reverse", "charAt", "charCodeAt", "substring", "substr", "split", "toUpperCase",
1640
+ "toLowerCase", "trim", "replace", "repeat", "startsWith", "endsWith",
1641
+ ]);
1642
+
1228
1643
  const ASSIGNMENT_OPERATORS = new Set<string>([
1229
1644
  "=",
1230
1645
  "+=",