xbintsc 0.1.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/bin/xbintsc.js +26 -0
  4. package/dist/src/ast/factory.d.ts +11 -0
  5. package/dist/src/ast/factory.js +36 -0
  6. package/dist/src/ast/factory.js.map +1 -0
  7. package/dist/src/ast/nodes.d.ts +848 -0
  8. package/dist/src/ast/nodes.js +247 -0
  9. package/dist/src/ast/nodes.js.map +1 -0
  10. package/dist/src/ast/visitor.d.ts +18 -0
  11. package/dist/src/ast/visitor.js +52 -0
  12. package/dist/src/ast/visitor.js.map +1 -0
  13. package/dist/src/binder/binder.d.ts +82 -0
  14. package/dist/src/binder/binder.js +443 -0
  15. package/dist/src/binder/binder.js.map +1 -0
  16. package/dist/src/cli/main.d.ts +16 -0
  17. package/dist/src/cli/main.js +191 -0
  18. package/dist/src/cli/main.js.map +1 -0
  19. package/dist/src/codegen/llvm.d.ts +31 -0
  20. package/dist/src/codegen/llvm.js +1178 -0
  21. package/dist/src/codegen/llvm.js.map +1 -0
  22. package/dist/src/codegen/values.d.ts +23 -0
  23. package/dist/src/codegen/values.js +32 -0
  24. package/dist/src/codegen/values.js.map +1 -0
  25. package/dist/src/diagnostics/diagnostic.d.ts +69 -0
  26. package/dist/src/diagnostics/diagnostic.js +104 -0
  27. package/dist/src/diagnostics/diagnostic.js.map +1 -0
  28. package/dist/src/diagnostics/source.d.ts +43 -0
  29. package/dist/src/diagnostics/source.js +73 -0
  30. package/dist/src/diagnostics/source.js.map +1 -0
  31. package/dist/src/driver/cache.d.ts +37 -0
  32. package/dist/src/driver/cache.js +82 -0
  33. package/dist/src/driver/cache.js.map +1 -0
  34. package/dist/src/driver/compiler.d.ts +41 -0
  35. package/dist/src/driver/compiler.js +128 -0
  36. package/dist/src/driver/compiler.js.map +1 -0
  37. package/dist/src/driver/index.d.ts +4 -0
  38. package/dist/src/driver/index.js +5 -0
  39. package/dist/src/driver/index.js.map +1 -0
  40. package/dist/src/driver/paths.d.ts +4 -0
  41. package/dist/src/driver/paths.js +22 -0
  42. package/dist/src/driver/paths.js.map +1 -0
  43. package/dist/src/driver/toolchain.d.ts +43 -0
  44. package/dist/src/driver/toolchain.js +76 -0
  45. package/dist/src/driver/toolchain.js.map +1 -0
  46. package/dist/src/extensions/node.d.ts +9 -0
  47. package/dist/src/extensions/node.js +20 -0
  48. package/dist/src/extensions/node.js.map +1 -0
  49. package/dist/src/extensions/registry.d.ts +45 -0
  50. package/dist/src/extensions/registry.js +76 -0
  51. package/dist/src/extensions/registry.js.map +1 -0
  52. package/dist/src/index.d.ts +18 -0
  53. package/dist/src/index.js +19 -0
  54. package/dist/src/index.js.map +1 -0
  55. package/dist/src/lexer/scanner.d.ts +48 -0
  56. package/dist/src/lexer/scanner.js +650 -0
  57. package/dist/src/lexer/scanner.js.map +1 -0
  58. package/dist/src/lexer/token.d.ts +170 -0
  59. package/dist/src/lexer/token.js +250 -0
  60. package/dist/src/lexer/token.js.map +1 -0
  61. package/dist/src/parser/parser.d.ts +127 -0
  62. package/dist/src/parser/parser.js +2334 -0
  63. package/dist/src/parser/parser.js.map +1 -0
  64. package/dist/tests/binder/binder.test.d.ts +1 -0
  65. package/dist/tests/binder/binder.test.js +89 -0
  66. package/dist/tests/binder/binder.test.js.map +1 -0
  67. package/dist/tests/cli/main.test.d.ts +1 -0
  68. package/dist/tests/cli/main.test.js +68 -0
  69. package/dist/tests/cli/main.test.js.map +1 -0
  70. package/dist/tests/cli/process.test.d.ts +9 -0
  71. package/dist/tests/cli/process.test.js +57 -0
  72. package/dist/tests/cli/process.test.js.map +1 -0
  73. package/dist/tests/codegen/llvm.test.d.ts +1 -0
  74. package/dist/tests/codegen/llvm.test.js +93 -0
  75. package/dist/tests/codegen/llvm.test.js.map +1 -0
  76. package/dist/tests/driver/cache.test.d.ts +1 -0
  77. package/dist/tests/driver/cache.test.js +66 -0
  78. package/dist/tests/driver/cache.test.js.map +1 -0
  79. package/dist/tests/driver/compiler.test.d.ts +1 -0
  80. package/dist/tests/driver/compiler.test.js +80 -0
  81. package/dist/tests/driver/compiler.test.js.map +1 -0
  82. package/dist/tests/driver/toolchain.test.d.ts +1 -0
  83. package/dist/tests/driver/toolchain.test.js +60 -0
  84. package/dist/tests/driver/toolchain.test.js.map +1 -0
  85. package/dist/tests/e2e/compile.test.d.ts +5 -0
  86. package/dist/tests/e2e/compile.test.js +93 -0
  87. package/dist/tests/e2e/compile.test.js.map +1 -0
  88. package/dist/tests/extensions/registry.test.d.ts +1 -0
  89. package/dist/tests/extensions/registry.test.js +47 -0
  90. package/dist/tests/extensions/registry.test.js.map +1 -0
  91. package/dist/tests/helpers.d.ts +23 -0
  92. package/dist/tests/helpers.js +41 -0
  93. package/dist/tests/helpers.js.map +1 -0
  94. package/dist/tests/lexer/scanner.test.d.ts +1 -0
  95. package/dist/tests/lexer/scanner.test.js +95 -0
  96. package/dist/tests/lexer/scanner.test.js.map +1 -0
  97. package/dist/tests/parser/parser.test.d.ts +1 -0
  98. package/dist/tests/parser/parser.test.js +137 -0
  99. package/dist/tests/parser/parser.test.js.map +1 -0
  100. package/package.json +49 -0
  101. package/runtime/ext_node.c +42 -0
  102. package/runtime/rt.h +177 -0
  103. package/runtime/xt_runtime.c +682 -0
  104. package/scripts/build-runtime.mjs +34 -0
  105. package/src/ast/factory.ts +37 -0
  106. package/src/ast/nodes.ts +1111 -0
  107. package/src/ast/visitor.ts +60 -0
  108. package/src/binder/binder.ts +549 -0
  109. package/src/cli/main.ts +210 -0
  110. package/src/codegen/llvm.ts +1324 -0
  111. package/src/codegen/values.ts +36 -0
  112. package/src/diagnostics/diagnostic.ts +132 -0
  113. package/src/diagnostics/source.ts +97 -0
  114. package/src/driver/cache.ts +96 -0
  115. package/src/driver/compiler.ts +197 -0
  116. package/src/driver/index.ts +4 -0
  117. package/src/driver/paths.ts +24 -0
  118. package/src/driver/toolchain.ts +103 -0
  119. package/src/extensions/node.ts +23 -0
  120. package/src/extensions/registry.ts +101 -0
  121. package/src/index.ts +33 -0
  122. package/src/lexer/scanner.ts +650 -0
  123. package/src/lexer/token.ts +266 -0
  124. package/src/parser/parser.ts +2396 -0
