open-standards/wai/wai-module-check/differential.mjs
Transaction Science ae91bc200a wai-module-check: mutated copies in the differential, and the specification's tests
wasm-smith writes valid modules, or ones invalid only as it is told to be, so
the differential tested how the two validators accept far more than how they
refuse: with check-module.mjs changed to accept an untyped select of
references, or to read an uninitialised local, the 10,000 modules still all
agreed. The differential now also writes two mutated copies of each module, one
or two bytes changed (a byte replaced, inserted or removed anywhere after the
preamble, or a code-section byte replaced by a one-byte opcode), most of them
outside R2. On the committed seed the checkers agree on all 60,000 verdicts of
its 30,000 modules, and it catches a changed section order, an unchecked if
without else or an unchecked alignment.

examples/spectest.rs writes every module the specification's test scripts
define, assert invalid or assert malformed, with the Rust checker's verdicts,
for differential.mjs. On the scripts of the specification build of 2026-11-04
(commit 859d3232a55c, test/core, 259 scripts, one of which the text parser of
this release does not read) it writes 5,712 modules: the Rust checker refuses
every invalid and malformed one by R2 and no valid one, and the two checkers
agree on all 11,424 verdicts. With check-module.mjs changed in any of ten
ways a validator can go wrong (a select of references, an uninitialised local,
a u32's unused bits, br_if's pushed types, duplicate exports, a custom section's
name, a final supertype, a field's mutability in subtyping, catch labels,
undeclared function references), they disagree. Its scripts are not in this
repository, so it is run by hand on a copy of them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:25:56 -05:00

58 lines
2.5 KiB
JavaScript

// differential.mjs: the differential's second half. examples/differential.rs writes
// modules from wasm-smith and the Rust checker's verdicts; this runs check-module.mjs on
// every one at both levels, under the caps the Rust side used, and names each verdict
// that differs.
//
// node differential.mjs DIR
//
// DIR/verdicts.txt holds one line a module, `<name> <caps> <integer verdict> | <float
// verdict>`, the module being DIR/<name>.wasm: `<i>` for wasm-smith's i-th, `<i>-<k>` for a
// mutated copy of it, `<script>.<n>` for a module of the specification's tests
// (examples/spectest.rs). Prints each disagreement, then how the verdicts spread over held and
// the rules. Exit status 0 when every verdict agrees, 1 otherwise, 2 for an input error.
// No dependency.
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { analyse, verdictOf, parseCaps } from './check-module.mjs';
const dir = process.argv[2];
if (!dir || process.argv.length !== 3) {
console.error('usage: node differential.mjs DIR');
process.exit(2);
}
let lines;
try {
lines = readFileSync(join(dir, 'verdicts.txt'), 'utf8').split('\n').filter(Boolean);
} catch (e) {
console.error(`differential: ${e.message}`);
process.exit(2);
}
const spread = new Map();
const count = (k) => spread.set(k, (spread.get(k) || 0) + 1);
let differ = 0, modules = 0;
for (const line of lines) {
const m = /^(\S+) (\S+) (.+) \| (.+)$/.exec(line);
const caps = m && parseCaps(m[2]);
if (!m || !caps) {
console.error(`differential: not a verdict line: ${line}`);
process.exit(2);
}
const bytes = new Uint8Array(readFileSync(join(dir, `${m[1]}.wasm`)));
const analysis = bytes.length > caps.module_bytes ? null : analyse(bytes);
modules++;
for (const [level, want] of [['integer', m[3]], ['float', m[4]]]) {
const got = verdictOf(analysis, bytes.length, level, caps);
const kind = m[1].includes('.') ? 'spec' : m[1].includes('-') ? 'mutated' : 'wasm-smith';
count(`${kind} ${level} ${want === 'held' ? 'held' : want.split(' ').slice(0, 2).join(' ')}`);
if (got !== want) {
differ++;
if (differ <= 50) console.log(`${m[1]}.wasm ${level} ${m[2]}: the Rust checker ${want}, check-module.mjs ${got}`);
}
}
}
for (const k of [...spread.keys()].sort()) console.log(` ${k}: ${spread.get(k)}`);
console.log(differ
? `differential: ${differ} verdicts of ${modules * 2} differ, over ${modules} modules`
: `differential: the two checkers agree on all ${modules * 2} verdicts of ${modules} modules`);
process.exit(differ ? 1 : 0);