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
package/src/cli/main.ts CHANGED
@@ -18,11 +18,11 @@ import { dirname, resolve } from "node:path";
18
18
  import { pathToFileURL } from "node:url";
19
19
  import { formatDiagnostic, type Diagnostic } from "../diagnostics/diagnostic.js";
20
20
  import { SourceFile } from "../diagnostics/source.js";
21
- import { build, compileEntry, COMPILER_VERSION, type EmitKind } from "../driver/compiler.js";
21
+ import { build, compileEntry, COMPILER_VERSION, type EmitKind, type PhaseTiming } from "../driver/compiler.js";
22
22
  import { findRuntimeDir, platformSlug } from "../driver/paths.js";
23
23
  import { runtimeLibDir } from "../driver/runtime-lib.js";
24
- import { resolveToolchain } from "../driver/toolchain-provider.js";
25
- import { realRunner, ToolchainError } from "../driver/toolchain.js";
24
+ import { resolveToolchain, type ResolvedToolchain } from "../driver/toolchain-provider.js";
25
+ import { MINIMUM_CLANG_MAJOR, probeClang, realRunner, ToolchainError } from "../driver/toolchain.js";
26
26
  import {
27
27
  DOC_CLI,
28
28
  DOC_TROUBLESHOOTING,
@@ -154,23 +154,39 @@ function buildRegistry(extNames: readonly string[], nativeManifests: readonly st
154
154
  }
155
155
 
156
156
  function printDiagnostics(diagnostics: readonly Diagnostic[], fileName: string, io: CliIo): void {
157
- let source: SourceFile | undefined;
158
- try {
159
- source = new SourceFile(fileName, readFileSync(fileName, "utf8"));
160
- } catch {
161
- source = undefined;
162
- }
157
+ /* A diagnostic can come from any module in a bundled program, so load (and
158
+ * cache) the source of the file it names instead of always using the entry:
159
+ * reusing the entry's text is what used to print another file's line with a
160
+ * caret under unrelated code. */
161
+ const sources = new Map<string, SourceFile | undefined>();
162
+ const sourceFor = (name: string): SourceFile | undefined => {
163
+ if (!sources.has(name)) {
164
+ try {
165
+ sources.set(name, new SourceFile(name, readFileSync(name, "utf8")));
166
+ } catch {
167
+ sources.set(name, undefined);
168
+ }
169
+ }
170
+ return sources.get(name);
171
+ };
163
172
  for (const diagnostic of diagnostics) {
164
173
  // Driver-level diagnostics (icon, config, …) have no source location.
165
174
  if (diagnostic.fileName === undefined && diagnostic.start === undefined) {
166
175
  io.stderr(`error TS${diagnostic.code}: ${diagnostic.message}\n`);
167
176
  continue;
168
177
  }
178
+ const source = sourceFor(diagnostic.fileName ?? fileName);
169
179
  io.stderr(formatDiagnostic(diagnostic, source) + "\n");
170
180
  }
171
181
  for (const hint of documentationHints(diagnostics)) emitHint(io, hint);
172
182
  }
173
183
 
184
+ /** Print the phases collected by `--time`. */
185
+ function printTimings(timings: readonly PhaseTiming[], io: CliIo): void {
186
+ const text = timings.map((timing) => `${timing.phase} ${timing.ms}ms`).join(", ");
187
+ io.stdout(`xbintsc: timing (${text})\n`);
188
+ }
189
+
174
190
  /** Print one `hint:` line unless the caller asked for plain output. */
175
191
  function emitHint(io: CliIo, line: string): void {
176
192
  if (hintsDisabled) return;
@@ -237,6 +253,8 @@ Options:
237
253
  --app-id <id> macOS bundle identifier (e.g. com.example.demo)
238
254
  --force Ignore the incremental cache
239
255
  --verbose Print progress information
256
+ --time Print per-phase build timings
257
+ --debug Emit debug info (DWARF/CodeView) and keep frame pointers
240
258
  --no-hints Do not append the "hint: … see <doc>" line to failures
241
259
 
242
260
  xbintsc compiles a subset of TypeScript to a native binary. Types are erased
@@ -247,26 +265,47 @@ Docs: doc/ai/README.md (task guide) - doc/ai/language-support.md (subset) -
247
265
  doc/ai/troubleshooting.md (failures) - llms.txt (index of every document)
248
266
  `;
249
267
 
250
- function firstLine(text: string): string {
251
- const index = text.indexOf("\n");
252
- return (index === -1 ? text : text.slice(0, index)).trim();
253
- }
254
-
255
- /** Report the resolved toolchain and runtime locations. */
268
+ /**
269
+ * Report the resolved toolchain and runtime locations.
270
+ *
271
+ * Returns non-zero when the resolved compiler cannot build anything (missing,
272
+ * or older than {@link MINIMUM_CLANG_MAJOR}), so CI and scripts can use it as a
273
+ * preflight instead of discovering the problem halfway through a build.
274
+ */
256
275
  function doctor(io: CliIo): number {
257
276
  io.stdout(`xbintsc ${COMPILER_VERSION}\n`);
258
277
  io.stdout(`platform : ${platformSlug()}\n`);
259
278
 
279
+ let toolchain: ResolvedToolchain | undefined;
280
+ let usable = false;
260
281
  try {
261
- const toolchain = resolveToolchain();
262
- const version = realRunner.run(toolchain.clang, ["--version"], { env: toolchain.env }).stdout;
282
+ toolchain = resolveToolchain();
283
+ const probe = probeClang(realRunner, toolchain.clang, toolchain.env);
263
284
  io.stdout(`toolchain : ${toolchain.clang} (${toolchain.source})\n`);
264
- io.stdout(`compiler : ${firstLine(version) || "unknown"}\n`);
265
- } catch {
266
- io.stdout("toolchain : not found\n");
267
- io.stdout(" install one of: Xcode Command Line Tools (`xcode-select --install`) on macOS,\n");
268
- io.stdout(" clang+lld (apt/dnf/pacman) on Linux, or LLVM + Visual Studio C++ build tools on Windows,\n");
269
- io.stdout(" then re-run; or set xbintsc_CLANG to a clang binary. See doc/requirements.md.\n");
285
+ io.stdout(`compiler : ${probe.summary || "unknown"}\n`);
286
+ const version = toolchain.version ?? probe.version;
287
+ if (!version) {
288
+ // An explicit wrapper need not print a clang-style version; let it run.
289
+ usable = true;
290
+ io.stdout(`status : version unknown (requires clang >= ${MINIMUM_CLANG_MAJOR})\n`);
291
+ } else if (version.major >= MINIMUM_CLANG_MAJOR) {
292
+ usable = true;
293
+ io.stdout(`status : ok (requires clang >= ${MINIMUM_CLANG_MAJOR})\n`);
294
+ } else {
295
+ io.stdout(
296
+ `status : too old: clang ${version.major}.${version.minor}.${version.patch} ` +
297
+ `(requires >= ${MINIMUM_CLANG_MAJOR})\n`,
298
+ );
299
+ io.stdout(" set xbintsc_CLANG to a newer clang; on Windows Visual Studio ships one under\n");
300
+ io.stdout(" <VS install>\\VC\\Tools\\Llvm\\x64\\bin\\clang.exe. See doc/requirements.md.\n");
301
+ }
302
+ } catch (error) {
303
+ const message = error instanceof Error ? error.message : String(error);
304
+ io.stdout("toolchain : not usable\n");
305
+ for (const line of message.split("\n")) {
306
+ if (line.trim().length > 0) io.stdout(` ${line.trim()}\n`);
307
+ }
308
+ io.stdout(" See doc/requirements.md for the per-platform install steps, or set xbintsc_CLANG.\n");
270
309
  }
271
310
 
272
311
  try {
@@ -282,13 +321,13 @@ function doctor(io: CliIo): number {
282
321
  }
283
322
 
284
323
  try {
285
- const toolchain = resolveToolchain();
324
+ if (!toolchain) throw new Error("no toolchain");
286
325
  const resource = resolveResourceCompiler(realRunner, toolchain.clang);
287
326
  io.stdout(`icon res : ${resource ? `${resource.tool} (${resource.kind})` : "none (.exe icon disabled)"}\n`);
288
327
  } catch {
289
328
  io.stdout("icon res : unknown\n");
290
329
  }
291
- return 0;
330
+ return usable ? 0 : 1;
292
331
  }
293
332
 
294
333
  type ConfigResolution =
@@ -388,7 +427,7 @@ function dispatch(argv: readonly string[], io: CliIo): number {
388
427
  const extNames = listFlag(args.flags, "ext") ?? resolved.config.extensions ?? [];
389
428
  const nativeManifests = listFlag(args.flags, "ext-native") ?? resolved.config.extNative ?? [];
390
429
  const registry = buildRegistry(extNames, nativeManifests);
391
- const { ir, diagnostics } = compileEntry(entry, registry);
430
+ const { ir, diagnostics } = compileEntry(entry, registry, { debug: Boolean(args.flags.get("debug")) });
392
431
  if (diagnostics.some((d) => d.category === "error")) {
393
432
  printDiagnostics(diagnostics, entry, io);
394
433
  return 1;
@@ -423,6 +462,7 @@ function dispatch(argv: readonly string[], io: CliIo): number {
423
462
  verbose: Boolean(args.flags.get("verbose")),
424
463
  extensions: registry,
425
464
  app: mergeAppConfig(args.flags, config),
465
+ debug: Boolean(args.flags.get("debug")),
426
466
  });
427
467
 
428
468
  if (result.diagnostics.some((d) => d.category === "error")) {
@@ -430,6 +470,8 @@ function dispatch(argv: readonly string[], io: CliIo): number {
430
470
  return 1;
431
471
  }
432
472
 
473
+ if (args.flags.has("time") && result.timings) printTimings(result.timings, io);
474
+
433
475
  if (command === "build") {
434
476
  const bundle = result.bundlePath ? ` (bundle: ${result.bundlePath})` : "";
435
477
  io.stdout(`xbintsc: wrote ${result.outputPath}${result.cached ? " (cached)" : ""}${bundle}\n`);
@@ -19,6 +19,7 @@ import {
19
19
  BUILTIN_METHODS,
20
20
  CONSOLE_METHODS,
21
21
  CTOR_FUNCTIONS,
22
+ ERROR_CONSTRUCTORS,
22
23
  GLOBAL_FUNCTIONS,
23
24
  MATH_FUNCTIONS,
24
25
  NAMESPACE_STATICS,
@@ -142,6 +143,10 @@ export const invocationCallMethods: InvocationCallMethods = {
142
143
  }
143
144
  const ctor = !symbol ? CTOR_FUNCTIONS[(callee as Identifier).text] : undefined;
144
145
  if (ctor) {
146
+ // `Error("x")` without `new`: point the innermost frame at the call.
147
+ if (ERROR_CONSTRUCTORS[(callee as Identifier).text]) {
148
+ this.emit(` call void @xt_stack_set_line(i32 ${this.lineOf(node)})`);
149
+ }
145
150
  const args = this.emitArguments(node.arguments);
146
151
  const result = this.reg();
147
152
  this.emit(` ${result} = call i64 @${ctor}(i32 ${args.argc}, i64* ${args.ptr})`);
@@ -54,11 +54,35 @@ export class GeneratorContext {
54
54
  readonly importDefaults = new Map<number, ModuleExport>();
55
55
  readonly globals: string[] = [];
56
56
  readonly functions: string[] = [];
57
- readonly strings = new Map<string, { label: string; length: number }>();
57
+ readonly strings = new Map<string, { label: string; length: number; slot: string }>();
58
58
  readonly classGlobals = new Map<number, string>();
59
59
  readonly moduleGlobals = new Map<number, string>();
60
60
  readonly extraDeclarations = new Set<string>();
61
+ /** Static C strings used only as metadata (stack frame names/files). */
62
+ readonly staticStrings = new Map<string, string>();
63
+ /** Lazily built line-start indexes per source file, for stack frames. */
64
+ readonly lineStartsByFile = new Map<string, number[]>();
61
65
  stringCounter = 0;
66
+ staticCounter = 0;
67
+ /**
68
+ * How many `UnsupportedFeature` diagnostics the function currently being
69
+ * emitted produced. Unsupported constructs can leave the generator with a
70
+ * partially-bound tree; reporting the resulting invariant failures as
71
+ * "Internal: …" only buries the actionable error, so they are suppressed
72
+ * while this is non-zero.
73
+ */
74
+ unsupportedCount = 0;
75
+ /** Emit DWARF debug metadata (`--debug`). */
76
+ readonly debug: boolean;
77
+ /** Numbered metadata definitions, emitted after the functions. */
78
+ metadataCounter = 0;
79
+ readonly metadata: string[] = [];
80
+ readonly debugFiles = new Map<string, string>();
81
+ readonly debugSubprograms = new Map<number, string>();
82
+ readonly debugLocations = new Map<string, string>();
83
+ debugUnit?: string;
84
+ debugFlags: string[] = [];
85
+ debugSubroutineType?: string;
62
86
  current!: FunctionState;
63
87
 
64
88
  constructor(sourceFile: SourceFileNode, diagnostics: DiagnosticBag, options: CodegenOptions = {}) {
@@ -69,6 +93,7 @@ export class GeneratorContext {
69
93
  this.modules = options.modules ?? {};
70
94
  this.moduleHints = options.moduleHints ?? {};
71
95
  this.target = options.target ?? { platform: process.platform, arch: process.arch };
96
+ this.debug = options.debug === true;
72
97
  this.resolveImports();
73
98
  }
74
99
 
@@ -120,7 +145,7 @@ export class GeneratorContext {
120
145
  DiagnosticCode.ModuleNotFound,
121
146
  `Module '"${specifier}"' has no exported member '${importedName}'`,
122
147
  specifierNode.name,
123
- this.sourceFile.fileName,
148
+ this.fileNameOf(specifierNode.name),
124
149
  );
125
150
  this.missingModuleSymbols.add(symbol.id);
126
151
  }
@@ -148,7 +173,7 @@ export class GeneratorContext {
148
173
  DiagnosticCode.ModuleNotFound,
149
174
  `module '${specifier}' is provided by the '${provider}' extension; pass --ext ${provider}`,
150
175
  declaration.moduleSpecifier,
151
- this.sourceFile.fileName,
176
+ this.fileNameOf(declaration.moduleSpecifier),
152
177
  );
153
178
  this.markMissingModuleSymbols(declaration);
154
179
  return;
@@ -165,7 +190,7 @@ export class GeneratorContext {
165
190
  `module '${specifier}' is not supported: xbintsc can import built-in platform modules, ` +
166
191
  "relative '.ts' files and ESM packages under node_modules; CommonJS packages are not supported",
167
192
  declaration.moduleSpecifier,
168
- this.sourceFile.fileName,
193
+ this.fileNameOf(declaration.moduleSpecifier),
169
194
  );
170
195
  for (const symbol of symbols) this.missingModuleSymbols.add(symbol.id);
171
196
  }
@@ -230,11 +255,12 @@ export class GeneratorContext {
230
255
  // -- emission primitives -------------------------------------------------
231
256
 
232
257
  emit(line: string): void {
233
- this.current.buffer.push(line);
258
+ const location = this.debug ? this.current.debugLocation : undefined;
259
+ this.current.buffer.push(location === undefined ? line : `${line}, !dbg ${location}`);
234
260
  }
235
261
 
236
262
  terminate(line: string): void {
237
- this.current.buffer.push(` ${line}`);
263
+ this.emit(` ${line}`);
238
264
  this.current.terminated = true;
239
265
  }
240
266
 
@@ -283,6 +309,19 @@ export class GeneratorContext {
283
309
  return ptr;
284
310
  }
285
311
 
312
+ /** Lay already-computed values out as a contiguous `i64*` argument buffer. */
313
+ emitValueArguments(values: readonly string[]): { argc: string; ptr: string } {
314
+ if (values.length === 0) return { argc: "0", ptr: "null" };
315
+ const ptr = `%args${this.current.allocas.length}`;
316
+ this.current.allocas.push(`${ptr} = alloca i64, i32 ${values.length}`);
317
+ for (let index = 0; index < values.length; index++) {
318
+ const slot = this.reg();
319
+ this.emit(` ${slot} = getelementptr i64, i64* ${ptr}, i32 ${index}`);
320
+ this.emit(` store i64 ${values[index]}, i64* ${slot}`);
321
+ }
322
+ return { argc: String(values.length), ptr };
323
+ }
324
+
286
325
  const(value: bigint): string {
287
326
  return i64(value);
288
327
  }
@@ -333,11 +372,15 @@ export class GeneratorContext {
333
372
  const slot = this.current.slots.get(symbol.id);
334
373
  if (!slot) {
335
374
  // Referenced from an inner function without a capture slot: this should
336
- // be impossible once the binder has run, but stay defensive.
337
- this.diagnostics.error(
338
- DiagnosticCode.CodegenError,
339
- `Internal: no slot for '${symbol.name}' in ${this.current.fn.name}`,
340
- );
375
+ // be impossible once the binder has run, but stay defensive. After an
376
+ // unsupported construct the tree can be partially bound, so do not add
377
+ // an "Internal" error on top of the actionable one.
378
+ if (this.unsupportedCount === 0) {
379
+ this.diagnostics.error(
380
+ DiagnosticCode.CodegenError,
381
+ `Internal: no slot for '${symbol.name}' in ${this.current.fn.name}`,
382
+ );
383
+ }
341
384
  return i64(XT_UNDEFINED);
342
385
  }
343
386
  const value = this.reg();
@@ -362,7 +405,9 @@ export class GeneratorContext {
362
405
  }
363
406
  const slot = this.current.slots.get(symbol.id);
364
407
  if (!slot) {
365
- this.diagnostics.error(DiagnosticCode.CodegenError, `Internal: no slot for '${symbol.name}'`);
408
+ if (this.unsupportedCount === 0) {
409
+ this.diagnostics.error(DiagnosticCode.CodegenError, `Internal: no slot for '${symbol.name}'`);
410
+ }
366
411
  return;
367
412
  }
368
413
  if (!slot.boxed) {
@@ -390,28 +435,94 @@ export class GeneratorContext {
390
435
  stringValue(text: string): string {
391
436
  const entry = this.internString(text);
392
437
  const result = this.reg();
393
- this.emit(` ${result} = call i64 @xt_string_new(i8* ${entry.label}, i64 ${entry.length})`);
438
+ // The value is created once in `main` and shared by every use, so a string
439
+ // literal (especially a property name in a hot loop) no longer allocates on
440
+ // each evaluation.
441
+ this.emit(` ${result} = load i64, i64* ${entry.slot}`);
394
442
  return result;
395
443
  }
396
444
 
397
- internString(text: string): { label: string; length: number } {
445
+ internString(text: string): { label: string; length: number; slot: string } {
398
446
  const existing = this.strings.get(text);
399
447
  if (existing) return existing;
400
- const label = `@.str.${this.stringCounter++}`;
448
+ const index = this.stringCounter++;
449
+ const label = `@.str.${index}`;
450
+ const slot = `@.strv.${index}`;
401
451
  const bytes = utf8Bytes(text);
402
- const entry = { label, length: bytes.length };
452
+ const entry = { label, length: bytes.length, slot };
403
453
  this.strings.set(text, entry);
404
454
  this.globals.push(`${label} = private unnamed_addr constant [${bytes.length + 1} x i8] c"${escapeBytes(bytes)}\\00"`);
455
+ // A GC-rooted global holding the runtime string value, initialised in main.
456
+ this.globals.push(`${slot} = internal global i64 0`);
405
457
  return entry;
406
458
  }
407
459
 
460
+ /**
461
+ * Intern a string purely as static module data (no `xt_string` value), for
462
+ * metadata such as shadow-stack frame names and file paths.
463
+ */
464
+ internStatic(text: string): string {
465
+ const existing = this.staticStrings.get(text);
466
+ if (existing) return existing;
467
+ const label = `@.meta.${this.staticCounter++}`;
468
+ const bytes = utf8Bytes(text);
469
+ this.staticStrings.set(text, label);
470
+ this.globals.push(`${label} = private unnamed_addr constant [${bytes.length + 1} x i8] c"${escapeBytes(bytes)}\\00"`);
471
+ return label;
472
+ }
473
+
474
+ /** Strip a bundler-generated module prefix (`m1_`) from a display name. */
475
+ displayNameOf(name: string): string {
476
+ for (const prefix of this.sourceFile.modulePrefixes ?? []) {
477
+ if (name.startsWith(prefix) && name.length > prefix.length) return name.slice(prefix.length);
478
+ }
479
+ return name;
480
+ }
481
+
482
+ /** 1-based line and column of `node` in the module it came from. */
483
+ positionOf(node: Node): { line: number; column: number } {
484
+ const file = this.fileNameOf(node);
485
+ const text = this.sourceFile.moduleTexts?.get(file) ?? this.sourceFile.text;
486
+ let starts = this.lineStartsByFile.get(file);
487
+ if (!starts) {
488
+ starts = [0];
489
+ for (let index = 0; index < text.length; index++) {
490
+ if (text.charCodeAt(index) === 10) starts.push(index + 1);
491
+ }
492
+ this.lineStartsByFile.set(file, starts);
493
+ }
494
+ const offset = Math.max(0, Math.min(node.start, text.length));
495
+ let low = 0;
496
+ let high = starts.length - 1;
497
+ while (low < high) {
498
+ const middle = (low + high + 1) >> 1;
499
+ if (starts[middle]! <= offset) low = middle;
500
+ else high = middle - 1;
501
+ }
502
+ return { line: low + 1, column: offset - starts[low]! + 1 };
503
+ }
504
+
505
+ /** 1-based line of `node` in the module it came from (for stack frames). */
506
+ lineOf(node: Node): number {
507
+ return this.positionOf(node).line;
508
+ }
509
+
510
+ /** The source file a node belongs to. Bundling merges every module into one
511
+ * `SourceFile` while keeping module-local offsets, so the origin map is what
512
+ * keeps diagnostics pointing at the file the user actually wrote.
513
+ */
514
+ fileNameOf(node: Node): string {
515
+ return this.sourceFile.origins?.get(node) ?? this.sourceFile.fileName;
516
+ }
517
+
408
518
  unsupported(node: Node, what: string): void {
409
519
  const label = kindName(node.kind);
520
+ this.unsupportedCount++;
410
521
  this.diagnostics.error(
411
522
  DiagnosticCode.UnsupportedFeature,
412
523
  `xbintsc does not yet support this ${what} (${label})`,
413
524
  node,
414
- this.sourceFile.fileName,
525
+ this.fileNameOf(node),
415
526
  );
416
527
  }
417
528
 
@@ -429,7 +540,7 @@ export class GeneratorContext {
429
540
  DiagnosticCode.UnsupportedFeature,
430
541
  `CommonJS \`require()\` is not supported; ${replacement}`,
431
542
  node,
432
- this.sourceFile.fileName,
543
+ this.fileNameOf(node),
433
544
  );
434
545
  }
