mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
274 lines
9.4 KiB
JavaScript
274 lines
9.4 KiB
JavaScript
/* Regenerates the executed WebAssembly spec-test corpus programs
|
|
* (tests/corpus/webassembly-spec-<group>/data.mjs): the binary modules and
|
|
* the commands of selected .wast files of the WebAssembly 2.0 core suite,
|
|
* which runner.mjs instantiates and invokes under Node and as a compiled
|
|
* binary. The differential harness compares the two outputs, so V8 is the
|
|
* oracle; the modules are inline base64 literals, which the build
|
|
* discovers and translates.
|
|
*
|
|
* node tests/wasm-spec/gen.mjs --wast2json <wabt>/bin/wast2json \
|
|
* --spec <WebAssembly/spec at tag wg-2.0>/test/core
|
|
*
|
|
* Left out: modules given as text, modules the decoder does not accept
|
|
* within the translatable feature level and every command that targets
|
|
* them, and commands with v128 values. The runner's spectest module
|
|
* provides the harness's memory and table (new WebAssembly.Memory and
|
|
* Table), so modules that import them run. Also left out, because they test NaN payloads, which neither
|
|
* side preserves the way the other does: actions whose f32 or f64
|
|
* arguments are NaNs other than the canonical quiet NaN (V8 and scriptc
|
|
* carry NaN payloads through JavaScript numbers differently), and
|
|
* float_exprs.wast's actions that pass a signaling NaN's bit pattern to
|
|
* arithmetic that must quiet it (LLVM may fold `x - 0.0` to `x`, leaving
|
|
* the payload; WebAssembly requires a quiet NaN). Files with more than
|
|
* MAX_ACTIONS actions keep an evenly spaced
|
|
* sample of them (every module and register command stays), which keeps
|
|
* the generated programs small.
|
|
*
|
|
* Commands are compact tuples (runner.mjs reads them):
|
|
* ["file", name]
|
|
* ["module", line, moduleIndex, name?] instantiate, make current
|
|
* ["instantiate", line, moduleIndex] assert_un{linkable,instantiable}
|
|
* ["register", as, name?]
|
|
* ["invoke" | "get", line, name | 0, field, args] args: "type:value"
|
|
* The suite's tests are Apache-2.0 licensed (WebAssembly Community
|
|
* Group). */
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { parseArgs } from "node:util";
|
|
import { analyzeWasm } from "../../packages/compiler/dist/wasm/binary.js";
|
|
|
|
export const GROUPS = {
|
|
int: ["i32", "i64", "int_exprs", "int_literals", "conversions", "traps"],
|
|
float: [
|
|
"f32",
|
|
"f64",
|
|
"f32_cmp",
|
|
"f64_cmp",
|
|
"f32_bitwise",
|
|
"f64_bitwise",
|
|
"float_exprs",
|
|
"float_literals",
|
|
"float_misc",
|
|
],
|
|
control: [
|
|
"block",
|
|
"br",
|
|
"br_if",
|
|
"br_table",
|
|
"call",
|
|
"call_indirect",
|
|
"fac",
|
|
"forward",
|
|
"func",
|
|
"func_ptrs",
|
|
"if",
|
|
"labels",
|
|
"loop",
|
|
"local_get",
|
|
"local_set",
|
|
"local_tee",
|
|
"nop",
|
|
"return",
|
|
"select",
|
|
"stack",
|
|
"switch",
|
|
"unreachable",
|
|
"unwind",
|
|
"left-to-right",
|
|
"global",
|
|
"start",
|
|
],
|
|
memory: [
|
|
"address",
|
|
"align",
|
|
"endianness",
|
|
"float_memory",
|
|
"load",
|
|
"store",
|
|
"memory",
|
|
"memory_grow",
|
|
"memory_redundancy",
|
|
"memory_size",
|
|
"memory_trap",
|
|
"bulk",
|
|
"memory_copy",
|
|
"memory_fill",
|
|
"memory_init",
|
|
"data",
|
|
],
|
|
table: [
|
|
"table_copy",
|
|
"table_fill",
|
|
"table_get",
|
|
"table_grow",
|
|
"table_init",
|
|
"table_set",
|
|
"table_size",
|
|
"elem",
|
|
"ref_func",
|
|
"ref_is_null",
|
|
"ref_null",
|
|
"exports",
|
|
"linking",
|
|
],
|
|
imports: ["imports", "table", "names"],
|
|
};
|
|
|
|
const MAX_ACTIONS = 400;
|
|
|
|
const { values } = parseArgs({
|
|
options: { wast2json: { type: "string" }, spec: { type: "string" } },
|
|
});
|
|
const repo = join(dirname(new URL(import.meta.url).pathname), "../..");
|
|
|
|
// Writes control and format characters (names.wast has several) as escapes, so the
|
|
// generated file holds the same strings without invisible characters in its source.
|
|
function escapeInvisible(literal) {
|
|
return literal.replace(/[\u007f-\u009f\u2028\u2029\p{Cf}]/gu, (c) => `\\u{${c.codePointAt(0).toString(16)}}`);
|
|
}
|
|
|
|
function value(v) {
|
|
if (v.type === "v128") return null;
|
|
return `${v.type}:${v.value}`;
|
|
}
|
|
|
|
/** Whether a "type:value" argument is a NaN other than the canonical
|
|
* quiet one. */
|
|
function oddNaN(arg) {
|
|
const [type, text] = arg.split(":");
|
|
if (type === "f32") {
|
|
const bits = Number(text);
|
|
return (bits & 0x7f800000) === 0x7f800000 && (bits & 0x7fffff) !== 0 && (bits & 0x7fffffff) !== 0x7fc00000;
|
|
}
|
|
if (type === "f64") {
|
|
const bits = BigInt(text);
|
|
const exp = 0x7ff0000000000000n;
|
|
const mantissa = bits & 0xfffffffffffffn;
|
|
return (bits & exp) === exp && mantissa !== 0n && (bits & 0x7fffffffffffffffn) !== 0x7ff8000000000000n;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Whether a "type:value" integer argument is the bit pattern of a
|
|
* signaling NaN. */
|
|
function signalingPattern(arg) {
|
|
const [type, text] = arg.split(":");
|
|
if (type === "i32") {
|
|
const bits = Number(text) >>> 0;
|
|
return (bits & 0x7fc00000) === 0x7f800000 && (bits & 0x3fffff) !== 0;
|
|
}
|
|
if (type === "i64") {
|
|
const bits = BigInt.asUintN(64, BigInt(text));
|
|
return (bits & 0x7ff8000000000000n) === 0x7ff0000000000000n && (bits & 0x7ffffffffffffn) !== 0n;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Keeps every structural command and an evenly spaced sample of at most
|
|
* MAX_ACTIONS actions. */
|
|
function sample(commands) {
|
|
const actions = commands.filter((c) => c[0] === "invoke" || c[0] === "get").length;
|
|
if (actions <= MAX_ACTIONS) return commands;
|
|
const step = actions / MAX_ACTIONS;
|
|
let seen = 0;
|
|
let next = 0;
|
|
return commands.filter((c) => {
|
|
if (c[0] !== "invoke" && c[0] !== "get") return true;
|
|
const keep = seen >= next;
|
|
seen++;
|
|
if (keep) next += step;
|
|
return keep;
|
|
});
|
|
}
|
|
|
|
for (const [group, names] of Object.entries(GROUPS)) {
|
|
const modules = [];
|
|
const moduleIndex = new Map();
|
|
const commands = [];
|
|
for (const name of names) {
|
|
const dir = mkdtempSync(join(tmpdir(), "wasm-spec-"));
|
|
try {
|
|
execFileSync(values.wast2json, [join(values.spec, `${name}.wast`), "-o", join(dir, "x.json")], {
|
|
stdio: "pipe",
|
|
});
|
|
const json = JSON.parse(readFileSync(join(dir, "x.json"), "utf8"));
|
|
const fileCommands = [["file", `${name}.wast`]];
|
|
// Module names that refer to a module left out: their commands go too.
|
|
const skipped = new Set();
|
|
let current = null;
|
|
let currentSkipped = false;
|
|
for (const c of json.commands) {
|
|
const where = c.line;
|
|
if (c.type === "module" || c.type === "assert_uninstantiable" || c.type === "assert_unlinkable") {
|
|
if (c.module_type === "text" || !c.filename?.endsWith(".wasm")) {
|
|
if (c.type === "module") {
|
|
currentSkipped = true;
|
|
if (c.name) skipped.add(c.name);
|
|
}
|
|
continue;
|
|
}
|
|
const bytes = readFileSync(join(dir, c.filename));
|
|
const summary = analyzeWasm(new Uint8Array(bytes));
|
|
if (summary.status !== "valid") {
|
|
if (c.type === "module") {
|
|
currentSkipped = true;
|
|
if (c.name) skipped.add(c.name);
|
|
}
|
|
continue;
|
|
}
|
|
const b64 = bytes.toString("base64");
|
|
let index = moduleIndex.get(b64);
|
|
if (index === undefined) {
|
|
index = modules.length;
|
|
modules.push(b64);
|
|
moduleIndex.set(b64, index);
|
|
}
|
|
if (c.type === "module") {
|
|
current = c.name ?? null;
|
|
currentSkipped = false;
|
|
fileCommands.push(["module", where, index, ...(c.name ? [c.name] : [])]);
|
|
} else {
|
|
fileCommands.push(["instantiate", where, index]);
|
|
}
|
|
continue;
|
|
}
|
|
if (c.type === "register") {
|
|
const target = c.name ?? current;
|
|
if ((c.name ? skipped.has(c.name) : currentSkipped)) continue;
|
|
fileCommands.push(["register", c.as, ...(target ? [target] : [])]);
|
|
continue;
|
|
}
|
|
if (!c.action) continue;
|
|
const a = c.action;
|
|
const target = a.module ?? null;
|
|
if (target ? skipped.has(target) : currentSkipped) continue;
|
|
const args = (a.args ?? []).map(value);
|
|
const expected = (c.expected ?? []).map(value);
|
|
if (args.includes(null) || expected.includes(null)) continue;
|
|
if (args.some(oddNaN)) continue;
|
|
if (name === "float_exprs" && args.some(signalingPattern)) continue;
|
|
fileCommands.push([a.type === "get" ? "get" : "invoke", where, target ?? 0, a.field, args]);
|
|
}
|
|
commands.push(...sample(fileCommands));
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
const out = join(repo, `tests/corpus/webassembly-spec-${group}`);
|
|
mkdirSync(out, { recursive: true });
|
|
const header = `/* Generated by tests/wasm-spec/gen.mjs from the WebAssembly 2.0 core test
|
|
* suite (WebAssembly/spec wg-2.0, Apache-2.0) with wabt 1.0.37 wast2json:
|
|
* ${names.map((n) => `${n}.wast`).join(", ")}. Do not edit. */\n`;
|
|
const body =
|
|
`export const modules = [\n${modules.map((m) => ` "${m}",`).join("\n")}\n];\n` +
|
|
`export const script = ${escapeInvisible(JSON.stringify(JSON.stringify(commands)))};\n`;
|
|
writeFileSync(join(out, "data.mjs"), header + body);
|
|
writeFileSync(
|
|
join(out, "main.mjs"),
|
|
`// Executes WebAssembly spec tests (${names.length} files); see tests/wasm-spec/runner.mjs.\nimport { run } from "../../wasm-spec/runner.mjs";\nimport { modules, script } from "./data.mjs";\n\nrun(modules, script);\n`,
|
|
);
|
|
console.log(`${group}: ${modules.length} modules, ${commands.length} commands`);
|
|
}
|