xbintsc 0.3.81 → 0.3.87

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 (157) hide show
  1. package/README.md +12 -2
  2. package/README.zh-CN.md +12 -4
  3. package/dist/src/ast/common.d.ts +23 -0
  4. package/dist/src/cli/main.js +68 -24
  5. package/dist/src/cli/main.js.map +1 -1
  6. package/dist/src/codegen/generator/calls/invocation.js +5 -1
  7. package/dist/src/codegen/generator/calls/invocation.js.map +1 -1
  8. package/dist/src/codegen/generator/context.d.ts +50 -0
  9. package/dist/src/codegen/generator/context.js +124 -13
  10. package/dist/src/codegen/generator/context.js.map +1 -1
  11. package/dist/src/codegen/generator/debug.d.ts +33 -0
  12. package/dist/src/codegen/generator/debug.js +97 -0
  13. package/dist/src/codegen/generator/debug.js.map +1 -0
  14. package/dist/src/codegen/generator/expressions/assignment.js +9 -5
  15. package/dist/src/codegen/generator/expressions/assignment.js.map +1 -1
  16. package/dist/src/codegen/generator/expressions/operators.js +35 -9
  17. package/dist/src/codegen/generator/expressions/operators.js.map +1 -1
  18. package/dist/src/codegen/generator/expressions/primary.js +5 -2
  19. package/dist/src/codegen/generator/expressions/primary.js.map +1 -1
  20. package/dist/src/codegen/generator/generator.d.ts +3 -1
  21. package/dist/src/codegen/generator/generator.js +3 -1
  22. package/dist/src/codegen/generator/generator.js.map +1 -1
  23. package/dist/src/codegen/generator/module.d.ts +1 -0
  24. package/dist/src/codegen/generator/module.js +76 -6
  25. package/dist/src/codegen/generator/module.js.map +1 -1
  26. package/dist/src/codegen/generator/numeric.d.ts +26 -0
  27. package/dist/src/codegen/generator/numeric.js +127 -0
  28. package/dist/src/codegen/generator/numeric.js.map +1 -0
  29. package/dist/src/codegen/generator/state.d.ts +17 -0
  30. package/dist/src/codegen/generator/state.js +5 -0
  31. package/dist/src/codegen/generator/state.js.map +1 -1
  32. package/dist/src/codegen/generator/statements/control-flow.js +1 -1
  33. package/dist/src/codegen/generator/statements/control-flow.js.map +1 -1
  34. package/dist/src/codegen/generator/statements/dispatch.js +3 -0
  35. package/dist/src/codegen/generator/statements/dispatch.js.map +1 -1
  36. package/dist/src/codegen/generator/statements/variables.js +8 -2
  37. package/dist/src/codegen/generator/statements/variables.js.map +1 -1
  38. package/dist/src/codegen/generator/tables.d.ts +7 -0
  39. package/dist/src/codegen/generator/tables.js +25 -0
  40. package/dist/src/codegen/generator/tables.js.map +1 -1
  41. package/dist/src/driver/bundler/merge.js +41 -5
  42. package/dist/src/driver/bundler/merge.js.map +1 -1
  43. package/dist/src/driver/compiler.d.ts +22 -3
  44. package/dist/src/driver/compiler.js +43 -57
  45. package/dist/src/driver/compiler.js.map +1 -1
  46. package/dist/src/driver/fingerprint.d.ts +27 -0
  47. package/dist/src/driver/fingerprint.js +88 -0
  48. package/dist/src/driver/fingerprint.js.map +1 -0
  49. package/dist/src/driver/toolchain-provider.d.ts +9 -2
  50. package/dist/src/driver/toolchain-provider.js +15 -15
  51. package/dist/src/driver/toolchain-provider.js.map +1 -1
  52. package/dist/src/driver/toolchain.d.ts +58 -1
  53. package/dist/src/driver/toolchain.js +169 -22
  54. package/dist/src/driver/toolchain.js.map +1 -1
  55. package/dist/src/parser/expressions.js +20 -1
  56. package/dist/src/parser/expressions.js.map +1 -1
  57. package/dist/tests/cli/main.test.js +41 -1
  58. package/dist/tests/cli/main.test.js.map +1 -1
  59. package/dist/tests/codegen/expressions.test.js +14 -3
  60. package/dist/tests/codegen/expressions.test.js.map +1 -1
  61. package/dist/tests/codegen/llvm.test.js +23 -0
  62. package/dist/tests/codegen/llvm.test.js.map +1 -1
  63. package/dist/tests/driver/toolchain-provider.test.js +49 -2
  64. package/dist/tests/driver/toolchain-provider.test.js.map +1 -1
  65. package/dist/tests/driver/toolchain.test.js +62 -3
  66. package/dist/tests/driver/toolchain.test.js.map +1 -1
  67. package/dist/tests/e2e/child-process.test.d.ts +10 -0
  68. package/dist/tests/e2e/child-process.test.js +59 -0
  69. package/dist/tests/e2e/child-process.test.js.map +1 -0
  70. package/dist/tests/e2e/compile-objects-classes.test.js +47 -0
  71. package/dist/tests/e2e/compile-objects-classes.test.js.map +1 -1
  72. package/dist/tests/e2e/error-stack.test.d.ts +9 -0
  73. package/dist/tests/e2e/error-stack.test.js +94 -0
  74. package/dist/tests/e2e/error-stack.test.js.map +1 -0
  75. package/dist/tests/e2e/string-concat.test.d.ts +9 -0
  76. package/dist/tests/e2e/string-concat.test.js +102 -0
  77. package/dist/tests/e2e/string-concat.test.js.map +1 -0
  78. package/dist/tests/helpers.d.ts +9 -0
  79. package/dist/tests/helpers.js +26 -3
  80. package/dist/tests/helpers.js.map +1 -1
  81. package/dist/tests/parser/expressions.test.js +7 -0
  82. package/dist/tests/parser/expressions.test.js.map +1 -1
  83. package/dist/tests/version.test.d.ts +1 -0
  84. package/dist/tests/version.test.js +24 -0
  85. package/dist/tests/version.test.js.map +1 -0
  86. package/doc/DESIGN.md +20 -12
  87. package/doc/ai/cli.md +2 -0
  88. package/doc/ai/contributing.md +7 -1
  89. package/doc/ai/language-support.md +4 -4
  90. package/doc/ai/troubleshooting.md +39 -2
  91. package/doc/ai/zh-CN/cli.md +2 -0
  92. package/doc/ai/zh-CN/contributing.md +6 -2
  93. package/doc/ai/zh-CN/language-support.md +4 -4
  94. package/doc/ai/zh-CN/troubleshooting.md +35 -2
  95. package/doc/implemented.md +3 -3
  96. package/doc/requirements.md +13 -4
  97. package/doc/unimplemented.md +11 -10
  98. package/doc/zh-CN/DESIGN.md +15 -9
  99. package/doc/zh-CN/implemented.md +3 -3
  100. package/doc/zh-CN/requirements.md +12 -3
  101. package/doc/zh-CN/unimplemented.md +10 -9
  102. package/package.json +3 -1
  103. package/runtime/ext_node/buffer/parts/construction.inc +2 -2
  104. package/runtime/ext_node/buffer/parts/methods.inc +1 -1
  105. package/runtime/ext_node/buffer/parts/search.inc +1 -1
  106. package/runtime/ext_node/child_process/child_process.c +25 -0
  107. package/runtime/ext_node/fs/fd_ops.c +1 -1
  108. package/runtime/rt.h +17 -1
  109. package/runtime/rt_internal.h +47 -4
  110. package/runtime/xt_alloc.c +14 -2
  111. package/runtime/xt_bigint/conversions.inc +1 -1
  112. package/runtime/xt_builtins.c +136 -14
  113. package/runtime/xt_containers/access.inc +17 -16
  114. package/runtime/xt_containers/objects.inc +4 -3
  115. package/runtime/xt_io/exception.inc +171 -2
  116. package/runtime/xt_io/inspect.inc +15 -13
  117. package/runtime/xt_io.c +5 -0
  118. package/runtime/xt_stdlib/arrays.inc +2 -2
  119. package/runtime/xt_stdlib/dispatch.inc +2 -2
  120. package/runtime/xt_stdlib/strings.inc +33 -21
  121. package/runtime/xt_stdlib2/array-statics.inc +2 -1
  122. package/runtime/xt_stdlib2/arrays.inc +1 -1
  123. package/runtime/xt_stdlib2/error.inc +22 -6
  124. package/runtime/xt_stdlib2/json.inc +3 -2
  125. package/runtime/xt_stdlib2/number-string-statics.inc +3 -1
  126. package/runtime/xt_stdlib2/regexp-match.inc +48 -33
  127. package/runtime/xt_stdlib2/strings.inc +23 -14
  128. package/runtime/xt_symbol.c +1 -1
  129. package/runtime/xt_values/comparison.inc +2 -2
  130. package/runtime/xt_values/constructors.inc +10 -5
  131. package/runtime/xt_values/conversions.inc +4 -3
  132. package/runtime/xt_values/strings.inc +78 -4
  133. package/scripts/bench.ts +172 -0
  134. package/scripts/build-runtime.ts +2 -1
  135. package/scripts/check-version.ts +53 -0
  136. package/src/ast/common.ts +23 -0
  137. package/src/cli/main.ts +68 -26
  138. package/src/codegen/generator/calls/invocation.ts +5 -0
  139. package/src/codegen/generator/context.ts +129 -18
  140. package/src/codegen/generator/debug.ts +132 -0
  141. package/src/codegen/generator/expressions/assignment.ts +9 -5
  142. package/src/codegen/generator/expressions/operators.ts +47 -8
  143. package/src/codegen/generator/expressions/primary.ts +5 -2
  144. package/src/codegen/generator/generator.ts +18 -2
  145. package/src/codegen/generator/module.ts +78 -6
  146. package/src/codegen/generator/numeric.ts +158 -0
  147. package/src/codegen/generator/state.ts +22 -0
  148. package/src/codegen/generator/statements/control-flow.ts +1 -1
  149. package/src/codegen/generator/statements/dispatch.ts +3 -0
  150. package/src/codegen/generator/statements/variables.ts +8 -2
  151. package/src/codegen/generator/tables.ts +26 -0
  152. package/src/driver/bundler/merge.ts +39 -5
  153. package/src/driver/compiler.ts +62 -55
  154. package/src/driver/fingerprint.ts +85 -0
  155. package/src/driver/toolchain-provider.ts +17 -16
  156. package/src/driver/toolchain.ts +210 -21
  157. package/src/parser/expressions.ts +20 -1