435
546
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * DWARF/CodeView metadata for `xbintsc build --debug`.
3
+ *
4
+ * The generated IR declares a compile unit, one `!DISubprogram` per compiled
5
+ * function and one cached `!DILocation` per statement position. With `-g`,
6
+ * clang turns these into DWARF (Linux/macOS) or CodeView (Windows), so a native
7
+ * debugger maps instructions back to the TypeScript source.
8
+ *
9
+ * Metadata is only emitted when debug is enabled: the nodes are large, and the
10
+ * default IR must stay byte-identical for the self-hosting fixpoint.
11
+ */
12
+
13
+ import type { Node } from "../../ast/nodes.js";
14
+ import type { FunctionInfo } from "../../binder/binder.js";
15
+ import { escapeBytes, utf8Bytes } from "./tables.js";
16
+ import type { Generator } from "./generator.js";
17
+
18
+ export interface DebugMethods {
19
+ /** Allocate a numbered metadata node (`!0`, `!1`, …). */
20
+ metadataId(this: Generator): string;
21
+ /** Allocate a metadata node and register its definition. */
22
+ addMetadata(this: Generator, definition: string): string;
23
+ /** `!DIFile` for a source path, normalising separators like `source_filename`. */
24
+ debugFileFor(this: Generator, fileName: string): string;
25
+ /** `!DISubprogram` for a compiled function (created on first use). */
26
+ debugSubprogramFor(this: Generator, fn: FunctionInfo): string | undefined;
27
+ /**
28
+ * Point subsequent instructions at `node`'s source position. Locations are
29
+ * cached per function/line/column so a statement emits one metadata node
30
+ * instead of one per instruction.
31
+ */
32
+ setDebugLocation(this: Generator, node: Node): void;
33
+ /** Module-level metadata declarations and definitions (`--debug`). */
34
+ debugMetadataLines(this: Generator): string[];
35
+ }
36
+
37
+ function escapeMetadata(text: string): string {
38
+ return escapeBytes(utf8Bytes(text));
39
+ }
40
+
41
+ /**
42
+ * Create the compile unit and module flags once. LLVM requires the `Debug Info
43
+ * Version` flag for any `!dbg` attachment, and clang only emits DWARF/CodeView
44
+ * for a module that declares a compile unit.
45
+ */
46
+ function ensureDebugUnit(self: Generator): void {
47
+ if (self.debugUnit !== undefined) return;
48
+ const entryFile = self.debugFileFor(self.sourceFile.fileName);
49
+ const unit = self.addMetadata(
50
+ 'distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: ' +
51
+ `${entryFile}, producer: "xbintsc", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)`,
52
+ );
53
+ self.debugUnit = unit;
54
+ self.debugFlags.push(
55
+ self.addMetadata('!{i32 7, !"Dwarf Version", i32 4}'),
56
+ self.addMetadata('!{i32 2, !"Debug Info Version", i32 3}'),
57
+ );
58
+ const types = self.addMetadata("!{null}");
59
+ self.debugSubroutineType = self.addMetadata(`!DISubroutineType(types: ${types})`);
60
+ }
61
+
62
+ export const debugMethods: DebugMethods = {
63
+ metadataId() {
64
+ return `!${this.metadataCounter++}`;
65
+ },
66
+
67
+ addMetadata(definition) {
68
+ const id = this.metadataId();
69
+ this.metadata.push(`${id} = ${definition}`);
70
+ return id;
71
+ },
72
+
73
+ debugFileFor(fileName) {
74
+ const normalized = fileName.split("\\").join("/");
75
+ const existing = this.debugFiles.get(normalized);
76
+ if (existing) return existing;
77
+ const slash = normalized.lastIndexOf("/");
78
+ const directory = slash >= 0 ? normalized.slice(0, slash) : ".";
79
+ const name = slash >= 0 ? normalized.slice(slash + 1) : normalized;
80
+ const id = this.addMetadata(
81
+ `!DIFile(filename: "${escapeMetadata(name)}", directory: "${escapeMetadata(directory)}")`,
82
+ );
83
+ this.debugFiles.set(normalized, id);
84
+ return id;
85
+ },
86
+
87
+ debugSubprogramFor(fn) {
88
+ if (!this.debug) return undefined;
89
+ const existing = this.debugSubprograms.get(fn.id);
90
+ if (existing) return existing;
91
+ ensureDebugUnit(this);
92
+ const file = this.debugFileFor(this.fileNameOf(fn.node));
93
+ const position = this.positionOf(fn.node);
94
+ const displayName = fn.isModule
95
+ ? "module"
96
+ : fn.isConstructor && fn.classInfo
97
+ ? `new ${this.displayNameOf(fn.classInfo.name)}`
98
+ : this.displayNameOf(fn.name.length > 0 ? fn.name : "anonymous");
99
+ const subprogram = this.addMetadata(
100
+ 'distinct !DISubprogram(name: ' +
101
+ `"${escapeMetadata(displayName)}", scope: ${this.debugUnit}, file: ${file}, ` +
102
+ `line: ${position.line}, type: ${this.debugSubroutineType}, scopeLine: ${position.line}, ` +
103
+ `spFlags: DISPFlagDefinition, unit: ${this.debugUnit})`,
104
+ );
105
+ this.debugSubprograms.set(fn.id, subprogram);
106
+ return subprogram;
107
+ },
108
+
109
+ setDebugLocation(node) {
110
+ if (!this.debug || this.current.debugSubprogram === undefined) return;
111
+ const position = this.positionOf(node);
112
+ const key = `${this.current.debugSubprogram}:${position.line}:${position.column}`;
113
+ let location = this.debugLocations.get(key);
114
+ if (!location) {
115
+ location = this.addMetadata(
116
+ `!DILocation(line: ${position.line}, column: ${position.column}, scope: ${this.current.debugSubprogram})`,
117
+ );
118
+ this.debugLocations.set(key, location);
119
+ }
120
+ this.current.debugLocation = location;
121
+ },
122
+
123
+ debugMetadataLines() {
124
+ if (!this.debug || this.debugUnit === undefined) return [];
125
+ return [
126
+ `!llvm.dbg.cu = !{${this.debugUnit}}`,
127
+ `!llvm.module.flags = !{${this.debugFlags.join(", ")}}`,
128
+ ...this.metadata,
129
+ "",
130
+ ];
131
+ },
132
+ };
@@ -58,9 +58,7 @@ export const assignmentExpressionMethods: AssignmentExpressionMethods = {
58
58
  this.unsupported(node, `assignment operator '${operator}'`);
59
59
  return i64(XT_UNDEFINED);
60
60
  }
