xbintsc 0.3.12 → 0.3.14

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 (76) hide show
  1. package/README.md +21 -3
  2. package/README.zh-CN.md +18 -3
  3. package/dist/src/codegen/generator/calls/access.js +13 -0
  4. package/dist/src/codegen/generator/calls/access.js.map +1 -1
  5. package/dist/src/codegen/generator/calls/invocation.js +8 -2
  6. package/dist/src/codegen/generator/calls/invocation.js.map +1 -1
  7. package/dist/src/codegen/generator/context.d.ts +23 -0
  8. package/dist/src/codegen/generator/context.js +91 -17
  9. package/dist/src/codegen/generator/context.js.map +1 -1
  10. package/dist/src/codegen/generator/expressions/primary.js +5 -0
  11. package/dist/src/codegen/generator/expressions/primary.js.map +1 -1
  12. package/dist/src/driver/compiler.d.ts +7 -0
  13. package/dist/src/driver/compiler.js +41 -5
  14. package/dist/src/driver/compiler.js.map +1 -1
  15. package/dist/src/driver/modules.d.ts +1 -1
  16. package/dist/src/driver/modules.js +235 -25
  17. package/dist/src/driver/modules.js.map +1 -1
  18. package/dist/src/extensions/node/fs/fs-ops.d.ts +5 -2
  19. package/dist/src/extensions/node/fs/fs-ops.js +45 -6
  20. package/dist/src/extensions/node/fs/fs-ops.js.map +1 -1
  21. package/dist/src/extensions/node/fs/index.d.ts +6 -2
  22. package/dist/src/extensions/node/fs/index.js +22 -3
  23. package/dist/src/extensions/node/fs/index.js.map +1 -1
  24. package/dist/src/extensions/node/fs-promises/index.d.ts +4 -3
  25. package/dist/src/extensions/node/fs-promises/index.js +31 -3
  26. package/dist/src/extensions/node/fs-promises/index.js.map +1 -1
  27. package/dist/tests/cli/main.test.js +17 -0
  28. package/dist/tests/cli/main.test.js.map +1 -1
  29. package/dist/tests/driver/compiler.test.js +93 -2
  30. package/dist/tests/driver/compiler.test.js.map +1 -1
  31. package/dist/tests/driver/modules.test.js +91 -4
  32. package/dist/tests/driver/modules.test.js.map +1 -1
  33. package/dist/tests/e2e/compile-node.test.js +240 -0
  34. package/dist/tests/e2e/compile-node.test.js.map +1 -1
  35. package/dist/tests/e2e/compile.test.js +12 -0
  36. package/dist/tests/e2e/compile.test.js.map +1 -1
  37. package/dist/tests/e2e/harness.js +11 -8
  38. package/dist/tests/e2e/harness.js.map +1 -1
  39. package/dist/tests/e2e/node-modules.test.js +1 -1
  40. package/dist/tests/e2e/node-modules.test.js.map +1 -1
  41. package/dist/tests/e2e/npm-modules.test.d.ts +9 -0
  42. package/dist/tests/e2e/npm-modules.test.js +55 -0
  43. package/dist/tests/e2e/npm-modules.test.js.map +1 -0
  44. package/dist/tests/e2e/runtime-coverage.test.d.ts +11 -0
  45. package/dist/tests/e2e/runtime-coverage.test.js +646 -0
  46. package/dist/tests/e2e/runtime-coverage.test.js.map +1 -0
  47. package/package.json +3 -2
  48. package/runtime/ext_node/buffer/parts/prototype.inc +7 -0
  49. package/runtime/ext_node/buffer/parts/search.inc +8 -4
  50. package/runtime/ext_node/child_process/child_process.c +5 -0
  51. package/runtime/ext_node/fs/constants.c +113 -0
  52. package/runtime/ext_node/fs/copy_ops.c +145 -0
  53. package/runtime/ext_node/fs/dir.c +101 -0
  54. package/runtime/ext_node/fs/fd_ops.c +405 -0
  55. package/runtime/ext_node/fs/fs_common.h +388 -5
  56. package/runtime/ext_node/fs/fs_ops.c +100 -139
  57. package/runtime/ext_node/fs/glob.c +232 -0
  58. package/runtime/ext_node/fs/link_ops.c +91 -0
  59. package/runtime/ext_node/fs/meta_ops.c +403 -0
  60. package/runtime/ext_node/fs/promises.c +275 -60
  61. package/runtime/ext_node/fs/read_file.c +8 -4
  62. package/runtime/ext_node/fs/watch.c +52 -0
  63. package/runtime/ext_node/fs/write_file.c +20 -11
  64. package/runtime/ext_node/stream/stream.c +19 -2
  65. package/runtime/rt.h +9 -0
  66. package/runtime/xt_io.c +21 -0
  67. package/scripts/coverage-runtime.ts +338 -0
  68. package/src/codegen/generator/calls/access.ts +15 -0
  69. package/src/codegen/generator/calls/invocation.ts +8 -2
  70. package/src/codegen/generator/context.ts +112 -21
  71. package/src/codegen/generator/expressions/primary.ts +5 -0
  72. package/src/driver/compiler.ts +42 -5
  73. package/src/driver/modules.ts +251 -26
  74. package/src/extensions/node/fs/fs-ops.ts +48 -6
  75. package/src/extensions/node/fs/index.ts +25 -3
  76. package/src/extensions/node/fs-promises/index.ts +33 -4
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Runtime (C) coverage for xbintsc.
4
+ *
5
+ * Vitest's V8 provider only sees the compiler's TypeScript sources
6
+ * (the `src/` tree, see vitest.config.ts). The C runtime under `runtime/` is
7
+ * invisible to it, so this script drives the *other* coverage system: clang's
8
+ * source-based instrumentation.
9
+ *
10
+ * The lever is `CCC_OVERRIDE_OPTIONS`. Exporting
11
+ * `+-fprofile-instr-generate +-fcoverage-mapping` makes clang append those
12
+ * flags to every compile and link it performs. The driver forwards the ambient
13
+ * environment to clang, so no xbintsc option or source change is needed to
14
+ * instrument the runtime. Instrumented programs then write `.profraw` files
15
+ * named by `LLVM_PROFILE_FILE`, which `llvm-profdata` and `llvm-cov` turn
16
+ * into a per-source report.
17
+ *
18
+ * Two things make this work against the test suite:
19
+ *
20
+ * - The e2e harness gives every suite its own temporary cache, so the
21
+ * instrumented runtime objects are always compiled fresh (no stale,
22
+ * un-instrumented object can be reused).
23
+ * - `llvm-cov report` needs a binary that *contains* the coverage mapping,
24
+ * but the harness deletes each compiled program when its suite ends. A
25
+ * single reference binary, built from the same runtime sources with the
26
+ * same clang and flags, carries the mapping for every runtime file, so the
27
+ * merged profile can be reported against it.
28
+ *
29
+ * Usage:
30
+ * npm run coverage:runtime instrument, run the tests, report
31
+ * npm run coverage:runtime -- --report-only report profiles from an earlier run
32
+ * npm run coverage:runtime -- tests/e2e forward args to `vitest run`
33
+ *
34
+ * Options:
35
+ * --report-only Skip the test run; merge whatever profiles already exist.
36
+ * --prof-dir <dir> Where compiled programs write .profraw (default build/runtime-prof).
37
+ * --out-dir <dir> Where the report is written (default coverage/runtime).
38
+ * --threshold <pct> Fail when total runtime line coverage is below <pct>.
39
+ * --html Also write an HTML report under <out-dir>/html.
40
+ * --keep-raw Keep the .profraw files after merging.
41
+ * --strict Fail (instead of skipping) when the LLVM tools are missing.
42
+ * -h, --help Show this help.
43
+ */
44
+
45
+ import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
46
+ import { spawnSync, type SpawnSyncOptions } from "node:child_process";
47
+ import { dirname, join, relative, resolve } from "node:path";
48
+ import { fileURLToPath } from "node:url";
49
+ import { build } from "../src/driver/compiler.js";
50
+ import { createDefaultRegistry } from "../src/extensions/registry.js";
51
+ import { nodeExtension } from "../src/extensions/node/index.js";
52
+ import { findRuntimeDir } from "../src/driver/paths.js";
53
+ import { resolveToolchain } from "../src/driver/toolchain-provider.js";
54
+
55
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
56
+
57
+ /** Flags clang appends to every compile/link in an instrumented build. */
58
+ const INSTRUMENT = "+-fprofile-instr-generate +-fcoverage-mapping";
59
+
60
+ interface Options {
61
+ reportOnly: boolean;
62
+ profDir: string;
63
+ outDir: string;
64
+ threshold?: number;
65
+ html: boolean;
66
+ keepRaw: boolean;
67
+ strict: boolean;
68
+ testArgs: string[];
69
+ }
70
+
71
+ interface CommandResult {
72
+ status: number;
73
+ stdout: string;
74
+ stderr: string;
75
+ }
76
+
77
+ function fail(message: string): never {
78
+ console.error(`coverage-runtime: ${message}`);
79
+ process.exit(1);
80
+ }
81
+
82
+ function skip(message: string, strict: boolean): never {
83
+ if (strict) fail(message);
84
+ console.warn(`coverage-runtime: skipping - ${message}`);
85
+ process.exit(0);
86
+ }
87
+
88
+ function usage(): void {
89
+ process.stdout.write(
90
+ [
91
+ "Usage: npm run coverage:runtime [-- options] [-- vitest args]",
92
+ "",
93
+ " --report-only merge profiles from an earlier instrumented run",
94
+ " --prof-dir <dir> .profraw output directory (default build/runtime-prof)",
95
+ " --out-dir <dir> report directory (default coverage/runtime)",
96
+ " --threshold <pct> fail below this total line coverage",
97
+ " --html also write an HTML report",
98
+ " --keep-raw keep .profraw files after merging",
99
+ " --strict fail when the LLVM tools are missing",
100
+ "",
101
+ ].join("\n"),
102
+ );
103
+ }
104
+
105
+ function parseArgs(argv: readonly string[]): Options {
106
+ const options: Options = {
107
+ reportOnly: false,
108
+ profDir: join(root, "build", "runtime-prof"),
109
+ outDir: join(root, "coverage", "runtime"),
110
+ html: false,
111
+ keepRaw: false,
112
+ strict: false,
113
+ testArgs: [],
114
+ };
115
+ for (let index = 0; index < argv.length; index += 1) {
116
+ const arg = argv[index]!;
117
+ if (arg === "--report-only") options.reportOnly = true;
118
+ else if (arg === "--html") options.html = true;
119
+ else if (arg === "--keep-raw") options.keepRaw = true;
120
+ else if (arg === "--strict") options.strict = true;
121
+ else if (arg === "--prof-dir") options.profDir = resolve(requireValue(argv, (index += 1), arg));
122
+ else if (arg === "--out-dir") options.outDir = resolve(requireValue(argv, (index += 1), arg));
123
+ else if (arg === "--threshold") {
124
+ const value = requireValue(argv, (index += 1), arg);
125
+ const parsed = Number(value);
126
+ if (!Number.isFinite(parsed)) fail(`--threshold expects a number, got '${value}'`);
127
+ options.threshold = parsed;
128
+ } else if (arg === "-h" || arg === "--help") {
129
+ usage();
130
+ process.exit(0);
131
+ } else if (arg.startsWith("--")) {
132
+ fail(`unknown option '${arg}' (see --help)`);
133
+ } else {
134
+ options.testArgs.push(arg);
135
+ }
136
+ }
137
+ return options;
138
+ }
139
+
140
+ function requireValue(argv: readonly string[], index: number, flag: string): string {
141
+ const value = argv[index];
142
+ if (value === undefined) fail(`${flag} needs a value`);
143
+ return value;
144
+ }
145
+
146
+ function run(command: string, args: readonly string[], options: SpawnSyncOptions = {}): CommandResult {
147
+ const result = spawnSync(command, [...args], { encoding: "utf8", maxBuffer: 512 * 1024 * 1024, ...options });
148
+ return {
149
+ status: result.status ?? (result.error ? 1 : 0),
150
+ stdout: result.stdout ?? "",
151
+ stderr: result.stderr ?? (result.error ? String(result.error.message) : ""),
152
+ };
153
+ }
154
+
155
+ /** True when `command` resolves and answers `--version` successfully. */
156
+ function runs(command: string): boolean {
157
+ const result = spawnSync(command, ["--version"], { encoding: "utf8" });
158
+ return !result.error && result.status === 0;
159
+ }
160
+
161
+ /**
162
+ * Locate the LLVM tool `name` (`llvm-profdata`/`llvm-cov`) that matches the
163
+ * clang the driver resolved. The bundled toolchains ship the tools next to
164
+ * clang and cover the versioned system installs, so search there first: mixing
165
+ * versions makes `llvm-profdata` reject the profile format.
166
+ */
167
+ function findTool(clang: string, name: string): string | undefined {
168
+ const exe = process.platform === "win32" ? ".exe" : "";
169
+ const candidates: string[] = [];
170
+ const directory = dirname(clang);
171
+ if (directory !== "." && directory !== "") candidates.push(join(directory, name + exe));
172
+ const version = run(clang, ["--version"]);
173
+ const major = /version (\d+)/.exec(`${version.stdout}${version.stderr}`)?.[1];
174
+ if (major) candidates.push(`${name}-${major}`, `${name}-${major}${exe}`);
175
+ candidates.push(name + exe, name);
176
+ for (const version of ["18", "19", "20", "21", "22", "23"]) {
177
+ candidates.push(`${name}-${version}`, `${name}-${version}${exe}`);
178
+ }
179
+ for (const candidate of candidates) {
180
+ if (runs(candidate)) return candidate;
181
+ }
182
+ if (process.platform === "darwin") {
183
+ // The Command Line Tools expose the tools through `xcrun` rather than PATH.
184
+ const found = spawnSync("xcrun", ["--find", name], { encoding: "utf8" });
185
+ const path = (found.stdout ?? "").trim();
186
+ if (!found.error && found.status === 0 && path.length > 0 && runs(path)) return path;
187
+ }
188
+ return undefined;
189
+ }
190
+
191
+ function collectProfiles(directory: string): string[] {
192
+ if (!existsSync(directory)) return [];
193
+ return readdirSync(directory)
194
+ .filter((name) => name.endsWith(".profraw"))
195
+ .sort()
196
+ .map((name) => join(directory, name));
197
+ }
198
+
199
+ /** Merge profiles, chunking the input so a huge list cannot overflow argv. */
200
+ function mergeProfiles(tool: string, inputs: readonly string[], output: string, scratch: string): void {
201
+ const chunkSize = 128;
202
+ if (inputs.length <= chunkSize) {
203
+ const result = run(tool, ["merge", "-sparse", ...inputs, "-o", output]);
204
+ if (result.status !== 0) fail(`llvm-profdata merge failed:\n${result.stderr}`);
205
+ return;
206
+ }
207
+ const parts: string[] = [];
208
+ for (let index = 0; index < inputs.length; index += chunkSize) {
209
+ const part = join(scratch, `part-${parts.length}.profdata`);
210
+ const result = run(tool, ["merge", "-sparse", ...inputs.slice(index, index + chunkSize), "-o", part]);
211
+ if (result.status !== 0) fail(`llvm-profdata merge failed:\n${result.stderr}`);
212
+ parts.push(part);
213
+ }
214
+ const result = run(tool, ["merge", "-sparse", ...parts, "-o", output]);
215
+ if (result.status !== 0) fail(`llvm-profdata merge failed:\n${result.stderr}`);
216
+ for (const part of parts) rmSync(part, { force: true });
217
+ }
218
+
219
+ /**
220
+ * Build the binary whose embedded coverage mapping the merged profile is
221
+ * reported against. It must be instrumented with the same clang and flags as
222
+ * the test-produced binaries, and it carries the Node extension so that
223
+ * extension's C sources are covered too.
224
+ */
225
+ function buildReference(directory: string): string {
226
+ mkdirSync(directory, { recursive: true });
227
+ const entry = join(directory, "reference.ts");
228
+ writeFileSync(entry, "console.log(1);\n");
229
+ const registry = createDefaultRegistry().register(nodeExtension);
230
+ const result = build(entry, {
231
+ emit: "exe",
232
+ outDir: directory,
233
+ cacheDir: join(directory, "cache"),
234
+ extensions: registry,
235
+ preferPrebuilt: false,
236
+ force: true,
237
+ });
238
+ const errors = result.diagnostics.filter((diagnostic) => diagnostic.category === "error");
239
+ if (errors.length > 0) fail(`reference build failed:\n${errors.map((e) => e.message).join("\n")}`);
240
+ return result.outputPath;
241
+ }
242
+
243
+ function runTests(options: Options): void {
244
+ const vitest = join(root, "node_modules", "vitest", "vitest.mjs");
245
+ if (!existsSync(vitest)) fail("vitest is not installed; run 'npm install' first");
246
+ rmSync(options.profDir, { recursive: true, force: true });
247
+ mkdirSync(options.profDir, { recursive: true });
248
+ console.log("coverage-runtime: running the test suite with instrumented clang");
249
+ const result = spawnSync(process.execPath, [vitest, "run", ...options.testArgs], {
250
+ stdio: "inherit",
251
+ cwd: root,
252
+ env: {
253
+ ...process.env,
254
+ LLVM_PROFILE_FILE: join(options.profDir, "%p-%m.profraw"),
255
+ xbintsc_CACHE_DIR: join(root, "build", "runtime-cov-cache"),
256
+ // A prebuilt runtime/lib archive is not instrumented, so force the driver
257
+ // to compile (and thus instrument) the C sources instead.
258
+ xbintsc_PREFER_PREBUILT: "0",
259
+ },
260
+ });
261
+ if (result.status !== 0) fail(`tests failed (exit ${result.status ?? "unknown"})`);
262
+ }
263
+
264
+ function main(): void {
265
+ const options = parseArgs(process.argv.slice(2));
266
+
267
+ // Set once, before anything shells out to clang, so both the test run and the
268
+ // reference build are instrumented even in --report-only mode.
269
+ const existing = process.env.CCC_OVERRIDE_OPTIONS;
270
+ process.env.CCC_OVERRIDE_OPTIONS = existing ? `${existing} ${INSTRUMENT}` : INSTRUMENT;
271
+
272
+ let clang: string;
273
+ try {
274
+ clang = resolveToolchain().clang;
275
+ } catch (error) {
276
+ skip(`no clang-compatible compiler available (${String(error)})`, options.strict);
277
+ }
278
+ const profdataTool = findTool(clang, "llvm-profdata");
279
+ const covTool = findTool(clang, "llvm-cov");
280
+ if (!profdataTool || !covTool) {
281
+ const missing = [
282
+ profdataTool ? undefined : "llvm-profdata",
283
+ covTool ? undefined : "llvm-cov",
284
+ ].filter(Boolean).join(", ");
285
+ skip(`${missing} not found for ${clang}; install LLVM or use the bundled toolchain`, options.strict);
286
+ }
287
+
288
+ if (!options.reportOnly) runTests(options);
289
+
290
+ const profiles = collectProfiles(options.profDir);
291
+ if (profiles.length === 0) {
292
+ skip(`no .profraw files in ${relative(root, options.profDir)} (did the tests run?)`, options.strict);
293
+ }
294
+ console.log(`coverage-runtime: merging ${profiles.length} profile(s)`);
295
+
296
+ mkdirSync(options.outDir, { recursive: true });
297
+ const referenceDir = join(root, "build", "runtime-coverage");
298
+ const reference = buildReference(referenceDir);
299
+ const profdata = join(options.outDir, "runtime.profdata");
300
+ mergeProfiles(profdataTool!, profiles, profdata, options.outDir);
301
+
302
+ const runtimeDir = findRuntimeDir();
303
+ const report = run(covTool!, ["report", reference, `--instr-profile=${profdata}`, runtimeDir]);
304
+ if (report.status !== 0) fail(`llvm-cov report failed:\n${report.stderr}`);
305
+ writeFileSync(join(options.outDir, "report.txt"), report.stdout);
306
+ process.stdout.write(report.stdout);
307
+
308
+ const summary = run(covTool!, ["export", reference, `--instr-profile=${profdata}`, "--summary-only"]);
309
+ if (summary.status !== 0) fail(`llvm-cov export failed:\n${summary.stderr}`);
310
+ const parsed = JSON.parse(summary.stdout) as {
311
+ data?: { totals?: { lines?: { percent?: number } } }[];
312
+ };
313
+ const percent = parsed.data?.[0]?.totals?.lines?.percent ?? 0;
314
+ console.log(`coverage-runtime: runtime line coverage ${percent.toFixed(2)}%`);
315
+ console.log(`coverage-runtime: report -> ${relative(root, join(options.outDir, "report.txt"))}`);
316
+
317
+ if (options.html) {
318
+ const htmlDir = join(options.outDir, "html");
319
+ rmSync(htmlDir, { recursive: true, force: true });
320
+ const shown = run(covTool!, [
321
+ "show",
322
+ reference,
323
+ `--instr-profile=${profdata}`,
324
+ "--format=html",
325
+ `--output-dir=${htmlDir}`,
326
+ runtimeDir,
327
+ ]);
328
+ if (shown.status !== 0) fail(`llvm-cov show failed:\n${shown.stderr}`);
329
+ console.log(`coverage-runtime: HTML report -> ${relative(root, htmlDir)}`);
330
+ }
331
+
332
+ if (!options.keepRaw) rmSync(options.profDir, { recursive: true, force: true });
333
+ if (options.threshold !== undefined && percent < options.threshold) {
334
+ fail(`runtime line coverage ${percent.toFixed(2)}% is below the ${options.threshold}% threshold`);
335
+ }
336
+ }
337
+
338
+ main();
@@ -106,6 +106,21 @@ export const accessCallMethods: AccessCallMethods = {
106
106
  }