@@ -7,15 +7,34 @@ import {
7
7
  BinaryOperator,
8
8
  PostfixUnaryOperator,
9
9
  PrefixUnaryOperator,
10
+ SyntaxKind,
10
11
  type BinaryExpression,
11
12
  type ConditionalExpression,
13
+ type Expression,
14
+ type Identifier,
12
15
  type PostfixUnaryExpression,
13
16
  type PrefixUnaryExpression,
14
17
  } from "../../../ast/nodes.js";
15
18
  import { i64, numberLiteral, XT_UNDEFINED } from "../../values.js";
16
- import { BINARY_RUNTIME, isAssignmentOperator } from "../tables.js";
19
+ import { BINARY_RUNTIME, BUILTIN_INSTANCEOF, isAssignmentOperator } from "../tables.js";
17
20
  import type { Generator } from "../generator.js";
18
21
 
22
+ /** True for an expression whose value is known to be a string. */
23
+ function isStringLike(node: Expression): boolean {
24
+ return node.kind === SyntaxKind.StringLiteral || node.kind === SyntaxKind.NoSubstitutionTemplateLiteral;
25
+ }
26
+
27
+ /**
28
+ * The built-in constructor a right-hand `instanceof` operand names, if any.
29
+ * A user binding with the same name shadows it.
30
+ */
31
+ function builtinInstanceofId(generator: Generator, expression: Expression): number | undefined {
32
+ if (expression.kind !== SyntaxKind.Identifier) return undefined;
33
+ const identifier = expression as Identifier;
34
+ if (generator.binding.symbolOfIdentifier.get(identifier)) return undefined;
35
+ return BUILTIN_INSTANCEOF[identifier.text];
36
+ }
37
+
19
38
  export interface OperatorExpressionMethods {
20
39
  emitBinaryOrAssignment(this: Generator, node: BinaryExpression): string;
21
40
  emitShortCircuit(this: Generator, node: BinaryExpression, kind: "and" | "or" | "nullish"): string;
@@ -44,6 +63,17 @@ export const operatorExpressionMethods: OperatorExpressionMethods = {
44
63
  default:
45
64
  break;
46
65
  }
66
+ // `x instanceof Array` and friends: the built-in constructor is not a
67
+ // first-class value, so compare by the value's runtime kind instead.
68
+ if (operator === BinaryOperator.InstanceOf) {
69
+ const builtin = builtinInstanceofId(this, node.right);
70
+ if (builtin !== undefined) {
71
+ const leftValue = this.emitExpression(node.left);
72
+ const result = this.reg();
73
+ this.emit(` ${result} = call i64 @xt_instance_of_builtin(i64 ${leftValue}, i32 ${builtin})`);
74
+ return result;
75
+ }
76
+ }
47
77
  const left = this.emitExpression(node.left);
48
78
  const right = this.emitExpression(node.right);
49
79
  const runtime = BINARY_RUNTIME[operator];
@@ -51,9 +81,10 @@ export const operatorExpressionMethods: OperatorExpressionMethods = {
51
81
  this.unsupported(node, `operator '${operator}'`);
52
82
  return i64(XT_UNDEFINED);
53
83
  }
54
- const result = this.reg();
55
- this.emit(` ${result} = call i64 @${runtime}(i64 ${left}, i64 ${right})`);
56
- return result;
84
+ // `+` concatenates when either side is known to be a string; skip the
85
+ // numeric tag checks for that shape and call the runtime directly.
86
+ const knownStringConcat = operator === "+" && (isStringLike(node.left) || isStringLike(node.right));
87
+ return this.emitBinaryOperation(left, right, operator, runtime, !knownStringConcat);
57
88
  },
