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.
- package/README.md +21 -3
- package/README.zh-CN.md +18 -3
- package/dist/src/codegen/generator/calls/access.js +13 -0
- package/dist/src/codegen/generator/calls/access.js.map +1 -1
- package/dist/src/codegen/generator/calls/invocation.js +8 -2
- package/dist/src/codegen/generator/calls/invocation.js.map +1 -1
- package/dist/src/codegen/generator/context.d.ts +23 -0
- package/dist/src/codegen/generator/context.js +91 -17
- package/dist/src/codegen/generator/context.js.map +1 -1
- package/dist/src/codegen/generator/expressions/primary.js +5 -0
- package/dist/src/codegen/generator/expressions/primary.js.map +1 -1
- package/dist/src/driver/compiler.d.ts +7 -0
- package/dist/src/driver/compiler.js +41 -5
- package/dist/src/driver/compiler.js.map +1 -1
- package/dist/src/driver/modules.d.ts +1 -1
- package/dist/src/driver/modules.js +235 -25
- package/dist/src/driver/modules.js.map +1 -1
- package/dist/src/extensions/node/fs/fs-ops.d.ts +5 -2
- package/dist/src/extensions/node/fs/fs-ops.js +45 -6
- package/dist/src/extensions/node/fs/fs-ops.js.map +1 -1
- package/dist/src/extensions/node/fs/index.d.ts +6 -2
- package/dist/src/extensions/node/fs/index.js +22 -3
- package/dist/src/extensions/node/fs/index.js.map +1 -1
- package/dist/src/extensions/node/fs-promises/index.d.ts +4 -3
- package/dist/src/extensions/node/fs-promises/index.js +31 -3
- package/dist/src/extensions/node/fs-promises/index.js.map +1 -1
- package/dist/tests/cli/main.test.js +17 -0
- package/dist/tests/cli/main.test.js.map +1 -1
- package/dist/tests/driver/compiler.test.js +93 -2
- package/dist/tests/driver/compiler.test.js.map +1 -1
- package/dist/tests/driver/modules.test.js +91 -4
- package/dist/tests/driver/modules.test.js.map +1 -1
- package/dist/tests/e2e/compile-node.test.js +240 -0
- package/dist/tests/e2e/compile-node.test.js.map +1 -1
- package/dist/tests/e2e/compile.test.js +12 -0
- package/dist/tests/e2e/compile.test.js.map +1 -1
- package/dist/tests/e2e/harness.js +11 -8
- package/dist/tests/e2e/harness.js.map +1 -1
- package/dist/tests/e2e/node-modules.test.js +1 -1
- package/dist/tests/e2e/node-modules.test.js.map +1 -1
- package/dist/tests/e2e/npm-modules.test.d.ts +9 -0
- package/dist/tests/e2e/npm-modules.test.js +55 -0
- package/dist/tests/e2e/npm-modules.test.js.map +1 -0
- package/dist/tests/e2e/runtime-coverage.test.d.ts +11 -0
- package/dist/tests/e2e/runtime-coverage.test.js +646 -0
- package/dist/tests/e2e/runtime-coverage.test.js.map +1 -0
- package/package.json +3 -2
- package/runtime/ext_node/buffer/parts/prototype.inc +7 -0
- package/runtime/ext_node/buffer/parts/search.inc +8 -4
- package/runtime/ext_node/child_process/child_process.c +5 -0
- package/runtime/ext_node/fs/constants.c +113 -0
- package/runtime/ext_node/fs/copy_ops.c +145 -0
- package/runtime/ext_node/fs/dir.c +101 -0
- package/runtime/ext_node/fs/fd_ops.c +405 -0
- package/runtime/ext_node/fs/fs_common.h +388 -5
- package/runtime/ext_node/fs/fs_ops.c +100 -139
- package/runtime/ext_node/fs/glob.c +232 -0
- package/runtime/ext_node/fs/link_ops.c +91 -0
- package/runtime/ext_node/fs/meta_ops.c +403 -0
- package/runtime/ext_node/fs/promises.c +275 -60
- package/runtime/ext_node/fs/read_file.c +8 -4
- package/runtime/ext_node/fs/watch.c +52 -0
- package/runtime/ext_node/fs/write_file.c +20 -11
- package/runtime/ext_node/stream/stream.c +19 -2
- package/runtime/rt.h +9 -0
- package/runtime/xt_io.c +21 -0
- package/scripts/coverage-runtime.ts +338 -0
- package/src/codegen/generator/calls/access.ts +15 -0
- package/src/codegen/generator/calls/invocation.ts +8 -2
- package/src/codegen/generator/context.ts +112 -21
- package/src/codegen/generator/expressions/primary.ts +5 -0
- package/src/driver/compiler.ts +42 -5
- package/src/driver/modules.ts +251 -26
- package/src/extensions/node/fs/fs-ops.ts +48 -6
- package/src/extensions/node/fs/index.ts +25 -3
- 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
|
-
|
|
95
|
-
|
|
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
|
|
88
|
+
const specifier = declaration.moduleSpecifier.value;
|
|
89
|
+
const module = this.modules[specifier];
|
|
90
90
|
if (!module) {
|
|
91
|
-
|
|
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
|
|
114
|
-
if (
|
|
115
|
-
const importedName =
|
|
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(
|
|
118
|
-
if (symbol
|
|
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
|
-
/**
|
|
148
|
-
private
|
|
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)
|
|
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}'`,
|
package/src/driver/compiler.ts
CHANGED
|
@@ -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 ??
|
|
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
|
|
258
|
+
resolvePreferPrebuilt(options.preferPrebuilt),
|
|
222
259
|
toolchain.env,
|
|
223
260
|
);
|
|
224
261
|
|