107
107
  }
108
108
  }
109
+ // Modules with no namespace dispatcher (`fs`, ...) can still expose value
110
+ // getters (`fs.constants`, `fs.promises`). Lower `fs.constants` the same
111
+ // way a named `import { constants } from "fs"` is lowered.
112
+ if (node.expression.kind === SyntaxKind.Identifier) {
113
+ const moduleExports = this.moduleExportsOfSymbol(
114
+ this.binding.symbolOfIdentifier.get(node.expression as Identifier),
115
+ );
116
+ const exported = moduleExports?.[node.name.text];
117
+ if (exported?.valueSymbol) {
118
+ this.extraDeclarations.add(`declare i64 @${exported.valueSymbol}(i32, i64*)`);
119
+ const result = this.reg();
120
+ this.emit(` ${result} = call i64 @${exported.valueSymbol}(i32 0, i64* null)`);
121
+ return result;
122
+ }
123
+ }
109
124
  const object = this.emitExpression(node.expression);
110
125
  return this.emitPropertyGet(object, node.name.text);
111
126
  },
@@ -91,8 +91,14 @@ export const invocationCallMethods: InvocationCallMethods = {
91
91
  return result;
92
92
  }
93
93
  if (callee.kind === SyntaxKind.Identifier) {
94
- if ((callee as Identifier).text === "super") return this.emitSuperConstructor(node);
95
- const symbol = this.binding.symbolOfIdentifier.get(callee as Identifier);
94
+ const identifier = callee as Identifier;
95
+ if (identifier.text === "super") return this.emitSuperConstructor(node);
96
+ const symbol = this.binding.symbolOfIdentifier.get(identifier);
97
+ // `require("x")` is CommonJS; the ESM form is the only supported import.
98
+ if (!symbol && identifier.text === "require") {
99
+ this.reportRequireUse(node);
100
+ return i64(XT_UNDEFINED);
101
+ }
96
102
  if (symbol && symbol.kind === SymbolKind.Import) {
97
103
  const exported = this.importExports.get(symbol.id);
98
104
  if (exported) return this.emitModuleExport(node, exported);
@@ -78,7 +78,6 @@ export class GeneratorContext {
78
78
  * the namespace dispatch tables.
79
79
  */
80
80
  private resolveImports(): void {
81
- if (Object.keys(this.modules).length === 0 && Object.keys(this.moduleHints).length === 0) return;
82
81
  for (const statement of this.sourceFile.statements) {
83
82
  if (statement.kind !== SyntaxKind.ImportDeclaration) continue;
84
83
  const declaration = statement as ImportDeclaration;
@@ -86,18 +85,10 @@ export class GeneratorContext {
86
85
  // Type-only imports are erased at runtime, so a missing host module is
87
86
  // not an error for them.
88
87
  if (!clause || clause.isTypeOnly) continue;
89
- const module = this.modules[declaration.moduleSpecifier.value];
88
+ const specifier = declaration.moduleSpecifier.value;
89
+ const module = this.modules[specifier];
90
90
  if (!module) {
91
- const provider = this.moduleHints[declaration.moduleSpecifier.value];
92
- if (provider) {
93
- this.diagnostics.error(
94
- DiagnosticCode.ModuleNotFound,
95
- `module '${declaration.moduleSpecifier.value}' is provided by the '${provider}' extension; pass --ext ${provider}`,
96
- declaration.moduleSpecifier,
97
- this.sourceFile.fileName,
98
- );
99
- this.markMissingModuleSymbols(declaration);
100
- }
91
+ this.reportMissingModule(declaration, specifier);
101
92
  continue;
102
93
  }
103
94
  if (clause.name) {
@@ -110,12 +101,13 @@ export class GeneratorContext {
110
101
  }
111
102
  const bindings = clause.namedBindings;
112
103
  if (bindings && bindings.kind === SyntaxKind.NamedImports) {
113
- for (const specifier of bindings.elements) {
114
- if (specifier.isTypeOnly) continue;
115
- const importedName = specifier.propertyName?.text ?? specifier.name.text;
104
+ for (const specifierNode of bindings.elements) {
105
+ if (specifierNode.isTypeOnly) continue;
106
+ const importedName = specifierNode.propertyName?.text ?? specifierNode.name.text;
116
107
  const exported = module.exports?.[importedName];
117
- const symbol = this.binding.symbolOfDeclaration.get(specifier.name);
118
- if (symbol && exported) {
108
+ const symbol = this.binding.symbolOfDeclaration.get(specifierNode.name);
109
+ if (!symbol) continue;
110
+ if (exported) {
119
111
  this.importExports.set(symbol.id, exported);
120
112
  // A named constructor (`import { Buffer } from "buffer"`) also
121
113
  // inherits its module's static dispatcher, so `Buffer.from(...)`
@@ -123,6 +115,14 @@ export class GeneratorContext {
123
115
  if (exported.isConstructor && module.namespace) {
124
116
  this.importNamespaces.set(symbol.id, module.namespace);
125
117
  }
118
+ } else if (this.usedAsValue(symbol)) {
119
+ this.diagnostics.error(
120
+ DiagnosticCode.ModuleNotFound,
121
+ `Module '"${specifier}"' has no exported member '${importedName}'`,
122
+ specifierNode.name,
123
+ this.sourceFile.fileName,
124
+ );
125
+ this.missingModuleSymbols.add(symbol.id);
126
126
  }
127
127
  }
128
128
  } else if (bindings && bindings.kind === SyntaxKind.NamespaceImport) {
@@ -132,6 +132,52 @@ export class GeneratorContext {
132
132
  }
133
133
  }
134
134
 
135
+ /**
136
+ * Diagnose an import of a module the registry cannot provide.
137
+ *
138
+ * A *hinted* module belongs to an extension the caller knows about but did
139
+ * not enable, so point at the flag that enables it. Anything else is a bare
140
+ * specifier xbintsc cannot link - almost always a third-party package from
141
+ * `node_modules` - and must be reported at the import site rather than
142
+ * degrading into a downstream "cannot be used as a value" error.
143
+ */
144
+ private reportMissingModule(declaration: ImportDeclaration, specifier: string): void {
145
+ const provider = this.moduleHints[specifier];
146
+ if (provider) {
147
+ this.diagnostics.error(
148
+ DiagnosticCode.ModuleNotFound,
149
+ `module '${specifier}' is provided by the '${provider}' extension; pass --ext ${provider}`,
150
+ declaration.moduleSpecifier,
151
+ this.sourceFile.fileName,
152
+ );
153
+ this.markMissingModuleSymbols(declaration);
154
+ return;
155
+ }
156
+ // A path that is not a bare specifier is a bundling concern; the driver
157
+ // already reports unresolved relative imports, so stay out of the way.
158
+ if (!isBareSpecifier(specifier)) return;
159
+ // Only report when a binding is actually read: a type-only use is erased
160
+ // by the binder and must keep compiling.
161
+ const symbols = this.importBindingSymbols(declaration);
162
+ if (!symbols.some((symbol) => this.usedAsValue(symbol))) return;
163
+ this.diagnostics.error(
164
+ DiagnosticCode.ModuleNotFound,
165
+ `module '${specifier}' is not supported: xbintsc can import built-in platform modules, ` +
166
+ "relative '.ts' files and ESM packages under node_modules; CommonJS packages are not supported",
167
+ declaration.moduleSpecifier,
168
+ this.sourceFile.fileName,
169
+ );
170
+ for (const symbol of symbols) this.missingModuleSymbols.add(symbol.id);
171
+ }
172
+
173
+ /**
174
+ * True when a binding is read as a runtime value. Type positions are not
175
+ * bound (the binder skips them), so a symbol with no references is erased.
176
+ */
177
+ private usedAsValue(symbol: SymbolInfo): boolean {
178
+ return symbol.references.length > 0;
179
+ }
180
+
135
181
  /**
136
182
  * Bind a default/namespace import to either the module's runtime namespace
137
183
  * dispatcher (when it has one) or its named exports (when it does not).
@@ -144,10 +190,10 @@ export class GeneratorContext {
144
190
  }
145
191
  }
146
192
 
147
- /** Record every symbol introduced by an import of a missing known module. */
148
- private markMissingModuleSymbols(declaration: ImportDeclaration): void {
193
+ /** Every binding an import declaration introduces (including type-only ones). */
194
+ private importBindingSymbols(declaration: ImportDeclaration): SymbolInfo[] {
149
195
  const clause = declaration.importClause;
150
- if (!clause) return;
196
+ if (!clause) return [];
151
197
  const names: Identifier[] = [];
152
198
  if (clause.name) names.push(clause.name);
153
199
  const bindings = clause.namedBindings;
@@ -156,10 +202,17 @@ export class GeneratorContext {
156
202
  } else if (bindings && bindings.kind === SyntaxKind.NamespaceImport) {
157
203
  names.push(bindings.name);
158
204
  }
205
+ const symbols: SymbolInfo[] = [];
159
206
  for (const name of names) {
160
207
  const symbol = this.binding.symbolOfDeclaration.get(name);
161
- if (symbol) this.missingModuleSymbols.add(symbol.id);
208
+ if (symbol) symbols.push(symbol);
162
209
  }
210
+ return symbols;
211
+ }
212
+
213
+ /** Record every symbol introduced by an import of a missing known module. */
214
+ private markMissingModuleSymbols(declaration: ImportDeclaration): void {
215
+ for (const symbol of this.importBindingSymbols(declaration)) this.missingModuleSymbols.add(symbol.id);
163
216
  }
164
217
 
165
218
  /** Namespace name an imported alias refers to, if any. */
@@ -361,4 +414,42 @@ export class GeneratorContext {
361
414
  this.sourceFile.fileName,
362
415
  );
363
416
  }
417
+
418
+ /**
419
+ * Report a CommonJS `require(...)` use. xbintsc has no CommonJS module
420
+ * runtime, so point the user at the ESM `import` form (with the requested
421
+ * module in the hint when it is a string literal).
422
+ */
423
+ reportRequireUse(node: Node): void {
424
+ const specifier = requireSpecifier(node);
425
+ const replacement = specifier
426
+ ? `use an ESM import instead, e.g. \`import value from ${JSON.stringify(specifier)}\``
427
+ : "use an ESM `import` statement instead";
428
+ this.diagnostics.error(
429
+ DiagnosticCode.UnsupportedFeature,
430
+ `CommonJS \`require()\` is not supported; ${replacement}`,
431
+ node,
432
+ this.sourceFile.fileName,
433
+ );
434
+ }
435
+ }
436
+
437
+ /** The string literal passed to `require("...")`, when there is one. */
438
+ function requireSpecifier(node: Node): string | undefined {
439
+ if (node.kind !== SyntaxKind.CallExpression) return undefined;
440
+ const first = (node as { arguments?: readonly Node[] }).arguments?.[0];
441
+ if (first && first.kind === SyntaxKind.StringLiteral) {
442
+ return (first as unknown as { value: string }).value;
443
+ }
444
+ return undefined;
445
+ }
446
+
447
+ /** A bare module specifier (`fs`, `node:fs`, `@scope/pkg`), not a file path. */
448
+ function isBareSpecifier(specifier: string): boolean {
449
+ return (
450
+ !specifier.startsWith(".") &&
451
+ !specifier.startsWith("/") &&
452
+ !specifier.startsWith("\\") &&
453
+ !/^[A-Za-z]:[\\/]/.test(specifier)
454
+ );
364
455
  }
@@ -235,6 +235,11 @@ export const primaryExpressionMethods: PrimaryExpressionMethods = {
235
235
  const nameValue = this.stringValue(identifier.text);
236
236
  return this.runtimeCall("xt_error_constructor", [nameValue]);
237
237
  }
238
+ // A bare `require` reference is CommonJS; steer the user to `import`.
239
+ if (identifier.text === "require") {
240
+ this.reportRequireUse(identifier);
241
+ return i64(XT_UNDEFINED);
242
+ }
238
243
  this.diagnostics.error(
239
244
  DiagnosticCode.CannotFindName,
240
245
  `Cannot find name '${identifier.text}'`,
@@ -77,6 +77,19 @@ export function compileString(source: string, fileName = "input.ts", extensions?
77
77
  return { ir, diagnostics: diagnostics.diagnostics };
78
78
  }
79
79
 
80
+ /**
81
+ * Every module specifier the registry can resolve at code generation time (the
82
+ * registered platform modules plus the hints for known-but-disabled
83
+ * extensions). Bare imports outside this set are looked up in `node_modules`
84
+ * and bundled as source.
85
+ */
86
+ function externalModuleSpecifiers(registry: ExtensionRegistry): Set<string> {
87
+ return new Set([
88
+ ...Object.keys(registry.modules()),
89
+ ...Object.keys(registry.moduleHints()),
90
+ ]);
91
+ }
92
+
80
93
  /**
81
94
  * Compile a file to LLVM IR, bundling every reachable relative module first so
82
95
  * that imports from other TypeScript sources resolve exactly like `build`.
@@ -88,6 +101,7 @@ export function compileEntry(entryPath: string, extensions?: ExtensionRegistry):
88
101
  const diagnostics = new DiagnosticBag();
89
102
  const parser = new Parser(file, diagnostics);
90
103
  let sourceFile = parser.parseSourceFile();
104
+ const registry = extensions ?? createDefaultRegistry();
91
105
  const isModule = sourceFile.statements.some(
92
106
  (statement) =>
93
107
  statement.kind === SyntaxKind.ImportDeclaration ||
@@ -95,10 +109,9 @@ export function compileEntry(entryPath: string, extensions?: ExtensionRegistry):
95
109
  statement.kind === SyntaxKind.ExportAssignment,
96
110
  );
97
111
  if (isModule) {
98
- const bundled = bundleModules(absoluteEntry, diagnostics);
112
+ const bundled = bundleModules(absoluteEntry, diagnostics, externalModuleSpecifiers(registry));
99
113
  if (bundled) sourceFile = bundled.sourceFile;
100
114
  }
101
- const registry = extensions ?? createDefaultRegistry();
102
115
  const { ir } = generate(sourceFile, diagnostics, {
103
116
  builtins: registry.builtins(),
104
117
  modules: registry.modules(),
@@ -107,6 +120,30 @@ export function compileEntry(entryPath: string, extensions?: ExtensionRegistry):
107
120
  return { ir, diagnostics: diagnostics.diagnostics };
108
121
  }
109
122
 
123
+ /**
124
+ * Where the incremental cache lives when the caller does not name a directory:
125
+ * `xbintsc_CACHE_DIR` when set, otherwise `<cwd>/.xbintsc`. The override keeps
126
+ * an instrumented build (see `scripts/coverage-runtime.ts`) from reusing the
127
+ * un-instrumented objects in a normal cache, which would silently yield no
128
+ * profile data.
129
+ */
130
+ function defaultCacheDir(): string {
131
+ return process.env.xbintsc_CACHE_DIR || join(process.cwd(), ".xbintsc");
132
+ }
133
+
134
+ /**
135
+ * Whether a build may link a prebuilt `runtime/lib` archive. The explicit option
136
+ * wins; otherwise `xbintsc_PREFER_PREBUILT` decides (`0`/`false` disables) and
137
+ * the default is to use an archive when one exists. Instrumented builds set it
138
+ * to `0` so the C runtime is compiled (and therefore instrumented) from source.
139
+ */
140
+ export function resolvePreferPrebuilt(explicit: boolean | undefined): boolean {
141
+ if (explicit !== undefined) return explicit;
142
+ const raw = process.env.xbintsc_PREFER_PREBUILT;
143
+ if (raw === undefined || raw === "") return true;
144
+ return raw !== "0" && raw.toLowerCase() !== "false";
145
+ }
146
+
110
147
  function executableName(entry: string, outDir: string, explicit: string | undefined): string {
111
148
  if (explicit) return resolve(explicit);
112
149
  const base = basename(entry, extname(entry));
@@ -118,7 +155,7 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
118
155
  const runner = options.runner ?? realRunner;
119
156
  const registry = options.extensions ?? createDefaultRegistry();
120
157
  const outDir = resolve(options.outDir ?? join(process.cwd(), "build"));
121
- const cacheDir = resolve(options.cacheDir ?? join(process.cwd(), ".xbintsc"));
158
+ const cacheDir = resolve(options.cacheDir ?? defaultCacheDir());
122
159
  const emit: EmitKind = options.emit ?? "exe";
123
160
  const optimize = options.optimize ?? "2";
124
161
 
@@ -148,7 +185,7 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
148
185
  );
149
186
  let cacheText = sourceText;
150
187
  if (isModule) {
151
- const bundled = bundleModules(absoluteEntry, diagnostics);
188
+ const bundled = bundleModules(absoluteEntry, diagnostics, externalModuleSpecifiers(registry));
152
189
  if (bundled) {
153
190
  sourceFile = bundled.sourceFile;
154
191
  cacheText = bundled.text;
@@ -218,7 +255,7 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
218
255
  runtimeDir,
219
256
  cacheDir,
220
257
  registry,
221
- options.preferPrebuilt ?? true,
258
+ resolvePreferPrebuilt(options.preferPrebuilt),
222
259
  toolchain.env,
223
260
  );
224
261