mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
fix: address wasm target review findings
This commit is contained in:
@@ -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 <c|llvm></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 <pkg[,pkg…]|auto></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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}`;
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
]);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/],
|
||||
|
||||
Reference in New Issue
Block a user