diff --git a/docs/src/app/cli/page.mdx b/docs/src/app/cli/page.mdx index 0d0d85842..52cc84845 100644 --- a/docs/src/app/cli/page.mdx +++ b/docs/src/app/cli/page.mdx @@ -63,7 +63,7 @@ Analyzes the program without producing a binary and reports, statement by statem
Bind signature-only TypeScript declarations to native C ABI symbols and link the manifest's archive, object, and system-library inputs. See Native FFI.
--backend <c|llvm>
-
Code generator: llvm (default — emits LLVM IR text, compiled by the same clang) or c (the readable debugging backend). Unset, a native build can fall back to C when the program is outside the LLVM tier. The production wasm32-wasi target never falls back: a missing LLVM lowering is SC3001. Use --backend c explicitly only when inspecting generated C.
+
Code generator: llvm (default — emits LLVM IR text, compiled by the same clang) or c (the readable debugging backend). Unset, a native build can fall back to C when the program is outside the LLVM tier. The production wasm32-wasi target never falls back: a missing LLVM lowering is SC3001. Use --backend c explicitly only when inspecting generated C; on WASI that inspection lane accepts async-free programs only and reports SC3001 for coroutine-dependent surfaces.
--npm-static <pkg[,pkg…]|auto>
EXPERIMENTAL. Compile the named npm packages' shipped JS statically as program modules instead of embedding them for the engine (repeatable; auto opts in every eligible direct import). A package the preflight refuses falls back to the island with a coverage-report note. See npm Dependencies for maturity notes.
diff --git a/docs/src/app/platforms/page.mdx b/docs/src/app/platforms/page.mdx index 054c41b83..560787b2d 100644 --- a/docs/src/app/platforms/page.mdx +++ b/docs/src/app/platforms/page.mdx @@ -37,9 +37,9 @@ The full corpus runs in a differential lane against Windows Node, including `--d ### WebAssembly (WASI Preview 1) -Set SCRIPTC_CC=zigcc and SCRIPTC_TARGET=wasm32-wasi to produce a standalone .wasm module. Without -o, scriptc build hello.ts writes .scriptc/hello.wasm. scriptc run hosts the module with Node's WASI implementation, inherits stdio and environment, preopens the current working directory as /, and maps the host's /tmp to the guest's /tmp. A built module can run in another WASI Preview 1 host instead. +Set SCRIPTC_CC=zigcc and SCRIPTC_TARGET=wasm32-wasi to produce a standalone .wasm module. Without -o, scriptc build hello.ts writes .scriptc/hello.wasm. scriptc run hosts the module with Node's WASI implementation, inherits stdio and environment, preopens the current working directory as /, and maps the host platform's temporary directory to the guest's /tmp. 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 --dynamic QuickJS island. The debugging C backend remains available only through an explicit --backend c; 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 --dynamic QuickJS island. The debugging C backend remains available for async-free inspection only through an explicit --backend c; coroutine-dependent programs report SC3001, 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, os.networkInterfaces(), and fs.watch therefore fail before linking with diagnostic SC3002. --sanitize and native FFI are unavailable too. Filesystem behavior is bounded by the host's preopens, and process/OS introspection follows WASI's reduced model. diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index b652922e4..619bfb6ea 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -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 diff --git a/packages/cli/src/paths.ts b/packages/cli/src/paths.ts index d15dff59c..d8a71d8df 100644 --- a/packages/cli/src/paths.ts +++ b/packages/cli/src/paths.ts @@ -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 { + return { "/": cwd, "/tmp": hostTmp }; +} diff --git a/packages/cli/src/wasi-runner.ts b/packages/cli/src/wasi-runner.ts index ee4baa870..c8cb148de 100644 --- a/packages/cli/src/wasi-runner.ts +++ b/packages/cli/src/wasi-runner.ts @@ -5,6 +5,7 @@ */ import { readFile } from "node:fs/promises"; import { WASI } from "node:wasi"; +import { wasiPreopens } from "./paths.js"; type WasiInstance = Parameters[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( diff --git a/packages/cli/test/paths.test.ts b/packages/cli/test/paths.test.ts index 9fab1c7fb..4b54508d5 100644 --- a/packages/cli/test/paths.test.ts +++ b/packages/cli/test/paths.test.ts @@ -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", diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index baab005cc..9a40c56e2 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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}`; }); diff --git a/packages/compiler/src/frontend/npm.ts b/packages/compiler/src/frontend/npm.ts index eb9a85247..c69da9181 100644 --- a/packages/compiler/src/frontend/npm.ts +++ b/packages/compiler/src/frontend/npm.ts @@ -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 { + const sourceFiles = new Map(); + 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(); + + 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, diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 6a300e381..83bb1f1c1 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -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(); diff --git a/packages/compiler/src/ir/nodes.ts b/packages/compiler/src/ir/nodes.ts index 6c4d75b01..d9b355f52 100644 --- a/packages/compiler/src/ir/nodes.ts +++ b/packages/compiler/src/ir/nodes.ts @@ -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 => { diff --git a/packages/compiler/test/npm-fetch-analysis.test.ts b/packages/compiler/test/npm-fetch-analysis.test.ts new file mode 100644 index 000000000..1b87c8551 --- /dev/null +++ b/packages/compiler/test/npm-fetch-analysis.test.ts @@ -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", + ]); +}); diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index 41deb177b..b2252526f 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -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); diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 76e8178a5..d17e3e6e7 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -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 diff --git a/tests/harness/wasi-differential.test.ts b/tests/harness/wasi-differential.test.ts index e7a7f9919..0009e1dc3 100644 --- a/tests/harness/wasi-differential.test.ts +++ b/tests/harness/wasi-differential.test.ts @@ -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/],