mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
new WebAssembly.Memory, Table and Global construct the runtime objects an instance exports, with Node 24's descriptor order, defaults, conversions and messages (V8's EnforceUint32 errors, the v128 and shared-memory errors; a valid shared memory is refused with a TypeError until threads), so either kind can be imported. Imports of memories, tables and globals now check limits, types and mutability with V8's LinkError messages, and exports keep V8's identity (imported objects re-export as themselves, one object per memory and table). The classes called without new throw Node's TypeError. WebAssembly.Module.imports, exports and customSections read the descriptor (ABI v2). Custom sections are carried only by programs that call customSections. Every function a module references reaches JavaScript as a callable function: the translator emits one call thunk per signature, so table.get and funcref globals return the module's unexported functions too, and exported functions can be stored in tables and globals from any instance. Discovery also finds .so WebAssembly shared libraries at resolved paths. Fixes to W3/W4 found on the way: externref Table.grow fills with undefined; Memory.grow/Table index errors and funcref messages match V8; an invalid funcref global import is a LinkError; global.valueOf() on a WebAssembly.Global-typed receiver answers the value; explicit-bounds memories keep the blocks grow replaced, so old ArrayBuffers never dangle. This unblocks quickjs-wasi's extension loader: quickjs-wasi with its url, encoding, headers and structured-clone extensions runs through its own glue identically to Node, and pi codemode's static coverage goes from 83% to 84% with no WebAssembly blocker left. Evidence: differential corpus programs for the API (webassembly-jsapi), a 261-test port of WPT wasm/jsapi (webassembly-wpt-jsapi), quickjs-wasi's dist/extensions.js run unmodified against dylink-shaped modules (webassembly-dylink), and the 2.0 spec suite's modules that import the spectest memory and table (new webassembly-spec-imports group plus the memory and table groups); all match Node in the plain and sanitized lanes. The zero-cost gate leaves every non-WebAssembly program byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
149 lines
4.1 KiB
JavaScript
149 lines
4.1 KiB
JavaScript
/* Executes WebAssembly spec-test commands (generated by gen.mjs) and prints
|
|
* one line per command. The webassembly-spec-* corpus programs call it;
|
|
* the differential harness runs them under Node and as compiled binaries
|
|
* and compares the output, so V8 decides every expected value, trap and
|
|
* error. Module bytes are inline base64 literals in each program's
|
|
* data.mjs, which the build translates. */
|
|
|
|
/** The spec harness's `spectest` module (the reference interpreter's JS
|
|
* harness), with its memory and table. */
|
|
function spectestImports(log) {
|
|
return {
|
|
print() {
|
|
log("print");
|
|
},
|
|
print_i32(a) {
|
|
log(`print_i32 ${a}`);
|
|
},
|
|
print_i64(a) {
|
|
log(`print_i64 ${a}`);
|
|
},
|
|
print_f32(a) {
|
|
log(`print_f32 ${show(a)}`);
|
|
},
|
|
print_f64(a) {
|
|
log(`print_f64 ${show(a)}`);
|
|
},
|
|
print_i32_f32(a, b) {
|
|
log(`print_i32_f32 ${a} ${show(b)}`);
|
|
},
|
|
print_f64_f64(a, b) {
|
|
log(`print_f64_f64 ${show(a)} ${show(b)}`);
|
|
},
|
|
global_i32: 666,
|
|
global_i64: 666n,
|
|
global_f32: 666.6,
|
|
global_f64: 666.6,
|
|
table: new WebAssembly.Table({ initial: 10, maximum: 20, element: "anyfunc" }),
|
|
memory: new WebAssembly.Memory({ initial: 1, maximum: 2 }),
|
|
};
|
|
}
|
|
|
|
function show(v) {
|
|
if (typeof v === "bigint") return `${v}n`;
|
|
if (typeof v === "number") return Object.is(v, -0) ? "-0" : String(v);
|
|
if (v === null) return "null";
|
|
if (v === undefined) return "undefined";
|
|
if (typeof v === "function") return "function";
|
|
if (Array.isArray(v)) return `[${v.map(show).join(",")}]`;
|
|
if (typeof v === "object" && typeof v.ref === "number") return `ref ${v.ref}`;
|
|
return typeof v;
|
|
}
|
|
|
|
const refs = new Map();
|
|
|
|
function decode(arg) {
|
|
const colon = arg.indexOf(":");
|
|
const type = arg.slice(0, colon);
|
|
const text = arg.slice(colon + 1);
|
|
if (type === "i32") return Number(text) | 0;
|
|
if (type === "i64") return BigInt.asIntN(64, BigInt(text));
|
|
if (type === "f32") {
|
|
const view = new DataView(new ArrayBuffer(4));
|
|
view.setUint32(0, Number(text));
|
|
return view.getFloat32(0);
|
|
}
|
|
if (type === "f64") {
|
|
const view = new DataView(new ArrayBuffer(8));
|
|
view.setBigUint64(0, BigInt(text));
|
|
return view.getFloat64(0);
|
|
}
|
|
if (text === "null") return null;
|
|
const n = Number(text);
|
|
let ref = refs.get(n);
|
|
if (ref === undefined) {
|
|
ref = { ref: n };
|
|
refs.set(n, ref);
|
|
}
|
|
return ref;
|
|
}
|
|
|
|
function failure(e) {
|
|
return e instanceof Error ? `${e.name}: ${e.message}` : `threw ${show(e)}`;
|
|
}
|
|
|
|
export function run(modules, script) {
|
|
const commands = JSON.parse(script);
|
|
const lines = [];
|
|
const log = (line) => {
|
|
lines.push(line);
|
|
};
|
|
const imports = { spectest: spectestImports(log) };
|
|
const named = new Map();
|
|
let current = null;
|
|
const instantiate = (index) =>
|
|
new WebAssembly.Instance(new WebAssembly.Module(Buffer.from(modules[index], "base64")), imports);
|
|
let file = "";
|
|
for (const c of commands) {
|
|
const op = c[0];
|
|
if (op === "file") {
|
|
file = c[1];
|
|
log(`# ${file}`);
|
|
continue;
|
|
}
|
|
if (op === "register") {
|
|
const instance = c.length > 2 ? named.get(c[2]) : current;
|
|
if (instance) imports[c[1]] = instance.exports;
|
|
continue;
|
|
}
|
|
const at = `${file}:${c[1]}`;
|
|
if (op === "module") {
|
|
try {
|
|
current = instantiate(c[2]);
|
|
if (c.length > 3) named.set(c[3], current);
|
|
log(`${at} module`);
|
|
} catch (e) {
|
|
current = null;
|
|
log(`${at} module ${failure(e)}`);
|
|
}
|
|
continue;
|
|
}
|
|
if (op === "instantiate") {
|
|
try {
|
|
instantiate(c[2]);
|
|
log(`${at} instantiated`);
|
|
} catch (e) {
|
|
log(`${at} ${e instanceof Error ? e.name : "threw"}`);
|
|
}
|
|
continue;
|
|
}
|
|
const instance = c[2] !== 0 ? named.get(c[2]) : current;
|
|
if (!instance) {
|
|
log(`${at} no instance`);
|
|
continue;
|
|
}
|
|
try {
|
|
const target = instance.exports[c[3]];
|
|
if (op === "get") {
|
|
log(`${at} ${show(target.value)}`);
|
|
continue;
|
|
}
|
|
const args = c[4].map(decode);
|
|
log(`${at} ${show(target(...args))}`);
|
|
} catch (e) {
|
|
log(`${at} ${failure(e)}`);
|
|
}
|
|
}
|
|
console.log(lines.join("\n"));
|
|
}
|