fix: address wasm target review findings

This commit is contained in:
Chris Tate
2026-08-11 12:04:17 -05:00
parent 2fa6d9e9e1
commit caa0d65f9e
14 changed files with 301 additions and 44 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ Analyzes the program without producing a binary and reports, statement by statem
<dd>Bind signature-only TypeScript declarations to native C ABI symbols and link the manifest's archive, object, and system-library inputs. See <a href="/ffi">Native FFI</a>.</dd>
<dt><code>--backend &lt;c|llvm&gt;</code></dt>
<dd>Code generator: <code>llvm</code> (default — emits LLVM IR text, compiled by the same clang) or <code>c</code> (the readable debugging backend). Unset, a native build can fall back to C when the program is outside the LLVM tier. The production <code>wasm32-wasi</code> target never falls back: a missing LLVM lowering is <code>SC3001</code>. Use <code>--backend c</code> explicitly only when inspecting generated C.</dd>
<dd>Code generator: <code>llvm</code> (default — emits LLVM IR text, compiled by the same clang) or <code>c</code> (the readable debugging backend). Unset, a native build can fall back to C when the program is outside the LLVM tier. The production <code>wasm32-wasi</code> target never falls back: a missing LLVM lowering is <code>SC3001</code>. Use <code>--backend c</code> explicitly only when inspecting generated C; on WASI that inspection lane accepts async-free programs only and reports <code>SC3001</code> for coroutine-dependent surfaces.</dd>
<dt><code>--npm-static &lt;pkg[,pkg…]|auto&gt;</code></dt>
<dd>EXPERIMENTAL. Compile the named npm packages' shipped JS statically as program modules instead of embedding them for the engine (repeatable; <code>auto</code> opts in every eligible direct import). A package the preflight refuses falls back to the island with a coverage-report note. See <a href="/dependencies">npm Dependencies</a> for maturity notes.</dd>
+2 -2
View File
@@ -37,9 +37,9 @@ The full corpus runs in a differential lane against Windows Node, including `--d
### WebAssembly (WASI Preview 1)
Set <code>SCRIPTC_CC=zigcc</code> and <code>SCRIPTC_TARGET=wasm32-wasi</code> to produce a standalone <code>.wasm</code> module. Without <code>-o</code>, <code>scriptc build hello.ts</code> writes <code>.scriptc/hello.wasm</code>. <code>scriptc run</code> hosts the module with Node's WASI implementation, inherits stdio and environment, preopens the current working directory as <code>/</code>, and maps the host's <code>/tmp</code> to the guest's <code>/tmp</code>. A built module can run in another WASI Preview 1 host instead.
Set <code>SCRIPTC_CC=zigcc</code> and <code>SCRIPTC_TARGET=wasm32-wasi</code> to produce a standalone <code>.wasm</code> module. Without <code>-o</code>, <code>scriptc build hello.ts</code> writes <code>.scriptc/hello.wasm</code>. <code>scriptc run</code> hosts the module with Node's WASI implementation, inherits stdio and environment, preopens the current working directory as <code>/</code>, and maps the host platform's temporary directory to the guest's <code>/tmp</code>. A built module can run in another WASI Preview 1 host instead.
WASI is a production LLVM target with the same language tiers as the native targets. Its 32-bit LLVM ABI supports collections, closures, exceptions, classes, checked dynamic values, async/await, promises, synchronous and asynchronous generators, timers, stdin/readline events, process-exit listeners, filesystem callbacks and promises, and the <code>--dynamic</code> QuickJS island. The debugging C backend remains available only through an explicit <code>--backend c</code>; WASI never silently falls back to it.
WASI is a production LLVM target with the same language tiers as the native targets. Its 32-bit LLVM ABI supports collections, closures, exceptions, classes, checked dynamic values, async/await, promises, synchronous and asynchronous generators, timers, stdin/readline events, process-exit listeners, filesystem callbacks and promises, and the <code>--dynamic</code> QuickJS island. The debugging C backend remains available for async-free inspection only through an explicit <code>--backend c</code>; coroutine-dependent programs report <code>SC3001</code>, and WASI never silently falls back to C.
The remaining boundary is host capability, not language coverage. WASI Preview 1 has no portable socket, process-spawn, OS-signal, network-interface, or filesystem-notification APIs. Networking/fetch, child processes, signal APIs, <code>os.networkInterfaces()</code>, and <code>fs.watch</code> therefore fail before linking with diagnostic <code>SC3002</code>. <code>--sanitize</code> and native FFI are unavailable too. Filesystem behavior is bounded by the host's preopens, and process/OS introspection follows WASI's reduced model.
+2 -1
View File
@@ -32,7 +32,8 @@ Options:
builds — the default lane emits C for it and a one-line
stderr note names the construct — while an explicit
--backend llvm fails with that construct named
wasm32-wasi is LLVM-only unless --backend c is explicit
wasm32-wasi is LLVM-only unless --backend c is explicit;
its C inspection lane accepts async-free programs only
--from-c treat input as a C (or .ll) file (toolchain plumbing/debugging)
--keep-c keep the generated program TU next to the executable
(default; the .ll — or the .c under --backend=c or
+11
View File
@@ -1,3 +1,4 @@
import { tmpdir } from "node:os";
import { buildTargetPlatform } from "@scriptc/compiler";
/** Default executable filename for the build target. Explicit --out paths
@@ -7,3 +8,13 @@ export function defaultExecutableName(stem: string, platform: string = buildTarg
if (platform === "wasi") return `${stem}.wasm`;
return stem;
}
/** Host paths exposed by `scriptc run` to a WASI Preview 1 module. Guest
* `/tmp` maps to the host's real platform temp directory instead of assuming
* the POSIX spelling exists (notably false on Windows). */
export function wasiPreopens(
cwd: string = process.cwd(),
hostTmp: string = tmpdir(),
): Record<string, string> {
return { "/": cwd, "/tmp": hostTmp };
}
+4 -2
View File
@@ -5,6 +5,7 @@
*/
import { readFile } from "node:fs/promises";
import { WASI } from "node:wasi";
import { wasiPreopens } from "./paths.js";
type WasiInstance = Parameters<WASI["start"]>[0];
declare const WebAssembly: {
@@ -22,8 +23,9 @@ const wasi = new WASI({
args: [binary],
env: process.env,
// A native scriptc executable inherits access to the caller's filesystem.
// WASI is capability-based, so expose the caller's working tree as `/`.
preopens: { "/": process.cwd(), "/tmp": "/tmp" },
// WASI is capability-based, so expose the caller's working tree as `/`
// and the platform's actual temporary directory as guest `/tmp`.
preopens: wasiPreopens(),
returnOnExit: true,
});
const instantiated = await WebAssembly.instantiate(
+8 -1
View File
@@ -1,6 +1,6 @@
import { buildTargetPlatform } from "@scriptc/compiler";
import { expect, test } from "vitest";
import { defaultExecutableName } from "../src/paths.js";
import { defaultExecutableName, wasiPreopens } from "../src/paths.js";
test("default executable names use the Windows PE suffix", () => {
expect(defaultExecutableName("main", "win32")).toBe("main.exe");
@@ -18,6 +18,13 @@ test("WASI cross-builds use the WebAssembly suffix", () => {
expect(defaultExecutableName("main", platform)).toBe("main.wasm");
});
test("WASI preopens map guest /tmp to the host platform temp directory", () => {
expect(wasiPreopens("C:\\work\\repo", "C:\\Users\\runner\\AppData\\Local\\Temp")).toEqual({
"/": "C:\\work\\repo",
"/tmp": "C:\\Users\\runner\\AppData\\Local\\Temp",
});
});
test("Windows cross-builds use the PE suffix on a non-Windows host", () => {
const platform = buildTargetPlatform({
SCRIPTC_CC: "zigcc",
+13 -22
View File
@@ -8687,8 +8687,8 @@ class LlEmitter {
* POINTER-SIZE slots off the emit tuple's va_list (textual LLVM IR
* cannot va_arg portably) and calls this function behind the wrapper
* closure's fn. It re-types each slot to the listener's declared
* parameter (f64 from its i64 bit pattern, bool from the zero-extended
* slot, refcounted values retained — the callee owns +1 per the
* parameter (f64/bool scalars load from the emit site's stack slot,
* refcounted values retain the direct pointer — the callee owns +1 per the
* universal convention) and calls the ORIGINAL listener held by the
* wrapper's one capture box. A non-void result is discarded (refcounted
* ones released) — Node ignores listener return values. Interned per
@@ -8720,16 +8720,10 @@ class LlEmitter {
cbT.params.forEach((p, i) => {
const ty = this.llType(p);
if (ty === "double") {
d.push(
` %x${i} = ptrtoint ptr %a${i} to i64`,
` %d${i} = bitcast i64 %x${i} to double`,
);
d.push(` %d${i} = load double, ptr %a${i}`);
passed.push(`double %d${i}`);
} else if (ty === "i1") {
d.push(
` %x${i} = ptrtoint ptr %a${i} to i64`,
` %b${i} = trunc i64 %x${i} to i1`,
);
d.push(` %b${i} = load i1, ptr %a${i}`);
passed.push(`i1 %b${i}`);
} else if (isRefCounted(p)) {
d.push(` %r${i} = call ptr ${retainSym(this, p)}(ptr %a${i})`);
@@ -12765,22 +12759,19 @@ class LlEmitter {
}
if (e.fn === "emitter.emit") {
// The variadic dispatch: the event's unified tuple rides the C
// variadic tail POINTER-CLASSED (f64 as its i64 bit pattern, bool
// zero-extended — scr_runtime.h's fixed-shim contract; the emitted
// adapters re-type on the way out), every argument borrowed. May
// variadic tail POINTER-CLASSED. Scalar values ride pointers to
// call-lived stack slots, while reference values ride directly; the
// fixed shim can therefore read one pointer-width slot per argument
// on both wasm32 and native targets. Every argument is borrowed. May
// throw (listeners run inside).
const args = e.args.map((a) => this.emitExpr(a));
const tuple = args.slice(2).map((a) => {
const ty = this.llType(a.type);
if (ty === "double") {
const x = B.tmp();
B.line(`${x} = bitcast double ${a.name} to i64`);
return `i64 ${x}`;
}
if (ty === "i1") {
const x = B.tmp();
B.line(`${x} = zext i1 ${a.name} to i64`);
return `i64 ${x}`;
if (ty === "double" || ty === "i1") {
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ${ty} ; EventEmitter scalar argument`);
B.line(`store ${ty} ${a.name}, ptr ${slot}`);
return `ptr ${slot}`;
}
return `ptr ${a.name}`;
});
+127
View File
@@ -97,6 +97,11 @@ export interface EmbeddedModule {
key: string;
source: string;
format: EmbeddedFormat;
/** The parsed source reaches Node's global fetch binding. This is a
* semantic capability fact, unlike the old whole-source word scan: comments,
* strings, property names, and locally-bound `fetch` identifiers do not set
* it. The IR/link gates consume this bit without reparsing embedded text. */
usesFetch?: true;
/** CommonJS only: the synthesized ESM facade the island's module loader
* evaluates when an ES module imports this file — default plus the LEXED
* named exports (cjsEsmFacadeSource). ESM sources compile natively and
@@ -234,6 +239,124 @@ export interface NpmRuntimeGraph {
errors: NpmGraphError[];
}
/** Embedded modules whose executable syntax reads Node's global fetch.
*
* The npm graph already owns every source string, so bind them together in a
* no-lib TypeScript program once and ask the checker whether a `fetch`
* identifier resolves locally. With no ambient library, an unresolved read is
* exactly the engine global; declarations/imports/parameters retain symbols.
* `globalThis.fetch` and `global.fetch` are recognized explicitly when their
* receiver is likewise unshadowed. Parsing is recovery-capable, matching the
* module-specifier sweep's treatment of shipped JavaScript. */
export function embeddedModulesUsingGlobalFetch(
modules: readonly EmbeddedModule[],
): ReadonlySet<string> {
const sourceFiles = new Map<string, ts.SourceFile>();
for (const mod of modules) {
if (mod.format === "json") continue;
sourceFiles.set(
mod.key,
ts.createSourceFile(mod.key, mod.source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS),
);
}
if (sourceFiles.size === 0) return new Set();
const options: ts.CompilerOptions = {
allowJs: true,
checkJs: false,
noLib: true,
target: ts.ScriptTarget.Latest,
module: ts.ModuleKind.ESNext,
moduleDetection: ts.ModuleDetectionKind.Force,
};
const canonical = (fileName: string): string => {
const normalized = fileName.replace(/\\/g, "/");
return ts.sys.useCaseSensitiveFileNames ? normalized : normalized.toLowerCase();
};
const byCanonical = new Map(
[...sourceFiles].map(([fileName, sourceFile]) => [canonical(fileName), sourceFile] as const),
);
const originalKeyByCanonical = new Map(
[...sourceFiles.keys()].map((fileName) => [canonical(fileName), fileName] as const),
);
const host: ts.CompilerHost = {
getSourceFile: (fileName) => byCanonical.get(canonical(fileName)),
getDefaultLibFileName: () => "",
writeFile: () => {},
getCurrentDirectory: () => "/",
getDirectories: () => [],
fileExists: (fileName) => byCanonical.has(canonical(fileName)),
readFile: (fileName) => byCanonical.get(canonical(fileName))?.text,
getCanonicalFileName: canonical,
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
getNewLine: () => "\n",
directoryExists: () => true,
realpath: (fileName) => fileName,
};
const program = ts.createProgram({ rootNames: [...sourceFiles.keys()], options, host });
const checker = program.getTypeChecker();
const found = new Set<string>();
const unboundGlobalObject = (node: ts.Expression): boolean => {
if (
!ts.isIdentifier(node) ||
(node.text !== "globalThis" && node.text !== "global")
) {
return false;
}
const symbol = checker.getSymbolAtLocation(node);
// TypeScript synthesizes a declaration-less globalThis symbol even in a
// no-lib program. A source shadow (parameter/local/import) always carries
// at least one declaration; the Node `global` alias remains unresolved.
return (
symbol === undefined ||
symbol.declarations === undefined ||
symbol.declarations.length === 0
);
};
for (const sourceFile of program.getSourceFiles()) {
let usesFetch = false;
const visit = (node: ts.Node): void => {
if (usesFetch) return;
if (
ts.isPropertyAccessExpression(node) &&
node.name.text === "fetch" &&
unboundGlobalObject(node.expression)
) {
usesFetch = true;
return;
}
if (
ts.isElementAccessExpression(node) &&
ts.isStringLiteralLike(node.argumentExpression) &&
node.argumentExpression.text === "fetch" &&
unboundGlobalObject(node.expression)
) {
usesFetch = true;
return;
}
if (ts.isIdentifier(node) && node.text === "fetch") {
// A property-access name is not a binding read (`obj.fetch`); the
// global-object case was handled above. Every real bare read has no
// symbol in this no-lib program unless source syntax binds it.
if (
!(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) &&
checker.getSymbolAtLocation(node) === undefined
) {
usesFetch = true;
return;
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
const originalKey = originalKeyByCanonical.get(canonical(sourceFile.fileName));
if (usesFetch && originalKey !== undefined) found.add(originalKey);
}
return found;
}
/** Builtins the island provides shims for (scr_island.c). Both spellings
* resolve to the canonical "node:" key. Subpath forms ("fs/promises") are
* NOT shimmed unless listed here explicitly. */
@@ -958,6 +1081,10 @@ export class NpmGraphBuilder {
finish(): NpmRuntimeGraph {
this.synthesizeCjsFacades();
for (const key of embeddedModulesUsingGlobalFetch([...this.modules.values()])) {
const mod = this.modules.get(key);
if (mod !== undefined) mod.usesFetch = true;
}
return {
modules: [...this.modules.values()],
edges: this.edges,
+24
View File
@@ -182,6 +182,22 @@ function llvmRefusalDiag(err: LlvmUnsupportedError, entryPath: string): ScrDiagn
};
}
/** A valid IR surface the explicitly-selected code generator cannot host.
* SC3001 is backend coverage (as with an LLVM tier refusal), not a target
* capability gap: wasm32-wasi's production LLVM lane still accepts it. */
function backendRefusalDiag(
backend: "c" | "llvm",
target: string,
surface: string,
loc: SrcLoc,
): ScrDiagnostic {
return {
code: "SC3001",
message: `${backend} backend does not support ${surface} for ${target}; use --backend llvm`,
loc,
};
}
/** A valid program surface that the selected execution target cannot host.
* SC3xxx stays the backend/target-coverage family: source semantics are
* valid, but this target deliberately refuses them instead of emitting a
@@ -865,6 +881,14 @@ export async function compile(entryPath: string, opts: CompileOptions): Promise<
if (unavailable !== null) {
return fail([targetRefusalDiag("wasm32-wasi", unavailable.surface, unavailable.loc)]);
}
if (opts.backend === "c") {
const asyncSurface = moduleLibAsyncSurface(lowered.module);
if (asyncSurface !== null) {
return fail([
backendRefusalDiag("c", "wasm32-wasi", asyncSurface.surface, asyncSurface.loc),
]);
}
}
}
entryText = fe.entryText();
sourceTexts = fe.sourceTexts();
+14 -8
View File
@@ -720,7 +720,14 @@ export interface IrModule {
* island loader evaluates when an ES module imports the CJS file:
* default plus the named exports LEXED from the source at build time
* (cjs-lexer.ts — the compiler's port of Node's vendored CJS lexer). */
modules: { key: string; source: string; format: "esm" | "cjs" | "json"; esm?: string }[];
modules: {
key: string;
source: string;
format: "esm" | "cjs" | "json";
esm?: string;
/** Parsed/bound embedded source reaches the engine's global fetch. */
usesFetch?: true;
}[];
/** `kind` picks Node's "exports" condition set per CALL FORM: one
* (from, specifier) can name a dual package's ESM entry behind an
* "import" edge AND its CJS entry behind a "require" edge; "any"
@@ -5856,16 +5863,15 @@ export function moduleUsesFileHandle(mod: IrModule): boolean {
}
/** True when user code lowers static fetch or the embedded npm graph
* references dynamic fetch — the link switch that pulls scr_fetch.c +
* reaches the engine's global fetch — the link switch that pulls scr_fetch.c +
* its socket/tls/zlib dependencies into the binary (cc.ts) and has the
* emitted main call scr_fetch_install. For embedded sources, use a
* word-boundary scan and err toward linking: a false positive costs one
* dylib reference; a false negative would leave embedded code without
* the global at runtime. Fetch-free graphs keep their exact historical
* link lines. */
* emitted main call scr_fetch_install. The npm graph records the embedded
* source fact with a parser + binder, so comments, strings, property names,
* and local `fetch` bindings do not change the link or target capability.
* Fetch-free graphs keep their exact historical link lines. */
export function moduleUsesFetch(mod: IrModule): boolean {
const embedded = mod.embedded;
if (embedded && embedded.modules.some((m) => /\bfetch\b/.test(m.source))) return true;
if (embedded && embedded.modules.some((m) => m.usesFetch === true)) return true;
// User-code fetch is either the engine global or the static libCall pair.
let found = false;
const visit = (v: unknown): void => {
@@ -0,0 +1,42 @@
import { expect, test } from "vitest";
import {
embeddedModulesUsingGlobalFetch,
type EmbeddedModule,
} from "../src/frontend/npm.js";
function js(key: string, source: string): EmbeddedModule {
return { key: `/${key}.js`, source, format: "cjs" };
}
test("embedded fetch capability analysis ignores text and local bindings", () => {
const modules = [
js("comment", "// fetch(url)\nmodule.exports = 1;"),
js("string", "module.exports = 'fetch(url)';"),
js("property", "const value = { fetch: 1 }; module.exports = value.fetch;"),
js("local", "const fetch = (x) => x; module.exports = fetch('local');"),
js("parameter", "module.exports = function (fetch) { return fetch('local'); };"),
js("import", "import fetch from 'a-local-package'; export default fetch('local');"),
js("shadow-global", "module.exports = function (globalThis) { return globalThis.fetch('local'); };"),
];
expect([...embeddedModulesUsingGlobalFetch(modules)]).toEqual([]);
});
test("embedded fetch capability analysis finds global reads", () => {
const modules = [
js("bare", "module.exports = fetch('https://example.com');"),
js("global-this", "module.exports = globalThis.fetch('https://example.com');"),
js("global", "module.exports = global['fetch']('https://example.com');"),
{
...js("windows-path", "module.exports = fetch('https://example.com');"),
key: "C:\\pkg\\index.js",
},
];
expect([...embeddedModulesUsingGlobalFetch(modules)]).toEqual([
"/bare.js",
"/global-this.js",
"/global.js",
"C:\\pkg\\index.js",
]);
});
+4 -4
View File
@@ -390,10 +390,10 @@ ScrEmitter *scr_emitter_on_via(ScrEmitter *em, ScrStr *name, ScrClosure *orig /*
/* ── fixed-arity invoke shims (scr_runtime.h's contract) ──────────────
* Read k pointer-size slots and call the fixed-signature adapter behind
* cb->fn. The LLVM lane's emit sites pass every tuple argument pointer-
* classed (f64 as its i64 bit pattern, bool zero-extended) and the
* runtime's own emits are pointer-only, so va_arg(ap, void *) reads every
* tuple a fixed-registered listener can observe. */
* cb->fn. The LLVM lane's emit sites pass scalars by pointer to typed stack
* slots and references directly; the runtime's own emits are pointer-only,
* so va_arg(ap, void *) reads every tuple a fixed-registered listener can
* observe on 32- and 64-bit targets. */
void scr_ee_inv_fixed0(ScrClosure *cb, va_list ap) {
(void)ap;
((void (*)(ScrClosure *))cb->fn)(cb);
+4 -3
View File
@@ -1427,9 +1427,10 @@ ScrEmitter *scr_emitter_on_via(ScrEmitter *em, ScrStr *name, ScrClosure *orig /*
* slots off the emit tuple and calls cb->fn behind the fixed signature
* `void (ScrClosure *, void * ×k)` — the fixed-signature adapter the
* backend provides. Such backends pass every user tuple argument
* pointer-classed at the emit site (f64 as its i64 bit pattern, bool
* zero-extended); the runtime's own emits (data/error/pipe/meta) carry
* pointers only, so the shims read every tuple either lane produces.
* pointer-classed at the emit site: scalar values point at call-lived
* typed stack slots and reference values ride directly. The runtime's own
* emits (data/error/pipe/meta) carry pointers only, so the shims read every
* tuple either lane produces on 32- and 64-bit targets.
* SCR_EE_FIXED_MAX is the registry's audited arity ceiling — backends
* refuse listeners past it rather than guess. */
#define SCR_EE_FIXED_MAX 4
+45
View File
@@ -105,6 +105,7 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => {
"1551-dyn-receiver-methods.ts",
"1558-any-joins-and-dyn-validation.ts",
"1634-inspect-classes.ts",
"1644-ee-basics.ts",
"1666-dyn-fn-identity.ts",
"1800-immediate-basics.ts",
"1805-timer-callback-args.ts",
@@ -152,6 +153,50 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => {
if (llvmResult.ok) expect(llvmResult.backend).toBe("llvm");
});
test("the explicit C backend still emits async-free wasm", async () => {
const outDir = await mkdtemp("/tmp/scriptc-wasi-c-");
const result = await compile(join(repoRoot, "tests/corpus/001-hello.ts"), {
outDir,
outPath: join(outDir, "program.wasm"),
backend: "c",
});
if (!result.ok) {
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
}
expect(result.backend).toBe("c");
});
test("the explicit C backend diagnoses coroutine-dependent programs", async () => {
const outDir = await mkdtemp("/tmp/scriptc-wasi-c-refusal-");
const result = await compile(join(repoRoot, "tests/corpus/1020-async-basics.ts"), {
outDir,
outPath: join(outDir, "program.wasm"),
backend: "c",
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.diagnostics).toHaveLength(1);
expect(result.diagnostics[0]?.code).toBe("SC3001");
expect(result.diagnostics[0]?.message).toMatch(
/c backend does not support an async function .* for wasm32-wasi; use --backend llvm/,
);
}
});
test("embedded source comments mentioning fetch do not trigger SC3002", async () => {
const entry = join(repoRoot, "tests/fixtures/npm/cases/island-web-plumbing/main.ts");
const outDir = await mkdtemp("/tmp/scriptc-wasi-fetch-comment-");
const result = await compile(entry, {
outDir,
outPath: join(outDir, "program.wasm"),
dynamic: true,
});
if (!result.ok) {
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
}
expect(result.backend).toBe("llvm");
});
test.each([
["1360-spawn-sync.ts", /child processes/],
["1443-signal-handlers.ts", /OS signals/],