mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
WebAssembly W0–W2: free handle tags, a cross-thread clone hook, the WebAssembly types and refusals, and a decoder that reports every module a program can load (#1339)
This commit is contained in:
@@ -8,6 +8,7 @@ node_modules/
|
||||
!tests/fixtures/npm/node_modules/
|
||||
!tests/corpus/*/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/
|
||||
!tests/coverage-fixtures/wasm/node_modules/
|
||||
!tests/fixtures/npm-static/nested/inner/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/*/node_modules/
|
||||
!tests/fixtures/npm/store/*/node_modules/
|
||||
|
||||
@@ -133,6 +133,9 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
|
||||
<dt><code>--explain-dynamic[=<name>]</code></dt>
|
||||
<dd>Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as <code>x</code>, <code>Point</code>, or <code>Point.x</code>, show only the matching units. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
|
||||
|
||||
<dt><code>--wasm <file.wasm></code></dt>
|
||||
<dd>Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and <code>.wasm</code> files shipped by packages that use WebAssembly; a <code>scriptc.wasm</code> array in <code>package.json</code> adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. scriptc does not compile WebAssembly modules to native code yet. Repeatable.</dd>
|
||||
|
||||
<dt><code>--sanitize</code></dt>
|
||||
<dd>Build an executable with AddressSanitizer and a reference-count audit at exit, the same checks scriptc's own test suite uses. Requires a C compiler and is available only for host builds. Not supported with <code>--emit=asm</code> or <code>--emit=obj</code>.</dd>
|
||||
|
||||
|
||||
@@ -240,6 +240,10 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/native-addons.mdx</include>
|
||||
|
||||
## WebAssembly modules
|
||||
|
||||
<include>../limitations/webassembly-modules.mdx</include>
|
||||
|
||||
## WASI target
|
||||
|
||||
<include>../limitations/wasi-target.mdx</include>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Compiled programs can't load WebAssembly modules yet. Code that only mentions the `WebAssembly` types type-checks, including packages whose declarations name them, and `typeof WebAssembly` answers as it does in Node.js. Every use of the API itself is rejected: `WebAssembly.compile`, `instantiate` and `validate`, `new WebAssembly.Module` and the other constructors, `instanceof` checks, and reads of `globalThis.WebAssembly`. TypeScript sources fail to build with SC2020 at each use, and JavaScript sources throw that diagnostic when the statement runs.
|
||||
|
||||
`scriptc coverage` lists the modules a program would load, with their size, whether they validate, and the WebAssembly proposals they use. It finds modules at paths the compiler resolves, modules inlined as base64 literals, `.wasm` files shipped by packages that use WebAssembly, and files named with [`--wasm`](/docs/cli) or a `scriptc.wasm` array in `package.json`.
|
||||
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "4421a7b808191ac7f410",
|
||||
"artifactVersion": "efc18bcd8a7a03fd8622",
|
||||
"rowCount": 3880
|
||||
}
|
||||
|
||||
@@ -330,8 +330,8 @@
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"refused": 0,
|
||||
"not-implemented": 80,
|
||||
"refused": 1,
|
||||
"not-implemented": 79,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -36590,9 +36590,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"tiers": {
|
||||
"static": {
|
||||
"audit-partial": 1162,
|
||||
"verify-gap": 2167,
|
||||
"replace-refusal": 40,
|
||||
"verify-gap": 2166,
|
||||
"replace-refusal": 41,
|
||||
"classify": 2,
|
||||
"implement": 85
|
||||
}
|
||||
@@ -34178,11 +34178,11 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:globals",
|
||||
"status": "refused",
|
||||
"action": "replace-refusal",
|
||||
"verification": "explicit-refusal",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:unsupported:stdlib.webassembly",
|
||||
"tests": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,8 +342,8 @@
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"refused": 0,
|
||||
"not-implemented": 80,
|
||||
"refused": 1,
|
||||
"not-implemented": 79,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -53521,8 +53521,8 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:globals"
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:stdlib.webassembly"
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
|
||||
@@ -203,6 +203,10 @@ function featureOf(row, entries, parentSignature) {
|
||||
|
||||
function symbolCandidates(row) {
|
||||
const result = new Set([row.apiSymbol]);
|
||||
// A global's own row (`WebAssembly`) matches the compiler's
|
||||
// `globalThis.<name>` entries.
|
||||
if (row.chapter === "globals" && row.apiSymbol && !row.apiSymbol.includes("."))
|
||||
result.add(`globalThis.${row.apiSymbol}`);
|
||||
const aliases = [
|
||||
["fsPromises.", "fs/promises."],
|
||||
["dnsPromises.", "dns/promises."],
|
||||
|
||||
+134
@@ -13,6 +13,9 @@
|
||||
* they MERGE as overloads instead of colliding — the Timeout handle maps
|
||||
* to the numeric timer id either way, and .unref()/.ref()/.hasRef() are
|
||||
* loop-liveness bookkeeping).
|
||||
* 4. The WebAssembly namespace and BufferSource, which the DOM library
|
||||
* supplies in ordinary TypeScript projects (types only; every reached
|
||||
* use of the API is refused at its site).
|
||||
*
|
||||
* This file ships to EVERY program scriptc builds — the lowering program
|
||||
* and preflight's project-world second-chance program alike (a project
|
||||
@@ -108,3 +111,134 @@ declare function clearTimeout(handle?: Timeout | number | null | undefined): voi
|
||||
* baked result is exactly what Node would compute at runtime (the
|
||||
* differential harness verifies this with a `comptime = (f) => f()` shim). */
|
||||
declare function comptime<T>(compute: () => T): T;
|
||||
|
||||
/* The JavaScript WebAssembly API, as the DOM library declares it (scriptc's
|
||||
* ES-only type world never loads lib.dom, and @types/node leaves it to
|
||||
* lib.dom). Packages whose declarations name these types (quickjs-wasi,
|
||||
* photon-node, ...) typecheck against them. These are types only: the
|
||||
* type mapper gives every type declared here the checked-dynamic
|
||||
* representation, and the lowerer refuses each reached use of the API with
|
||||
* a located diagnostic (frontend/lowering/builtins/webassembly.ts).
|
||||
* BufferSource admits every ArrayBufferView, as lib.dom did before
|
||||
* TypeScript 5.7: Node's Buffer types are views over ArrayBufferLike. */
|
||||
type BufferSource = ArrayBufferView | ArrayBuffer;
|
||||
declare namespace WebAssembly {
|
||||
interface CompileError extends Error {}
|
||||
var CompileError: {
|
||||
prototype: CompileError;
|
||||
new (message?: string): CompileError;
|
||||
(message?: string): CompileError;
|
||||
};
|
||||
interface Global<T extends ValueType = ValueType> {
|
||||
value: ValueTypeMap[T];
|
||||
valueOf(): ValueTypeMap[T];
|
||||
}
|
||||
var Global: {
|
||||
prototype: Global;
|
||||
new <T extends ValueType = ValueType>(
|
||||
descriptor: GlobalDescriptor<T>,
|
||||
v?: ValueTypeMap[T],
|
||||
): Global<T>;
|
||||
};
|
||||
interface Instance {
|
||||
readonly exports: Exports;
|
||||
}
|
||||
var Instance: {
|
||||
prototype: Instance;
|
||||
new (module: Module, importObject?: Imports): Instance;
|
||||
};
|
||||
interface LinkError extends Error {}
|
||||
var LinkError: {
|
||||
prototype: LinkError;
|
||||
new (message?: string): LinkError;
|
||||
(message?: string): LinkError;
|
||||
};
|
||||
interface Memory {
|
||||
readonly buffer: ArrayBuffer;
|
||||
grow(delta: number): number;
|
||||
}
|
||||
var Memory: {
|
||||
prototype: Memory;
|
||||
new (descriptor: MemoryDescriptor): Memory;
|
||||
};
|
||||
interface Module {}
|
||||
var Module: {
|
||||
prototype: Module;
|
||||
new (bytes: BufferSource): Module;
|
||||
customSections(moduleObject: Module, sectionName: string): ArrayBuffer[];
|
||||
exports(moduleObject: Module): ModuleExportDescriptor[];
|
||||
imports(moduleObject: Module): ModuleImportDescriptor[];
|
||||
};
|
||||
interface RuntimeError extends Error {}
|
||||
var RuntimeError: {
|
||||
prototype: RuntimeError;
|
||||
new (message?: string): RuntimeError;
|
||||
(message?: string): RuntimeError;
|
||||
};
|
||||
interface Table {
|
||||
readonly length: number;
|
||||
get(index: number): any;
|
||||
grow(delta: number, value?: any): number;
|
||||
set(index: number, value?: any): void;
|
||||
}
|
||||
var Table: {
|
||||
prototype: Table;
|
||||
new (descriptor: TableDescriptor, value?: any): Table;
|
||||
};
|
||||
interface GlobalDescriptor<T extends ValueType = ValueType> {
|
||||
mutable?: boolean;
|
||||
value: T;
|
||||
}
|
||||
interface MemoryDescriptor {
|
||||
initial: number;
|
||||
maximum?: number;
|
||||
shared?: boolean;
|
||||
}
|
||||
interface ModuleExportDescriptor {
|
||||
kind: ImportExportKind;
|
||||
name: string;
|
||||
}
|
||||
interface ModuleImportDescriptor {
|
||||
kind: ImportExportKind;
|
||||
module: string;
|
||||
name: string;
|
||||
}
|
||||
interface TableDescriptor {
|
||||
element: TableKind;
|
||||
initial: number;
|
||||
maximum?: number;
|
||||
}
|
||||
interface ValueTypeMap {
|
||||
anyfunc: Function;
|
||||
externref: any;
|
||||
f32: number;
|
||||
f64: number;
|
||||
i32: number;
|
||||
i64: bigint;
|
||||
v128: never;
|
||||
}
|
||||
interface WebAssemblyInstantiatedSource {
|
||||
instance: Instance;
|
||||
module: Module;
|
||||
}
|
||||
type ImportExportKind = "function" | "global" | "memory" | "table";
|
||||
type TableKind = "anyfunc" | "externref";
|
||||
type ExportValue = Function | Global | Memory | Table;
|
||||
type Exports = Record<string, ExportValue>;
|
||||
type ImportValue = ExportValue | number;
|
||||
type Imports = Record<string, ModuleImports>;
|
||||
type ModuleImports = Record<string, ImportValue>;
|
||||
type ValueType = keyof ValueTypeMap;
|
||||
function compile(bytes: BufferSource): Promise<Module>;
|
||||
function compileStreaming(source: Response | PromiseLike<Response>): Promise<Module>;
|
||||
function instantiate(
|
||||
bytes: BufferSource,
|
||||
importObject?: Imports,
|
||||
): Promise<WebAssemblyInstantiatedSource>;
|
||||
function instantiate(moduleObject: Module, importObject?: Imports): Promise<Instance>;
|
||||
function instantiateStreaming(
|
||||
source: Response | PromiseLike<Response>,
|
||||
importObject?: Imports,
|
||||
): Promise<WebAssemblyInstantiatedSource>;
|
||||
function validate(bytes: BufferSource): boolean;
|
||||
}
|
||||
|
||||
@@ -285,6 +285,11 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
}
|
||||
externalTypes[specifier] = declarationPath;
|
||||
}
|
||||
const wasmArgs = values.wasm ?? [];
|
||||
if (wasmArgs.length > 0 && command !== "coverage") {
|
||||
fail(`--wasm is a coverage-only option\n\n${USAGE}`);
|
||||
}
|
||||
const wasmFiles = wasmArgs.map((file) => resolve(file));
|
||||
const ffiProfilePath = values.ffi !== undefined ? resolve(values.ffi) : undefined;
|
||||
if (values.backend !== undefined && values.backend !== "llvm") {
|
||||
fail(`unknown backend "${values.backend}" (supported: llvm)\n\n${USAGE}`);
|
||||
@@ -364,6 +369,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}),
|
||||
...(tier !== undefined ? { tier } : {}),
|
||||
...(explainDynamic !== undefined ? { explainDynamic: true } : {}),
|
||||
...(wasmFiles.length > 0 ? { wasm: wasmFiles } : {}),
|
||||
});
|
||||
if (printDiagnostics) {
|
||||
// Keep stdout pure JSON for tooling.
|
||||
|
||||
@@ -80,6 +80,11 @@ Options:
|
||||
values and the bindings, fields, functions, classes and
|
||||
modules the dynamic tier would take over, with the
|
||||
refusal behind each; a name shows only that unit's chain
|
||||
--wasm <file.wasm>
|
||||
coverage only: report a WebAssembly module the program
|
||||
loads whose path its sources do not spell (repeatable;
|
||||
modules the sources name, inline modules and packaged
|
||||
.wasm files are found without it)
|
||||
-h, --help show this help
|
||||
-v, --version print the version
|
||||
`;
|
||||
@@ -103,6 +108,7 @@ export const CLI_OPTIONS = {
|
||||
"external-types": { type: "string", multiple: true },
|
||||
tier: { type: "string" },
|
||||
"explain-dynamic": { type: "string" },
|
||||
wasm: { type: "string", multiple: true },
|
||||
lib: { type: "boolean", default: false },
|
||||
profile: { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
|
||||
@@ -171,6 +171,9 @@ export interface AnalyzeOptions {
|
||||
/** `--explain-dynamic`: attach the tier-assignment report
|
||||
* (CoverageInput.tiers). Such analyses bypass the verdict cache. */
|
||||
explainDynamic?: boolean;
|
||||
/** `--wasm <file>`: WebAssembly module files the program loads that its
|
||||
* sources do not name (absolute paths); reported with the discovered ones. */
|
||||
wasm?: readonly string[];
|
||||
}
|
||||
|
||||
export interface AnalyzeResult {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { renderDiagnostics } from "../diagnostics/render.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { ProvenanceSources } from "../frontend/provenance-registry.js";
|
||||
import type { DemotionUnit, TierReport } from "../frontend/tier-analysis.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
|
||||
export interface CoverageInput {
|
||||
file: string;
|
||||
@@ -57,6 +58,9 @@ export interface CoverageInput {
|
||||
/** --explain-dynamic: the report-only tier assignment (which bindings,
|
||||
* fields, functions, classes and modules the dynamic tier would take). */
|
||||
tiers?: TierReport;
|
||||
/** The WebAssembly modules the build can see (wasm/discover.ts): their
|
||||
* size, validity and the proposals they use. */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
/** True when tsc preflight failed — no coverage can be computed. */
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
@@ -217,6 +221,51 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// The WebAssembly modules the build can see: what translating them would
|
||||
// take (wasm/discover.ts). No module compiles to native code yet; the
|
||||
// sites that load one are blockers or deferred fences above.
|
||||
const wasm = input.wasmModules ?? [];
|
||||
if (wasm.length > 0) {
|
||||
out.push(
|
||||
` ${c(DIM, "WebAssembly modules:")} ${c(DIM, "(found at build time; none compile to native code yet)")}`,
|
||||
);
|
||||
for (const m of wasm) {
|
||||
const name = m.path !== undefined ? displayPath(m.path, opts.root) : "inline module";
|
||||
const state =
|
||||
m.status === "valid"
|
||||
? c(GREEN, "valid")
|
||||
: m.status === "unsupported"
|
||||
? c(YELLOW, `uses ${m.unsupported.join(", ")}`)
|
||||
: c(RED, `${m.status}: ${m.error ?? ""}`);
|
||||
out.push(` ${name} ${c(DIM, `${groupDigits(m.size)} bytes`)} ${state}`);
|
||||
const imports = m.importModules.length > 0 ? ` (${m.importModules.join(", ")})` : "";
|
||||
out.push(
|
||||
` ${c(DIM, `${counted(m.functions, "function")}, ${counted(m.imports, "import")}${imports}, ${counted(m.exports, "export")}`)}`,
|
||||
);
|
||||
if (m.features.length > 0) out.push(` ${c(DIM, `features: ${m.features.join(", ")}`)}`);
|
||||
const shown = m.sources.slice(0, SITES_SHOWN);
|
||||
for (const s of shown) {
|
||||
const at =
|
||||
s.line !== undefined
|
||||
? `${displayPath(s.file, opts.root)}:${s.line}`
|
||||
: displayPath(s.file, opts.root);
|
||||
const how =
|
||||
s.kind === "path"
|
||||
? `path at ${at}`
|
||||
: s.kind === "inline"
|
||||
? `inline at ${at}`
|
||||
: s.kind === "package"
|
||||
? `shipped by the package at ${displayPath(dirOf(s.file), opts.root)}`
|
||||
: `configured by ${at}`;
|
||||
out.push(` ${c(DIM, `found: ${how}`)}`);
|
||||
}
|
||||
if (m.sources.length > shown.length) {
|
||||
out.push(` ${c(DIM, `found: +${m.sources.length - shown.length} more`)}`);
|
||||
}
|
||||
}
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// --provenance-sources: per-package attribution — the answer to "did
|
||||
// the DEPENDENCY's statements compile static?", computed by summing
|
||||
// statsByFile entries under each package's source dir. Elided
|
||||
@@ -335,6 +384,27 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
/** "1 function", "1,698 functions". */
|
||||
function counted(n: number, noun: string): string {
|
||||
return `${groupDigits(n)} ${noun}${n === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
/** 637405 → "637,405". */
|
||||
function groupDigits(n: number): string {
|
||||
const digits = String(n);
|
||||
let out = "";
|
||||
for (let i = 0; i < digits.length; i++) {
|
||||
if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
|
||||
out += digits[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function dirOf(file: string): string {
|
||||
const slash = file.lastIndexOf("/");
|
||||
return slash <= 0 ? file : file.slice(0, slash);
|
||||
}
|
||||
|
||||
function DIMPath(file: string, color: boolean): string {
|
||||
return color ? DIM + file + RESET : file;
|
||||
}
|
||||
@@ -453,6 +523,7 @@ export function coverageEnvelope(
|
||||
},
|
||||
}),
|
||||
...(input.tiers !== undefined ? { tiers: tiersEnvelope(input.tiers, locator) } : {}),
|
||||
...(input.wasmModules !== undefined ? { wasmModules: input.wasmModules } : {}),
|
||||
groups,
|
||||
diagnostics: located,
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ import { InternalCompilerError } from "../errors.js";
|
||||
* harness asserts refusals match entry codes. renderSurfaceManifest is
|
||||
* byte-deterministic: entries sort by id, keys are emitted in one fixed
|
||||
* order, and the output carries no timestamps or absolute paths. */
|
||||
import { FENCE_CODES, UNSUPPORTED } from "../diagnostics/diagnostic.js";
|
||||
import { FENCE_CODES, UNSUPPORTED, webAssemblyDiag } from "../diagnostics/diagnostic.js";
|
||||
import { NODE24_FETCH_COMPAT_PROFILE } from "../compat/fetch-profile.js";
|
||||
import { SUPPORTED_BUILTIN_MODULES, SUPPORTED_NODE_MODULES } from "../frontend/builtin-modules.js";
|
||||
import {
|
||||
@@ -444,6 +444,19 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
}
|
||||
}
|
||||
|
||||
// ── the WebAssembly global: every reached use refuses (webAssemblyDiag) ─
|
||||
{
|
||||
const refusal = webAssemblyDiag("WebAssembly", { file: "", start: 0, end: 0 });
|
||||
add({
|
||||
id: "stdlib.webassembly",
|
||||
kind: "stdlib",
|
||||
name: "globalThis.WebAssembly",
|
||||
status: "unsupported",
|
||||
code: refusal.code,
|
||||
...(refusal.hint !== undefined ? { note: refusal.hint } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
// ── fetch/Web platform: one explicit, versioned compatibility profile ─
|
||||
const fetchTarget =
|
||||
`Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} / ` +
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { AnalyzeOptions, AnalyzeResult } from "../compile-types.js";
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { NpmStaticStatus } from "./report.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
import {
|
||||
FrontendInputTracker,
|
||||
frontendInputsStillMatch,
|
||||
@@ -38,6 +39,7 @@ export interface CachedCoverage {
|
||||
unreached?: { stats: LowerStats; diagnostics: ScrDiagnostic[]; runtimeFences?: ScrDiagnostic[] };
|
||||
divergences?: ScrDiagnostic[];
|
||||
npmStatic?: NpmStaticStatus[];
|
||||
wasmModules?: WasmModuleReport[];
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
|
||||
@@ -125,6 +127,8 @@ export function verdictKey(
|
||||
external.join("\n"),
|
||||
// Demotion (stage S6) makes the tier mode part of the verdict.
|
||||
...(opts.tier === "static-only" ? ["tier=static-only"] : []),
|
||||
// The module files' contents are probes; their names are options.
|
||||
...(opts.wasm !== undefined && opts.wasm.length > 0 ? [`wasm=${opts.wasm.join("\n")}`] : []),
|
||||
].join("\0"),
|
||||
);
|
||||
}
|
||||
@@ -146,6 +150,7 @@ function cacheable(coverage: AnalyzeResult["coverage"]): CachedCoverage | null {
|
||||
if (coverage.unreached !== undefined) out.unreached = coverage.unreached;
|
||||
if (coverage.divergences !== undefined) out.divergences = coverage.divergences;
|
||||
if (coverage.npmStatic !== undefined) out.npmStatic = coverage.npmStatic;
|
||||
if (coverage.wasmModules !== undefined) out.wasmModules = coverage.wasmModules;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -162,6 +167,7 @@ function restore(cached: CachedCoverage, sources: CachedSource[]): AnalyzeResult
|
||||
if (cached.unreached !== undefined) coverage.unreached = cached.unreached;
|
||||
if (cached.divergences !== undefined) coverage.divergences = cached.divergences;
|
||||
if (cached.npmStatic !== undefined) coverage.npmStatic = cached.npmStatic;
|
||||
if (cached.wasmModules !== undefined) coverage.wasmModules = cached.wasmModules;
|
||||
return { coverage, sourceTexts };
|
||||
}
|
||||
|
||||
|
||||
@@ -751,6 +751,20 @@ export function noLoweringDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** A REACHED use of the WebAssembly API (SC2020): scriptc.d.ts declares
|
||||
* its types so packages that mention them typecheck, but no module
|
||||
* compiles to native code yet. `surface` names what was used
|
||||
* ('WebAssembly.compile', 'new WebAssembly.Module', 'globalThis.WebAssembly'). */
|
||||
export function webAssemblyDiag(surface: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2020",
|
||||
message: `'${surface}' is not supported yet: scriptc does not compile WebAssembly modules to native code yet`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)",
|
||||
};
|
||||
}
|
||||
|
||||
/** A resolved native addon needs Node's embedding ABI. The migration uses the
|
||||
* underlying native operation through an ordinary C ABI declaration. */
|
||||
export function nativeAddonDiag(specifier: string, loc: SrcLoc): ScrDiagnostic {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* bumps it. */
|
||||
import { readFileSync } from "node:fs";
|
||||
import type { ScrDiagnostic } from "./diagnostic.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
|
||||
export const DIAGNOSTICS_SCHEMA = "scriptc-diagnostics";
|
||||
export const DIAGNOSTICS_SCHEMA_VERSION = 1;
|
||||
@@ -138,6 +139,9 @@ export interface DiagnosticsEnvelope {
|
||||
artifact?: string;
|
||||
/** coverage --explain-dynamic only: the report-only tier assignment. */
|
||||
tiers?: TiersEnvelope;
|
||||
/** coverage only: the WebAssembly modules the build can see, with their
|
||||
* size, validity, proposals and the sources that found them. */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
groups: DiagnosticGroup[];
|
||||
diagnostics: LocatedDiagnostic[];
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ export function analyzeWithFrontend(
|
||||
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
|
||||
...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
|
||||
...(opts.explainDynamic === true ? { tierReport: { preflight } } : {}),
|
||||
...(opts.wasm !== undefined && opts.wasm.length > 0 ? { wasmFiles: opts.wasm } : {}),
|
||||
});
|
||||
const provenance = provenanceSources();
|
||||
return {
|
||||
@@ -113,6 +114,7 @@ export function analyzeWithFrontend(
|
||||
...(lowered.statsByFile ? { statsByFile: lowered.statsByFile } : {}),
|
||||
...(lowered.provenanceElided ? { provenanceElided: lowered.provenanceElided } : {}),
|
||||
...(lowered.tiers !== undefined ? { tiers: lowered.tiers } : {}),
|
||||
...(lowered.wasmModules !== undefined ? { wasmModules: lowered.wasmModules } : {}),
|
||||
preflightFailed: false,
|
||||
},
|
||||
sourceTexts: fe.sourceTexts(),
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, PoisonError } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { webAssemblyDiag } from "../../../diagnostics/diagnostic.js";
|
||||
import { STRING, type IrExpr } from "../../../ir/ir.js";
|
||||
import { stdlibGlobalNameOf } from "../surfaces.js";
|
||||
|
||||
/* The WebAssembly API. scriptc.d.ts declares its types (lib.dom's
|
||||
* namespace) so that packages whose declarations mention them typecheck,
|
||||
* and the type mapper gives those types the checked-dynamic representation.
|
||||
* No module compiles to native code yet, so every reached use of the API
|
||||
* value fences at its expression with webAssemblyDiag: a build error in
|
||||
* TypeScript sources, a located runtime fence in JavaScript ones. typeof
|
||||
* needs no value and folds to Node's answer. */
|
||||
|
||||
/** Strips the spellings that leave a value unchanged: parentheses, type
|
||||
* assertions, `satisfies` and non-null assertions. */
|
||||
function bare(expr: ts.Expression): ts.Expression {
|
||||
for (;;) {
|
||||
if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) expr = expr.expression;
|
||||
else if (ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) expr = expr.expression;
|
||||
else if (ts.isSatisfiesExpression(expr)) expr = expr.expression;
|
||||
else return expr;
|
||||
}
|
||||
}
|
||||
|
||||
/** The global object itself, as `globalThis` or `global` spells it. */
|
||||
function isGlobalThis(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
const node = bare(expr);
|
||||
return (
|
||||
ts.isIdentifier(node) &&
|
||||
(node.text === "globalThis" || node.text === "global") &&
|
||||
stdlibGlobalNameOf(lowerer, node) === "globalThis"
|
||||
);
|
||||
}
|
||||
|
||||
/** THE WebAssembly namespace value: `WebAssembly` or `globalThis.WebAssembly`
|
||||
* (and the global object reached through a cast, as in
|
||||
* `(globalThis as any).WebAssembly`). */
|
||||
function isWebAssemblyGlobal(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
expr = bare(expr);
|
||||
if (ts.isIdentifier(expr)) {
|
||||
return expr.text === "WebAssembly" && stdlibGlobalNameOf(lowerer, expr) === "WebAssembly";
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr)) {
|
||||
return expr.name.text === "WebAssembly" && isGlobalThis(lowerer, expr.expression);
|
||||
}
|
||||
if (ts.isElementAccessExpression(expr)) {
|
||||
const key = expr.argumentExpression;
|
||||
return (
|
||||
(ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) &&
|
||||
key.text === "WebAssembly" &&
|
||||
isGlobalThis(lowerer, expr.expression)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The surface a WebAssembly API expression names ('WebAssembly',
|
||||
* 'globalThis.WebAssembly', 'WebAssembly.compile'), or null. A cheap
|
||||
* spelling check runs first: almost every expression is rejected without a
|
||||
* checker query. */
|
||||
function webAssemblySurface(lowerer: Lowerer, expr: ts.Expression): string | null {
|
||||
// `globalThis as unknown as { WebAssembly: ... }`: the cast's target type
|
||||
// names the member, and the value it checks is the global object's
|
||||
// WebAssembly (the spelling pi's codemode uses).
|
||||
if (
|
||||
(ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) &&
|
||||
isGlobalThis(lowerer, expr.expression)
|
||||
) {
|
||||
const target = lowerer.checker.getTypeAtLocation(expr.type);
|
||||
return lowerer.checker.getPropertyOfType(target, "WebAssembly") !== undefined
|
||||
? "globalThis.WebAssembly"
|
||||
: null;
|
||||
}
|
||||
const node = bare(expr);
|
||||
if (ts.isIdentifier(node)) {
|
||||
return node.text === "WebAssembly" && isWebAssemblyGlobal(lowerer, node) ? "WebAssembly" : null;
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(node)) {
|
||||
if (node.name.text === "WebAssembly") {
|
||||
return isWebAssemblyGlobal(lowerer, node) ? "globalThis.WebAssembly" : null;
|
||||
}
|
||||
// WebAssembly.<member>, and members of members
|
||||
// (WebAssembly.Module.customSections).
|
||||
const names = [node.name.text];
|
||||
let receiver = bare(node.expression);
|
||||
while (ts.isPropertyAccessExpression(receiver) && receiver.name.text !== "WebAssembly") {
|
||||
names.unshift(receiver.name.text);
|
||||
receiver = bare(receiver.expression);
|
||||
}
|
||||
const spelled =
|
||||
(ts.isIdentifier(receiver) && receiver.text === "WebAssembly") ||
|
||||
(ts.isPropertyAccessExpression(receiver) && receiver.name.text === "WebAssembly");
|
||||
return spelled && isWebAssemblyGlobal(lowerer, receiver)
|
||||
? `WebAssembly.${names.join(".")}`
|
||||
: null;
|
||||
}
|
||||
if (ts.isElementAccessExpression(node)) {
|
||||
return isWebAssemblyGlobal(lowerer, node) ? "globalThis.WebAssembly" : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Fences a reached use of the WebAssembly API. `prefix` qualifies a
|
||||
* construction ('new '). Returns normally when `expr` is anything else. */
|
||||
export function refuseWebAssemblyUse(lowerer: Lowerer, expr: ts.Expression, prefix = ""): void {
|
||||
const surface = webAssemblySurface(lowerer, expr);
|
||||
if (surface === null) return;
|
||||
lowerer.pushDiag(webAssemblyDiag(`${prefix}${surface}`, locOf(expr)));
|
||||
throw new PoisonError();
|
||||
}
|
||||
|
||||
/** `typeof WebAssembly` is "object" and `typeof WebAssembly.<member>` is
|
||||
* "function" for every member the namespace declares, as in Node. The
|
||||
* operand is an effect-free global read, so nothing else evaluates. */
|
||||
export function lowerWebAssemblyTypeof(lowerer: Lowerer, operand: ts.Expression): IrExpr | null {
|
||||
const surface = webAssemblySurface(lowerer, operand);
|
||||
if (surface === null) return null;
|
||||
const loc = locOf(operand);
|
||||
if (surface === "WebAssembly" || surface === "globalThis.WebAssembly") {
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
}
|
||||
const t = lowerer.typeOf(operand);
|
||||
if (
|
||||
lowerer.checker.getCallSignatures(t).length > 0 ||
|
||||
lowerer.checker.getConstructSignatures(t).length > 0
|
||||
) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -315,7 +315,13 @@ export function refreshErrorPropertyDispatch(
|
||||
info.def.name,
|
||||
member,
|
||||
value,
|
||||
[varRef("value", STRING, loc)],
|
||||
[
|
||||
// A JavaScript setter takes its value untyped.
|
||||
lowerer.coerceToExpected(
|
||||
varRef("value", STRING, loc),
|
||||
accessor.sig.params[0]?.type ?? STRING,
|
||||
),
|
||||
],
|
||||
VOID,
|
||||
loc,
|
||||
),
|
||||
|
||||
@@ -11,6 +11,7 @@ import { lowerWorkerMetadata } from "./builtins/workers.js";
|
||||
import { esmImportBindingKind } from "./builtins/module-bindings.js";
|
||||
import { lowerReadlineProperty } from "./builtins/readline.js";
|
||||
import { lowerBlobInstanceof } from "./builtins/blob.js";
|
||||
import { lowerWebAssemblyTypeof, refuseWebAssemblyUse } from "./builtins/webassembly.js";
|
||||
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
|
||||
import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js";
|
||||
import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js";
|
||||
@@ -458,6 +459,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (externalTypeSpecifier !== null) {
|
||||
lowerer.externalHostFence(externalTypeSpecifier, expr);
|
||||
}
|
||||
// Every reached use of the WebAssembly API fences at its expression.
|
||||
refuseWebAssemblyUse(lowerer, expr);
|
||||
|
||||
if (ts.isNumericLiteral(expr)) {
|
||||
const value = Number(expr.text.replace(/_/g, ""));
|
||||
@@ -623,6 +626,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
);
|
||||
}
|
||||
if (ts.isTypeOfExpression(expr)) {
|
||||
const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression);
|
||||
if (webAssembly) return webAssembly;
|
||||
if (lowerer.isStdlibGlobal(expr.expression, "process"))
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
if (lowerer.isStdlibGlobal(expr.expression, "fetch"))
|
||||
@@ -10340,6 +10345,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken
|
||||
) {
|
||||
const test =
|
||||
lowerWebAssemblyTypeofTest(lowerer, expr, loc) ??
|
||||
lowerErrorCodeTypeofTest(lowerer, expr, loc) ??
|
||||
lowerer.lowerCaughtTypeofTest(expr, loc) ??
|
||||
lowerDynTypeofTest(lowerer, expr, loc) ??
|
||||
@@ -11732,6 +11738,24 @@ function errorRootedObjectOf(lowerer: Lowerer, t: ts.Type): IrType | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `typeof WebAssembly === "object"` and `typeof WebAssembly.<member> ===
|
||||
* "function"`: the operand needs no value, so the test folds as bare typeof
|
||||
* does (builtins/webassembly.ts) before any dyn-typed operand lowers. */
|
||||
function lowerWebAssemblyTypeofTest(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const negated = expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
for (const [a, b] of operandPairs(expr)) {
|
||||
if (!ts.isTypeOfExpression(a) || !ts.isStringLiteral(b)) continue;
|
||||
const folded = lowerWebAssemblyTypeof(lowerer, a.expression);
|
||||
if (folded?.kind !== "strLit") continue;
|
||||
return { kind: "boolLit", value: (folded.value === b.text) !== negated, type: BOOL, loc };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `typeof e.code === "string"` / `"undefined"` (and the `!==` forms)
|
||||
* where e — through parens and as-casts — is a catch binding or a value
|
||||
* of %Error-rooted type: the runtime error's code slot is a string
|
||||
@@ -12429,6 +12453,7 @@ export function caughtLocalOf(lowerer: Lowerer, node: ts.Expression): IrLocal |
|
||||
* Folding is limited to side-effect-free operands — dropping a computed
|
||||
* operand would skip its effects, so those are rejected instead. */
|
||||
export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
|
||||
refuseWebAssemblyUse(lowerer, expr.right);
|
||||
// `x instanceof net.Socket` over a union with a netSocket arm — the
|
||||
// h2 compat 'connect' narrowing (lower-server.ts): a union tag test.
|
||||
const sockTest = lowerSocketInstanceOf(lowerer, expr, loc);
|
||||
|
||||
@@ -26,6 +26,9 @@ import { isSettledIteratorValueRead } from "./lower-generators.js";
|
||||
import { concreteCollectionNarrow } from "./collection-narrowing.js";
|
||||
import { lowerWorkerMetadata } from "./builtins/workers.js";
|
||||
import { fenceStaticBlobMember, lowerBlobNew, lowerStaticBlobCall } from "./builtins/blob.js";
|
||||
import { refuseWebAssemblyUse } from "./builtins/webassembly.js";
|
||||
import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover.js";
|
||||
import { resolveRequireRuntime } from "../runtime-resolve.js";
|
||||
import { lowerKeyObjectCall } from "./builtins/key-object.js";
|
||||
import { hashStreamOf } from "./builtins/hash-stream.js";
|
||||
import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js";
|
||||
@@ -724,6 +727,8 @@ export interface LowerResult {
|
||||
/** Coverage only (LowerOptions.coverage): the unreached remainder,
|
||||
* lowered in a throwaway pass — blockers in it can never fail a build. */
|
||||
unreached?: { diagnostics: ScrDiagnostic[]; stats: LowerStats; runtimeFences?: ScrDiagnostic[] };
|
||||
/** Coverage only: the WebAssembly modules the build can see (wasm/discover.ts). */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
/** Compile-time-detectable behavior differences from Node (SC6xxx
|
||||
* divergence warnings) in the program's own modules. Never failures. */
|
||||
divergences?: ScrDiagnostic[];
|
||||
@@ -744,6 +749,8 @@ export interface LowerOptions {
|
||||
* diagnostics and stats under `unreached` — the whole-program analysis
|
||||
* builds deliberately gave up. */
|
||||
coverage?: boolean;
|
||||
/** Coverage only: module files named by --wasm (absolute). */
|
||||
wasmFiles?: readonly string[];
|
||||
/** The platform the build TARGETS ("win32" under a windows cross triple,
|
||||
* the host platform otherwise — see buildTargetPlatform in index.ts).
|
||||
* The whole program compiles for one platform, so the platform-keyed
|
||||
@@ -1125,6 +1132,23 @@ export function lowerToIr(
|
||||
},
|
||||
};
|
||||
if (tiers !== undefined) covered.tiers = tiers;
|
||||
const wasmModules = discoverWasmModules({
|
||||
files: moduleOrder.includes(entry) ? moduleOrder : [...moduleOrder, entry],
|
||||
entry: entry.fileName,
|
||||
configured: options.wasmFiles ?? [],
|
||||
resolveRequest: (fromFile, specifier) => {
|
||||
const resolved = resolveRequireRuntime(
|
||||
fromFile,
|
||||
specifier,
|
||||
targetPlatform,
|
||||
undefined,
|
||||
options.frontendServices,
|
||||
);
|
||||
return resolved.ok ? resolved.value : null;
|
||||
},
|
||||
});
|
||||
timing("wasm-discovery", { modules: wasmModules.length });
|
||||
if (wasmModules.length > 0) covered.wasmModules = wasmModules;
|
||||
return covered;
|
||||
}
|
||||
|
||||
@@ -11405,6 +11429,7 @@ export class Lowerer {
|
||||
// dispatch. The general identifier-call paths have no lowering for a
|
||||
// promise-returning ambient global, and `import` is a keyword callee no
|
||||
// identifier path matches.
|
||||
refuseWebAssemblyUse(this, expr.expression);
|
||||
return (
|
||||
lowerLifetimeRegionCall(this, expr) ??
|
||||
lowerInternTableCall(this, expr) ??
|
||||
@@ -11630,6 +11655,7 @@ export class Lowerer {
|
||||
}
|
||||
|
||||
lowerNew(expr: ts.NewExpression): IrExpr {
|
||||
refuseWebAssemblyUse(this, expr.expression, "new ");
|
||||
const builtin =
|
||||
lowerLifetimeRegionNew(this, expr) ??
|
||||
lowerInternTableNew(this, expr) ??
|
||||
|
||||
@@ -920,6 +920,24 @@ function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean
|
||||
return false;
|
||||
}
|
||||
|
||||
/** A type declared in the shipped WebAssembly namespace (scriptc.d.ts):
|
||||
* its modules, instances, memories, tables, globals, error classes and the
|
||||
* Exports/Imports records. */
|
||||
export function isWebAssemblyType(
|
||||
type: ts.Type,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
): boolean {
|
||||
const symbol = type.getAliasSymbol() ?? type.getSymbol();
|
||||
if (!symbol) return false;
|
||||
return ctx.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(d) =>
|
||||
ctx.isStdlibFile(d.getSourceFile()) &&
|
||||
isDeclaredInAmbientNamespace(d as ts.Declaration, "WebAssembly"),
|
||||
);
|
||||
}
|
||||
|
||||
/** True when a type contains a record anywhere (the trigger for the
|
||||
* shape-mismatch diagnostic SC2002 — non-record mismatches are tsc's or
|
||||
* the validator's business). */
|
||||
@@ -1419,6 +1437,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
return { kind: "object", className: "%Error" };
|
||||
}
|
||||
if (isOsConstantsType(type, ctx) || isLegacyUrlType(type, ctx)) return DYN;
|
||||
// The WebAssembly API's values have the checked-dynamic representation, so
|
||||
// declarations that merely mention them compile; each reached use of the
|
||||
// API itself is refused at its site (builtins/webassembly.ts).
|
||||
if (isWebAssemblyType(type, ctx)) return DYN;
|
||||
// A union over one of them (quickjs-wasi's `BufferSource | WebAssembly.Module`
|
||||
// option) holds that dynamic value too, so the whole union is dynamic.
|
||||
if (type.isUnionType() && ts.constituentTypes(type).some((t) => isWebAssemblyType(t, ctx)))
|
||||
return DYN;
|
||||
// Immutable<T> (@scriptc/types) is a deep-readonly view: the value is
|
||||
// still the T it maps.
|
||||
const immutableSource = immutableSourceType(type, ctx);
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { analyzeWasm, isTranslatableFeature, isWasmHeader } from "./binary.js";
|
||||
|
||||
/** V8's verdict (the compiler's type world declares no WebAssembly value). */
|
||||
const v8Validate = (bytes: Uint8Array): boolean =>
|
||||
(
|
||||
globalThis as unknown as { WebAssembly: { validate(bytes: Uint8Array): boolean } }
|
||||
).WebAssembly.validate(bytes);
|
||||
|
||||
// A minimal module assembler: enough structure to aim at one decoder rule
|
||||
// per test. Every module is also checked against V8 (Node is the oracle).
|
||||
|
||||
function leb(n: number): number[] {
|
||||
const out: number[] = [];
|
||||
do {
|
||||
let b = n % 128;
|
||||
n = Math.floor(n / 128);
|
||||
if (n > 0) b |= 0x80;
|
||||
out.push(b);
|
||||
} while (n > 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
function name(text: string): number[] {
|
||||
const bytes = [...new TextEncoder().encode(text)];
|
||||
return [...leb(bytes.length), ...bytes];
|
||||
}
|
||||
|
||||
function vec(items: number[][]): number[] {
|
||||
return [...leb(items.length), ...items.flat()];
|
||||
}
|
||||
|
||||
function section(id: number, body: number[]): number[] {
|
||||
return [id, ...leb(body.length), ...body];
|
||||
}
|
||||
|
||||
function module(...sections: number[][]): Uint8Array {
|
||||
return new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...sections.flat()]);
|
||||
}
|
||||
|
||||
const I32 = 0x7f;
|
||||
const I64 = 0x7e;
|
||||
const F32 = 0x7d;
|
||||
const V128 = 0x7b;
|
||||
const EXTERNREF = 0x6f;
|
||||
|
||||
const funcType = (params: number[], results: number[]): number[] => [
|
||||
0x60,
|
||||
...vec(params.map((p) => [p])),
|
||||
...vec(results.map((r) => [r])),
|
||||
];
|
||||
const types = (...fts: number[][]): number[] => section(1, vec(fts));
|
||||
const funcs = (...typeIndices: number[]): number[] =>
|
||||
section(3, vec(typeIndices.map((t) => leb(t))));
|
||||
const body = (code: number[], locals: number[][] = []): number[] => {
|
||||
const content = [...vec(locals), ...code, 0x0b];
|
||||
return [...leb(content.length), ...content];
|
||||
};
|
||||
const code = (...bodies: number[][]): number[] => section(10, vec(bodies));
|
||||
const memory = (...limits: number[][]): number[] => section(5, vec(limits));
|
||||
const exportsOf = (...entries: number[][]): number[] => section(7, vec(entries));
|
||||
|
||||
function check(bytes: Uint8Array) {
|
||||
const summary = analyzeWasm(bytes);
|
||||
const accepted = summary.status === "valid" || summary.status === "unsupported";
|
||||
expect(accepted, summary.error ?? "").toBe(v8Validate(bytes));
|
||||
return summary;
|
||||
}
|
||||
|
||||
describe("analyzeWasm", () => {
|
||||
test("an empty module is valid and uses no proposal", () => {
|
||||
const s = check(module());
|
||||
expect(s).toMatchObject({ status: "valid", error: null, features: [], functions: 0, size: 8 });
|
||||
expect(isWasmHeader(module())).toBe(true);
|
||||
});
|
||||
|
||||
test("counts functions, imports, exports, memories, tables and globals", () => {
|
||||
const s = check(
|
||||
module(
|
||||
types(funcType([I32, I32], [I32]), funcType([], [])),
|
||||
section(
|
||||
2,
|
||||
vec([
|
||||
[...name("env"), ...name("log"), 0x00, 0x01],
|
||||
[...name("wasi_snapshot_preview1"), ...name("proc_exit"), 0x00, 0x01],
|
||||
]),
|
||||
),
|
||||
funcs(0),
|
||||
section(4, vec([[0x70, 0x00, 0x01]])),
|
||||
memory([0x00, 0x01]),
|
||||
section(6, vec([[I32, 0x01, 0x41, 0x00, 0x0b]])),
|
||||
exportsOf([...name("add"), 0x00, 0x02], [...name("memory"), 0x02, 0x00]),
|
||||
code(body([0x20, 0x00, 0x20, 0x01, 0x6a])),
|
||||
),
|
||||
);
|
||||
expect(s).toMatchObject({
|
||||
status: "valid",
|
||||
functions: 1,
|
||||
imports: 2,
|
||||
exports: 2,
|
||||
importModules: ["env", "wasi_snapshot_preview1"],
|
||||
memories: 1,
|
||||
tables: 1,
|
||||
globals: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
[
|
||||
"sign-extension",
|
||||
module(types(funcType([I32], [I32])), funcs(0), code(body([0x20, 0x00, 0xc0]))),
|
||||
],
|
||||
[
|
||||
"nontrapping-fptoint",
|
||||
module(types(funcType([F32], [I32])), funcs(0), code(body([0x20, 0x00, 0xfc, 0x00]))),
|
||||
],
|
||||
[
|
||||
"bulk-memory",
|
||||
module(
|
||||
types(funcType([], [])),
|
||||
funcs(0),
|
||||
memory([0x00, 0x01]),
|
||||
code(body([0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xfc, 0x0b, 0x00])),
|
||||
),
|
||||
],
|
||||
[
|
||||
"multi-value",
|
||||
module(types(funcType([], [I32, I64])), funcs(0), code(body([0x41, 0x01, 0x42, 0x02]))),
|
||||
],
|
||||
["reference-types", module(section(4, vec([[EXTERNREF, 0x00, 0x01]])))],
|
||||
["extended-const", module(section(6, vec([[I32, 0x00, 0x41, 0x01, 0x41, 0x02, 0x6a, 0x0b]])))],
|
||||
[
|
||||
"mutable-globals",
|
||||
module(section(2, vec([[...name("env"), ...name("sp"), 0x03, I32, 0x01]]))),
|
||||
],
|
||||
])("detects %s and stays translatable", (feature, bytes) => {
|
||||
const s = check(bytes);
|
||||
expect(s.status).toBe("valid");
|
||||
expect(s.features).toContain(feature);
|
||||
expect(isTranslatableFeature(feature)).toBe(true);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[
|
||||
"simd",
|
||||
module(
|
||||
types(funcType([], [V128])),
|
||||
funcs(0),
|
||||
code(body([0xfd, 0x0c, ...Array.from({ length: 16 }, () => 0)])),
|
||||
),
|
||||
],
|
||||
["threads", module(memory([0x03, 0x01, 0x01]))],
|
||||
["exceptions", module(types(funcType([], [])), section(13, vec([[0x00, 0x00]])))],
|
||||
["tail-call", module(types(funcType([], [])), funcs(0), code(body([0x12, 0x00])))],
|
||||
["memory64", module(memory([0x04, 0x01]))],
|
||||
["multi-memory", module(memory([0x00, 0x01], [0x00, 0x01]))],
|
||||
["gc", module(section(1, vec([[0x5f, 0x00]])))],
|
||||
])("reports %s as outside the translatable set", (feature, bytes) => {
|
||||
const s = check(bytes);
|
||||
expect(s.status).toBe("unsupported");
|
||||
expect(s.unsupported).toEqual([feature]);
|
||||
expect(isTranslatableFeature(feature)).toBe(false);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["bad magic", new Uint8Array([0x00, 0x61, 0x73, 0x6e, 1, 0, 0, 0]), "malformed", "magic"],
|
||||
["bad version", new Uint8Array([0x00, 0x61, 0x73, 0x6d, 2, 0, 0, 0]), "malformed", "version"],
|
||||
["sections out of order", module(funcs(), types()), "malformed", "unexpected section"],
|
||||
["section size mismatch", module([0x01, 0x02, 0x00, 0x00]), "malformed", "section size"],
|
||||
[
|
||||
"overlong integer",
|
||||
module([0x01, 0x06, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00]),
|
||||
"malformed",
|
||||
"integer",
|
||||
],
|
||||
["invalid UTF-8 name", module(exportsOf([0x01, 0xff, 0x00, 0x00])), "malformed", "UTF-8"],
|
||||
[
|
||||
"function and code counts differ",
|
||||
module(types(funcType([], [])), funcs(0, 0), code(body([]))),
|
||||
"malformed",
|
||||
"inconsistent",
|
||||
],
|
||||
[
|
||||
"illegal opcode",
|
||||
module(types(funcType([], [])), funcs(0), code(body([0xff]))),
|
||||
"malformed",
|
||||
"illegal opcode",
|
||||
],
|
||||
[
|
||||
"memory.init without a data count section",
|
||||
module(
|
||||
types(funcType([], [])),
|
||||
funcs(0),
|
||||
memory([0x00, 0x01]),
|
||||
code(body([0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xfc, 0x08, 0x00, 0x00])),
|
||||
),
|
||||
"malformed",
|
||||
"data count",
|
||||
],
|
||||
[
|
||||
"type mismatch",
|
||||
module(types(funcType([], [I32])), funcs(0), code(body([0x42, 0x00]))),
|
||||
"invalid",
|
||||
"type mismatch",
|
||||
],
|
||||
[
|
||||
"unknown function",
|
||||
module(types(funcType([], [])), funcs(0), code(body([0x10, 0x05]))),
|
||||
"invalid",
|
||||
"unknown function",
|
||||
],
|
||||
[
|
||||
"duplicate export",
|
||||
module(
|
||||
types(funcType([], [])),
|
||||
funcs(0),
|
||||
exportsOf([...name("f"), 0x00, 0x00], [...name("f"), 0x00, 0x00]),
|
||||
code(body([])),
|
||||
),
|
||||
"invalid",
|
||||
"duplicate export",
|
||||
],
|
||||
["memory above 4 GiB", module(memory([0x00, ...leb(65537)])), "invalid", "65536 pages"],
|
||||
[
|
||||
"undeclared function reference",
|
||||
module(types(funcType([], [])), funcs(0), code(body([0xd2, 0x00, 0x1a]))),
|
||||
"invalid",
|
||||
"undeclared",
|
||||
],
|
||||
])("rejects %s", (_label, bytes, status, message) => {
|
||||
const s = check(bytes);
|
||||
expect(s.status).toBe(status);
|
||||
expect(s.error).toContain(message);
|
||||
});
|
||||
|
||||
test("never throws on truncated input", () => {
|
||||
const whole = module(
|
||||
types(funcType([I32, I32], [I32])),
|
||||
funcs(0),
|
||||
code(body([0x20, 0x00, 0x20, 0x01, 0x6a])),
|
||||
);
|
||||
for (let n = 0; n < whole.length; n++) check(whole.subarray(0, n));
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { decodeBase64, packageRootOf } from "./discover.js";
|
||||
|
||||
describe("decodeBase64", () => {
|
||||
test("decodes standard and URL-safe spellings, with or without padding", () => {
|
||||
const bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 0xfb, 0xff]);
|
||||
const standard = Buffer.from(bytes).toString("base64");
|
||||
expect(decodeBase64(standard)).toEqual(bytes);
|
||||
expect(decodeBase64(standard.replace(/=+$/, ""))).toEqual(bytes);
|
||||
expect(decodeBase64(Buffer.from(bytes).toString("base64url"))).toEqual(bytes);
|
||||
});
|
||||
|
||||
test("answers null for text that is not base64", () => {
|
||||
expect(decodeBase64("AGFzbQ*")).toBeNull();
|
||||
expect(decodeBase64("AGFzb")).toBeNull();
|
||||
expect(decodeBase64("AGFzbQ===")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("packageRootOf", () => {
|
||||
test("finds the innermost package, scoped or not", () => {
|
||||
expect(packageRootOf("/p/node_modules/quickjs-wasi/dist/index.js")).toBe(
|
||||
"/p/node_modules/quickjs-wasi",
|
||||
);
|
||||
expect(packageRootOf("/p/node_modules/a/node_modules/@scope/b/lib/x.js")).toBe(
|
||||
"/p/node_modules/a/node_modules/@scope/b",
|
||||
);
|
||||
expect(packageRootOf("/p/src/main.ts")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,426 @@
|
||||
/* Build-time discovery of the WebAssembly modules a program can load.
|
||||
*
|
||||
* `scriptc coverage` reports every module whose bytes the build can see,
|
||||
* from four sources:
|
||||
*
|
||||
* 1. path: a `.wasm` path the compiler resolves from the source text
|
||||
* (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`,
|
||||
* `new URL("./x.wasm", import.meta.url)`, and `path.join(__dirname, ...)`
|
||||
* or `__dirname + "/x.wasm"`);
|
||||
* 2. inline: a string literal that decodes as base64 to a module, or an
|
||||
* array literal of its bytes;
|
||||
* 3. package: every `.wasm` file shipped by a reached npm package whose
|
||||
* program files mention `WebAssembly` (this finds paths computed at run
|
||||
* time, such as a loader's default argument);
|
||||
* 4. config: the `scriptc.wasm` list in the entry's package.json and the
|
||||
* `--wasm <file>` flag.
|
||||
*
|
||||
* Each module is decoded and validated (binary.ts) and reported once, by
|
||||
* content, with every source that found it. Nothing here changes what a
|
||||
* build emits. */
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import * as ts from "../frontend/ts7/adapter.js";
|
||||
import {
|
||||
trackedAccessibleEntries,
|
||||
trackedFileExists,
|
||||
trackedReadFile,
|
||||
} from "../frontend/input-tracker.js";
|
||||
import { analyzeWasm, isWasmHeader, type WasmModuleSummary } from "./binary.js";
|
||||
|
||||
export type WasmSourceKind = "path" | "inline" | "package" | "config";
|
||||
|
||||
export interface WasmSource {
|
||||
kind: WasmSourceKind;
|
||||
/** The source file (path, inline) or the file that names the module. */
|
||||
file: string;
|
||||
/** 1-based line of the site, when there is one. */
|
||||
line?: number;
|
||||
}
|
||||
|
||||
/** One discovered module. JSON-shaped: the coverage verdict cache stores it. */
|
||||
export interface WasmModuleReport extends WasmModuleSummary {
|
||||
sha256: string;
|
||||
/** The module file, absent for inline modules. */
|
||||
path?: string;
|
||||
sources: WasmSource[];
|
||||
}
|
||||
|
||||
export interface WasmDiscoveryInput {
|
||||
/** The program's source files (entry and modules). */
|
||||
files: readonly ts.SourceFile[];
|
||||
/** The entry file. */
|
||||
entry: string;
|
||||
/** --wasm files, absolute. */
|
||||
configured: readonly string[];
|
||||
/** require.resolve from `fromFile`, or null when it does not resolve. */
|
||||
resolveRequest: (fromFile: string, specifier: string) => string | null;
|
||||
}
|
||||
|
||||
const SCAN_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
||||
ts.SyntaxKind.StringLiteral,
|
||||
ts.SyntaxKind.NoSubstitutionTemplateLiteral,
|
||||
ts.SyntaxKind.CallExpression,
|
||||
ts.SyntaxKind.NewExpression,
|
||||
ts.SyntaxKind.BinaryExpression,
|
||||
ts.SyntaxKind.ArrayLiteralExpression,
|
||||
]);
|
||||
|
||||
/** The base64 spelling of a module's first bytes ("\0asm"). */
|
||||
const BASE64_MAGIC = "AGFzbQ";
|
||||
const BYTES_MAGIC = /\[\s*(?:0|0x0+)\s*,\s*(?:97|0x61)\s*,\s*(?:115|0x73)\s*,\s*(?:109|0x6d)\s*,/i;
|
||||
/** Package directories deeper than this are not searched for modules. */
|
||||
const PACKAGE_SCAN_DEPTH = 6;
|
||||
/** Larger files are reported by size but not read. */
|
||||
const MAX_MODULE_BYTES = 256 * 1024 * 1024;
|
||||
|
||||
class Discovery {
|
||||
input: WasmDiscoveryInput;
|
||||
byHash = new Map<string, WasmModuleReport>();
|
||||
byPath = new Map<string, string>();
|
||||
order: string[] = [];
|
||||
|
||||
constructor(input: WasmDiscoveryInput) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
/** Records a module and one source that found it; answers its hash. */
|
||||
addBytes(bytes: Uint8Array, path: string | undefined, source: WasmSource): string {
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
let report = this.byHash.get(sha256);
|
||||
if (report === undefined) {
|
||||
const summary = analyzeWasm(bytes);
|
||||
report = {
|
||||
size: summary.size,
|
||||
status: summary.status,
|
||||
error: summary.error,
|
||||
features: summary.features,
|
||||
unsupported: summary.unsupported,
|
||||
functions: summary.functions,
|
||||
imports: summary.imports,
|
||||
exports: summary.exports,
|
||||
importModules: summary.importModules,
|
||||
memories: summary.memories,
|
||||
tables: summary.tables,
|
||||
globals: summary.globals,
|
||||
customSections: summary.customSections,
|
||||
sha256,
|
||||
sources: [],
|
||||
};
|
||||
this.byHash.set(sha256, report);
|
||||
this.order.push(sha256);
|
||||
}
|
||||
if (report.path === undefined && path !== undefined) report.path = path;
|
||||
addSource(report, source);
|
||||
return sha256;
|
||||
}
|
||||
|
||||
addFile(path: string, source: WasmSource): void {
|
||||
path = resolve(path);
|
||||
const known = this.byPath.get(path);
|
||||
if (known !== undefined) {
|
||||
addSource(this.byHash.get(known)!, source);
|
||||
return;
|
||||
}
|
||||
// The tracked read makes the coverage verdict cache depend on the file.
|
||||
if (trackedReadFile(path) === null) return;
|
||||
let bytes: Uint8Array;
|
||||
try {
|
||||
bytes = new Uint8Array(readFileSync(path));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (bytes.length > MAX_MODULE_BYTES) return;
|
||||
this.byPath.set(path, this.addBytes(bytes, path, source));
|
||||
}
|
||||
|
||||
// ── source 1 and 2: the program's source text ───────────────────────
|
||||
|
||||
scanFile(sf: ts.SourceFile): void {
|
||||
const text = sf.text;
|
||||
const paths = text.includes(".wasm");
|
||||
const base64 = text.includes(BASE64_MAGIC);
|
||||
const bytes = BYTES_MAGIC.test(text);
|
||||
if (!paths && !base64 && !bytes) return;
|
||||
const file = sf.fileName;
|
||||
const lineOf = (node: ts.Node): number =>
|
||||
ts.getLineAndCharacterOfPosition(sf, node.getStart()).line + 1;
|
||||
ts.walkPreorder(
|
||||
sf,
|
||||
(node) => {
|
||||
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
||||
if (base64 && node.text.startsWith(BASE64_MAGIC)) {
|
||||
const decoded = decodeBase64(node.text);
|
||||
if (decoded !== null && isWasmHeader(decoded)) {
|
||||
this.addBytes(decoded, undefined, { kind: "inline", file, line: lineOf(node) });
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (ts.isArrayLiteralExpression(node)) {
|
||||
if (bytes) {
|
||||
const literal = byteArrayLiteral(node);
|
||||
if (literal !== null && isWasmHeader(literal)) {
|
||||
this.addBytes(literal, undefined, { kind: "inline", file, line: lineOf(node) });
|
||||
return "skip";
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (!paths) return undefined;
|
||||
const path = this.resolvedPath(sf, node);
|
||||
if (path !== null) this.addFile(path, { kind: "path", file, line: lineOf(node) });
|
||||
return undefined;
|
||||
},
|
||||
SCAN_KINDS,
|
||||
);
|
||||
}
|
||||
|
||||
/** The module path a resolution expression names, when it ends in .wasm. */
|
||||
resolvedPath(sf: ts.SourceFile, node: ts.Node): string | null {
|
||||
const dir = dirname(sf.fileName);
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callee = node.expression;
|
||||
const first = node.arguments[0];
|
||||
if (first === undefined) return null;
|
||||
// require.resolve / createRequire(...).resolve / import.meta.resolve
|
||||
if (ts.isPropertyAccessExpression(callee) && callee.name.text === "resolve") {
|
||||
const specifier = wasmLiteral(first);
|
||||
if (specifier === null) {
|
||||
return this.joinedPath(dir, node.arguments);
|
||||
}
|
||||
const receiver = callee.expression;
|
||||
if (ts.isMetaProperty(receiver) || isRequireLike(receiver)) {
|
||||
if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
||||
return resolve(dir, specifier);
|
||||
}
|
||||
if (isAbsolute(specifier)) return specifier;
|
||||
return this.input.resolveRequest(sf.fileName, specifier);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// path.join(__dirname, "x.wasm") and its resolve/bare spellings
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(callee) &&
|
||||
(callee.name.text === "join" || callee.name.text === "resolve")) ||
|
||||
(ts.isIdentifier(callee) && (callee.text === "join" || callee.text === "resolve"))
|
||||
) {
|
||||
return this.joinedPath(dir, node.arguments);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (ts.isNewExpression(node)) {
|
||||
// new URL("./x.wasm", import.meta.url)
|
||||
if (!ts.isIdentifier(node.expression) || node.expression.text !== "URL") return null;
|
||||
const args = node.arguments;
|
||||
if (args === undefined || args.length !== 2) return null;
|
||||
const specifier = wasmLiteral(args[0]!);
|
||||
const base = args[1]!;
|
||||
if (specifier === null || !isImportMetaUrl(base)) return null;
|
||||
if (specifier.startsWith("./") || specifier.startsWith("../")) return resolve(dir, specifier);
|
||||
return null;
|
||||
}
|
||||
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
||||
// __dirname + "/x.wasm"
|
||||
if (!isDirname(node.left)) return null;
|
||||
const rest = wasmLiteral(node.right);
|
||||
return rest === null ? null : join(dir, rest);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
joinedPath(dir: string, args: readonly ts.Expression[]): string | null {
|
||||
if (args.length < 2 || !isDirname(args[0]!)) return null;
|
||||
const parts: string[] = [dir];
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const arg = args[i]!;
|
||||
if (!ts.isStringLiteral(arg) && !ts.isNoSubstitutionTemplateLiteral(arg)) return null;
|
||||
parts.push(arg.text);
|
||||
}
|
||||
if (!parts[parts.length - 1]!.endsWith(".wasm")) return null;
|
||||
let path = parts[0]!;
|
||||
for (let i = 1; i < parts.length; i++) path = resolve(path, parts[i]!);
|
||||
return path;
|
||||
}
|
||||
|
||||
// ── source 3: package files ─────────────────────────────────────────
|
||||
|
||||
scanPackages(): void {
|
||||
const roots = new Map<string, boolean>();
|
||||
for (const sf of this.input.files) {
|
||||
const root = packageRootOf(sf.fileName);
|
||||
if (root === null) continue;
|
||||
const mentions = sf.text.includes("WebAssembly");
|
||||
roots.set(root, (roots.get(root) ?? false) || mentions);
|
||||
}
|
||||
for (const [root, mentions] of roots) {
|
||||
if (!mentions) continue;
|
||||
const found: string[] = [];
|
||||
collectWasmFiles(root, 0, found);
|
||||
found.sort();
|
||||
for (const path of found)
|
||||
this.addFile(path, { kind: "package", file: join(root, "package.json") });
|
||||
}
|
||||
}
|
||||
|
||||
// ── source 4: configuration ─────────────────────────────────────────
|
||||
|
||||
scanConfig(): void {
|
||||
const manifest = nearestPackageJson(dirname(this.input.entry));
|
||||
if (manifest !== null) {
|
||||
const text = trackedReadFile(manifest);
|
||||
if (text !== null) {
|
||||
let listed: unknown;
|
||||
try {
|
||||
const parsed = JSON.parse(text) as { scriptc?: { wasm?: unknown } };
|
||||
listed = parsed.scriptc?.wasm;
|
||||
} catch {
|
||||
listed = undefined;
|
||||
}
|
||||
if (Array.isArray(listed)) {
|
||||
for (const entry of listed) {
|
||||
if (typeof entry === "string") {
|
||||
this.addFile(resolve(dirname(manifest), entry), { kind: "config", file: manifest });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const path of this.input.configured) {
|
||||
this.addFile(path, { kind: "config", file: path });
|
||||
}
|
||||
}
|
||||
|
||||
run(): WasmModuleReport[] {
|
||||
for (const sf of this.input.files) this.scanFile(sf);
|
||||
this.scanPackages();
|
||||
this.scanConfig();
|
||||
return this.order.map((sha) => this.byHash.get(sha)!);
|
||||
}
|
||||
}
|
||||
|
||||
function addSource(report: WasmModuleReport, source: WasmSource): void {
|
||||
for (const s of report.sources) {
|
||||
if (s.kind === source.kind && s.file === source.file && s.line === source.line) return;
|
||||
}
|
||||
report.sources.push(source);
|
||||
}
|
||||
|
||||
function wasmLiteral(node: ts.Expression): string | null {
|
||||
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) return null;
|
||||
return node.text.endsWith(".wasm") ? node.text : null;
|
||||
}
|
||||
|
||||
function isDirname(node: ts.Expression): boolean {
|
||||
return ts.isIdentifier(node) && node.text === "__dirname";
|
||||
}
|
||||
|
||||
function isImportMetaUrl(node: ts.Expression): boolean {
|
||||
return (
|
||||
ts.isPropertyAccessExpression(node) &&
|
||||
node.name.text === "url" &&
|
||||
ts.isMetaProperty(node.expression)
|
||||
);
|
||||
}
|
||||
|
||||
/** `require`, a `createRequire(...)` call, or a binding named like one. */
|
||||
function isRequireLike(node: ts.Expression): boolean {
|
||||
if (ts.isIdentifier(node)) return node.text === "require" || /require$/i.test(node.text);
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callee = node.expression;
|
||||
return (
|
||||
(ts.isIdentifier(callee) && callee.text === "createRequire") ||
|
||||
(ts.isPropertyAccessExpression(callee) && callee.name.text === "createRequire")
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The bytes of an array literal of integer literals, or null. */
|
||||
function byteArrayLiteral(node: ts.ArrayLiteralExpression): Uint8Array | null {
|
||||
const elements = node.elements;
|
||||
const out = new Uint8Array(elements.length);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i]!;
|
||||
if (!ts.isNumericLiteral(element)) return null;
|
||||
const value = Number(element.text);
|
||||
if (!Number.isInteger(value) || value < 0 || value > 255) return null;
|
||||
out[i] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const BASE64_VALUES = (() => {
|
||||
const table = new Int16Array(128).fill(-1);
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
for (let i = 0; i < alphabet.length; i++) table[alphabet.charCodeAt(i)] = i;
|
||||
// base64url spellings
|
||||
table["-".charCodeAt(0)] = 62;
|
||||
table["_".charCodeAt(0)] = 63;
|
||||
return table;
|
||||
})();
|
||||
|
||||
/** Decodes standard or URL-safe base64 (padding optional); null when the
|
||||
* text is not base64. */
|
||||
export function decodeBase64(text: string): Uint8Array | null {
|
||||
let end = text.length;
|
||||
while (end > 0 && text.charCodeAt(end - 1) === 61) end--;
|
||||
if (text.length - end > 2 || end % 4 === 1) return null;
|
||||
const out = new Uint8Array(Math.floor((end * 3) / 4));
|
||||
let acc = 0;
|
||||
let bits = 0;
|
||||
let j = 0;
|
||||
for (let i = 0; i < end; i++) {
|
||||
const c = text.charCodeAt(i);
|
||||
const v = c < 128 ? BASE64_VALUES[c]! : -1;
|
||||
if (v < 0) return null;
|
||||
acc = ((acc << 6) | v) & 0xffffff;
|
||||
bits += 6;
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out[j++] = (acc >> bits) & 0xff;
|
||||
}
|
||||
}
|
||||
return j === out.length ? out : out.subarray(0, j);
|
||||
}
|
||||
|
||||
/** The root directory of the npm package that contains `file` (the
|
||||
* directory below its innermost node_modules), or null outside one. */
|
||||
export function packageRootOf(file: string): string | null {
|
||||
const marker = "/node_modules/";
|
||||
const at = file.lastIndexOf(marker);
|
||||
if (at < 0) return null;
|
||||
const rest = file.slice(at + marker.length);
|
||||
const segments = rest.split("/");
|
||||
const take = segments[0]!.startsWith("@") ? 2 : 1;
|
||||
if (segments.length <= take) return null;
|
||||
return file.slice(0, at + marker.length) + segments.slice(0, take).join("/");
|
||||
}
|
||||
|
||||
function collectWasmFiles(dir: string, depth: number, out: string[]): void {
|
||||
const entries = trackedAccessibleEntries(dir);
|
||||
if (entries === null) return;
|
||||
for (const name of entries.files) if (name.endsWith(".wasm")) out.push(join(dir, name));
|
||||
if (depth >= PACKAGE_SCAN_DEPTH) return;
|
||||
for (const name of entries.directories) {
|
||||
if (name === "node_modules" || name.startsWith(".")) continue;
|
||||
collectWasmFiles(join(dir, name), depth + 1, out);
|
||||
}
|
||||
}
|
||||
|
||||
function nearestPackageJson(dir: string): string | null {
|
||||
for (;;) {
|
||||
const candidate = join(dir, "package.json");
|
||||
if (trackedFileExists(candidate)) return candidate;
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) return null;
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/** Every WebAssembly module the program's sources, packages and
|
||||
* configuration make visible to the build, in discovery order. */
|
||||
export function discoverWasmModules(input: WasmDiscoveryInput): WasmModuleReport[] {
|
||||
return new Discovery(input).run();
|
||||
}
|
||||
@@ -5630,6 +5630,14 @@
|
||||
"status": "static",
|
||||
"note": "the lowered call form takes no arguments"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.webassembly",
|
||||
"kind": "stdlib",
|
||||
"name": "globalThis.WebAssembly",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)"
|
||||
},
|
||||
{
|
||||
"id": "syntax.async-generators",
|
||||
"kind": "syntax",
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/* Regenerates wasm-spec-cases.txt: the binary modules of the WebAssembly
|
||||
* specification test suite at the feature level the decoder validates
|
||||
* (src/wasm/binary.ts), each with the suite's verdict and V8's.
|
||||
*
|
||||
* node gen-wasm-spec-cases.mjs --wast2json <wabt>/bin/wast2json \
|
||||
* --spec <WebAssembly/spec at tag wg-2.0>/test/core \
|
||||
* --extended-const <WebAssembly/extended-const>/test/core > wasm-spec-cases.txt
|
||||
*
|
||||
* Inputs: every .wast of the 2.0 core suite except simd/ (SIMD is outside
|
||||
* the translatable set; the decoder only reports it), plus the extended
|
||||
* constant expression proposal's global, data and elem tests. Modules given
|
||||
* as text for assert_malformed are skipped (they test the text format).
|
||||
* Identical modules are kept once, at their first location.
|
||||
*
|
||||
* Line format: <suite verdict> <V8 verdict> <file>:<line> <base64>, where a
|
||||
* verdict is "valid", "invalid" or "malformed" (V8: "valid" or "invalid").
|
||||
* The suite's tests are Apache-2.0 licensed (WebAssembly Community Group). */
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
wast2json: { type: "string" },
|
||||
spec: { type: "string" },
|
||||
"extended-const": { type: "string" },
|
||||
},
|
||||
});
|
||||
const inputs = [];
|
||||
for (const name of readdirSync(values.spec).sort()) {
|
||||
if (name.endsWith(".wast")) inputs.push({ label: name, path: join(values.spec, name) });
|
||||
}
|
||||
for (const name of ["global.wast", "data.wast", "elem.wast"]) {
|
||||
inputs.push({ label: `extended-const/${name}`, path: join(values["extended-const"], name) });
|
||||
}
|
||||
const seen = new Set();
|
||||
const lines = [
|
||||
"# WebAssembly spec test modules for src/wasm/binary.ts; generated by gen-wasm-spec-cases.mjs.",
|
||||
"# Sources: WebAssembly/spec wg-2.0 test/core (without simd/) and WebAssembly/extended-const",
|
||||
"# test/core global, data and elem, converted with wabt 1.0.37 wast2json (Apache-2.0).",
|
||||
"# <suite verdict> <V8 verdict> <file>:<line> <base64>",
|
||||
];
|
||||
for (const { label, path } of inputs) {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wasm-spec-"));
|
||||
try {
|
||||
execFileSync(values.wast2json, [path, "-o", join(dir, "x.json")], { stdio: "pipe" });
|
||||
const json = JSON.parse(readFileSync(join(dir, "x.json"), "utf8"));
|
||||
for (const command of json.commands) {
|
||||
let verdict;
|
||||
if (["module", "assert_unlinkable", "assert_uninstantiable"].includes(command.type))
|
||||
verdict = "valid";
|
||||
else if (command.type === "assert_invalid") verdict = "invalid";
|
||||
else if (command.type === "assert_malformed") verdict = "malformed";
|
||||
else continue;
|
||||
if (command.module_type === "text" || !command.filename?.endsWith(".wasm")) continue;
|
||||
const bytes = readFileSync(join(dir, command.filename));
|
||||
const b64 = bytes.toString("base64");
|
||||
if (seen.has(b64)) continue;
|
||||
seen.add(b64);
|
||||
const v8 = WebAssembly.validate(bytes) ? "valid" : "invalid";
|
||||
lines.push(`${verdict} ${v8} ${label}:${command.line} ${b64}`);
|
||||
}
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
process.stdout.write(`${lines.join("\n")}\n`);
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/error-js-accessor-name-message.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/webassembly-feature-probe.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/webassembly-type-mentions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/webassembly-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,168 @@
|
||||
/* The WebAssembly decoder (src/wasm/binary.ts) against the specification's
|
||||
* test suite, against V8 on mutations of those modules, and on the inline
|
||||
* modules of real packages in this workspace. V8 (the Node running the
|
||||
* test) is the oracle: scriptc must accept exactly the modules Node
|
||||
* accepts, within the translatable feature level. */
|
||||
import { globSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { analyzeWasm } from "../src/wasm/binary.js";
|
||||
import { decodeBase64 } from "../src/wasm/discover.js";
|
||||
|
||||
/** V8's verdict (the compiler's type world declares no WebAssembly value). */
|
||||
const v8Validate = (bytes: Uint8Array): boolean =>
|
||||
(
|
||||
globalThis as unknown as { WebAssembly: { validate(bytes: Uint8Array): boolean } }
|
||||
).WebAssembly.validate(bytes);
|
||||
|
||||
interface SpecCase {
|
||||
suite: string;
|
||||
v8: string;
|
||||
where: string;
|
||||
bytes: Uint8Array;
|
||||
}
|
||||
|
||||
const cases: SpecCase[] = readFileSync(join(import.meta.dirname, "wasm-spec-cases.txt"), "utf8")
|
||||
.split("\n")
|
||||
.filter((line) => line !== "" && !line.startsWith("#"))
|
||||
.map((line) => {
|
||||
const [suite, v8, where, b64] = line.split(" ");
|
||||
return {
|
||||
suite: suite!,
|
||||
v8: v8!,
|
||||
where: where!,
|
||||
bytes: new Uint8Array(Buffer.from(b64!, "base64")),
|
||||
};
|
||||
});
|
||||
|
||||
const accepted = (status: string): boolean => status === "valid" || status === "unsupported";
|
||||
|
||||
describe("WebAssembly spec test suite", () => {
|
||||
test("covers the 2.0 core suite and extended constant expressions", () => {
|
||||
expect(cases.length).toBeGreaterThan(3000);
|
||||
const extended = cases.filter((c) => c.where.startsWith("extended-const/"));
|
||||
expect(extended.length).toBeGreaterThan(10);
|
||||
expect(
|
||||
extended.filter((c) => analyzeWasm(c.bytes).features.includes("extended-const")).length,
|
||||
).toBeGreaterThan(5);
|
||||
});
|
||||
|
||||
test("every module gets V8's verdict and needs no other proposal", () => {
|
||||
const wrong: string[] = [];
|
||||
for (const c of cases) {
|
||||
const s = analyzeWasm(c.bytes);
|
||||
const v8 = v8Validate(c.bytes);
|
||||
if ((v8 ? "valid" : "invalid") !== c.v8) wrong.push(`${c.where}: V8 changed its verdict`);
|
||||
// Multiple memories are the one proposal outside the translatable set
|
||||
// that 2.0 tests exercise (as invalid; V8 accepts them).
|
||||
const allowed = s.status !== "unsupported" || s.unsupported.join() === "multi-memory";
|
||||
if (accepted(s.status) !== v8 || !allowed) {
|
||||
wrong.push(`${c.where}: ${s.status} ${s.error ?? ""} (V8 ${v8 ? "accepts" : "rejects"})`);
|
||||
}
|
||||
}
|
||||
expect(wrong).toEqual([]);
|
||||
});
|
||||
|
||||
test("differs from the 2.0 suite only where WebAssembly 3.0 and V8 relaxed it", () => {
|
||||
// Multiple memories became valid, and memory.size/grow's reserved byte
|
||||
// a memory index, so a long LEB128 zero is accepted (multi-memory);
|
||||
// global initializers and segment offsets may read defined immutable
|
||||
// globals (GC proposal).
|
||||
const relaxed = cases.filter((c) => c.suite !== "valid" && c.v8 === "valid");
|
||||
expect(relaxed.length).toBeGreaterThan(0);
|
||||
for (const c of relaxed) {
|
||||
expect(c.where).toMatch(
|
||||
/^(?:extended-const\/)?(?:binary|data|elem|global|imports|memory)\.wast:\d+$/,
|
||||
);
|
||||
}
|
||||
expect(relaxed.length).toBeLessThan(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mutations of the suite's valid modules", () => {
|
||||
test("never disagree with V8 (deterministic sample)", () => {
|
||||
const seeds = cases.filter((c) => c.v8 === "valid").map((c) => c.bytes);
|
||||
let state = 0x9e3779b9;
|
||||
const random = (n: number): number => {
|
||||
state ^= state << 13;
|
||||
state >>>= 0;
|
||||
state ^= state >>> 17;
|
||||
state ^= state << 5;
|
||||
state >>>= 0;
|
||||
return state % n;
|
||||
};
|
||||
const disagreements: string[] = [];
|
||||
for (let i = 0; i < 8000; i++) {
|
||||
const seed = seeds[random(seeds.length)]!;
|
||||
const at = 8 + random(Math.max(1, seed.length - 8));
|
||||
let bytes: Uint8Array;
|
||||
switch (random(4)) {
|
||||
case 0:
|
||||
bytes = Uint8Array.from(seed);
|
||||
bytes[at] = bytes[at]! ^ (1 << random(8));
|
||||
break;
|
||||
case 1:
|
||||
bytes = Uint8Array.from(seed);
|
||||
bytes[at] = random(256);
|
||||
break;
|
||||
case 2:
|
||||
bytes = new Uint8Array([...seed.subarray(0, at), ...seed.subarray(at + 1)]);
|
||||
break;
|
||||
default:
|
||||
bytes = new Uint8Array([...seed.subarray(0, at), random(256), ...seed.subarray(at)]);
|
||||
}
|
||||
const s = analyzeWasm(bytes);
|
||||
if (s.status === "unsupported") continue;
|
||||
if (accepted(s.status) !== v8Validate(bytes)) {
|
||||
disagreements.push(
|
||||
`${s.status} ${s.error ?? ""}: ${Buffer.from(bytes).toString("base64")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
expect(disagreements).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("modules inlined by packages in this workspace", () => {
|
||||
// Lockfile-pinned installs: undici's llhttp (with and without SIMD) and
|
||||
// es-module-lexer, both inlined as base64 string literals.
|
||||
const root = join(import.meta.dirname, "../../..");
|
||||
const inline = (pattern: string): Uint8Array[] =>
|
||||
globSync(join(root, pattern))
|
||||
.sort()
|
||||
.flatMap((file) =>
|
||||
[...readFileSync(file, "utf8").matchAll(/AGFzbQ[A-Za-z0-9+/=]*/g)].map((m) =>
|
||||
decodeBase64(m[0])!,
|
||||
),
|
||||
);
|
||||
|
||||
test("undici's llhttp: the scalar module translates, the SIMD one needs SIMD", () => {
|
||||
const scalar = inline(
|
||||
"node_modules/.pnpm/undici@7.*/node_modules/undici/lib/llhttp/llhttp-wasm.js",
|
||||
);
|
||||
const simd = inline(
|
||||
"node_modules/.pnpm/undici@7.*/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js",
|
||||
);
|
||||
expect(scalar.length).toBeGreaterThan(0);
|
||||
expect(simd.length).toBeGreaterThan(0);
|
||||
for (const bytes of scalar) {
|
||||
expect(analyzeWasm(bytes)).toMatchObject({ status: "valid", importModules: ["env"] });
|
||||
expect(v8Validate(bytes)).toBe(true);
|
||||
}
|
||||
for (const bytes of simd) {
|
||||
expect(analyzeWasm(bytes)).toMatchObject({ status: "unsupported", unsupported: ["simd"] });
|
||||
expect(v8Validate(bytes)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("es-module-lexer's module is plain MVP", () => {
|
||||
const modules = inline(
|
||||
"node_modules/.pnpm/es-module-lexer@1.*/node_modules/es-module-lexer/dist/lexer.js",
|
||||
);
|
||||
expect(modules.length).toBeGreaterThan(0);
|
||||
for (const bytes of modules) {
|
||||
expect(analyzeWasm(bytes)).toMatchObject({ status: "valid", imports: 0 });
|
||||
expect(v8Validate(bytes)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -174,6 +174,17 @@ typedef enum {
|
||||
SCR_DYNH_COUNT,
|
||||
} ScrDynHandleTag;
|
||||
|
||||
/* Every per-tag table (scr_dynh_ops in scr_json.c, the message clone hooks
|
||||
* in scr_message.c) is sized by this fixed capacity, never by
|
||||
* SCR_DYNH_COUNT. Appending a tag for a link-gated unit therefore changes
|
||||
* no always-linked storage or code: a program that never boxes the new
|
||||
* handle links byte-identical objects. Raising the capacity is a
|
||||
* program-wide layout change; do it in a change of its own, measured as
|
||||
* neutral, and only when the enum outgrows it. */
|
||||
#define SCR_DYNH_CAPACITY 128
|
||||
_Static_assert(SCR_DYNH_COUNT <= SCR_DYNH_CAPACITY,
|
||||
"ScrDynHandleTag outgrew SCR_DYNH_CAPACITY: raise it in its own change");
|
||||
|
||||
typedef struct ScrDyn ScrDyn;
|
||||
typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */
|
||||
typedef struct ScrClosure ScrClosure; /* full definition below (C11 repeat) */
|
||||
@@ -1162,7 +1173,25 @@ typedef struct ScrDynHandleOps {
|
||||
* lookup): +1 value, or NULL when the class has no member for `key`
|
||||
* (the read answers undefined). NULL hook = no symbol-keyed members. */
|
||||
ScrDyn *(*symbol_get)(void *h, const ScrDyn *key);
|
||||
/* Cross-thread structured clone (postMessage, workerData); NULL =
|
||||
* DataCloneError. See ScrDynHandleTransfer. */
|
||||
const struct ScrDynHandleTransfer *transfer;
|
||||
} ScrDynHandleOps;
|
||||
|
||||
/* How a handle crosses threads, supplied by the handle's owning unit so the
|
||||
* message encoder (scr_message.c) needs no reference to gated units and no
|
||||
* per-tag registry. share runs on the sending thread: it answers a payload
|
||||
* that any thread may use, +1, or NULL when this handle cannot be cloned
|
||||
* (with an exception pending, or DataCloneError is thrown). adopt runs on
|
||||
* the receiving thread and answers a fresh box over the payload (+1; the
|
||||
* message keeps its payload reference). drop releases the payload when the
|
||||
* message is freed, whether or not it was delivered. All three must be
|
||||
* callable from any thread. */
|
||||
typedef struct ScrDynHandleTransfer {
|
||||
void *(*share)(void *h);
|
||||
ScrDyn *(*adopt)(void *payload);
|
||||
void (*drop)(void *payload);
|
||||
} ScrDynHandleTransfer;
|
||||
ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection);
|
||||
|
||||
/* ── promises in the checked-dynamic tree (SCR_DYN_PROMISE) ────────────────────────────
|
||||
|
||||
@@ -3651,7 +3651,7 @@ ScrDyn *scr_dyn_bind(ScrDyn *target, ScrDyn *const *args, size_t argc) {
|
||||
* always-linked core never references gated units. A missing tag at use
|
||||
* is an internal error: emitted programs install a unit's ops whenever
|
||||
* they can box its handles. */
|
||||
static SCR_TL const ScrDynHandleOps *scr_dynh_ops[SCR_DYNH_COUNT];
|
||||
static SCR_TL const ScrDynHandleOps *scr_dynh_ops[SCR_DYNH_CAPACITY];
|
||||
|
||||
/* BroadcastChannel's EventTarget table (scr_et_broadcast_install). */
|
||||
SCR_TL const ScrEtBroadcastOps *scr_et_broadcast_ops = NULL;
|
||||
|
||||
@@ -23,6 +23,8 @@ typedef enum {
|
||||
SCR_MESSAGE_TYPED_REF,
|
||||
/* A transferred MessagePort: its channel end (+1), the side in number. */
|
||||
SCR_MESSAGE_PORT,
|
||||
/* A native handle cloned through its unit's ScrDynHandleTransfer. */
|
||||
SCR_MESSAGE_HANDLE,
|
||||
} ScrMessageKind;
|
||||
|
||||
const ScrMessagePortOps *scr_message_port_ops = NULL;
|
||||
@@ -49,6 +51,9 @@ typedef struct {
|
||||
bool data_view;
|
||||
ScrDyn *published; /* SCR_MESSAGE_PUBLISHED: immortal root, owns nothing */
|
||||
ScrChannel *channel; /* SCR_MESSAGE_PORT */
|
||||
/* SCR_MESSAGE_HANDLE: the owning unit's hooks and its payload (+1). */
|
||||
const ScrDynHandleTransfer *transfer;
|
||||
void *payload;
|
||||
/* SCR_MESSAGE_TYPED_REF: the capsule's process-wide adapters and type key
|
||||
* (vtables, adapters and type keys are shared by every thread). */
|
||||
struct {
|
||||
@@ -389,7 +394,20 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input)
|
||||
break;
|
||||
}
|
||||
bool buffer = scr_buffer_storage_is(value);
|
||||
if (value->kind == SCR_DYN_HANDLE && !buffer) goto unsupported;
|
||||
if (value->kind == SCR_DYN_HANDLE && !buffer) {
|
||||
const ScrDynHandleTransfer *transfer = scr_dyn_handle_ops_of(value)->transfer;
|
||||
if (!transfer) goto unsupported;
|
||||
void *payload = transfer->share(value->v.handle.ptr);
|
||||
if (!payload) {
|
||||
if (scr_exc_pending()) return SIZE_MAX;
|
||||
goto unsupported;
|
||||
}
|
||||
node = &encoder->message->nodes[id];
|
||||
node->kind = SCR_MESSAGE_HANDLE;
|
||||
node->transfer = transfer;
|
||||
node->payload = payload;
|
||||
break;
|
||||
}
|
||||
const ScrBytes *bytes = buffer ? value->v.handle.ptr : value->v.bytes;
|
||||
size_t storage = scr_message_storage(encoder, bytes);
|
||||
/* Adding the storage node can reallocate the node array. */
|
||||
@@ -765,6 +783,7 @@ ScrDyn *scr_message_decode(const ScrMessage *message) {
|
||||
case SCR_MESSAGE_PORT:
|
||||
values[i] = scr_message_port_ops->adopt(node->channel, (int)node->number);
|
||||
break;
|
||||
case SCR_MESSAGE_HANDLE: values[i] = node->transfer->adopt(node->payload); break;
|
||||
}
|
||||
if (!values[i] || scr_exc_pending()) goto done;
|
||||
}
|
||||
@@ -820,6 +839,7 @@ void scr_message_free(ScrMessage *message) {
|
||||
free(node->data);
|
||||
scr_shared_release(node->shared);
|
||||
if (node->kind == SCR_MESSAGE_PORT) scr_message_port_ops->release(node->channel);
|
||||
if (node->kind == SCR_MESSAGE_HANDLE) node->transfer->drop(node->payload);
|
||||
}
|
||||
free(message->nodes);
|
||||
free(message);
|
||||
|
||||
@@ -172,6 +172,115 @@ static void check_documents(void) {
|
||||
scr_message_free(message);
|
||||
}
|
||||
|
||||
/* A link-gated unit's handle crosses threads through the transfer hooks in
|
||||
* its ops: share on the sender, adopt on the receiver, drop with the
|
||||
* message. The test borrows the last capacity slot, which no unit uses, so
|
||||
* it also proves that slots past SCR_DYNH_COUNT work. */
|
||||
#define TRANSFER_TAG ((ScrDynHandleTag)(SCR_DYNH_CAPACITY - 1))
|
||||
typedef struct {
|
||||
_Atomic size_t rc;
|
||||
int value;
|
||||
} TransferBox;
|
||||
static _Atomic int transfer_shares, transfer_adopts, transfer_drops;
|
||||
static void *transfer_retain(void *h) {
|
||||
((TransferBox *)h)->rc++;
|
||||
return h;
|
||||
}
|
||||
static void transfer_release(void *h) {
|
||||
if (--((TransferBox *)h)->rc == 0) free(h);
|
||||
}
|
||||
static void *transfer_share(void *h) {
|
||||
TransferBox *source = h;
|
||||
if (source->value < 0) return NULL;
|
||||
transfer_shares++;
|
||||
TransferBox *copy = malloc(sizeof *copy);
|
||||
copy->rc = 1;
|
||||
copy->value = source->value;
|
||||
return copy;
|
||||
}
|
||||
static ScrDyn *transfer_adopt(void *payload) {
|
||||
transfer_adopts++;
|
||||
return scr_dyn_new_handle(payload, TRANSFER_TAG);
|
||||
}
|
||||
static void transfer_drop(void *payload) {
|
||||
transfer_drops++;
|
||||
transfer_release(payload);
|
||||
}
|
||||
static const ScrDynHandleTransfer transfer_hooks = {transfer_share, transfer_adopt, transfer_drop};
|
||||
static const ScrDynHandleOps transfer_ops = {
|
||||
.cls = "Transferable",
|
||||
.retain = transfer_retain,
|
||||
.release = transfer_release,
|
||||
.transfer = &transfer_hooks,
|
||||
};
|
||||
static const ScrDynHandleOps opaque_ops = {
|
||||
.cls = "Opaque",
|
||||
.retain = transfer_retain,
|
||||
.release = transfer_release,
|
||||
};
|
||||
|
||||
static void *receive_handles(void *data) {
|
||||
scr_context_enter(1);
|
||||
scr_init();
|
||||
scr_lib_init(0, NULL);
|
||||
scr_dyn_handle_install(TRANSFER_TAG, &transfer_ops);
|
||||
ScrDyn *root = scr_message_decode(data);
|
||||
assert(root && !scr_exc_pending());
|
||||
ScrDyn *first = scr_dyn_obj_get(root, "first", 5);
|
||||
assert(first->kind == SCR_DYN_HANDLE && first->v.handle.tag == TRANSFER_TAG);
|
||||
assert(((TransferBox *)first->v.handle.ptr)->value == 7);
|
||||
/* One handle reached twice decodes to one box. */
|
||||
assert(first == scr_dyn_obj_get(root, "again", 5));
|
||||
assert(transfer_adopts == 1);
|
||||
scr_dyn_release(root);
|
||||
scr_collect_cycles();
|
||||
scr_context_cleanup();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static ScrDyn *transfer_handle(int value, const ScrDynHandleOps *ops) {
|
||||
scr_dyn_handle_install(TRANSFER_TAG, ops);
|
||||
TransferBox *box = malloc(sizeof *box);
|
||||
box->rc = 1;
|
||||
box->value = value;
|
||||
ScrDyn *handle = scr_dyn_new_handle(box, TRANSFER_TAG);
|
||||
transfer_release(box);
|
||||
return handle;
|
||||
}
|
||||
|
||||
static void check_handle_transfer(void) {
|
||||
ScrDyn *root = scr_dyn_new_obj();
|
||||
ScrDyn *handle = transfer_handle(7, &transfer_ops);
|
||||
scr_dyn_obj_set(root, "first", 5, scr_dyn_retain(handle));
|
||||
scr_dyn_obj_set(root, "again", 5, handle);
|
||||
ScrMessage *message = scr_message_encode(root);
|
||||
assert(message && !scr_exc_pending());
|
||||
assert(transfer_shares == 1);
|
||||
scr_dyn_release(root);
|
||||
pthread_t receiver;
|
||||
assert(pthread_create(&receiver, NULL, receive_handles, message) == 0);
|
||||
assert(pthread_join(receiver, NULL) == 0);
|
||||
assert(transfer_drops == 0);
|
||||
scr_message_free(message);
|
||||
assert(transfer_drops == 1);
|
||||
|
||||
/* A handle whose unit declines (share answers NULL) and a handle with no
|
||||
* transfer hooks both throw DataCloneError, releasing what was shared. */
|
||||
root = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(root, transfer_handle(1, &transfer_ops));
|
||||
scr_dyn_arr_push(root, transfer_handle(-1, &transfer_ops));
|
||||
assert(!scr_message_encode(root) && scr_exc_pending());
|
||||
scr_exc_clear();
|
||||
assert(transfer_shares == 2 && transfer_drops == 2);
|
||||
scr_dyn_release(root);
|
||||
root = transfer_handle(3, &opaque_ops);
|
||||
assert(!scr_message_encode(root) && scr_exc_pending());
|
||||
scr_exc_clear();
|
||||
assert(transfer_shares == 2);
|
||||
scr_dyn_release(root);
|
||||
scr_collect_cycles();
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
scr_init();
|
||||
scr_lib_init(0, NULL);
|
||||
@@ -188,6 +297,8 @@ int main(void) {
|
||||
check_getters();
|
||||
check_rejection();
|
||||
check_documents();
|
||||
check_handle_transfer();
|
||||
scr_context_cleanup();
|
||||
puts("message graphs preserve identity, sparse values, byte aliases and getter order");
|
||||
puts("native handles cross threads through their transfer hooks");
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ test.skipIf(process.platform === "win32")(
|
||||
env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" },
|
||||
});
|
||||
expect(result.stdout).toBe(
|
||||
"message graphs preserve identity, sparse values, byte aliases and getter order\n",
|
||||
"message graphs preserve identity, sparse values, byte aliases and getter order\n" +
|
||||
"native handles cross threads through their transfer hooks\n",
|
||||
);
|
||||
expect(result.stderr).toBe("");
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// A JavaScript Error subclass that overrides name and message with accessor
|
||||
// pairs, as quickjs-wasi's JSException does. Writing `.name` or `.message`
|
||||
// on an ordinary Error builds the Error property dispatchers, whose branch
|
||||
// for this class calls the JavaScript setters with an untyped value.
|
||||
class GuestException extends Error {
|
||||
#name;
|
||||
#message;
|
||||
constructor(name, message) {
|
||||
super();
|
||||
this.#name = name;
|
||||
this.#message = message;
|
||||
}
|
||||
get name() {
|
||||
return this.#name;
|
||||
}
|
||||
set name(v) {
|
||||
this.#name = v;
|
||||
}
|
||||
get message() {
|
||||
return this.#message;
|
||||
}
|
||||
set message(v) {
|
||||
this.#message = `[${v}]`;
|
||||
}
|
||||
}
|
||||
|
||||
const guest = new GuestException("GuestError", "boom");
|
||||
console.log(guest.name, guest.message, String(guest));
|
||||
guest.name = "Renamed";
|
||||
guest.message = "changed";
|
||||
console.log(guest.name, guest.message, String(guest));
|
||||
|
||||
const plain = new Error("plain");
|
||||
plain.name = "PlainName";
|
||||
plain.message = "plain message";
|
||||
console.log(plain.name, plain.message, String(plain));
|
||||
|
||||
/** @type {Error[]} */
|
||||
const errors = [plain, guest, new TypeError("bad")];
|
||||
for (const err of errors) console.log(String(err));
|
||||
try {
|
||||
throw guest;
|
||||
} catch (caught) {
|
||||
console.log(caught instanceof GuestException, caught instanceof Error, String(caught));
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// @deferred-fences: 2
|
||||
// A JavaScript dependency's WebAssembly glue: the feature probe answers as
|
||||
// in Node, and the loader that would instantiate a module is never reached
|
||||
// (reaching it would throw scriptc's located WebAssembly fence).
|
||||
function supportsWasm() {
|
||||
return typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function";
|
||||
}
|
||||
|
||||
async function instantiate(bytes, imports) {
|
||||
const { instance } = await WebAssembly.instantiate(bytes, imports);
|
||||
return instance.exports;
|
||||
}
|
||||
|
||||
const backend = process.argv.includes("--wasm") ? instantiate : null;
|
||||
console.log("supports wasm:", supportsWasm());
|
||||
console.log("backend:", backend === null ? "javascript" : "wasm");
|
||||
@@ -0,0 +1,22 @@
|
||||
// Declarations that mention the WebAssembly API's types compile: the types
|
||||
// have the checked-dynamic representation, and only a reached use of the
|
||||
// API itself is refused. typeof needs no value and folds to Node's answer.
|
||||
interface LoaderOptions {
|
||||
wasm?: BufferSource | WebAssembly.Module;
|
||||
imports?: WebAssembly.Imports;
|
||||
memory?: WebAssembly.Memory;
|
||||
}
|
||||
|
||||
function describe(name: string, options: LoaderOptions): string {
|
||||
const parts = [name];
|
||||
parts.push(options.wasm === undefined ? "no module" : "module given");
|
||||
parts.push(options.imports === undefined ? "no imports" : "imports given");
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
||||
console.log(describe("empty", {}));
|
||||
console.log(describe("bytes", { wasm: new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]) }));
|
||||
console.log(typeof WebAssembly, typeof globalThis.WebAssembly);
|
||||
console.log(typeof WebAssembly.compile, typeof WebAssembly.instantiate, typeof WebAssembly.validate);
|
||||
console.log(typeof WebAssembly.Module, typeof WebAssembly.Instance, typeof WebAssembly.Memory);
|
||||
console.log(typeof WebAssembly.RuntimeError, typeof WebAssembly.CompileError);
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
// (module (func (export "answer") (result i32) i32.const 42)), inlined the
|
||||
// way undici inlines llhttp.
|
||||
const ANSWER = "AGFzbQEAAAABBQFgAAF/AwIBAAcKAQZhbnN3ZXIAAAoGAQQAQSoL";
|
||||
|
||||
export function hasModule() {
|
||||
return existsSync(new URL("./add.wasm", import.meta.url).pathname);
|
||||
}
|
||||
|
||||
export function inlineModule() {
|
||||
return ANSWER;
|
||||
}
|
||||
|
||||
export async function instantiate(bytes) {
|
||||
const { instance } = await WebAssembly.instantiate(bytes, {});
|
||||
return instance.exports;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// The WebAssembly modules this program can load are visible to the build in
|
||||
// four ways (wasm/discover.ts): a path the compiler resolves (glue.js), an
|
||||
// inline base64 literal (glue.js), a .wasm file shipped by a package whose
|
||||
// code mentions WebAssembly (wasm-fixture-pkg), and the scriptc.wasm list in
|
||||
// package.json (build/generated.wasm).
|
||||
import { hasModule, inlineModule } from "./glue.js";
|
||||
import { describeModule } from "wasm-fixture-pkg";
|
||||
|
||||
console.log(hasModule(), inlineModule().length, describeModule());
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function describeModule(path?: string): string;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Loads lib/core.wasm from a path computed at run time, so only the
|
||||
// package scan finds the module.
|
||||
export function describeModule(path) {
|
||||
const where = path ?? "lib/core.wasm";
|
||||
return typeof WebAssembly === "object" ? `wasm at ${where}` : "no wasm";
|
||||
}
|
||||
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "wasm-fixture-pkg",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "wasm-coverage-fixture",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scriptc": {
|
||||
"wasm": ["build/generated.wasm"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// No WebAssembly module compiles to native code yet: every reached use of
|
||||
// the API refuses at its site, in each spelling, while declarations that
|
||||
// only mention its types compile.
|
||||
interface LoaderOptions {
|
||||
wasm?: BufferSource | WebAssembly.Module;
|
||||
imports?: WebAssembly.Imports;
|
||||
}
|
||||
|
||||
async function load(options: LoaderOptions, bytes: Uint8Array): Promise<void> {
|
||||
console.log(WebAssembly.validate(bytes));
|
||||
const module = await WebAssembly.compile(bytes);
|
||||
const { instance } = await WebAssembly.instantiate(bytes, options.imports);
|
||||
console.log(instance instanceof WebAssembly.Instance, module);
|
||||
const direct = new WebAssembly.Instance(new WebAssembly.Module(bytes));
|
||||
console.log(direct.exports);
|
||||
const memory = new WebAssembly.Memory({ initial: 1 });
|
||||
console.log(memory.buffer.byteLength);
|
||||
const api = globalThis.WebAssembly;
|
||||
console.log(api);
|
||||
console.log(WebAssembly.Module.imports(module).length);
|
||||
}
|
||||
|
||||
// pi's codemode spelling: the global object viewed through a cast.
|
||||
interface WebAssemblyGlobal {
|
||||
compile(bytes: Uint8Array): Promise<unknown>;
|
||||
}
|
||||
const { WebAssembly: viewed } = globalThis as unknown as { WebAssembly: WebAssemblyGlobal };
|
||||
console.log(typeof viewed.compile);
|
||||
|
||||
await load({}, new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]));
|
||||
@@ -0,0 +1,31 @@
|
||||
scriptc coverage tests/coverage-fixtures/wasm/main.ts
|
||||
|
||||
statements analyzed 8
|
||||
compile statically 6 (75%)
|
||||
in dynamic tier 2 (operate on values typed only at runtime; --explain-dynamic lists them)
|
||||
|
||||
npm package compilation:
|
||||
wasm-fixture-pkg static
|
||||
|
||||
WebAssembly modules: (found at build time; none compile to native code yet)
|
||||
inline module 39 bytes valid
|
||||
1 function, 0 imports, 1 export
|
||||
found: inline at tests/coverage-fixtures/wasm/glue.js:5
|
||||
tests/coverage-fixtures/wasm/add.wasm 41 bytes valid
|
||||
1 function, 0 imports, 1 export
|
||||
found: path at tests/coverage-fixtures/wasm/glue.js:8
|
||||
tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/lib/core.wasm 27 bytes invalid: type mismatch (at byte 27)
|
||||
1 function, 0 imports, 0 exports
|
||||
found: shipped by the package at tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg
|
||||
tests/coverage-fixtures/wasm/build/generated.wasm 43 bytes uses simd
|
||||
1 function, 0 imports, 0 exports
|
||||
features: simd
|
||||
found: configured by tests/coverage-fixtures/wasm/package.json
|
||||
|
||||
in unreached code (never lowered — cannot fail a build)
|
||||
×1 SC2004 uses of 'instance' inherit the blocker on its declaration
|
||||
at tests/coverage-fixtures/wasm/glue.js:17:10
|
||||
hint: the declaration of 'instance' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
×1 SC2020 'WebAssembly.instantiate' is not supported yet: scriptc does not compile WebAssembly modules to native code yet
|
||||
at tests/coverage-fixtures/wasm/glue.js:16:30
|
||||
hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)
|
||||
@@ -0,0 +1,98 @@
|
||||
webassembly-fences.ts:10:15 - error SC2020: 'WebAssembly.validate' is not supported yet: scriptc does not compile WebAssembly modules to native code yet
|
||||
|
||||
9 | async function load(options: LoaderOptions, bytes: Uint8Array): Promise<void> {
|
||||
10 | console.log(WebAssembly.validate(bytes));
|
||||
| ^~~~~~~~~~~~~~~~~~~~
|
||||
11 | const module = await WebAssembly.compile(bytes);
|
||||
|
||||
hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)
|
||||
|
||||
webassembly-fences.ts:11:24 - error SC2020: 'WebAssembly.compile' is not supported yet: scriptc does not compile WebAssembly modules to native code yet
|
||||
|
||||
10 | console.log(WebAssembly.validate(bytes));
|
||||
11 | const module = await WebAssembly.compile(bytes);
|
||||
| ^~~~~~~~~~~~~~~~~~~
|
||||
12 | const { instance } = await WebAssembly.instantiate(bytes, options.imports);
|
||||
|
||||
hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)
|
||||
|
||||
webassembly-fences.ts:12:30 - error SC2020: 'WebAssembly.instantiate' is not supported yet: scriptc does not compile WebAssembly modules to native code yet
|
||||
|
||||
11 | const module = await WebAssembly.compile(bytes);
|
||||
12 | const { instance } = await WebAssembly.instantiate(bytes, options.imports);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~
|
||||
13 | console.log(instance instanceof WebAssembly.Instance, module);
|
||||
|
||||
hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)
|
||||
|
||||
webassembly-fences.ts:13:35 - error SC2020: 'WebAssembly.Instance' is not supported yet: scriptc does not compile WebAssembly modules to native code yet
|
||||
|
||||
12 | const { instance } = await WebAssembly.instantiate(bytes, options.imports);
|
||||
13 | console.log(instance instanceof WebAssembly.Instance, module);
|
||||
| ^~~~~~~~~~~~~~~~~~~~
|
||||
14 | const direct = new WebAssembly.Instance(new WebAssembly.Module(bytes));
|
||||
|
||||
hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)
|
||||
|
||||
webassembly-fences.ts:14:22 - error SC2020: 'new WebAssembly.Instance' is not supported yet: scriptc does not compile WebAssembly modules to native code yet
|
||||
|
||||
13 | console.log(instance instanceof WebAssembly.Instance, module);
|
||||
14 | const direct = new WebAssembly.Instance(new WebAssembly.Module(bytes));
|
||||
| ^~~~~~~~~~~~~~~~~~~~
|
||||
15 | console.log(direct.exports);
|
||||
|
||||
hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)
|
||||
|
||||
webassembly-fences.ts:16:22 - error SC2020: 'new WebAssembly.Memory' is not supported yet: scriptc does not compile WebAssembly modules to native code yet
|
||||
|
||||
15 | console.log(direct.exports);
|
||||
16 | const memory = new WebAssembly.Memory({ initial: 1 });
|
||||
| ^~~~~~~~~~~~~~~~~~
|
||||
17 | console.log(memory.buffer.byteLength);
|
||||
|
||||
hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)
|
||||
|
||||
webassembly-fences.ts:17:15 - error SC2004: uses of 'memory' inherit the blocker on its declaration
|
||||
|
||||
16 | const memory = new WebAssembly.Memory({ initial: 1 });
|
||||
17 | console.log(memory.buffer.byteLength);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~
|
||||
18 | const api = globalThis.WebAssembly;
|
||||
|
||||
hint: the declaration of 'memory' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
|
||||
webassembly-fences.ts:18:15 - error SC2020: 'globalThis.WebAssembly' is not supported yet: scriptc does not compile WebAssembly modules to native code yet
|
||||
|
||||
17 | console.log(memory.buffer.byteLength);
|
||||
18 | const api = globalThis.WebAssembly;
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~
|
||||
19 | console.log(api);
|
||||
|
||||
hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)
|
||||
|
||||
webassembly-fences.ts:19:15 - error SC2004: uses of 'api' inherit the blocker on its declaration
|
||||
|
||||
18 | const api = globalThis.WebAssembly;
|
||||
19 | console.log(api);
|
||||
| ^~~
|
||||
20 | console.log(WebAssembly.Module.imports(module).length);
|
||||
|
||||
hint: the declaration of 'api' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
|
||||
webassembly-fences.ts:20:15 - error SC2020: 'WebAssembly.Module.imports' is not supported yet: scriptc does not compile WebAssembly modules to native code yet
|
||||
|
||||
19 | console.log(api);
|
||||
20 | console.log(WebAssembly.Module.imports(module).length);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
21 | }
|
||||
|
||||
hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)
|
||||
|
||||
webassembly-fences.ts:27:33 - error SC2020: 'globalThis.WebAssembly' is not supported yet: scriptc does not compile WebAssembly modules to native code yet
|
||||
|
||||
26 | }
|
||||
27 | const { WebAssembly: viewed } = globalThis as unknown as { WebAssembly: WebAssemblyGlobal };
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
28 | console.log(typeof viewed.compile);
|
||||
|
||||
hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)
|
||||
@@ -43,6 +43,36 @@ test("unreached blockers report in their own group", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("WebAssembly modules the build can see report with their sources", async () => {
|
||||
// A resolved path and an inline literal (glue.js), a packaged .wasm
|
||||
// (wasm-fixture-pkg), and package.json's scriptc.wasm list.
|
||||
await expect(report(fixture("wasm/main.ts"))).toMatchFileSnapshot(
|
||||
"__snapshots__/coverage-wasm.txt",
|
||||
);
|
||||
const { coverage } = analyze(fixture("wasm/main.ts"), { wasm: [fixture("wasm/add.wasm")] });
|
||||
const modules = coverage.wasmModules ?? [];
|
||||
expect(
|
||||
modules.map((m) => [
|
||||
m.path?.replace(repoRoot + "/", "") ?? "inline",
|
||||
m.status,
|
||||
m.unsupported,
|
||||
m.sources.map((s) => s.kind),
|
||||
]),
|
||||
).toEqual([
|
||||
["inline", "valid", [], ["inline"]],
|
||||
["tests/coverage-fixtures/wasm/add.wasm", "valid", [], ["path", "config"]],
|
||||
[
|
||||
"tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/lib/core.wasm",
|
||||
"invalid",
|
||||
[],
|
||||
["package"],
|
||||
],
|
||||
["tests/coverage-fixtures/wasm/build/generated.wasm", "unsupported", ["simd"], ["config"]],
|
||||
]);
|
||||
const envelope = coverageEnvelope(coverage, { compilerVersion: "test" });
|
||||
expect(envelope.wasmModules?.map((m) => m.sha256.length)).toEqual([64, 64, 64, 64]);
|
||||
});
|
||||
|
||||
test("divergences report as warnings, never as blockers", async () => {
|
||||
const { coverage } = analyze(fixture("divergences.ts"));
|
||||
expect(coverage.diagnostics).toEqual([]);
|
||||
|
||||
Reference in New Issue
Block a user