58
89
 
59
90
  emitShortCircuit(node: BinaryExpression, kind: "and" | "or" | "nullish"): string {
@@ -159,8 +190,12 @@ export const operatorExpressionMethods: OperatorExpressionMethods = {
159
190
  const one = numberLiteral(1);
160
191
  const current = this.emitExpression(node.operand);
161
192
  const fn = node.operator === PrefixUnaryOperator.PlusPlus ? "xt_add" : "xt_sub";
162
- const next = this.reg();
163
- this.emit(` ${next} = call i64 @${fn}(i64 ${current}, i64 ${one})`);
193
+ const next = this.emitBinaryOperation(
194
+ current,
195
+ one,
196
+ node.operator === PrefixUnaryOperator.PlusPlus ? "+" : "-",
197
+ fn,
198
+ );
164
199
  this.emitAssignmentTarget(node.operand, next);
165
200
  return next;
166
201
  }
@@ -174,8 +209,12 @@ export const operatorExpressionMethods: OperatorExpressionMethods = {
174
209
  const one = numberLiteral(1);
175
210
  const current = this.emitExpression(node.operand);
176
211
  const fn = node.operator === PostfixUnaryOperator.PlusPlus ? "xt_add" : "xt_sub";
177
- const next = this.reg();
178
- this.emit(` ${next} = call i64 @${fn}(i64 ${current}, i64 ${one})`);
212
+ const next = this.emitBinaryOperation(
213
+ current,
214
+ one,
215
+ node.operator === PostfixUnaryOperator.PlusPlus ? "+" : "-",
216
+ fn,
217
+ );
179
218
  this.emitAssignmentTarget(node.operand, next);
180
219
  return current;
181
220
  },
@@ -193,7 +193,7 @@ export const primaryExpressionMethods: PrimaryExpressionMethods = {
193
193
  DiagnosticCode.CodegenError,
194
194
  `Imported binding '${identifier.text}' cannot be used as a value`,
195
195
  identifier,
196
- this.sourceFile.fileName,
196
+ this.fileNameOf(identifier),
197
197
  );
198
198
  return i64(XT_UNDEFINED);
199
199
  }