@@ -0,0 +1,1324 @@
1
+ /**
2
+ * LLVM IR code generation.
3
+ *
4
+ * The generator lowers the bound AST to textual LLVM IR. It follows three
5
+ * rules that keep the output simple and correct without a register allocator
6
+ * of our own:
7
+ *
8
+ * 1. Every value crossing a statement or block boundary lives in an
9
+ * `alloca` (emitted in the entry block so LLVM's mem2reg can promote it).
10
+ * 2. Control flow is expressed with explicit basic blocks; conditionals and
11
+ * short-circuit operators materialise into a temporary slot instead of
12
+ * `phi` nodes, which the optimizer folds away.
13
+ * 3. All JavaScript semantics (addition with coercion, comparisons, member
14
+ * lookup, ...) are delegated to the C runtime through `@xt_*` calls.
15
+ *
16
+ * Later pipeline stages (optimization passes, object emission) live in the
17
+ * driver; this module only produces IR text.
18
+ */
19
+
20
+ import {
21
+ SyntaxKind,
22
+ type Node,
23
+ type SourceFileNode,
24
+ type Expression,
25
+ type Statement,
26
+ type Identifier,
27
+ type CallExpression,
28
+ type PropertyAccessExpression,
29
+ type ElementAccessExpression,
30
+ type BinaryExpression,
31
+ type ConditionalExpression,
32
+ type ArrowFunction,
33
+ type FunctionExpression,
34
+ type FunctionDeclaration,
35
+ type VariableStatement,
36
+ type VariableDeclarationList,
37
+ type Block,
38
+ type IfStatement,
39
+ type WhileStatement,
40
+ type DoStatement,
41
+ type ForStatement,
42
+ type ForOfStatement,
43
+ type ForInStatement,
44
+ type ReturnStatement,
45
+ type ThrowStatement,
46
+ type ObjectLiteralExpression,
47
+ type ArrayLiteralExpression,
48
+ type TemplateLiteral,
49
+ type PrefixUnaryExpression,
50
+ type PostfixUnaryExpression,
51
+ type Parameter,
52
+ } from "../ast/nodes.js";
53
+ import {
54
+ AssignmentOperator,
55
+ BinaryOperator,
56
+ PrefixUnaryOperator,
57
+ PostfixUnaryOperator,
58
+ } from "../ast/nodes.js";
59
+ import {
60
+ bind,
61
+ type BindResult,
62
+ type FunctionInfo,
63
+ type SymbolInfo,
64
+ SymbolKind,
65
+ } from "../binder/binder.js";
66
+ import type { DiagnosticBag } from "../diagnostics/diagnostic.js";
67
+ import { DiagnosticCode } from "../diagnostics/diagnostic.js";
68
+ import type { BuiltinFunction } from "../extensions/registry.js";
69
+ import { i64, numberLiteral, XT_UNDEFINED, XT_NULL, XT_FALSE, XT_TRUE } from "./values.js";
70
+
71
+ interface Slot {
72
+ /** Register holding the `i64*` to the storage. */
73
+ readonly ptr: string;
74
+ readonly boxed: boolean;
75
+ }
76
+
77
+ interface LoopLabels {
78
+ readonly breakLabel: string;
79
+ readonly continueLabel: string;
80
+ }
81
+
82
+ interface FunctionState {
83
+ readonly fn: FunctionInfo;
84
+ readonly buffer: string[];
85
+ readonly allocas: string[];
86
+ readonly slots: Map<number, Slot>;
87
+ reg: number;
88
+ label: number;
89
+ terminated: boolean;
90
+ readonly loops: LoopLabels[];
91
+ }
92
+
93
+ const RUNTIME_DECLARATIONS: readonly string[] = [
94
+ "declare i64 @xt_undefined()",
95
+ "declare i64 @xt_null()",
96
+ "declare i64 @xt_bool(i32)",
97
+ "declare i64 @xt_number(double)",
98
+ "declare i64 @xt_string_new(i8*, i64)",
99
+ "declare i64 @xt_string_from_cstr(i8*)",
100
+ "declare i64 @xt_arg(i32, i64*, i32)",
101
+ "declare i64 @xt_closure_new(i8*, i32, i64*)",
102
+ "declare i64 @xt_closure_call(i64, i32, i64*)",
103
+ "declare i64 @xt_closure_env(i64, i32)",
104
+ "declare i32 @xt_truthy(i64)",
105
+ "declare double @xt_to_number(i64)",
106
+ "declare i64 @xt_to_string(i64)",
107
+ "declare i64 @xt_typeof(i64)",
108
+ "declare i64 @xt_add(i64, i64)",
109
+ "declare i64 @xt_sub(i64, i64)",
110
+ "declare i64 @xt_mul(i64, i64)",
111
+ "declare i64 @xt_div(i64, i64)",
112
+ "declare i64 @xt_mod(i64, i64)",
113
+ "declare i64 @xt_pow(i64, i64)",
114
+ "declare i64 @xt_neg(i64)",
115
+ "declare i64 @xt_pos(i64)",
116
+ "declare i64 @xt_bit_and(i64, i64)",
117
+ "declare i64 @xt_bit_or(i64, i64)",
118
+ "declare i64 @xt_bit_xor(i64, i64)",
119
+ "declare i64 @xt_bit_not(i64)",
120
+ "declare i64 @xt_shl(i64, i64)",
121
+ "declare i64 @xt_shr(i64, i64)",
122
+ "declare i64 @xt_ushr(i64, i64)",
123
+ "declare i64 @xt_lt(i64, i64)",
124
+ "declare i64 @xt_le(i64, i64)",
125
+ "declare i64 @xt_gt(i64, i64)",
126
+ "declare i64 @xt_ge(i64, i64)",
127
+ "declare i64 @xt_eq(i64, i64)",
128
+ "declare i64 @xt_ne(i64, i64)",
129
+ "declare i64 @xt_seq(i64, i64)",
130
+ "declare i64 @xt_sne(i64, i64)",
131
+ "declare i64 @xt_not(i64)",
132
+ "declare i64 @xt_object_new()",
133
+ "declare i64 @xt_object_get_cstr(i64, i8*)",
134
+ "declare i64 @xt_object_set(i64, i64, i64)",
135
+ "declare i64 @xt_object_has(i64, i64)",
136
+ "declare i64 @xt_array_new(i32, i64*)",
137
+ "declare i64 @xt_array_push(i64, i64)",
138
+ "declare i64 @xt_array_length(i64)",
139
+ "declare i64 @xt_array_spread(i64, i64)",
140
+ "declare i64 @xt_get(i64, i64)",
141
+ "declare i64 @xt_set(i64, i64, i64)",
142
+ "declare i64 @xt_box_new(i64)",
143
+ "declare i64 @xt_box_get(i64)",
144
+ "declare i64 @xt_box_set(i64, i64)",
145
+ "declare i64 @xt_is_nullish(i64)",
146
+ "declare void @xt_throw(i64)",
147
+ "declare void @xt_console_log(i32, i64*)",
148
+ ];
149
+
150
+ export interface CodegenResult {
151
+ readonly ir: string;
152
+ readonly binding: BindResult;
153
+ }
154
+
155
+ export interface CodegenOptions {
156
+ /** Global names supplied by registered extensions. */
157
+ readonly builtins?: Readonly<Record<string, BuiltinFunction>>;
158
+ }
159
+
160
+ export function generate(
161
+ sourceFile: SourceFileNode,
162
+ diagnostics: DiagnosticBag,
163
+ options: CodegenOptions = {},
164
+ ): CodegenResult {
165
+ const generator = new Generator(sourceFile, diagnostics, options);
166
+ return { ir: generator.run(), binding: generator.binding };
167
+ }
168
+
169
+ class Generator {
170
+ readonly binding: BindResult;
171
+ private readonly globals: string[] = [];
172
+ private readonly functions: string[] = [];
173
+ private readonly strings = new Map<string, { label: string; length: number }>();
174
+ private readonly extraDeclarations = new Set<string>();
175
+ private readonly builtins: Readonly<Record<string, BuiltinFunction>>;
176
+ private stringCounter = 0;
177
+ private current!: FunctionState;
178
+
179
+ constructor(
180
+ private readonly sourceFile: SourceFileNode,
181
+ private readonly diagnostics: DiagnosticBag,
182
+ options: CodegenOptions = {},
183
+ ) {
184
+ this.binding = bind(sourceFile);
185
+ this.builtins = options.builtins ?? {};
186
+ }
187
+
188
+ run(): string {
189
+ for (const fn of this.binding.functions) this.emitFunction(fn);
190
+ this.emitMain();
191
+ const header = ["; ModuleID = 'xbintsc'", "source_filename = \"" + this.sourceFile.fileName + "\"", ""];
192
+ return [...header, ...RUNTIME_DECLARATIONS, ...this.extraDeclarations, "", ...this.globals, "", ...this.functions, ""].join("\n");
193
+ }
194
+
195
+ // -- module level --------------------------------------------------------
196
+
197
+ private emitFunction(fn: FunctionInfo): void {
198
+ const name = this.functionName(fn);
199
+ const state: FunctionState = {
200
+ fn,
201
+ buffer: [],
202
+ allocas: [],
203
+ slots: new Map(),
204
+ reg: 0,
205
+ label: 0,
206
+ terminated: false,
207
+ loops: [],
208
+ };
209
+ this.current = state;
210
+
211
+ const header = `define i64 @${name}(i64 %env, i32 %argc, i64* %argv) {`;
212
+ this.emit(`%saved.env = alloca i64`);
213
+ this.emit(`store i64 %env, i64* %saved.env`);
214
+ this.emit(`%saved.argc = alloca i32`);
215
+ this.emit(`store i32 %argc, i32* %saved.argc`);
216
+ this.emit(`%saved.argv = alloca i64*`);
217
+ this.emit(`store i64* %argv, i64** %saved.argv`);
218
+
219
+ // Parameters.
220
+ for (let index = 0; index < fn.params.length; index++) {
221
+ const symbol = fn.params[index]!;
222
+ const value = this.reg();
223
+ this.emit(` ${value} = call i64 @xt_arg(i32 %argc, i64* %argv, i32 ${index})`);
224
+ this.declareSlot(symbol, value);
225
+ }
226
+
227
+ // Captures threaded through the environment.
228
+ for (const symbol of fn.captures) {
229
+ const index = fn.captureIndex.get(symbol.id)!;
230
+ const value = this.reg();
231
+ this.emit(` ${value} = call i64 @xt_closure_env(i64 %env, i32 ${index})`);
232
+ this.defineCaptureSlot(symbol, value);
233
+ }
234
+
235
+ // Body.
236
+ if (fn.node.kind === SyntaxKind.SourceFile) {
237
+ this.emitStatements(this.sourceFile.statements);
238
+ } else if (fn.node.kind === SyntaxKind.ArrowFunction && (fn.node as ArrowFunction).body.kind !== SyntaxKind.Block) {
239
+ const value = this.emitExpression((fn.node as ArrowFunction).body as Expression);
240
+ this.terminate(`ret i64 ${value}`);
241
+ } else {
242
+ const body = (fn.node as { body: Block }).body;
243
+ this.emitStatements(body.statements);
244
+ }
245
+
246
+ if (!this.current.terminated) this.terminate(`ret i64 ${i64(XT_UNDEFINED)}`);
247
+
248
+ const lines = [...state.allocas.map((a) => ` ${a}`), ...state.buffer];
249
+ this.functions.push([header, ...lines, "}", ""].join("\n"));
250
+ }
251
+
252
+ private emitMain(): void {
253
+ const moduleName = this.functionName(this.binding.moduleFunction);
254
+ this.functions.push(
255
+ [
256
+ "define i32 @main(i32 %argc, i8** %argv) {",
257
+ ` %result = call i64 @${moduleName}(i64 ${i64(XT_UNDEFINED)}, i32 0, i64* null)`,
258
+ " ret i32 0",
259
+ "}",
260
+ "",
261
+ ].join("\n"),
262
+ );
263
+ }
264
+
265
+ private functionName(fn: FunctionInfo): string {
266
+ return fn.isModule ? "xt_module" : `xt_fn_${fn.id}`;
267
+ }
268
+
269
+ // -- emission primitives -------------------------------------------------
270
+
271
+ private emit(line: string): void {
272
+ this.current.buffer.push(line);
273
+ }
274
+
275
+ private terminate(line: string): void {
276
+ this.current.buffer.push(` ${line}`);
277
+ this.current.terminated = true;
278
+ }
279
+
280
+ private reg(): string {
281
+ return `%r${this.current.reg++}`;
282
+ }
283
+
284
+ private label(prefix: string): string {
285
+ return `${prefix}.${this.current.label++}`;
286
+ }
287
+
288
+ private startBlock(label: string): void {
289
+ this.current.buffer.push(`${label}:`);
290
+ this.current.terminated = false;
291
+ }
292
+
293
+ private alloca(): string {
294
+ const ptr = `%slot${this.current.allocas.length}`;
295
+ this.current.allocas.push(`${ptr} = alloca i64`);
296
+ return ptr;
297
+ }
298
+
299
+ private const(hex: bigint): string {
300
+ return i64(hex);
301
+ }
302
+
303
+ // -- variables -----------------------------------------------------------
304
+
305
+ private declareSlot(symbol: SymbolInfo, initial: string): void {
306
+ const ptr = this.alloca();
307
+ if (symbol.boxed) {
308
+ const box = this.reg();
309
+ this.emit(` ${box} = call i64 @xt_box_new(i64 ${initial})`);
310
+ this.emit(` store i64 ${box}, i64* ${ptr}`);
311
+ } else {
312
+ this.emit(` store i64 ${initial}, i64* ${ptr}`);
313
+ }
314
+ this.current.slots.set(symbol.id, { ptr, boxed: symbol.boxed });
315
+ }
316
+
317
+ private defineCaptureSlot(symbol: SymbolInfo, box: string): void {
318
+ const ptr = this.alloca();
319
+ this.emit(` store i64 ${box}, i64* ${ptr}`);
320
+ // A captured symbol is always boxed; the environment holds the box.
321
+ this.current.slots.set(symbol.id, { ptr, boxed: true });
322
+ }
323
+
324
+ private readSlot(symbol: SymbolInfo): string {
325
+ const slot = this.current.slots.get(symbol.id);
326
+ if (!slot) {
327
+ // Referenced from an inner function without a capture slot: this should
328
+ // be impossible once the binder has run, but stay defensive.
329
+ this.diagnostics.error(
330
+ DiagnosticCode.CodegenError,
331
+ `Internal: no slot for '${symbol.name}' in ${this.current.fn.name}`,
332
+ );
333
+ return i64(XT_UNDEFINED);
334
+ }
335
+ const value = this.reg();
336
+ this.emit(` ${value} = load i64, i64* ${slot.ptr}`);
337
+ if (!slot.boxed) return value;
338
+ const unboxed = this.reg();
339
+ this.emit(` ${unboxed} = call i64 @xt_box_get(i64 ${value})`);
340
+ return unboxed;
341
+ }
342
+
343
+ private writeSlot(symbol: SymbolInfo, value: string): void {
344
+ const slot = this.current.slots.get(symbol.id);
345
+ if (!slot) {
346
+ this.diagnostics.error(DiagnosticCode.CodegenError, `Internal: no slot for '${symbol.name}'`);
347
+ return;
348
+ }
349
+ if (!slot.boxed) {
350
+ this.emit(` store i64 ${value}, i64* ${slot.ptr}`);
351
+ return;
352
+ }
353
+ const box = this.reg();
354
+ this.emit(` ${box} = load i64, i64* ${slot.ptr}`);
355
+ this.emit(` call i64 @xt_box_set(i64 ${box}, i64 ${value})`);
356
+ }
357
+
358
+ // -- statements ----------------------------------------------------------
359
+
360
+ private emitStatements(statements: readonly Statement[]): void {
361
+ for (const statement of statements) {
362
+ if (this.current.terminated) break;
363
+ this.emitStatement(statement);
364
+ }
365
+ }
366
+
367
+ private emitStatement(statement: Statement): void {
368
+ switch (statement.kind) {
369
+ case SyntaxKind.ExpressionStatement:
370
+ this.emitExpression((statement as { expression: Expression }).expression);
371
+ return;
372
+ case SyntaxKind.VariableStatement:
373
+ this.emitVariableStatement(statement as VariableStatement);
374
+ return;
375
+ case SyntaxKind.Block:
376
+ this.emitStatements((statement as Block).statements);
377
+ return;
378
+ case SyntaxKind.EmptyStatement:
379
+ case SyntaxKind.DebuggerStatement:
380
+ case SyntaxKind.FunctionDeclaration:
381
+ case SyntaxKind.InterfaceDeclaration:
382
+ case SyntaxKind.TypeAliasDeclaration:
383
+ return; // functions are emitted at module scope
384
+ case SyntaxKind.IfStatement:
385
+ this.emitIf(statement as IfStatement);
386
+ return;
387
+ case SyntaxKind.WhileStatement:
388
+ this.emitWhile(statement as WhileStatement);
389
+ return;
390
+ case SyntaxKind.DoStatement:
391
+ this.emitDo(statement as DoStatement);
392
+ return;
393
+ case SyntaxKind.ForStatement:
394
+ this.emitFor(statement as ForStatement);
395
+ return;
396
+ case SyntaxKind.ForOfStatement:
397
+ case SyntaxKind.ForInStatement:
398
+ this.emitForOf(statement as ForOfStatement | ForInStatement);
399
+ return;
400
+ case SyntaxKind.ReturnStatement:
401
+ this.emitReturn(statement as ReturnStatement);
402
+ return;
403
+ case SyntaxKind.BreakStatement: {
404
+ const loop = this.current.loops[this.current.loops.length - 1];
405
+ if (loop) this.terminate(`br label %${loop.breakLabel}`);
406
+ return;
407
+ }
408
+ case SyntaxKind.ContinueStatement: {
409
+ const loop = this.current.loops[this.current.loops.length - 1];
410
+ if (loop) this.terminate(`br label %${loop.continueLabel}`);
411
+ return;
412
+ }
413
+ case SyntaxKind.ThrowStatement: {
414
+ const value = this.emitExpression((statement as ThrowStatement).expression);
415
+ this.emit(` call void @xt_throw(i64 ${value})`);
416
+ this.terminate("unreachable");
417
+ return;
418
+ }
419
+ case SyntaxKind.ExportDeclaration: {
420
+ const declaration = (statement as { declaration?: Statement }).declaration;
421
+ if (declaration) this.emitStatement(declaration);
422
+ return;
423
+ }
424
+ default:
425
+ this.unsupported(statement, "statement");
426
+ }
427
+ }
428
+
429
+ private emitVariableStatement(statement: VariableStatement): void {
430
+ for (const declaration of statement.declarationList.declarations) {
431
+ const symbol = this.binding.symbolOfDeclaration.get(declaration);
432
+ if (!symbol) continue;
433
+ if (this.current.slots.has(symbol.id)) continue; // hoisted `var`
434
+ const initial = declaration.initializer
435
+ ? this.emitExpression(declaration.initializer)
436
+ : i64(XT_UNDEFINED);
437
+ this.declareSlot(symbol, initial);
438
+ }
439
+ }
440
+
441
+ private emitIf(statement: IfStatement): void {
442
+ const thenLabel = this.label("if.then");
443
+ const elseLabel = statement.elseStatement ? this.label("if.else") : undefined;
444
+ const endLabel = this.label("if.end");
445
+ const condition = this.emitExpression(statement.condition);
446
+ const truthy = this.reg();
447
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${condition})`);
448
+ const nonzero = this.reg();
449
+ this.emit(` ${nonzero} = icmp ne i32 ${truthy}, 0`);
450
+ this.terminate(`br i1 ${nonzero}, label %${thenLabel}, label %${elseLabel ?? endLabel}`);
451
+
452
+ this.startBlock(thenLabel);
453
+ this.emitStatement(statement.thenStatement);
454
+ if (!this.current.terminated) this.terminate(`br label %${endLabel}`);
455
+
456
+ if (elseLabel) {
457
+ this.startBlock(elseLabel);
458
+ this.emitStatement(statement.elseStatement!);
459
+ if (!this.current.terminated) this.terminate(`br label %${endLabel}`);
460
+ }
461
+
462
+ this.startBlock(endLabel);
463
+ }
464
+
465
+ private emitWhile(statement: WhileStatement): void {
466
+ const condLabel = this.label("while.cond");
467
+ const bodyLabel = this.label("while.body");
468
+ const endLabel = this.label("while.end");
469
+ this.terminate(`br label %${condLabel}`);
470
+ this.startBlock(condLabel);
471
+ const condition = this.emitExpression(statement.condition);
472
+ const truthy = this.reg();
473
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${condition})`);
474
+ const nonzero = this.reg();
475
+ this.emit(` ${nonzero} = icmp ne i32 ${truthy}, 0`);
476
+ this.terminate(`br i1 ${nonzero}, label %${bodyLabel}, label %${endLabel}`);
477
+
478
+ this.startBlock(bodyLabel);
479
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: condLabel });
480
+ this.emitStatement(statement.statement);
481
+ this.current.loops.pop();
482
+ if (!this.current.terminated) this.terminate(`br label %${condLabel}`);
483
+ this.startBlock(endLabel);
484
+ }
485
+
486
+ private emitDo(statement: DoStatement): void {
487
+ const bodyLabel = this.label("do.body");
488
+ const condLabel = this.label("do.cond");
489
+ const endLabel = this.label("do.end");
490
+ this.terminate(`br label %${bodyLabel}`);
491
+ this.startBlock(bodyLabel);
492
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: condLabel });
493
+ this.emitStatement(statement.statement);
494
+ this.current.loops.pop();
495
+ if (!this.current.terminated) this.terminate(`br label %${condLabel}`);
496
+
497
+ this.startBlock(condLabel);
498
+ const condition = this.emitExpression(statement.condition);
499
+ const truthy = this.reg();
500
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${condition})`);
501
+ const nonzero = this.reg();
502
+ this.emit(` ${nonzero} = icmp ne i32 ${truthy}, 0`);
503
+ this.terminate(`br i1 ${nonzero}, label %${bodyLabel}, label %${endLabel}`);
504
+ this.startBlock(endLabel);
505
+ }
506
+
507
+ private emitFor(statement: ForStatement): void {
508
+ if (statement.initializer && statement.initializer.kind === SyntaxKind.VariableDeclarationList) {
509
+ this.emitVariableStatement({
510
+ kind: SyntaxKind.VariableStatement,
511
+ declarationList: statement.initializer as VariableDeclarationList,
512
+ modifiers: [],
513
+ start: statement.initializer.start,
514
+ end: statement.initializer.end,
515
+ });
516
+ } else if (statement.initializer) {
517
+ this.emitExpression(statement.initializer as Expression);
518
+ }
519
+ const condLabel = this.label("for.cond");
520
+ const bodyLabel = this.label("for.body");
521
+ const updateLabel = this.label("for.update");
522
+ const endLabel = this.label("for.end");
523
+ this.terminate(`br label %${condLabel}`);
524
+
525
+ this.startBlock(condLabel);
526
+ if (statement.condition) {
527
+ const condition = this.emitExpression(statement.condition);
528
+ const truthy = this.reg();
529
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${condition})`);
530
+ const nonzero = this.reg();
531
+ this.emit(` ${nonzero} = icmp ne i32 ${truthy}, 0`);
532
+ this.terminate(`br i1 ${nonzero}, label %${bodyLabel}, label %${endLabel}`);
533
+ } else {
534
+ this.terminate(`br label %${bodyLabel}`);
535
+ }
536
+
537
+ this.startBlock(bodyLabel);
538
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: updateLabel });
539
+ this.emitStatement(statement.statement);
540
+ this.current.loops.pop();
541
+ if (!this.current.terminated) this.terminate(`br label %${updateLabel}`);
542
+
543
+ this.startBlock(updateLabel);
544
+ if (statement.incrementor) this.emitExpression(statement.incrementor);
545
+ this.terminate(`br label %${condLabel}`);
546
+ this.startBlock(endLabel);
547
+ }
548
+
549
+ /** `for (const x of xs)` / `for (const k in obj)` over arrays and objects. */
550
+ private emitForOf(statement: ForOfStatement | ForInStatement): void {
551
+ const iterable = this.emitExpression(statement.expression);
552
+ const indexPtr = this.alloca();
553
+ this.emit(` store i64 ${numberLiteral(0)}, i64* ${indexPtr}`);
554
+ const lengthValue = this.reg();
555
+ this.emit(` ${lengthValue} = call i64 @xt_array_length(i64 ${iterable})`);
556
+
557
+ const condLabel = this.label("forof.cond");
558
+ const bodyLabel = this.label("forof.body");
559
+ const updateLabel = this.label("forof.update");
560
+ const endLabel = this.label("forof.end");
561
+ this.terminate(`br label %${condLabel}`);
562
+
563
+ this.startBlock(condLabel);
564
+ const index = this.reg();
565
+ this.emit(` ${index} = load i64, i64* ${indexPtr}`);
566
+ const inRange = this.reg();
567
+ this.emit(` ${inRange} = call i64 @xt_lt(i64 ${index}, i64 ${lengthValue})`);
568
+ const truthy = this.reg();
569
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${inRange})`);
570
+ const nonzero = this.reg();
571
+ this.emit(` ${nonzero} = icmp ne i32 ${truthy}, 0`);
572
+ this.terminate(`br i1 ${nonzero}, label %${bodyLabel}, label %${endLabel}`);
573
+
574
+ this.startBlock(bodyLabel);
575
+ const element = this.reg();
576
+ this.emit(` ${element} = call i64 @xt_get(i64 ${iterable}, i64 ${index})`);
577
+ this.bindLoopVariable(statement.initializer, element);
578
+ this.current.loops.push({ breakLabel: endLabel, continueLabel: updateLabel });
579
+ this.emitStatement(statement.statement);
580
+ this.current.loops.pop();
581
+ if (!this.current.terminated) this.terminate(`br label %${updateLabel}`);
582
+
583
+ this.startBlock(updateLabel);
584
+ const next = this.reg();
585
+ this.emit(` ${next} = call i64 @xt_add(i64 ${index}, i64 ${numberLiteral(1)})`);
586
+ this.emit(` store i64 ${next}, i64* ${indexPtr}`);
587
+ this.terminate(`br label %${condLabel}`);
588
+ this.startBlock(endLabel);
589
+ }
590
+
591
+ private bindLoopVariable(initializer: VariableDeclarationList | Expression, value: string): void {
592
+ if (initializer.kind === SyntaxKind.VariableDeclarationList) {
593
+ const declaration = initializer.declarations[0];
594
+ if (!declaration) return;
595
+ const symbol = this.binding.symbolOfDeclaration.get(declaration);
596
+ if (symbol) {
597
+ if (this.current.slots.has(symbol.id)) this.writeSlot(symbol, value);
598
+ else this.declareSlot(symbol, value);
599
+ }
600
+ return;
601
+ }
602
+ this.emitAssignmentTarget(initializer as Expression, value);
603
+ }
604
+
605
+ private emitReturn(statement: ReturnStatement): void {
606
+ const value = statement.expression ? this.emitExpression(statement.expression) : i64(XT_UNDEFINED);
607
+ this.terminate(`ret i64 ${value}`);
608
+ }
609
+
610
+ // -- expressions ---------------------------------------------------------
611
+
612
+ private emitExpression(node: Expression): string {
613
+ switch (node.kind) {
614
+ case SyntaxKind.Identifier:
615
+ return this.emitIdentifier(node as Identifier);
616
+ case SyntaxKind.NumericLiteral: {
617
+ const value = (node as { value: number }).value;
618
+ return numberLiteral(value);
619
+ }
620
+ case SyntaxKind.BigIntLiteral:
621
+ return numberLiteral(Number((node as { value: bigint }).value));
622
+ case SyntaxKind.StringLiteral:
623
+ return this.stringValue((node as { value: string }).value);
624
+ case SyntaxKind.NoSubstitutionTemplateLiteral:
625
+ return this.stringValue((node as { value: string }).value);
626
+ case SyntaxKind.TemplateLiteral:
627
+ return this.emitTemplate(node as TemplateLiteral);
628
+ case SyntaxKind.TrueKeyword:
629
+ return i64(XT_TRUE);
630
+ case SyntaxKind.FalseKeyword:
631
+ return i64(XT_FALSE);
632
+ case SyntaxKind.NullKeyword:
633
+ return i64(XT_NULL);
634
+ case SyntaxKind.UndefinedKeyword:
635
+ return i64(XT_UNDEFINED);
636
+ case SyntaxKind.ParenthesizedExpression:
637
+ return this.emitExpression((node as { expression: Expression }).expression);
638
+ case SyntaxKind.AsExpression:
639
+ case SyntaxKind.SatisfiesExpression:
640
+ case SyntaxKind.NonNullExpression:
641
+ return this.emitExpression((node as { expression: Expression }).expression);
642
+ case SyntaxKind.BinaryExpression:
643
+ return this.emitBinaryOrAssignment(node as BinaryExpression);
644
+ case SyntaxKind.PrefixUnaryExpression:
645
+ return this.emitPrefix(node as PrefixUnaryExpression);
646
+ case SyntaxKind.PostfixUnaryExpression:
647
+ return this.emitPostfix(node as PostfixUnaryExpression);
648
+ case SyntaxKind.ConditionalExpression:
649
+ return this.emitConditional(node as ConditionalExpression);
650
+ case SyntaxKind.CallExpression:
651
+ return this.emitCall(node as CallExpression);
652
+ case SyntaxKind.PropertyAccessExpression:
653
+ return this.emitPropertyAccess(node as PropertyAccessExpression);
654
+ case SyntaxKind.ElementAccessExpression:
655
+ return this.emitElementAccess(node as ElementAccessExpression);
656
+ case SyntaxKind.ArrayLiteralExpression:
657
+ return this.emitArrayLiteral(node as ArrayLiteralExpression);
658
+ case SyntaxKind.ObjectLiteralExpression:
659
+ return this.emitObjectLiteral(node as ObjectLiteralExpression);
660
+ case SyntaxKind.ArrowFunction:
661
+ case SyntaxKind.FunctionExpression:
662
+ return this.emitClosure(node as ArrowFunction | FunctionExpression);
663
+ default:
664
+ this.unsupported(node, "expression");
665
+ return i64(XT_UNDEFINED);
666
+ }
667
+ }
668
+
669
+ private emitIdentifier(identifier: Identifier): string {
670
+ const symbol = this.binding.symbolOfIdentifier.get(identifier);
671
+ if (symbol) {
672
+ if (symbol.kind === SymbolKind.Function && !this.current.slots.has(symbol.id)) {
673
+ return this.emitFunctionValue(symbol);
674
+ }
675
+ return this.readSlot(symbol);
676
+ }
677
+ switch (identifier.text) {
678
+ case "undefined":
679
+ return i64(XT_UNDEFINED);
680
+ case "NaN":
681
+ return numberLiteral(NaN);
682
+ case "Infinity":
683
+ return numberLiteral(Infinity);
684
+ case "console":
685
+ return i64(XT_UNDEFINED);
686
+ default:
687
+ this.diagnostics.error(
688
+ DiagnosticCode.CannotFindName,
689
+ `Cannot find name '${identifier.text}'`,
690
+ identifier,
691
+ this.sourceFile.fileName,
692
+ );
693
+ return i64(XT_UNDEFINED);
694
+ }
695
+ }
696
+
697
+ private emitFunctionValue(symbol: SymbolInfo): string {
698
+ const declaration = symbol.declarations[0];
699
+ const fn = declaration ? this.binding.functionOfNode.get(declaration) : undefined;
700
+ if (!fn) return i64(XT_UNDEFINED);
701
+ const cast = this.reg();
702
+ this.emit(` ${cast} = bitcast i64 (i64, i32, i64*)* @${this.functionName(fn)} to i8*`);
703
+ const closure = this.reg();
704
+ this.emit(` ${closure} = call i64 @xt_closure_new(i8* ${cast}, i32 0, i64* null)`);
705
+ return closure;
706
+ }
707
+
708
+ private emitTemplate(node: TemplateLiteral): string {
709
+ let accumulator = this.stringValue(node.head);
710
+ for (const span of node.spans) {
711
+ const expression = this.emitExpression(span.expression);
712
+ const text = this.reg();
713
+ this.emit(` ${text} = call i64 @xt_to_string(i64 ${expression})`);
714
+ const joined = this.reg();
715
+ this.emit(` ${joined} = call i64 @xt_add(i64 ${accumulator}, i64 ${text})`);
716
+ accumulator = joined;
717
+ if (span.literal.length > 0) {
718
+ const literal = this.stringValue(span.literal);
719
+ const withLiteral = this.reg();
720
+ this.emit(` ${withLiteral} = call i64 @xt_add(i64 ${accumulator}, i64 ${literal})`);
721
+ accumulator = withLiteral;
722
+ }
723
+ }
724
+ return accumulator;
725
+ }
726
+
727
+ private emitBinaryOrAssignment(node: BinaryExpression): string {
728
+ const operator = node.operator as string;
729
+ if (isAssignmentOperator(operator)) {
730
+ return this.emitAssignment(node);
731
+ }
732
+ switch (operator) {
733
+ case BinaryOperator.AmpersandAmpersand:
734
+ return this.emitShortCircuit(node, "and");
735
+ case BinaryOperator.BarBar:
736
+ return this.emitShortCircuit(node, "or");
737
+ case BinaryOperator.QuestionQuestion:
738
+ return this.emitShortCircuit(node, "nullish");
739
+ case BinaryOperator.Comma: {
740
+ this.emitExpression(node.left);
741
+ return this.emitExpression(node.right);
742
+ }
743
+ default:
744
+ break;
745
+ }
746
+ const left = this.emitExpression(node.left);
747
+ const right = this.emitExpression(node.right);
748
+ const runtime = BINARY_RUNTIME[operator];
749
+ if (!runtime) {
750
+ this.unsupported(node, `operator '${operator}'`);
751
+ return i64(XT_UNDEFINED);
752
+ }
753
+ const result = this.reg();
754
+ this.emit(` ${result} = call i64 @${runtime}(i64 ${left}, i64 ${right})`);
755
+ return result;
756
+ }
757
+
758
+ private emitShortCircuit(node: BinaryExpression, kind: "and" | "or" | "nullish"): string {
759
+ const result = this.alloca();
760
+ const left = this.emitExpression(node.left);
761
+ this.emit(` store i64 ${left}, i64* ${result}`);
762
+ const rightLabel = this.label("sc.right");
763
+ const endLabel = this.label("sc.end");
764
+ let condition: string;
765
+ if (kind === "and") {
766
+ const truthy = this.reg();
767
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${left})`);
768
+ condition = this.reg();
769
+ this.emit(` ${condition} = icmp ne i32 ${truthy}, 0`);
770
+ } else if (kind === "or") {
771
+ const truthy = this.reg();
772
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${left})`);
773
+ const isTruthy = this.reg();
774
+ this.emit(` ${isTruthy} = icmp ne i32 ${truthy}, 0`);
775
+ condition = this.reg();
776
+ this.emit(` ${condition} = xor i1 ${isTruthy}, true`);
777
+ } else {
778
+ const nullish = this.reg();
779
+ this.emit(` ${nullish} = call i64 @xt_is_nullish(i64 ${left})`);
780
+ const truthy = this.reg();
781
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${nullish})`);
782
+ condition = this.reg();
783
+ this.emit(` ${condition} = icmp ne i32 ${truthy}, 0`);
784
+ }
785
+ this.terminate(`br i1 ${condition}, label %${rightLabel}, label %${endLabel}`);
786
+ this.startBlock(rightLabel);
787
+ const right = this.emitExpression(node.right);
788
+ this.emit(` store i64 ${right}, i64* ${result}`);
789
+ this.terminate(`br label %${endLabel}`);
790
+ this.startBlock(endLabel);
791
+ const value = this.reg();
792
+ this.emit(` ${value} = load i64, i64* ${result}`);
793
+ return value;
794
+ }
795
+
796
+ private emitConditional(node: ConditionalExpression): string {
797
+ const result = this.alloca();
798
+ const condition = this.emitExpression(node.condition);
799
+ const truthy = this.reg();
800
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${condition})`);
801
+ const nonzero = this.reg();
802
+ this.emit(` ${nonzero} = icmp ne i32 ${truthy}, 0`);
803
+ const trueLabel = this.label("cond.true");
804
+ const falseLabel = this.label("cond.false");
805
+ const endLabel = this.label("cond.end");
806
+ this.terminate(`br i1 ${nonzero}, label %${trueLabel}, label %${falseLabel}`);
807
+ this.startBlock(trueLabel);
808
+ const whenTrue = this.emitExpression(node.whenTrue);
809
+ this.emit(` store i64 ${whenTrue}, i64* ${result}`);
810
+ this.terminate(`br label %${endLabel}`);
811
+ this.startBlock(falseLabel);
812
+ const whenFalse = this.emitExpression(node.whenFalse);
813
+ this.emit(` store i64 ${whenFalse}, i64* ${result}`);
814
+ this.terminate(`br label %${endLabel}`);
815
+ this.startBlock(endLabel);
816
+ const value = this.reg();
817
+ this.emit(` ${value} = load i64, i64* ${result}`);
818
+ return value;
819
+ }
820
+
821
+ private emitPrefix(node: PrefixUnaryExpression): string {
822
+ switch (node.operator) {
823
+ case PrefixUnaryOperator.Minus: {
824
+ const operand = this.emitExpression(node.operand);
825
+ const result = this.reg();
826
+ this.emit(` ${result} = call i64 @xt_neg(i64 ${operand})`);
827
+ return result;
828
+ }
829
+ case PrefixUnaryOperator.Plus: {
830
+ const operand = this.emitExpression(node.operand);
831
+ const result = this.reg();
832
+ this.emit(` ${result} = call i64 @xt_pos(i64 ${operand})`);
833
+ return result;
834
+ }
835
+ case PrefixUnaryOperator.Exclamation: {
836
+ const operand = this.emitExpression(node.operand);
837
+ const result = this.reg();
838
+ this.emit(` ${result} = call i64 @xt_not(i64 ${operand})`);
839
+ return result;
840
+ }
841
+ case PrefixUnaryOperator.Tilde: {
842
+ const operand = this.emitExpression(node.operand);
843
+ const result = this.reg();
844
+ this.emit(` ${result} = call i64 @xt_bit_not(i64 ${operand})`);
845
+ return result;
846
+ }
847
+ case PrefixUnaryOperator.TypeOf: {
848
+ const operand = this.emitExpression(node.operand);
849
+ const result = this.reg();
850
+ this.emit(` ${result} = call i64 @xt_typeof(i64 ${operand})`);
851
+ return result;
852
+ }
853
+ case PrefixUnaryOperator.Void:
854
+ if (node.operand) this.emitExpression(node.operand);
855
+ return i64(XT_UNDEFINED);
856
+ case PrefixUnaryOperator.PlusPlus:
857
+ case PrefixUnaryOperator.MinusMinus: {
858
+ const one = numberLiteral(1);
859
+ const current = this.emitExpression(node.operand);
860
+ const fn = node.operator === PrefixUnaryOperator.PlusPlus ? "xt_add" : "xt_sub";
861
+ const next = this.reg();
862
+ this.emit(` ${next} = call i64 @${fn}(i64 ${current}, i64 ${one})`);
863
+ this.emitAssignmentTarget(node.operand, next);
864
+ return next;
865
+ }
866
+ default:
867
+ this.unsupported(node, `unary operator '${node.operator}'`);
868
+ return i64(XT_UNDEFINED);
869
+ }
870
+ }
871
+
872
+ private emitPostfix(node: PostfixUnaryExpression): string {
873
+ const one = numberLiteral(1);
874
+ const current = this.emitExpression(node.operand);
875
+ const fn = node.operator === PostfixUnaryOperator.PlusPlus ? "xt_add" : "xt_sub";
876
+ const next = this.reg();
877
+ this.emit(` ${next} = call i64 @${fn}(i64 ${current}, i64 ${one})`);
878
+ this.emitAssignmentTarget(node.operand, next);
879
+ return current;
880
+ }
881
+
882
+ private emitAssignment(node: BinaryExpression): string {
883
+ const operator = node.operator as unknown as AssignmentOperator;
884
+ const target = node.left;
885
+ let value: string;
886
+ if (operator === AssignmentOperator.Assign) {
887
+ value = this.emitExpression(node.right);
888
+ } else if (
889
+ operator === AssignmentOperator.AmpersandAmpersandAssign ||
890
+ operator === AssignmentOperator.BarBarAssign ||
891
+ operator === AssignmentOperator.QuestionQuestionAssign
892
+ ) {
893
+ value = this.emitLogicalAssignment(node);
894
+ } else {
895
+ const current = this.emitExpression(target);
896
+ const right = this.emitExpression(node.right);
897
+ const binaryOperator = compoundToBinary(operator);
898
+ const runtime = BINARY_RUNTIME[binaryOperator];
899
+ if (!runtime) {
900
+ this.unsupported(node, `assignment operator '${operator}'`);
901
+ return i64(XT_UNDEFINED);
902
+ }
903
+ const result = this.reg();
904
+ this.emit(` ${result} = call i64 @${runtime}(i64 ${current}, i64 ${right})`);
905
+ value = result;
906
+ }
907
+ this.emitAssignmentTarget(target, value);
908
+ return value;
909
+ }
910
+
911
+ private emitLogicalAssignment(node: BinaryExpression): string {
912
+ const operator = node.operator as unknown as AssignmentOperator;
913
+ const target = node.left;
914
+ const current = this.emitExpression(target);
915
+ const result = this.alloca();
916
+ this.emit(` store i64 ${current}, i64* ${result}`);
917
+ const rightLabel = this.label("assign.right");
918
+ const endLabel = this.label("assign.end");
919
+ let condition: string;
920
+ if (operator === AssignmentOperator.AmpersandAmpersandAssign) {
921
+ const truthy = this.reg();
922
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${current})`);
923
+ condition = this.reg();
924
+ this.emit(` ${condition} = icmp ne i32 ${truthy}, 0`);
925
+ } else if (operator === AssignmentOperator.BarBarAssign) {
926
+ const truthy = this.reg();
927
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${current})`);
928
+ const isTruthy = this.reg();
929
+ this.emit(` ${isTruthy} = icmp ne i32 ${truthy}, 0`);
930
+ condition = this.reg();
931
+ this.emit(` ${condition} = xor i1 ${isTruthy}, true`);
932
+ } else {
933
+ const nullish = this.reg();
934
+ this.emit(` ${nullish} = call i64 @xt_is_nullish(i64 ${current})`);
935
+ const truthy = this.reg();
936
+ this.emit(` ${truthy} = call i32 @xt_truthy(i64 ${nullish})`);
937
+ condition = this.reg();
938
+ this.emit(` ${condition} = icmp ne i32 ${truthy}, 0`);
939
+ }
940
+ this.terminate(`br i1 ${condition}, label %${rightLabel}, label %${endLabel}`);
941
+ this.startBlock(rightLabel);
942
+ const right = this.emitExpression(node.right);
943
+ this.emit(` store i64 ${right}, i64* ${result}`);
944
+ this.terminate(`br label %${endLabel}`);
945
+ this.startBlock(endLabel);
946
+ const value = this.reg();
947
+ this.emit(` ${value} = load i64, i64* ${result}`);
948
+ this.emitAssignmentTarget(target, value);
949
+ return value;
950
+ }
951
+
952
+ private emitAssignmentTarget(target: Expression, value: string): void {
953
+ switch (target.kind) {
954
+ case SyntaxKind.Identifier: {
955
+ const symbol = this.binding.symbolOfIdentifier.get(target as Identifier);
956
+ if (symbol) this.writeSlot(symbol, value);
957
+ return;
958
+ }
959
+ case SyntaxKind.PropertyAccessExpression: {
960
+ const access = target as PropertyAccessExpression;
961
+ const object = this.emitExpression(access.expression);
962
+ const key = this.stringValue(access.name.text);
963
+ this.emit(` call i64 @xt_set(i64 ${object}, i64 ${key}, i64 ${value})`);
964
+ return;
965
+ }
966
+ case SyntaxKind.ElementAccessExpression: {
967
+ const access = target as ElementAccessExpression;
968
+ const object = this.emitExpression(access.expression);
969
+ const key = this.emitExpression(access.argumentExpression);
970
+ this.emit(` call i64 @xt_set(i64 ${object}, i64 ${key}, i64 ${value})`);
971
+ return;
972
+ }
973
+ case SyntaxKind.ParenthesizedExpression:
974
+ this.emitAssignmentTarget((target as { expression: Expression }).expression, value);
975
+ return;
976
+ default:
977
+ this.unsupported(target, "assignment target");
978
+ }
979
+ }
980
+
981
+ private emitCall(node: CallExpression): string {
982
+ const callee = node.expression;
983
+ if (callee.kind === SyntaxKind.PropertyAccessExpression) {
984
+ const special = this.tryEmitBuiltinCall(node, callee as PropertyAccessExpression);
985
+ if (special) return special;
986
+ }
987
+ if (callee.kind === SyntaxKind.Identifier) {
988
+ const symbol = this.binding.symbolOfIdentifier.get(callee as Identifier);
989
+ if (symbol && symbol.kind === SymbolKind.Function) {
990
+ const declaration = symbol.declarations[0];
991
+ const fn = declaration ? this.binding.functionOfNode.get(declaration) : undefined;
992
+ if (fn) {
993
+ const args = this.emitArguments(node.arguments);
994
+ const result = this.reg();
995
+ this.emit(
996
+ ` ${result} = call i64 @${this.functionName(fn)}(i64 ${i64(XT_UNDEFINED)}, i32 ${args.argc}, i64* ${args.ptr})`,
997
+ );
998
+ return result;
999
+ }
1000
+ }
1001
+ const builtin = this.builtins[(callee as Identifier).text];
1002
+ if (!symbol && builtin) {
1003
+ this.extraDeclarations.add(
1004
+ builtin.returnVoid ? `declare void @${builtin.symbol}(i32, i64*)` : `declare i64 @${builtin.symbol}(i32, i64*)`,
1005
+ );
1006
+ const args = this.emitArguments(node.arguments);
1007
+ if (builtin.returnVoid) {
1008
+ this.emit(` call void @${builtin.symbol}(i32 ${args.argc}, i64* ${args.ptr})`);
1009
+ return i64(XT_UNDEFINED);
1010
+ }
1011
+ const result = this.reg();
1012
+ this.emit(` ${result} = call i64 @${builtin.symbol}(i32 ${args.argc}, i64* ${args.ptr})`);
1013
+ return result;
1014
+ }
1015
+ }
1016
+ const calleeValue = this.emitExpression(callee);
1017
+ const args = this.emitArguments(node.arguments);
1018
+ const result = this.reg();
1019
+ this.emit(` ${result} = call i64 @xt_closure_call(i64 ${calleeValue}, i32 ${args.argc}, i64* ${args.ptr})`);
1020
+ return result;
1021
+ }
1022
+
1023
+ private tryEmitBuiltinCall(node: CallExpression, callee: PropertyAccessExpression): string | undefined {
1024
+ const target = callee.expression;
1025
+ const method = callee.name.text;
1026
+ if (target.kind === SyntaxKind.Identifier && (target as Identifier).text === "console") {
1027
+ 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);
1030
+ }
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);
1035
+ 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;
1048
+ }
1049
+ this.unsupported(node, `array method '${method}'`);
1050
+ return i64(XT_UNDEFINED);
1051
+ }
1052
+ return undefined;
1053
+ }
1054
+
1055
+ private runtimeCall(name: string, args: readonly string[]): string {
1056
+ const result = this.reg();
1057
+ this.emit(` ${result} = call i64 @${name}(${args.map((a) => `i64 ${a}`).join(", ")})`);
1058
+ return result;
1059
+ }
1060
+
1061
+ private emitArguments(args: readonly Expression[]): { argc: number; ptr: string } {
1062
+ if (args.length === 0) return { argc: 0, ptr: "null" };
1063
+ const ptr = `%args${this.current.allocas.length}`;
1064
+ this.current.allocas.push(`${ptr} = alloca i64, i32 ${args.length}`);
1065
+ for (let index = 0; index < args.length; index++) {
1066
+ const value = this.emitExpression(args[index]!);
1067
+ const slot = this.reg();
1068
+ this.emit(` ${slot} = getelementptr i64, i64* ${ptr}, i32 ${index}`);
1069
+ this.emit(` store i64 ${value}, i64* ${slot}`);
1070
+ }
1071
+ return { argc: args.length, ptr };
1072
+ }
1073
+
1074
+ 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;
1080
+ }
1081
+ 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;
1086
+ }
1087
+
1088
+ private emitElementAccess(node: ElementAccessExpression): string {
1089
+ 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;
1094
+ }
1095
+
1096
+ private emitArrayLiteral(node: ArrayLiteralExpression): string {
1097
+ const hasSpread = node.elements.some((element) => element.kind === SyntaxKind.SpreadElement);
1098
+ if (!hasSpread) {
1099
+ const args = this.emitArguments(node.elements);
1100
+ const result = this.reg();
1101
+ this.emit(` ${result} = call i64 @xt_array_new(i32 ${args.argc}, i64* ${args.ptr})`);
1102
+ return result;
1103
+ }
1104
+ let array = this.runtimeCall("xt_array_new", [`0`, `null`]);
1105
+ for (const element of node.elements) {
1106
+ if (element.kind === SyntaxKind.SpreadElement) {
1107
+ const spread = this.emitExpression((element as { expression: Expression }).expression);
1108
+ array = this.runtimeCall("xt_array_spread", [array, spread]);
1109
+ } else {
1110
+ const value = this.emitExpression(element);
1111
+ array = this.runtimeCall("xt_array_push", [array, value]);
1112
+ }
1113
+ }
1114
+ return array;
1115
+ }
1116
+
1117
+ private emitObjectLiteral(node: ObjectLiteralExpression): string {
1118
+ const object = this.reg();
1119
+ this.emit(` ${object} = call i64 @xt_object_new()`);
1120
+ for (const property of node.properties) {
1121
+ if (property.kind === SyntaxKind.PropertyAssignment) {
1122
+ const name = propertyNameText(property.name);
1123
+ const key = this.stringValue(name);
1124
+ const value = this.emitExpression(property.initializer);
1125
+ this.emit(` call i64 @xt_set(i64 ${object}, i64 ${key}, i64 ${value})`);
1126
+ } else if (property.kind === SyntaxKind.ShorthandPropertyAssignment) {
1127
+ const identifier = property.name;
1128
+ const key = this.stringValue(identifier.text);
1129
+ const value = this.emitIdentifier(identifier);
1130
+ this.emit(` call i64 @xt_set(i64 ${object}, i64 ${key}, i64 ${value})`);
1131
+ } else {
1132
+ this.unsupported(property, "object spread");
1133
+ }
1134
+ }
1135
+ return object;
1136
+ }
1137
+
1138
+ private emitClosure(node: ArrowFunction | FunctionExpression): string {
1139
+ const fn = this.binding.functionOfNode.get(node);
1140
+ if (!fn) {
1141
+ this.unsupported(node, "closure");
1142
+ return i64(XT_UNDEFINED);
1143
+ }
1144
+ let envPtr = "null";
1145
+ if (fn.captures.length > 0) {
1146
+ envPtr = `%env${this.current.allocas.length}`;
1147
+ this.current.allocas.push(`${envPtr} = alloca i64, i32 ${fn.captures.length}`);
1148
+ for (let index = 0; index < fn.captures.length; index++) {
1149
+ const symbol = fn.captures[index]!;
1150
+ // The current function must be able to read the capture: either from
1151
+ // its own slot (local) or from one of its own capture slots.
1152
+ const slot = this.current.slots.get(symbol.id);
1153
+ let raw: string;
1154
+ if (slot) {
1155
+ raw = this.reg();
1156
+ this.emit(` ${raw} = load i64, i64* ${slot.ptr}`);
1157
+ } else {
1158
+ const outer = this.binding.functionOfNode.get(node);
1159
+ void outer;
1160
+ raw = i64(XT_UNDEFINED);
1161
+ }
1162
+ const target = this.reg();
1163
+ this.emit(` ${target} = getelementptr i64, i64* ${envPtr}, i32 ${index}`);
1164
+ this.emit(` store i64 ${raw}, i64* ${target}`);
1165
+ }
1166
+ }
1167
+ const cast = this.reg();
1168
+ this.emit(` ${cast} = bitcast i64 (i64, i32, i64*)* @${this.functionName(fn)} to i8*`);
1169
+ const closure = this.reg();
1170
+ this.emit(` ${closure} = call i64 @xt_closure_new(i8* ${cast}, i32 ${fn.captures.length}, i64* ${envPtr})`);
1171
+ return closure;
1172
+ }
1173
+
1174
+ // -- string pool ---------------------------------------------------------
1175
+
1176
+ private stringValue(text: string): string {
1177
+ const entry = this.internString(text);
1178
+ const result = this.reg();
1179
+ this.emit(` ${result} = call i64 @xt_string_new(i8* ${entry.label}, i64 ${entry.length})`);
1180
+ return result;
1181
+ }
1182
+
1183
+ private internString(text: string): { label: string; length: number } {
1184
+ const existing = this.strings.get(text);
1185
+ if (existing) return existing;
1186
+ const label = `@.str.${this.stringCounter++}`;
1187
+ const bytes = utf8Bytes(text);
1188
+ const entry = { label, length: bytes.length };
1189
+ this.strings.set(text, entry);
1190
+ this.globals.push(`${label} = private unnamed_addr constant [${bytes.length + 1} x i8] c"${escapeBytes(bytes)}\\00"`);
1191
+ return entry;
1192
+ }
1193
+
1194
+ private unsupported(node: Node, what: string): void {
1195
+ const label = kindName(node.kind);
1196
+ this.diagnostics.error(
1197
+ DiagnosticCode.UnsupportedFeature,
1198
+ `xbintsc does not yet support this ${what} (${label})`,
1199
+ node,
1200
+ this.sourceFile.fileName,
1201
+ );
1202
+ }
1203
+ }
1204
+
1205
+ const BINARY_RUNTIME: Record<string, string | undefined> = {
1206
+ [BinaryOperator.Add]: "xt_add",
1207
+ [BinaryOperator.Subtract]: "xt_sub",
1208
+ [BinaryOperator.Multiply]: "xt_mul",
1209
+ [BinaryOperator.Divide]: "xt_div",
1210
+ [BinaryOperator.Remainder]: "xt_mod",
1211
+ [BinaryOperator.Exponent]: "xt_pow",
1212
+ [BinaryOperator.LessThan]: "xt_lt",
1213
+ [BinaryOperator.LessThanEquals]: "xt_le",
1214
+ [BinaryOperator.GreaterThan]: "xt_gt",
1215
+ [BinaryOperator.GreaterThanEquals]: "xt_ge",
1216
+ [BinaryOperator.EqualsEquals]: "xt_eq",
1217
+ [BinaryOperator.ExclamationEquals]: "xt_ne",
1218
+ [BinaryOperator.EqualsEqualsEquals]: "xt_seq",
1219
+ [BinaryOperator.ExclamationEqualsEquals]: "xt_sne",
1220
+ [BinaryOperator.Ampersand]: "xt_bit_and",
1221
+ [BinaryOperator.Bar]: "xt_bit_or",
1222
+ [BinaryOperator.Caret]: "xt_bit_xor",
1223
+ [BinaryOperator.LessThanLessThan]: "xt_shl",
1224
+ [BinaryOperator.GreaterThanGreaterThan]: "xt_shr",
1225
+ [BinaryOperator.GreaterThanGreaterThanGreaterThan]: "xt_ushr",
1226
+ };
1227
+
1228
+ const ASSIGNMENT_OPERATORS = new Set<string>([
1229
+ "=",
1230
+ "+=",
1231
+ "-=",
1232
+ "*=",
1233
+ "/=",
1234
+ "%=",
1235
+ "**=",
1236
+ "<<=",
1237
+ ">>=",
1238
+ ">>>=",
1239
+ "&=",
1240
+ "|=",
1241
+ "^=",
1242
+ "&&=",
1243
+ "||=",
1244
+ "??=",
1245
+ ]);
1246
+
1247
+ function isAssignmentOperator(operator: string): boolean {
1248
+ return ASSIGNMENT_OPERATORS.has(operator);
1249
+ }
1250
+
1251
+ function compoundToBinary(operator: AssignmentOperator): BinaryOperator {
1252
+ const text = operator.slice(0, -1);
1253
+ return text as BinaryOperator;
1254
+ }
1255
+
1256
+ function propertyNameText(name: Node): string {
1257
+ switch (name.kind) {
1258
+ case SyntaxKind.Identifier:
1259
+ case SyntaxKind.PrivateIdentifier:
1260
+ return (name as unknown as { text: string }).text;
1261
+ case SyntaxKind.StringLiteral:
1262
+ return (name as unknown as { value: string }).value;
1263
+ case SyntaxKind.NumericLiteral:
1264
+ return String((name as unknown as { value: number }).value);
1265
+ default:
1266
+ return "";
1267
+ }
1268
+ }
1269
+
1270
+ /** Human readable syntax kind for diagnostics (const enums have no reverse map). */
1271
+ function kindName(kind: number): string {
1272
+ const entry = KIND_NAMES.get(kind);
1273
+ return entry ?? String(kind);
1274
+ }
1275
+
1276
+ const KIND_NAMES = new Map<number, string>([
1277
+ [SyntaxKind.CallExpression, "call expression"],
1278
+ [SyntaxKind.NewExpression, "new expression"],
1279
+ [SyntaxKind.ClassDeclaration, "class declaration"],
1280
+ [SyntaxKind.ClassExpression, "class expression"],
1281
+ [SyntaxKind.EnumDeclaration, "enum declaration"],
1282
+ [SyntaxKind.SwitchStatement, "switch statement"],
1283
+ [SyntaxKind.TryStatement, "try statement"],
1284
+ [SyntaxKind.SpreadElement, "spread element"],
1285
+ [SyntaxKind.ThisKeyword, "this expression"],
1286
+ [SyntaxKind.RegularExpressionLiteral, "regular expression"],
1287
+ [SyntaxKind.TaggedTemplateExpression, "tagged template"],
1288
+ [SyntaxKind.DeleteExpression, "delete expression"],
1289
+ [SyntaxKind.AwaitExpression, "await expression"],
1290
+ [SyntaxKind.YieldExpression, "yield expression"],
1291
+ [SyntaxKind.SwitchStatement, "switch statement"],
1292
+ ]);
1293
+
1294
+ function utf8Bytes(text: string): number[] {
1295
+ const bytes: number[] = [];
1296
+ for (const character of text) {
1297
+ const code = character.codePointAt(0)!;
1298
+ if (code < 0x80) bytes.push(code);
1299
+ else if (code < 0x800) {
1300
+ bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
1301
+ } else if (code < 0x10000) {
1302
+ bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
1303
+ } else {
1304
+ bytes.push(
1305
+ 0xf0 | (code >> 18),
1306
+ 0x80 | ((code >> 12) & 0x3f),
1307
+ 0x80 | ((code >> 6) & 0x3f),
1308
+ 0x80 | (code & 0x3f),
1309
+ );
1310
+ }
1311
+ }
1312
+ return bytes;
1313
+ }
1314
+
1315
+ function escapeBytes(bytes: readonly number[]): string {
1316
+ return bytes
1317
+ .map((byte) => {
1318
+ if (byte === 0x22) return '\\22';
1319
+ if (byte === 0x5c) return '\\5C';
1320
+ if (byte >= 0x20 && byte < 0x7f) return String.fromCharCode(byte);
1321
+ return `\\${byte.toString(16).toUpperCase().padStart(2, "0")}`;
1322
+ })
1323
+ .join("");
1324
+ }