wai/wai-int-module is one WebAssembly module with the interface of waism §5 and nothing else: no import, one memory with a declared maximum, and every allocation trapping where memory cannot grow (R13). It decodes the integer payloads (WIV1, WIH1, WIA1, WIS1), int_synth, int_mlicpp, opus.dred.int and avatar.face through wai-rs's integer decoders, whose networks run on wai-ops/1; networks and weights arrive as each capability's set. - record/: the scalar and SIMD128 builds (7,099 vector instructions), named by SHA-256 in manifest.json, each held at the integer level by the Rust checker and check-module.mjs. build.sh builds both with the machine's paths remapped and checks both with both checkers. A build on another host has other bytes (the compiler's crate hashes take in each path dependency's absolute path and the host) and gives every case the same outcome. - corpus.json (corpus.py): every committed vector and refusal fixture of those capabilities, 120 cases, from the committed corpora. - host/ (wasmtime 50.0.1 Cranelift and Winch, Wasmi 2.0.0 with floats off), run-node.mjs and browser/run-browser.mjs run every case as a sink does and print one run digest; every run, on Linux x86_64, Linux arm64 and macOS arm64, gives the same. - plant.py plants a float or a relaxed vector instruction in a module, for the checkers. - waism §5, The set: one file as its bytes, several as their file sequence. avatars §6 names generator.wfg and identity.wfi; avatars §7.8, opus-dred §4.8 and determinism-tiers §5 state the module as a fabric. Co-Authored-By: Claude Opus 5.5 <[email protected]>
95 lines
3.8 KiB
JavaScript
95 lines
3.8 KiB
JavaScript
// run-node.mjs: the module of record on Node's engine, as a sink runs it.
|
|
//
|
|
// node run-node.mjs [--external DIR] [--only PREFIX] [--limit SECONDS] CORPUS MODULE...
|
|
//
|
|
// For each module, every case of the module corpus (corpus.json) on an instance of its own,
|
|
// through sink.mjs (waism §5; §6 checks 11 to 13), in a worker that this script ends when one
|
|
// run passes the wall-clock limit (waism §7, `wall-clock`; default 300 seconds a case). Each
|
|
// output is held to the corpus, and each run prints the same line as the native host
|
|
// (host/): its module, engine, the cases it held and its run digest. Every run digest must
|
|
// be the same.
|
|
//
|
|
// Exit status 0 when every case of every run is the corpus's and every run digest is the
|
|
// same; 1 otherwise; 2 for a usage or input error.
|
|
import { readFileSync } from 'node:fs';
|
|
import { arch } from 'node:process';
|
|
import { Worker } from 'node:worker_threads';
|
|
import { judge, loadCases, sha256 } from './cases.mjs';
|
|
|
|
function usage(why) {
|
|
if (why) console.error(`run-node.mjs: ${why}`);
|
|
console.error('usage: node run-node.mjs [--external DIR] [--only PREFIX] [--limit SECONDS] CORPUS MODULE...');
|
|
process.exit(2);
|
|
}
|
|
|
|
const argv = process.argv.slice(2);
|
|
let external = null, only = null, limit = 300;
|
|
const rest = [];
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === '--external') external = argv[++i] ?? usage();
|
|
else if (a === '--only') only = argv[++i] ?? usage();
|
|
else if (a === '--limit') limit = Number(argv[++i] ?? usage());
|
|
else if (a.startsWith('--')) usage(`no option ${a}`);
|
|
else rest.push(a);
|
|
}
|
|
if (rest.length < 2 || !(limit > 0)) usage();
|
|
|
|
let cases;
|
|
try {
|
|
cases = loadCases(rest[0], { external, only });
|
|
} catch (e) {
|
|
console.error(`run-node.mjs: ${e.message}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
/** Every case through one module in a worker, each under the limit. */
|
|
function runModule(moduleBytes) {
|
|
return new Promise((done) => {
|
|
const results = new Array(cases.length);
|
|
const w = new Worker(new URL('./node-worker.mjs', import.meta.url), { workerData: { moduleBytes, cases } });
|
|
let timer = null, current = -1;
|
|
const finish = () => { clearTimeout(timer); done(results); };
|
|
w.on('message', (m) => {
|
|
if (m.start !== undefined) {
|
|
current = m.start;
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => {
|
|
results[current] = { sink: 'module_timeout', detail: `past ${limit} s` };
|
|
for (let i = current + 1; i < cases.length; i++) results[i] = { sink: 'module_timeout', detail: 'not run: an earlier case timed out' };
|
|
w.terminate().then(finish);
|
|
}, limit * 1000);
|
|
} else if (m.done !== undefined) {
|
|
results[m.done] = m.r;
|
|
} else if (m.end) {
|
|
w.terminate().then(finish);
|
|
}
|
|
});
|
|
w.on('error', (e) => {
|
|
for (let i = Math.max(current, 0); i < cases.length; i++) results[i] ??= { sink: 'module_trap', detail: e.message };
|
|
finish();
|
|
});
|
|
});
|
|
}
|
|
|
|
const digests = new Map();
|
|
let failed = false;
|
|
const engine = `node@${process.versions.node}/v8@${process.versions.v8}`;
|
|
for (const path of rest.slice(1)) {
|
|
const bytes = new Uint8Array(readFileSync(path));
|
|
const t0 = Date.now();
|
|
const results = await runModule(bytes);
|
|
const { wrong, report, digest } = judge(cases, results);
|
|
for (const line of report) console.log(line);
|
|
console.log(`${path} (SHA-256 ${sha256(bytes)}) on ${engine}, ${arch}: ${cases.length - wrong} of ${cases.length} cases the corpus's; run digest ${digest}; ${((Date.now() - t0) / 1000).toFixed(1)} s`);
|
|
failed ||= wrong > 0;
|
|
digests.set(path, digest);
|
|
}
|
|
const distinct = new Set(digests.values());
|
|
if (distinct.size > 1) {
|
|
console.log(`run-node.mjs: the runs differ: ${distinct.size} run digests`);
|
|
failed = true;
|
|
} else {
|
|
console.log(`run-node.mjs: ${digests.size} runs, one run digest ${[...distinct][0]}`);
|
|
}
|
|
process.exit(failed ? 1 : 0);
|