xbintsc 0.3.11 → 0.3.13
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.
- package/README.md +21 -3
- package/README.zh-CN.md +18 -3
- package/dist/src/cli/main.js +8 -0
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/codegen/generator/context.d.ts +27 -0
- package/dist/src/codegen/generator/context.js +103 -10
- package/dist/src/codegen/generator/context.js.map +1 -1
- package/dist/src/codegen/generator/expressions/primary.js +4 -0
- package/dist/src/codegen/generator/expressions/primary.js.map +1 -1
- package/dist/src/codegen/generator/state.d.ts +7 -0
- package/dist/src/codegen/generator/state.js.map +1 -1
- package/dist/src/driver/compiler.d.ts +7 -0
- package/dist/src/driver/compiler.js +29 -2
- package/dist/src/driver/compiler.js.map +1 -1
- package/dist/src/extensions/catalog.d.ts +14 -0
- package/dist/src/extensions/catalog.js +17 -0
- package/dist/src/extensions/catalog.js.map +1 -0
- package/dist/src/extensions/registry.d.ts +15 -0
- package/dist/src/extensions/registry.js +27 -0
- package/dist/src/extensions/registry.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/tests/cli/main.test.js +24 -0
- package/dist/tests/cli/main.test.js.map +1 -1
- package/dist/tests/driver/compiler.test.js +81 -1
- package/dist/tests/driver/compiler.test.js.map +1 -1
- package/dist/tests/extensions/registry.test.js +11 -0
- package/dist/tests/extensions/registry.test.js.map +1 -1
- package/package.json +3 -2
- package/scripts/coverage-runtime.ts +338 -0
- package/src/cli/main.ts +7 -0
- package/src/codegen/generator/context.ts +113 -8
- package/src/codegen/generator/expressions/primary.ts +3 -0
- package/src/codegen/generator/state.ts +7 -0
- package/src/driver/compiler.ts +29 -2
- package/src/extensions/catalog.ts +19 -0
- package/src/extensions/registry.ts +28 -0
- package/src/index.ts +1 -0
|
@@ -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();
|
package/src/cli/main.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { runtimeLibDir } from "../driver/runtime-lib.js";
|
|
|
23
23
|
import { resolveToolchain } from "../driver/toolchain-provider.js";
|
|
24
24
|
import { realRunner } from "../driver/toolchain.js";
|
|
25
25
|
import { createDefaultRegistry, type ExtensionRegistry } from "../extensions/registry.js";
|
|
26
|
+
import { bundledExtensions } from "../extensions/catalog.js";
|
|
26
27
|
import { nodeExtension } from "../extensions/node/index.js";
|
|
27
28
|
import { nativeExtensionFromManifest } from "../extensions/native.js";
|
|
28
29
|
|
|
@@ -112,6 +113,12 @@ function buildRegistry(flags: Map<string, string | boolean>): ExtensionRegistry
|
|
|
112
113
|
registry.register(nativeExtensionFromManifest(manifest));
|
|
113
114
|
}
|
|
114
115
|
}
|
|
116
|
+
// Hint the extensions that ship with xbintsc but were not enabled, so a
|
|
117
|
+
// missing `import` of a known module points at the flag that enables it
|
|
118
|
+
// (`pass --ext node`) instead of failing later with a confusing error.
|
|
119
|
+
for (const extension of bundledExtensions()) {
|
|
120
|
+
if (!registry.has(extension.name)) registry.hintExtension(extension);
|
|
121
|
+
}
|
|
115
122
|
return registry;
|
|
116
123
|
}
|
|
117
124
|
|
|
@@ -26,6 +26,14 @@ export class GeneratorContext {
|
|
|
26
26
|
readonly diagnostics: DiagnosticBag;
|
|
27
27
|
readonly builtins: Readonly<Record<string, BuiltinFunction>>;
|
|
28
28
|
readonly modules: Readonly<Record<string, ExtensionModule>>;
|
|
29
|
+
/** Module specifier -> extension name for known but unregistered extensions. */
|
|
30
|
+
readonly moduleHints: Readonly<Record<string, string>>;
|
|
31
|
+
/**
|
|
32
|
+
* Imported symbols whose providing module was already reported as missing
|
|
33
|
+
* (hint emitted). Referencing them is skipped so the actionable hint is the
|
|
34
|
+
* only diagnostic, instead of a second "cannot be used as a value" error.
|
|
35
|
+
*/
|
|
36
|
+
readonly missingModuleSymbols = new Set<number>();
|
|
29
37
|
/** Host the emitted IR targets (the process by default; see `CodegenOptions`). */
|
|
30
38
|
readonly target: { readonly platform: string; readonly arch: string };
|
|
31
39
|
/** Imported symbol id -> the module binding it refers to. */
|
|
@@ -59,6 +67,7 @@ export class GeneratorContext {
|
|
|
59
67
|
this.binding = bind(sourceFile);
|
|
60
68
|
this.builtins = options.builtins ?? {};
|
|
61
69
|
this.modules = options.modules ?? {};
|
|
70
|
+
this.moduleHints = options.moduleHints ?? {};
|
|
62
71
|
this.target = options.target ?? { platform: process.platform, arch: process.arch };
|
|
63
72
|
this.resolveImports();
|
|
64
73
|
}
|
|
@@ -69,14 +78,19 @@ export class GeneratorContext {
|
|
|
69
78
|
* the namespace dispatch tables.
|
|
70
79
|
*/
|
|
71
80
|
private resolveImports(): void {
|
|
72
|
-
if (Object.keys(this.modules).length === 0) return;
|
|
73
81
|
for (const statement of this.sourceFile.statements) {
|
|
74
82
|
if (statement.kind !== SyntaxKind.ImportDeclaration) continue;
|
|
75
83
|
const declaration = statement as ImportDeclaration;
|
|
76
|
-
const module = this.modules[declaration.moduleSpecifier.value];
|
|
77
|
-
if (!module) continue;
|
|
78
84
|
const clause = declaration.importClause;
|
|
79
|
-
|
|
85
|
+
// Type-only imports are erased at runtime, so a missing host module is
|
|
86
|
+
// not an error for them.
|
|
87
|
+
if (!clause || clause.isTypeOnly) continue;
|
|
88
|
+
const specifier = declaration.moduleSpecifier.value;
|
|
89
|
+
const module = this.modules[specifier];
|
|
90
|
+
if (!module) {
|
|
91
|
+
this.reportMissingModule(declaration, specifier);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
80
94
|
if (clause.name) {
|
|
81
95
|
const symbol = this.binding.symbolOfDeclaration.get(clause.name);
|
|
82
96
|
if (symbol) {
|
|
@@ -87,11 +101,13 @@ export class GeneratorContext {
|
|
|
87
101
|
}
|
|
88
102
|
const bindings = clause.namedBindings;
|
|
89
103
|
if (bindings && bindings.kind === SyntaxKind.NamedImports) {
|
|
90
|
-
for (const
|
|
91
|
-
|
|
104
|
+
for (const specifierNode of bindings.elements) {
|
|
105
|
+
if (specifierNode.isTypeOnly) continue;
|
|
106
|
+
const importedName = specifierNode.propertyName?.text ?? specifierNode.name.text;
|
|
92
107
|
const exported = module.exports?.[importedName];
|
|
93
|
-
const symbol = this.binding.symbolOfDeclaration.get(
|
|
94
|
-
if (symbol
|
|
108
|
+
const symbol = this.binding.symbolOfDeclaration.get(specifierNode.name);
|
|
109
|
+
if (!symbol) continue;
|
|
110
|
+
if (exported) {
|
|
95
111
|
this.importExports.set(symbol.id, exported);
|
|
96
112
|
// A named constructor (`import { Buffer } from "buffer"`) also
|
|
97
113
|
// inherits its module's static dispatcher, so `Buffer.from(...)`
|
|
@@ -99,6 +115,14 @@ export class GeneratorContext {
|
|
|
99
115
|
if (exported.isConstructor && module.namespace) {
|
|
100
116
|
this.importNamespaces.set(symbol.id, module.namespace);
|
|
101
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);
|
|
102
126
|
}
|
|
103
127
|
}
|
|
104
128
|
} else if (bindings && bindings.kind === SyntaxKind.NamespaceImport) {
|
|
@@ -108,6 +132,52 @@ export class GeneratorContext {
|
|
|
108
132
|
}
|
|
109
133
|
}
|
|
110
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 only import built-in platform modules and ` +
|
|
166
|
+
"relative '.ts' files; third-party npm packages (node_modules) are not implemented yet",
|
|
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
|
+
|
|
111
181
|
/**
|
|
112
182
|
* Bind a default/namespace import to either the module's runtime namespace
|
|
113
183
|
* dispatcher (when it has one) or its named exports (when it does not).
|
|
@@ -120,6 +190,31 @@ export class GeneratorContext {
|
|
|
120
190
|
}
|
|
121
191
|
}
|
|
122
192
|
|
|
193
|
+
/** Every binding an import declaration introduces (including type-only ones). */
|
|
194
|
+
private importBindingSymbols(declaration: ImportDeclaration): SymbolInfo[] {
|
|
195
|
+
const clause = declaration.importClause;
|
|
196
|
+
if (!clause) return [];
|
|
197
|
+
const names: Identifier[] = [];
|
|
198
|
+
if (clause.name) names.push(clause.name);
|
|
199
|
+
const bindings = clause.namedBindings;
|
|
200
|
+
if (bindings && bindings.kind === SyntaxKind.NamedImports) {
|
|
201
|
+
for (const specifier of bindings.elements) names.push(specifier.name);
|
|
202
|
+
} else if (bindings && bindings.kind === SyntaxKind.NamespaceImport) {
|
|
203
|
+
names.push(bindings.name);
|
|
204
|
+
}
|
|
205
|
+
const symbols: SymbolInfo[] = [];
|
|
206
|
+
for (const name of names) {
|
|
207
|
+
const symbol = this.binding.symbolOfDeclaration.get(name);
|
|
208
|
+
if (symbol) symbols.push(symbol);
|
|
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);
|
|
216
|
+
}
|
|
217
|
+
|
|
123
218
|
/** Namespace name an imported alias refers to, if any. */
|
|
124
219
|
namespaceOfSymbol(symbol: SymbolInfo | undefined): string | undefined {
|
|
125
220
|
if (!symbol || symbol.kind !== SymbolKind.Import) return undefined;
|
|
@@ -320,3 +415,13 @@ export class GeneratorContext {
|
|
|
320
415
|
);
|
|
321
416
|
}
|
|
322
417
|
}
|
|
418
|
+
|
|
419
|
+
/** A bare module specifier (`fs`, `node:fs`, `@scope/pkg`), not a file path. */
|
|
420
|
+
function isBareSpecifier(specifier: string): boolean {
|
|
421
|
+
return (
|
|
422
|
+
!specifier.startsWith(".") &&
|
|
423
|
+
!specifier.startsWith("/") &&
|
|
424
|
+
!specifier.startsWith("\\") &&
|
|
425
|
+
!/^[A-Za-z]:[\\/]/.test(specifier)
|
|
426
|
+
);
|
|
427
|
+
}
|
|
@@ -166,6 +166,9 @@ export const primaryExpressionMethods: PrimaryExpressionMethods = {
|
|
|
166
166
|
return this.emitFunctionValue(symbol);
|
|
167
167
|
}
|
|
168
168
|
if (symbol.kind === SymbolKind.Import) {
|
|
169
|
+
// The providing module was reported as missing (with a `pass --ext`
|
|
170
|
+
// hint) while resolving imports; don't pile on a second error.
|
|
171
|
+
if (this.missingModuleSymbols.has(symbol.id)) return i64(XT_UNDEFINED);
|
|
169
172
|
// Some module bindings are plain values rather than functions
|
|
170
173
|
// (`isMainThread`, `workerData`, ...). They resolve through a nullary
|
|
171
174
|
// runtime getter instead of being called or namespaced.
|
|
@@ -62,6 +62,13 @@ export interface CodegenOptions {
|
|
|
62
62
|
readonly builtins?: Readonly<Record<string, BuiltinFunction>>;
|
|
63
63
|
/** Importable modules supplied by registered extensions. */
|
|
64
64
|
readonly modules?: Readonly<Record<string, ExtensionModule>>;
|
|
65
|
+
/**
|
|
66
|
+
* Module specifier -> extension name for modules that a *known but
|
|
67
|
+
* unregistered* extension provides. Used to report an actionable
|
|
68
|
+
* `pass --ext node` diagnostic when such a module is imported without
|
|
69
|
+
* enabling the extension.
|
|
70
|
+
*/
|
|
71
|
+
readonly moduleHints?: Readonly<Record<string, string>>;
|
|
65
72
|
/**
|
|
66
73
|
* Host the IR is generated for. xbintsc builds for its own host, so this
|
|
67
74
|
* defaults to the running process; it is overridable for tests.
|
package/src/driver/compiler.ts
CHANGED
|
@@ -72,6 +72,7 @@ export function compileString(source: string, fileName = "input.ts", extensions?
|
|
|
72
72
|
const { ir } = generate(sourceFile, diagnostics, {
|
|
73
73
|
builtins: registry.builtins(),
|
|
74
74
|
modules: registry.modules(),
|
|
75
|
+
moduleHints: registry.moduleHints(),
|
|
75
76
|
});
|
|
76
77
|
return { ir, diagnostics: diagnostics.diagnostics };
|
|
77
78
|
}
|
|
@@ -101,10 +102,35 @@ export function compileEntry(entryPath: string, extensions?: ExtensionRegistry):
|
|
|
101
102
|
const { ir } = generate(sourceFile, diagnostics, {
|
|
102
103
|
builtins: registry.builtins(),
|
|
103
104
|
modules: registry.modules(),
|
|
105
|
+
moduleHints: registry.moduleHints(),
|
|
104
106
|
});
|
|
105
107
|
return { ir, diagnostics: diagnostics.diagnostics };
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Where the incremental cache lives when the caller does not name a directory:
|
|
112
|
+
* `xbintsc_CACHE_DIR` when set, otherwise `<cwd>/.xbintsc`. The override keeps
|
|
113
|
+
* an instrumented build (see `scripts/coverage-runtime.ts`) from reusing the
|
|
114
|
+
* un-instrumented objects in a normal cache, which would silently yield no
|
|
115
|
+
* profile data.
|
|
116
|
+
*/
|
|
117
|
+
function defaultCacheDir(): string {
|
|
118
|
+
return process.env.xbintsc_CACHE_DIR || join(process.cwd(), ".xbintsc");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Whether a build may link a prebuilt `runtime/lib` archive. The explicit option
|
|
123
|
+
* wins; otherwise `xbintsc_PREFER_PREBUILT` decides (`0`/`false` disables) and
|
|
124
|
+
* the default is to use an archive when one exists. Instrumented builds set it
|
|
125
|
+
* to `0` so the C runtime is compiled (and therefore instrumented) from source.
|
|
126
|
+
*/
|
|
127
|
+
export function resolvePreferPrebuilt(explicit: boolean | undefined): boolean {
|
|
128
|
+
if (explicit !== undefined) return explicit;
|
|
129
|
+
const raw = process.env.xbintsc_PREFER_PREBUILT;
|
|
130
|
+
if (raw === undefined || raw === "") return true;
|
|
131
|
+
return raw !== "0" && raw.toLowerCase() !== "false";
|
|
132
|
+
}
|
|
133
|
+
|
|
108
134
|
function executableName(entry: string, outDir: string, explicit: string | undefined): string {
|
|
109
135
|
if (explicit) return resolve(explicit);
|
|
110
136
|
const base = basename(entry, extname(entry));
|
|
@@ -116,7 +142,7 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
|
|
|
116
142
|
const runner = options.runner ?? realRunner;
|
|
117
143
|
const registry = options.extensions ?? createDefaultRegistry();
|
|
118
144
|
const outDir = resolve(options.outDir ?? join(process.cwd(), "build"));
|
|
119
|
-
const cacheDir = resolve(options.cacheDir ??
|
|
145
|
+
const cacheDir = resolve(options.cacheDir ?? defaultCacheDir());
|
|
120
146
|
const emit: EmitKind = options.emit ?? "exe";
|
|
121
147
|
const optimize = options.optimize ?? "2";
|
|
122
148
|
|
|
@@ -173,6 +199,7 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
|
|
|
173
199
|
const { ir } = generate(sourceFile, diagnostics, {
|
|
174
200
|
builtins: registry.builtins(),
|
|
175
201
|
modules: registry.modules(),
|
|
202
|
+
moduleHints: registry.moduleHints(),
|
|
176
203
|
});
|
|
177
204
|
|
|
178
205
|
if (options.verbose) {
|
|
@@ -215,7 +242,7 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
|
|
|
215
242
|
runtimeDir,
|
|
216
243
|
cacheDir,
|
|
217
244
|
registry,
|
|
218
|
-
options.preferPrebuilt
|
|
245
|
+
resolvePreferPrebuilt(options.preferPrebuilt),
|
|
219
246
|
toolchain.env,
|
|
220
247
|
);
|
|
221
248
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog of the extensions shipped with xbintsc.
|
|
3
|
+
*
|
|
4
|
+
* Callers (the CLI, embedding tools) use this to hint the compiler about
|
|
5
|
+
* modules that *would* be available if an extension were enabled, so an import
|
|
6
|
+
* of a known module without its `--ext` flag fails with an actionable message
|
|
7
|
+
* (`pass --ext node`) rather than a confusing downstream error.
|
|
8
|
+
*
|
|
9
|
+
* The core compiler stays platform agnostic: it only sees the resulting hint
|
|
10
|
+
* table, never the Node extension itself.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Extension } from "./registry.js";
|
|
14
|
+
import { nodeExtension } from "./node/index.js";
|
|
15
|
+
|
|
16
|
+
/** Every extension bundled with xbintsc, in registration order. */
|
|
17
|
+
export function bundledExtensions(): readonly Extension[] {
|
|
18
|
+
return [nodeExtension];
|
|
19
|
+
}
|