@@ -244,7 +244,7 @@ export const primaryExpressionMethods: PrimaryExpressionMethods = {
244
244
  DiagnosticCode.CannotFindName,
245
245
  `Cannot find name '${identifier.text}'`,
246
246
  identifier,
247
- this.sourceFile.fileName,
247
+ this.fileNameOf(identifier),
248
248
  );
249
249
  return i64(XT_UNDEFINED);
250
250
  }
@@ -292,6 +292,9 @@ export const primaryExpressionMethods: PrimaryExpressionMethods = {
292
292
 
293
293
  emitNew(node: NewExpression): string {
294
294
  const callee = node.expression;
295
+ // A `new` expression is where the innermost frame's line should point for
296
+ // `Error.stack`; harmless for every other constructor.
297
+ this.emit(` call void @xt_stack_set_line(i32 ${this.lineOf(node)})`);
295
298
  if (callee.kind === SyntaxKind.Identifier) {
296
299
  const identifier = callee as Identifier;
297
300
  const symbol = this.binding.symbolOfIdentifier.get(identifier);
@@ -14,13 +14,29 @@ import { moduleMethods, type ModuleMethods } from "./module.js";
14
14
  import { statementMethods, type StatementMethods } from "./statements.js";
15
15
  import { expressionMethods, type ExpressionMethods } from "./expressions.js";
16
16
  import { callMethods, type CallMethods } from "./calls.js";
17
+ import { numericMethods, type NumericMethods } from "./numeric.js";
18
+ import { debugMethods, type DebugMethods } from "./debug.js";
17
19
  import type { CodegenOptions, CodegenResult } from "./state.js";
18
20
 
19
21
  export class Generator extends GeneratorContext {}
20
22
 
21
- export interface Generator extends ModuleMethods, StatementMethods, ExpressionMethods, CallMethods {}
23
+ export interface Generator
24
+ extends ModuleMethods,
25
+ StatementMethods,
26
+ ExpressionMethods,
27
+ CallMethods,
28
+ NumericMethods,
29
+ DebugMethods {}
22
30
 
23
- Object.assign(Generator.prototype, moduleMethods, statementMethods, expressionMethods, callMethods);
31
+ Object.assign(
32
+ Generator.prototype,
33
+ moduleMethods,
34
+ statementMethods,
35
+ expressionMethods,
36
+ callMethods,
37
+ numericMethods,
38
+ debugMethods,
39
+ );
24
40
 
25
41
  export type { CodegenOptions, CodegenResult } from "./state.js";
26
42
 
@@ -29,9 +29,16 @@ function isParameterProperty(parameter: Parameter): boolean {
29
29
  );
30
30
  }
31
31
 
32
+ /** A readable, IR-symbol-safe suffix for a function name. */
33
+ function symbolSuffix(name: string): string {
34
+ if (name.length === 0 || name.startsWith("<")) return "";
35
+ return name.replace(/[^A-Za-z0-9_]/g, "_").replace(/^_+/, "").slice(0, 32);
36
+ }
37
+
32
38
  export interface ModuleMethods {
33
39
  run(this: Generator): string;
34
40
  emitFunction(this: Generator, fn: FunctionInfo): void;
41
+ emitFunctionReturn(this: Generator, value: string): void;
35
42
  emitMain(this: Generator): void;
36
43
  functionName(this: Generator, fn: FunctionInfo): string;
37
44
  wrapAsync(this: Generator, value: string): string;
@@ -77,13 +84,32 @@ export const moduleMethods: ModuleMethods = {
77
84
  ];
78
85
  const runtimeDeclarations = new Set(RUNTIME_DECLARATIONS);
79
86
  const extraDeclarations = [...this.extraDeclarations].filter((line) => !runtimeDeclarations.has(line));
80
- return [...header, ...RUNTIME_DECLARATIONS, ...extraDeclarations, "", ...this.globals, "", ...this.functions, ""].join("\n");
87
+ return [
88
+ ...header,
89
+ ...RUNTIME_DECLARATIONS,
90
+ ...extraDeclarations,
91
+ "",
92
+ ...this.globals,
93
+ "",
94
+ ...this.functions,
95
+ "",
96
+ ...this.debugMetadataLines(),
97
+ ].join("\n");
81
98
  },
82
99
 
83
100
  // -- module level --------------------------------------------------------
84
101
 
85
102
  emitFunction(fn: FunctionInfo): void {
103
+ this.unsupportedCount = 0;
104
+ // `async function*` would need an async iterator and a promise-returning
105
+ // `.next()`; the generator/async lowering below would silently produce a
106
+ // synchronous generator instead, so reject it rather than miscompile.
107
+ if (fn.isAsync && fn.isGenerator) {
108
+ this.unsupported(fn.node, "async generator function");
109
+ return;
110
+ }
86
111
  const name = this.functionName(fn);
112
+ const debugSubprogram = this.debugSubprogramFor(fn);
87
113
  const state: FunctionState = {
88
114
  fn,
89
115
  buffer: [],
@@ -98,10 +124,14 @@ export const moduleMethods: ModuleMethods = {
98
124
  finallyStack: [],
99
125
  escapePointers: [],
100
126
  usesTry: false,
127
+ trackFrames: !fn.isGenerator,
128
+ ...(debugSubprogram ? { debugSubprogram } : {}),
101
129
  };
102
130
  this.current = state;
131
+ if (debugSubprogram) this.setDebugLocation(fn.node);
103
132
 
104
- const header = `define i64 @${name}(i64 %this, i64 %env, i32 %argc, i64* %argv) {`;
133
+ const debugSuffix = debugSubprogram ? ` !dbg ${debugSubprogram}` : "";
134
+ const header = `define i64 @${name}(i64 %this, i64 %env, i32 %argc, i64* %argv)${debugSuffix} {`;
105
135
  this.emit(`%saved.env = alloca i64`);
106
136
  this.emit(`store i64 %env, i64* %saved.env`);
107
137
  this.emit(`%saved.this = alloca i64`);
@@ -112,6 +142,23 @@ export const moduleMethods: ModuleMethods = {
112
142
  this.emit(`%saved.argv = alloca i64*`);
113
143
  this.emit(`store i64* %argv, i64** %saved.argv`);
114
144
 
145
+ // Shadow-stack frame: `Error.stack` and fatal-signal dumps walk this.
146
+ if (state.trackFrames) {
147
+ const displayName = fn.isModule
148
+ ? "module"
149
+ : fn.isConstructor && fn.classInfo
150
+ ? `new ${this.displayNameOf(fn.classInfo.name)}`
151
+ : this.displayNameOf(fn.name.length > 0 ? fn.name : "anonymous");
152
+ const namePointer = this.internStatic(displayName);
153
+ /* Normalise separators like `source_filename`: the compiler may run under
154
+ * Node (`\`) or as a self-hosted binary (`/`), and the IR must stay
155
+ * byte-identical across the two. */
156
+ const filePointer = this.internStatic(this.fileNameOf(fn.node).split("\\").join("/"));
157
+ this.emit(
158
+ ` call void @xt_stack_push(i8* ${namePointer}, i8* ${filePointer}, i32 ${this.lineOf(fn.node)})`,
159
+ );
160
+ }
161
+
115
162
  // Parameters.
116
163
  const parameterNodes = (fn.node as { parameters?: Parameter[] }).parameters ?? [];
117
164
  for (let index = 0; index < fn.params.length; index++) {
@@ -207,7 +254,7 @@ export const moduleMethods: ModuleMethods = {
207
254
  this.emitStatements(this.sourceFile.statements);
208
255
  } else if (fn.node.kind === SyntaxKind.ArrowFunction && (fn.node as ArrowFunction).body.kind !== SyntaxKind.Block) {
209
256
  const value = this.emitExpression((fn.node as ArrowFunction).body as Expression);
210
- this.terminate(`ret i64 ${fn.isAsync ? this.wrapAsync(value) : value}`);
257
+ this.emitFunctionReturn(fn.isAsync ? this.wrapAsync(value) : value);
211
258
  } else {
212
259
  const body = (fn.node as { body: Block }).body;
213
260
  if (body) this.emitStatements(body.statements);
@@ -215,7 +262,7 @@ export const moduleMethods: ModuleMethods = {
215
262
 
216
263
  if (!this.current.terminated) {
217
264
  const fallback = fn.isAsync ? this.wrapAsync(i64(XT_UNDEFINED)) : i64(XT_UNDEFINED);
218
- this.terminate(`ret i64 ${fallback}`);
265
+ this.emitFunctionReturn(fallback);
219
266
  }
220
267
 
221
268
  const escapes = state.usesTry
@@ -229,8 +276,22 @@ export const moduleMethods: ModuleMethods = {
229
276
  const moduleName = this.functionName(this.binding.moduleFunction);
230
277
  // Module bindings and class constructors live in globals; register them as
231
278
  // GC roots before the module body runs so a collection cannot free them.
232
- const roots = [...this.classGlobals.values(), ...this.moduleGlobals.values()];
279
+ const stringEntries = [...this.strings.values()];
280
+ const roots = [
281
+ ...this.classGlobals.values(),
282
+ ...this.moduleGlobals.values(),
283
+ ...stringEntries.map((entry) => entry.slot),
284
+ ];
233
285
  const rootLines = roots.map((name) => ` call void @xt_gc_add_root(i64* ${name})`);
286
+ // Every interned string literal is materialised once here and shared by all
287
+ // uses, so property names and other literals stop allocating per access.
288
+ const stringInit = stringEntries.flatMap((entry) => {
289
+ const register = `%strv.${entry.slot.replace("@.strv.", "")}`;
290
+ return [
291
+ ` ${register} = call i64 @xt_string_new(i8* ${entry.label}, i64 ${entry.length})`,
292
+ ` store i64 ${register}, i64* ${entry.slot}`,
293
+ ];
294
+ });
234
295
  this.functions.push(
235
296
  [
236
297
  "define i32 @main(i32 %argc, i8** %argv) {",
@@ -239,6 +300,7 @@ export const moduleMethods: ModuleMethods = {
239
300
  " call void @xt_gc_set_stack_base(i8* %stackbase.ptr)",
240
301
  " call void @xt_gc_init()",
241
302
  ...rootLines,
303
+ ...stringInit,
242
304
  " call void @xt_gc_arm()",
243
305
  " call void @xt_set_program_args(i32 %argc, i8** %argv)",
244
306
  ` %result = call i64 @${moduleName}(i64 ${i64(XT_UNDEFINED)}, i64 ${i64(XT_UNDEFINED)}, i32 0, i64* null)`,
@@ -252,7 +314,17 @@ export const moduleMethods: ModuleMethods = {
252
314
  },
253
315
 
254
316
  functionName(fn: FunctionInfo): string {
255
- return fn.isModule ? "xt_module" : `xt_fn_${fn.id}`;
317
+ if (fn.isModule) return "xt_module";
318
+ /* Keep the original name in the symbol so native debuggers, crash
319
+ * backtraces and `--emit ir` are readable (`xt_fn_3_sum`). */
320
+ const suffix = symbolSuffix(this.displayNameOf(fn.name));
321
+ return suffix.length > 0 ? `xt_fn_${fn.id}_${suffix}` : `xt_fn_${fn.id}`;
322
+ },
323
+
324
+ /** Emit the function's return, dropping its shadow-stack frame first. */
325
+ emitFunctionReturn(value: string): void {
326
+ if (this.current.trackFrames) this.emit(" call void @xt_stack_pop()");
327
+ this.terminate(`ret i64 ${value}`);
256
328
  },
257
329
 
258
330
  /** Wrap a returned value in a resolved promise (for `async` functions). */
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Inline fast paths for numeric operators.
3
+ *
4
+ * Every JavaScript operator used to be a runtime call (`xt_add`, `xt_lt`, …).
5
+ * That is correct but expensive in numeric code: a `for` loop paid three calls
6
+ * per iteration, and clang cannot inline across the generated IR / C runtime
7
+ * boundary. For two unboxed doubles the operation is just a hardware
8
+ * instruction, so emit that inline and keep the runtime call as the cold path
9
+ * for every other combination (strings, BigInt, coercion, …).
10
+ *
11
+ * Doubles are stored bit-for-bit in `xt_value`, so the tag check is an integer
12
+ * mask and the operation is `bitcast` → `fadd`/`fcmp` → `bitcast`.
13
+ */
14
+
15
+ import { i64, XT_FALSE, XT_NUMBER_MASK, XT_TRUE } from "../values.js";
16
+ import type { Generator } from "./generator.js";
17
+
18
+ export interface NumericMethods {
19
+ /**
20
+ * Emit `left <operator> right`, using an inline double fast path when both
21
+ * operands turn out to be numbers and {@link allowFastPath} is set.
22
+ */
23
+ emitBinaryOperation(
24
+ this: Generator,
25
+ left: string,
26
+ right: string,
27
+ operator: string,
28
+ runtime: string,
29
+ allowFastPath?: boolean,
30
+ ): string;
31
+ emitNumericBinary(this: Generator, left: string, right: string, instruction: string, runtime: string): string;
32
+ emitNumericComparison(this: Generator, left: string, right: string, predicate: string, runtime: string): string;
33
+ /** Emit an i1 SSA value that is true when `value` is an unboxed double. */
34
+ isNumberValue(this: Generator, value: string): string;
35
+ }
36
+
37
+ /** Canonical quiet NaN; the runtime uses the same pattern in `xt_from_double`. */
38
+ const CANONICAL_NAN = 0x7ff8000000000000n;
39
+
40
+ const NUMERIC_ARITHMETIC: Record<string, string | undefined> = {
41
+ "+": "fadd",
42
+ "-": "fsub",
43
+ "*": "fmul",
44
+ "/": "fdiv",
45
+ "%": "frem",
46
+ };
47
+
48
+ const NUMERIC_COMPARISON: Record<string, string | undefined> = {
49
+ "<": "olt",
50
+ "<=": "ole",
51
+ ">": "ogt",
52
+ ">=": "oge",
53
+ "==": "oeq",
54
+ "!=": "une",
55
+ "===": "oeq",
56
+ "!==": "une",
57
+ };
58
+
59
+ export const numericMethods: NumericMethods = {
60
+ emitBinaryOperation(left, right, operator, runtime, allowFastPath = true) {
61
+ if (allowFastPath) {
62
+ const instruction = NUMERIC_ARITHMETIC[operator];
63
+ if (instruction) return this.emitNumericBinary(left, right, instruction, runtime);
64
+ const predicate = NUMERIC_COMPARISON[operator];
65
+ if (predicate) return this.emitNumericComparison(left, right, predicate, runtime);
66
+ }
67
+ const result = this.reg();
68
+ this.emit(` ${result} = call i64 @${runtime}(i64 ${left}, i64 ${right})`);
69
+ return result;
70
+ },
71
+
72
+ /** Emit an i1 SSA value that is true when `value` is an unboxed double. */
73
+ isNumberValue(value: string): string {
74
+ const masked = this.reg();
75
+ this.emit(` ${masked} = and i64 ${value}, ${i64(XT_NUMBER_MASK)}`);
76
+ const result = this.reg();
77
+ this.emit(` ${result} = icmp ne i64 ${masked}, ${i64(XT_NUMBER_MASK)}`);
78
+ return result;
79
+ },
80
+
81
+ emitNumericBinary(left, right, instruction, runtime) {
82
+ const result = this.alloca();
83
+ const leftIsNumber = this.isNumberValue(left);
84
+ const rightIsNumber = this.isNumberValue(right);
85
+ const bothNumbers = this.reg();
86
+ this.emit(` ${bothNumbers} = and i1 ${leftIsNumber}, ${rightIsNumber}`);
87
+ const fast = this.label("num.fast");
88
+ const slow = this.label("num.slow");
89
+ const end = this.label("num.end");
90
+ this.terminate(`br i1 ${bothNumbers}, label %${fast}, label %${slow}`);
91
+
92
+ this.startBlock(fast);
93
+ const leftDouble = this.reg();
94
+ this.emit(` ${leftDouble} = bitcast i64 ${left} to double`);
95
+ const rightDouble = this.reg();
96
+ this.emit(` ${rightDouble} = bitcast i64 ${right} to double`);
97
+ const computed = this.reg();
98
+ this.emit(` ${computed} = ${instruction} double ${leftDouble}, ${rightDouble}`);
99
+ // Hardware NaNs carry many payloads; the value model only reserves the
100
+ // canonical quiet NaN, and `-NaN` would otherwise collide with XT_UNDEFINED.
101
+ const bits = this.reg();
102
+ this.emit(` ${bits} = bitcast double ${computed} to i64`);
103
+ const unordered = this.reg();
104
+ this.emit(` ${unordered} = fcmp uno double ${computed}, ${computed}`);
105
+ const canonical = this.reg();
106
+ this.emit(` ${canonical} = select i1 ${unordered}, i64 ${i64(CANONICAL_NAN)}, i64 ${bits}`);
107
+ this.emit(` store i64 ${canonical}, i64* ${result}`);
108
+ this.terminate(`br label %${end}`);
109
+
110
+ this.startBlock(slow);
111
+ const called = this.reg();
112
+ this.emit(` ${called} = call i64 @${runtime}(i64 ${left}, i64 ${right})`);
113
+ this.emit(` store i64 ${called}, i64* ${result}`);
114
+ this.terminate(`br label %${end}`);
115
+
116
+ this.startBlock(end);
117
+ const value = this.reg();
118
+ this.emit(` ${value} = load i64, i64* ${result}`);
119
+ return value;
120
+ },
121
+
122
+ emitNumericComparison(left, right, predicate, runtime) {
123
+ const result = this.alloca();
124
+ const leftIsNumber = this.isNumberValue(left);
125
+ const rightIsNumber = this.isNumberValue(right);
126
+ const bothNumbers = this.reg();
127
+ this.emit(` ${bothNumbers} = and i1 ${leftIsNumber}, ${rightIsNumber}`);
128
+ const fast = this.label("cmp.fast");
129
+ const slow = this.label("cmp.slow");
130
+ const end = this.label("cmp.end");
131
+ this.terminate(`br i1 ${bothNumbers}, label %${fast}, label %${slow}`);
132
+
133
+ this.startBlock(fast);
134
+ const leftDouble = this.reg();
135
+ this.emit(` ${leftDouble} = bitcast i64 ${left} to double`);
136
+ const rightDouble = this.reg();
137
+ this.emit(` ${rightDouble} = bitcast i64 ${right} to double`);
138
+ // Ordered comparisons are false for NaN and unordered `!=` is true, which
139
+ // is exactly ECMAScript's behaviour for two numbers.
140
+ const compared = this.reg();
141
+ this.emit(` ${compared} = fcmp ${predicate} double ${leftDouble}, ${rightDouble}`);
142
+ const value = this.reg();
143
+ this.emit(` ${value} = select i1 ${compared}, i64 ${i64(XT_TRUE)}, i64 ${i64(XT_FALSE)}`);
144
+ this.emit(` store i64 ${value}, i64* ${result}`);
145
+ this.terminate(`br label %${end}`);
146
+
147
+ this.startBlock(slow);
148
+ const called = this.reg();
149
+ this.emit(` ${called} = call i64 @${runtime}(i64 ${left}, i64 ${right})`);
150
+ this.emit(` store i64 ${called}, i64* ${result}`);
151
+ this.terminate(`br label %${end}`);
152
+
153
+ this.startBlock(end);
154
+ const merged = this.reg();
155
+ this.emit(` ${merged} = load i64, i64* ${result}`);
156
+ return merged;
157
+ },
158
+ };
@@ -50,6 +50,16 @@ export interface FunctionState {
50
50
  thisPtr?: string;
51
51
  /** Set when the function contains a `try`, forcing locals to live in memory. */
52
52
  usesTry: boolean;
53
+ /**
54
+ * True when the function pushes/pops a shadow-stack frame. Generator bodies
55
+ * are excluded: they switch stacks and can be suspended, so a global shadow
56
+ * stack would attribute their frames to the wrong caller.
57
+ */
58
+ trackFrames: boolean;
59
+ /** `!DISubprogram` for this function when DWARF emission is enabled. */
60
+ debugSubprogram?: string;
61
+ /** Current `!DILocation` attached to every emitted instruction. */
62
+ debugLocation?: string;
53
63
  }
54
64
 
55
65
  export interface CodegenResult {
@@ -74,6 +84,13 @@ export interface CodegenOptions {
74
84
  * defaults to the running process; it is overridable for tests.
75
85
  */
76
86
  readonly target?: { readonly platform: string; readonly arch: string };
87
+ /**
88
+ * Emit LLVM debug metadata (`!DICompileUnit`, `!DISubprogram`,
89
+ * `!DILocation`) so `clang -g` produces DWARF/CodeView for the generated
90
+ * program. Off by default: the metadata is large and only useful when
91
+ * debugging; `xbintsc build --debug` turns it on.
92
+ */
93
+ readonly debug?: boolean;
77
94
  }
78
95
 
79
96
  export const RUNTIME_DECLARATIONS: readonly string[] = [
@@ -93,6 +110,7 @@ export const RUNTIME_DECLARATIONS: readonly string[] = [
93
110
  "declare i64 @xt_this()",
94
111
  "declare i64 @xt_new(i64, i32, i64*)",
95
112
  "declare i64 @xt_instance_of(i64, i64)",
113
+ "declare i64 @xt_instance_of_builtin(i64, i32)",
96
114
  "declare i64 @xt_function_set_prototype(i64, i64)",
97
115
  "declare i64 @xt_function_get_prototype(i64)",
98
116
  "declare i64 @xt_object_new_with_proto(i64)",
@@ -225,6 +243,7 @@ export const RUNTIME_DECLARATIONS: readonly string[] = [
225
243
  "declare i64 @xt_object_entries(i64)",
226
244
  "declare i64 @xt_object_assign(i32, i64*)",
227
245
  "declare i64 @xt_object_spread(i64, i64)",
246
+ "declare i64 @xt_object_rest(i64, i32, i64*)",
228
247
  "declare i64 @xt_call_method(i64, i64, i32, i64*)",
229
248
  "declare i64 @xt_math_call(i64, i32, i64*)",
230
249
  "declare i64 @xt_parse_int(i32, i64*)",
@@ -298,6 +317,9 @@ export const RUNTIME_DECLARATIONS: readonly string[] = [
298
317
  "declare i8* @xt_try_enter()",
299
318
  "declare i64 @xt_try_exception(i8*)",
300
319
  "declare void @xt_try_leave(i8*)",
320
+ "declare void @xt_stack_push(i8*, i8*, i32)",
321
+ "declare void @xt_stack_pop()",
322
+ "declare void @xt_stack_set_line(i32)",
301
323
  "declare void @xt_console_log(i32, i64*)",
302
324
  "declare void @xt_console_info(i32, i64*)",
303
325
  "declare void @xt_console_warn(i32, i64*)",
@@ -255,7 +255,7 @@ export const controlFlowStatementMethods: ControlFlowStatementMethods = {
255
255
  this.runFinallysBeforeExit(0);
256
256
  if (this.current.terminated) return;
257
257
  this.popTryFramesTo(0);
258
- this.terminate(`ret i64 ${value}`);
258
+ this.emitFunctionReturn(value);
259
259
  },
260
260
 
261
261
  /** Emit `xt_try_leave` for every active frame above `depth` (innermost first). */
@@ -41,6 +41,9 @@ export const statementDispatchMethods: StatementDispatchMethods = {
41
41
  },
42
42
 
43
43
  emitStatement(statement: Statement): void {
44
+ /* Every instruction emitted while lowering this statement (including
45
+ * nested expressions) points at its source position when DWARF is on. */
46
+ this.setDebugLocation(statement);
44
47
  switch (statement.kind) {
45
48
  case SyntaxKind.ExpressionStatement:
46
49
  this.emitExpression((statement as { expression: Expression }).expression);
@@ -87,13 +87,19 @@ export const variableStatementMethods: VariableStatementMethods = {
87
87
  return;
88
88
  }
89
89
  const pattern = name as ObjectBindingPattern;
90
+ /* Keys named by the pattern are excluded from `...rest`. */
91
+ const excludedKeys: string[] = [];
90
92
  for (const element of pattern.elements) {
91
- const key = element.propertyName ?? (element.name.kind === SyntaxKind.Identifier ? element.name : undefined);
92
93
  if (element.dotDotDotToken) {
93
- this.unsupported(element, "object rest destructuring");
94
+ const rest = this.reg();
95
+ const keys = this.emitValueArguments(excludedKeys);
96
+ this.emit(` ${rest} = call i64 @xt_object_rest(i64 ${value}, i32 ${keys.argc}, i64* ${keys.ptr})`);
97
+ this.emitBindingPattern(element.name, rest);
94
98
  continue;
95
99
  }
100
+ const key = element.propertyName ?? (element.name.kind === SyntaxKind.Identifier ? element.name : undefined);
96
101
  const keyValue = this.stringValue(key ? propertyNameText(key) : "undefined");
102
+ excludedKeys.push(keyValue);
97
103
  let elementValue = this.reg();
98
104
  this.emit(` ${elementValue} = call i64 @xt_get(i64 ${value}, i64 ${keyValue})`);
99
105
  if (element.initializer) elementValue = this.emitBindingDefault(elementValue, element.initializer);
@@ -22,6 +22,32 @@ export function requiresSetjmpex(platform: string, arch: string): boolean {
22
22
  return platform === "win32" && arch === "arm64";
23
23
  }
24
24
 
25
+ /**
26
+ * Built-in constructors usable on the right of `instanceof`, mapped to the id
27
+ * `xt_instance_of_builtin` expects. Keep in sync with the `XT_INSTANCE_*` enum
28
+ * in `runtime/xt_builtins.c`; the typed-array ids follow the runtime's
29
+ * `XT_TA_*` order (Uint8Array first).
30
+ */
31
+ export const BUILTIN_INSTANCEOF: Readonly<Record<string, number>> = {
32
+ Array: 0,
33
+ Object: 1,
34
+ Function: 2,
35
+ Map: 3,
36
+ Set: 4,
37
+ Date: 5,
38
+ RegExp: 6,
39
+ Promise: 7,
40
+ Uint8Array: 16,
41
+ Int8Array: 17,
42
+ Uint8ClampedArray: 18,
43
+ Uint16Array: 19,
44
+ Int16Array: 20,
45
+ Uint32Array: 21,
46
+ Int32Array: 22,
47
+ Float32Array: 23,
48
+ Float64Array: 24,
49
+ };
50
+
25
51
  export const BINARY_RUNTIME: Record<string, string | undefined> = {
26
52
  [BinaryOperator.Add]: "xt_add",
27
53
  [BinaryOperator.Subtract]: "xt_sub",