61
- const result = this.reg();
62
- this.emit(` ${result} = call i64 @${runtime}(i64 ${current}, i64 ${right})`);
63
- value = result;
61
+ value = this.emitBinaryOperation(current, right, binaryOperator, runtime);
64
62
  }
65
63
  this.emitAssignmentTarget(target, value);
66
64
  return value;
@@ -120,7 +118,7 @@ export const assignmentExpressionMethods: AssignmentExpressionMethods = {
120
118
  ? `Cannot assign to '${symbol.name}' because it is an import.`
121
119
  : `Cannot assign to '${symbol.name}' because it is a constant.`,
122
120
  identifier,
123
- this.sourceFile.fileName,
121
+ this.fileNameOf(identifier),
124
122
  );
125
123
  return;
126
124
  }
@@ -187,9 +185,13 @@ export const assignmentExpressionMethods: AssignmentExpressionMethods = {
187
185
 
188
186
  /** `{ a, b } = value` / `for ({ a } of xs)`: assign each property of `value`. */
189
187
  emitObjectAssignmentTarget(pattern: ObjectLiteralExpression, value: string): void {
188
+ const excludedKeys: string[] = [];
190
189
  for (const property of pattern.properties) {
191
190
  if (property.kind === SyntaxKind.SpreadElement) {
192
- this.unsupported(property, "object rest destructuring");
191
+ const rest = this.reg();
192
+ const keys = this.emitValueArguments(excludedKeys);
193
+ this.emit(` ${rest} = call i64 @xt_object_rest(i64 ${value}, i32 ${keys.argc}, i64* ${keys.ptr})`);
194
+ this.emitAssignmentTarget((property as SpreadElement).expression, rest);
193
195
  continue;
194
196
  }
195
197
  if (property.kind === SyntaxKind.PropertyAssignment) {
@@ -200,6 +202,7 @@ export const assignmentExpressionMethods: AssignmentExpressionMethods = {
200
202
  } else {
201
203
  key = this.stringValue(propertyNameText(assignment.name));
202
204
  }
205
+ excludedKeys.push(key);
203
206
  let target: Expression = assignment.initializer;
204
207
  let fallback: Expression | undefined;
205
208
  if (isPlainAssignment(target)) {
@@ -215,6 +218,7 @@ export const assignmentExpressionMethods: AssignmentExpressionMethods = {
215
218
  }
216
219
  const shorthand = property as ShorthandPropertyAssignment;
217
220
  const key = this.stringValue(shorthand.name.text);
221
+ excludedKeys.push(key);
218
222
  let elementValue = this.reg();
219
223
  this.emit(` ${elementValue} = call i64 @xt_get(i64 ${value}, i64 ${key})`);
220
224
  if (shorthand.initializer) elementValue = this.emitBindingDefault(elementValue, shorthand.initializer);