open-standards/wai/wai-module-check/check-module.mjs
Transaction Science d053c8bd6e wai: the deterministic module profile is WA²ISM™, WAISM in running text: extensions/waism.md, levels waism/1-int and waism/1-det
The name replaces the working one across the specification, the two checkers and
their planted set. The planted module that carries a profile name in a custom section
is rebuilt with build.py. CI's step label stays ASCII.

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

1374 lines
67 KiB
JavaScript

// check-module.mjs: a module's verdict under WAI's WAISM profile
// (wai/extensions/waism.md), with no dependency. It is the checker a page
// runs before it instantiates a decoder module, and the second implementation beside the
// Rust checker of this directory, written from the profile's text and from WebAssembly 3.0,
// sharing no code with it. It generalises wai-vector's check-mvp.mjs, which reads a module
// of WebAssembly 1.0, to every construct of WebAssembly 3.0 and to the profile's rules.
//
// The verdict for a module at a level under caps (§4.3) is one line of ASCII: `held`, or
// `refused R<n> <offset>`:
// 1. R1: a module longer than `module_bytes` is `refused R1 0`, and is not read further.
// 2. R2: a module that does not decode under WebAssembly 3.0's binary format, or is not
// valid, within the validation limits of §4.2, is `refused R2 -`. This file decodes
// every section and instruction and validates them as WebAssembly 3.0 does, the types
// of garbage collection, exception handling and typed function references included.
// 3. R3 to R12 over the module's items in binary order: the first item that breaks one is
// refused at its offset, by the lowest-numbered rule it breaks.
// 4. Then R6 where the module defines no memory, and R10 where an export of §5 is
// missing, at the offset equal to the module's length.
//
// In a page: import { check, levelOf } from './check-module.mjs';
// check(bytes, 'integer') → 'held' | 'refused R<n> <offset>'
// From a shell: node check-module.mjs [--level integer|float] [--caps NAME=VALUE,...] FILE...
// node check-module.mjs --cases DIR
//
// With --level, one verdict line a file; without it, the level a sink holds the module at
// (§6, check 9b) or the verdict that refuses it. --cases checks every line of DIR/cases.txt
// (`<case> <level> <caps> <verdict>`; the module is DIR/<case>.wasm). Exit status: 0 when
// every module is held (with --cases, when every verdict is the one stated), 1 otherwise,
// 2 for a usage or input error.
/** The profile's caps (§4.2). */
export const PROFILE_CAPS = Object.freeze({ module_bytes: 16777216, functions: 50000, memory_pages: 65536 });
/** The validation limits of §4.2, part of R2. */
const LIMIT = Object.freeze({
types: 1000000, functions: 1000000, globals: 1000000, tags: 1000000,
tables: 100000, memories: 100, elems: 100000, datas: 100000, segmentItems: 10000000,
name: 100000, body: 7654321, locals: 50000, params: 1000, results: 1000,
fields: 10000, catches: 10000, depth: 63, interface: 999998,
});
/** The exports of §5: name, kind (0 a function, 2 the memory) and a function's type. */
const ABI = [
['memory', 2, null],
['wai_abi', 0, [[], ['i32']]],
['wai_alloc', 0, [['i32'], ['i32']]],
['wai_run', 0, [['i32', 'i32', 'i32', 'i32', 'i32', 'i32'], ['i32']]],
['wai_out_ptr', 0, [[], ['i32']]],
['wai_out_len', 0, [[], ['i32']]],
];
// A module outside R2: one that does not decode, or is not valid. The message is for a
// reader of this file; the verdict carries none of it.
class Refused2 extends Error {}
function bad(why) { throw new Refused2(why); }
// ------------------------------------------------------------------------------ bytes
class Reader {
constructor(b, at, end) { this.b = b; this.at = at; this.end = end; }
eof() { return this.at >= this.end; }
byte() { if (this.at >= this.end) bad('unexpected end'); return this.b[this.at++]; }
peek() { if (this.at >= this.end) bad('unexpected end'); return this.b[this.at]; }
skip(n) { if (n > this.end - this.at) bad('unexpected end'); this.at += n; }
// Unsigned LEB128 of 32 bits: at most 5 bytes, the unused bits of the last zero.
u32() {
let v = 0, scale = 1;
for (let i = 0; i < 5; i++) {
const c = this.byte();
if (i === 4 && (c & 0xf0) !== 0) bad('u32 too large');
v += (c & 0x7f) * scale;
if (!(c & 0x80)) return v;
scale *= 128;
}
return v;
}
// Unsigned LEB128 of 64 bits, exact, as a BigInt: at most 10 bytes.
u64() {
let v = 0n, shift = 0n;
for (let i = 0; i < 10; i++) {
const c = this.byte();
if (i === 9 && (c & 0xfe) !== 0) bad('u64 too large');
v |= BigInt(c & 0x7f) << shift;
if (!(c & 0x80)) return v;
shift += 7n;
}
return v;
}
// The same encoding, as a Number: exact below 2^53, which is all a comparison here needs.
u64n() {
let v = 0, scale = 1;
for (let i = 0; i < 10; i++) {
const c = this.byte();
if (i === 9 && (c & 0xfe) !== 0) bad('u64 too large');
v += (c & 0x7f) * scale;
if (!(c & 0x80)) return v;
scale *= 128;
}
return v;
}
// Signed LEB128: at most `max` bytes, the unused bits of the last a sign extension; the
// last byte's bits from `keep` up must all equal bit `keep - 1`. Returns the value as a
// Number (s33 is exact; s32 and s64 values are not used).
signed(max, keep) {
let v = 0, scale = 1;
for (let i = 0; i < max; i++) {
const c = this.byte();
if (i === max - 1) {
if (c & 0x80) bad('signed LEB128 too long');
const high = c >> (keep - 1); // bit keep-1 up to bit 6
if (high !== 0 && high !== (0x7f >> (keep - 1))) bad('signed LEB128 too large');
}
v += (c & 0x7f) * scale;
scale *= 128;
if (!(c & 0x80)) return (c & 0x40) ? v - scale : v;
}
return v;
}
s32() { return this.signed(5, 4); }
s33() { return this.signed(5, 5); }
s64() { return this.signed(10, 1); }
// A name: a vector of bytes that is UTF-8, at most the name limit long.
name() {
const n = this.u32();
if (n > LIMIT.name) bad('name too long');
if (n > this.end - this.at) bad('unexpected end');
utf8(this.b, this.at, this.at + n);
const s = this.at;
this.at += n;
return String.fromCharCode.apply(null, Array.from(this.b.subarray(s, s + n)));
}
}
// Valid UTF-8: no overlong form, no surrogate, nothing above U+10FFFF.
function utf8(b, at, end) {
while (at < end) {
const c = b[at];
if (c < 0x80) { at++; continue; }
let n, min;
if (c >= 0xc2 && c <= 0xdf) { n = 1; min = 0x80; }
else if (c >= 0xe0 && c <= 0xef) { n = 2; min = 0x800; }
else if (c >= 0xf0 && c <= 0xf4) { n = 3; min = 0x10000; }
else bad('not UTF-8');
if (at + n >= end) bad('not UTF-8');
let cp = c & (0x3f >> n);
for (let i = 1; i <= n; i++) {
const d = b[at + i];
if ((d & 0xc0) !== 0x80) bad('not UTF-8');
cp = cp * 64 + (d & 0x3f);
}
if (cp < min || cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff)) bad('not UTF-8');
at += n + 1;
}
}
// ------------------------------------------------------------------------------ types
//
// A value type is a string ('i32', 'i64', 'f32', 'f64', 'v128'), 'bot' (the unknown type of
// an unreachable operand), or a reference {nullable, heap}: heap is an abstract heap type
// ('func', 'nofunc', 'extern', 'noextern', 'any', 'eq', 'i31', 'struct', 'array', 'none',
// 'exn', 'noexn', or 'bot') or a canonical type id, a number: two type indices whose
// definitions are equivalent (iso-recursively, by their recursion groups) have one id.
const ABSTRACT = {
0x70: 'func', 0x6f: 'extern', 0x6e: 'any', 0x6d: 'eq', 0x6c: 'i31', 0x6b: 'struct',
0x6a: 'array', 0x69: 'exn', 0x73: 'nofunc', 0x72: 'noextern', 0x71: 'none', 0x74: 'noexn',
};
const NUMTYPE = { 0x7f: 'i32', 0x7e: 'i64', 0x7d: 'f32', 0x7c: 'f64', 0x7b: 'v128' };
const FUNCREF = Object.freeze({ nullable: true, heap: 'func' });
const BOTREF = Object.freeze({ nullable: false, heap: 'bot' });
const isValTypeByte = (c) => c in NUMTYPE || c in ABSTRACT || c === 0x63 || c === 0x64;
const isRef = (t) => typeof t === 'object';
const isFloat = (t) => t === 'f32' || t === 'f64';
function analyze(b) {
const findings = []; // {at, rule, int?, cap?, value?}
const canon = []; // canonical types: {kind, params, results, fields, final, sup, depth}
const groups = new Map();
const types = []; // type index → canonical id
const funcs = []; // function index → canonical id of its type
const tables = []; // {at: 'i32'|'i64', elem}
const mems = []; // {at}
const tags = []; // canonical id
const globals = []; // {type, mut}
const elems = []; // reference type
const refs = new Set();
let dataCount = null, dataSegments = null, definedFuncs = null, codeEntries = null;
let nImportedFuncs = 0, definedMems = 0, iface = 1;
const exported = new Set(), abiSeen = new Set();
const valueItem = (t, at) => {
if (isFloat(t)) findings.push({ at, rule: 3, int: true });
else if (isRef(t)) findings.push({ at, rule: 9 });
};
const addInterface = (n) => { iface += n; if (iface > LIMIT.interface + 1) bad('interface size'); };
const funcType = (x) => {
if (x >= types.length) bad('unknown type');
const id = types[x];
if (canon[id].kind !== 'func') bad('not a function type');
return canon[id];
};
// -------------------------------------------------------------------- subtyping
function subHeap(a, c) {
if (a === c || a === 'bot') return true;
if (c === 'bot') return false;
if (typeof a === 'number') {
if (typeof c === 'number') {
for (let x = canon[a].sup; x !== null; x = canon[x].sup) if (x === c) return true;
return false;
}
const k = canon[a].kind;
if (k === 'func') return c === 'func';
if (k === 'struct') return c === 'struct' || c === 'eq' || c === 'any';
return c === 'array' || c === 'eq' || c === 'any';
}
if (typeof c === 'number') return canon[c].kind === 'func' ? a === 'nofunc' : a === 'none';
switch (a) {
case 'none': return c === 'i31' || c === 'struct' || c === 'array' || c === 'eq' || c === 'any';
case 'i31': case 'struct': case 'array': return c === 'eq' || c === 'any';
case 'eq': return c === 'any';
case 'nofunc': return c === 'func';
case 'noextern': return c === 'extern';
case 'noexn': return c === 'exn';
default: return false;
}
}
function sub(a, c) {
if (a === 'bot') return true;
if (typeof a === 'string' || typeof c === 'string') return a === c;
return (!a.nullable || c.nullable) && subHeap(a.heap, c.heap);
}
const same = (a, c) => (typeof a === 'string' ? a === c : typeof c === 'object' && a.nullable === c.nullable && a.heap === c.heap);
const sameStorage = (a, c) => same(a, c);
const subStorage = (a, c) => (a === 'i8' || a === 'i16' || c === 'i8' || c === 'i16' ? a === c : sub(a, c));
const fieldMatch = (f, g) => f.mut === g.mut && (f.mut ? sameStorage(f.st, g.st) : subStorage(f.st, g.st));
function compMatch(e, s) {
if (e.kind !== s.kind) return false;
if (e.kind === 'func') {
return e.params.length === s.params.length && e.results.length === s.results.length
&& e.params.every((t, i) => sub(s.params[i], t)) && e.results.every((t, i) => sub(t, s.results[i]));
}
if (e.kind === 'struct') return e.fields.length >= s.fields.length && s.fields.every((g, i) => fieldMatch(e.fields[i], g));
return fieldMatch(e.fields[0], s.fields[0]);
}
function top(h) {
if (typeof h === 'number') return canon[h].kind === 'func' ? 'func' : 'any';
switch (h) {
case 'func': case 'nofunc': return 'func';
case 'extern': case 'noextern': return 'extern';
case 'exn': case 'noexn': return 'exn';
default: return 'any';
}
}
const unpack = (st) => (st === 'i8' || st === 'i16' ? 'i32' : st);
const defaultable = (t) => !isRef(t) || t.nullable;
// -------------------------------------------------------------------- reading types
function heapType(r, raw) {
const c = r.peek();
if (c in ABSTRACT) { r.at++; return ABSTRACT[c]; }
const x = r.s33();
if (x < 0) bad('bad heap type');
if (raw) return { mod: x };
if (x >= types.length) bad('unknown type');
return types[x];
}
function valType(r, raw) {
const c = r.byte();
if (c in NUMTYPE) return NUMTYPE[c];
if (c in ABSTRACT) return { nullable: true, heap: ABSTRACT[c] };
if (c === 0x63 || c === 0x64) return { nullable: c === 0x63, heap: heapType(r, raw) };
bad('bad value type');
}
function refType(r) {
const t = valType(r, false);
if (!isRef(t)) bad('not a reference type');
return t;
}
function storageType(r) {
const c = r.peek();
if (c === 0x78) { r.at++; return 'i8'; }
if (c === 0x77) { r.at++; return 'i16'; }
return valType(r, true);
}
function mutability(r) {
const m = r.byte();
if (m > 1) bad('bad mutability');
return m === 1;
}
// One subtype of a type entry; the value types of a plain function type with their offsets.
function subType(r) {
let final = true, supers = [];
let c = r.byte();
if (c === 0x50 || c === 0x4f) {
final = c === 0x4f;
const n = r.u32();
if (n > 1) bad('more than one supertype');
for (let i = 0; i < n; i++) supers.push(r.u32());
c = r.byte();
}
const items = [];
if (c === 0x60) {
const lists = [];
for (const limit of [LIMIT.params, LIMIT.results]) {
const n = r.u32();
if (n > limit) bad('too many parameters or results');
const ts = [];
for (let i = 0; i < n; i++) { const at = r.at; const t = valType(r, true); items.push([t, at]); ts.push(t); }
lists.push(ts);
}
return { final, supers, kind: 'func', params: lists[0], results: lists[1], fields: null, items };
}
if (c === 0x5f) {
const n = r.u32();
if (n > LIMIT.fields) bad('too many fields');
const fields = [];
for (let i = 0; i < n; i++) { const st = storageType(r); fields.push({ st, mut: mutability(r) }); }
return { final, supers, kind: 'struct', params: null, results: null, fields, items };
}
if (c === 0x5e) {
const st = storageType(r);
return { final, supers, kind: 'array', params: null, results: null, fields: [{ st, mut: mutability(r) }], items };
}
bad('bad composite type');
}
// A recursion group: bounds, canonical ids (one per equivalent group), then validity.
function defineGroup(subs) {
const base = types.length, n = subs.length;
const hk = (h) => (typeof h === 'string' ? h : (h.mod >= base ? 'r' + (h.mod - base) : 'c' + types[h.mod]));
const vk = (t) => (typeof t === 'string' ? t : (t.nullable ? '?' : '!') + hk(t.heap));
const check = (t) => { if (isRef(t) && typeof t.heap === 'object' && t.heap.mod >= base + n) bad('unknown type'); };
subs.forEach((s, k) => {
for (const x of s.supers) if (x >= base + k) bad('supertypes must be defined before subtypes');
(s.params || []).forEach(check); (s.results || []).forEach(check);
(s.fields || []).forEach((f) => check(f.st));
});
const key = subs.map((s) => (s.final ? 'F' : 'N') + s.supers.map((x) => hk({ mod: x })).join(',') + ':' + s.kind + '('
+ (s.kind === 'func' ? s.params.map(vk).join(',') + ')(' + s.results.map(vk).join(',')
: s.fields.map((f) => (f.mut ? 'm' : 'c') + vk(f.st)).join(',')) + ')').join('|');
let start = groups.get(key);
const fresh = start === undefined;
if (fresh) {
start = canon.length;
groups.set(key, start);
const cv = (t) => (typeof t === 'string' ? t
: { nullable: t.nullable, heap: typeof t.heap === 'string' ? t.heap : (t.heap.mod >= base ? start + t.heap.mod - base : types[t.heap.mod]) });
for (const s of subs) {
canon.push({
kind: s.kind, final: s.final, depth: 0,
sup: s.supers.length ? (s.supers[0] >= base ? start + s.supers[0] - base : types[s.supers[0]]) : null,
params: s.params && s.params.map(cv), results: s.results && s.results.map(cv),
fields: s.fields && s.fields.map((f) => ({ st: f.st === 'i8' || f.st === 'i16' ? f.st : cv(f.st), mut: f.mut })),
});
}
}
for (let k = 0; k < n; k++) types.push(start + k);
if (!fresh) return;
for (let k = 0; k < n; k++) {
const e = canon[start + k];
if (e.sup === null) continue;
const s = canon[e.sup];
if (s.final) bad('final supertype');
if (!compMatch(e, s)) bad('subtype does not match its supertype');
e.depth = s.depth + 1;
if (e.depth > LIMIT.depth) bad('subtype too deep');
}
}
function limits(r, i64Bound) {
const flags = r.byte();
if (flags !== 0 && flags !== 1 && flags !== 4 && flags !== 5) bad('bad limits');
const at = flags & 4 ? 'i64' : 'i32';
const read = () => (at === 'i64' ? r.u64() : BigInt(r.u32()));
const min = read();
const max = flags & 1 ? read() : null;
if (max !== null && min > max) bad('minimum above maximum');
const bound = at === 'i64' ? i64Bound : (i64Bound === null ? null : 65536n);
if (bound !== null && (min > bound || (max !== null && max > bound))) bad('memory too large');
return { at, min, max };
}
const memType = (r) => limits(r, 1n << 48n);
function tableType(r) {
const elem = refType(r);
const l = limits(r, null);
return { at: l.at, min: l.min, max: l.max, elem };
}
function globalType(r) {
const type = valType(r, false);
return { type, mut: mutability(r) };
}
// -------------------------------------------------------------------- constant expressions
function constExpr(r, expect) {
const st = [];
const pop = (t) => { if (!st.length) bad('constant: empty stack'); const v = st.pop(); if (!sub(v, t)) bad('constant: type mismatch'); return v; };
for (;;) {
const at = r.at, op = r.byte();
switch (op) {
case 0x0b:
if (st.length !== 1 || !sub(st[0], expect)) bad('constant: type mismatch');
return;
case 0x41: r.s32(); st.push('i32'); break;
case 0x42: r.s64(); st.push('i64'); break;
case 0x43: r.skip(4); st.push('f32'); findings.push({ at, rule: 3, int: true }); break;
case 0x44: r.skip(8); st.push('f64'); findings.push({ at, rule: 3, int: true }); break;
case 0x23: {
const x = r.u32();
if (x >= globals.length) bad('unknown global');
if (globals[x].mut) bad('constant: mutable global');
st.push(globals[x].type); break;
}
case 0xd0: st.push({ nullable: true, heap: heapType(r, false) }); break;
case 0xd2: {
const x = r.u32();
if (x >= funcs.length) bad('unknown function');
refs.add(x); st.push({ nullable: false, heap: funcs[x] }); break;
}
case 0x6a: case 0x6b: case 0x6c: pop('i32'); pop('i32'); st.push('i32'); break;
case 0x7c: case 0x7d: case 0x7e: pop('i64'); pop('i64'); st.push('i64'); break;
case 0xfd: {
if (r.u32() !== 12) bad('constant expression required');
r.skip(16); st.push('v128'); break;
}
case 0xfb: {
const s = r.u32();
findings.push({ at, rule: 12 });
if (s === 0 || s === 1) {
const x = r.u32(), e = gcType(x, 'struct');
if (s === 0) for (let i = e.fields.length - 1; i >= 0; i--) pop(unpack(e.fields[i].st));
else if (!e.fields.every((f) => defaultable(unpack(f.st)))) bad('not defaultable');
st.push({ nullable: false, heap: types[x] });
} else if (s === 6 || s === 7 || s === 8) {
const x = r.u32(), e = gcType(x, 'array');
if (s === 6) { pop('i32'); pop(unpack(e.fields[0].st)); }
else if (s === 7) { pop('i32'); if (!defaultable(unpack(e.fields[0].st))) bad('not defaultable'); }
else { const k = r.u32(); for (let i = 0; i < k; i++) pop(unpack(e.fields[0].st)); }
st.push({ nullable: false, heap: types[x] });
} else if (s === 28) { pop('i32'); st.push({ nullable: false, heap: 'i31' }); }
else if (s === 26 || s === 27) {
const from = s === 26 ? 'extern' : 'any', to = s === 26 ? 'any' : 'extern';
const v = pop({ nullable: true, heap: from });
st.push({ nullable: v.nullable, heap: to });
} else bad('constant expression required');
break;
}
default: bad('constant expression required');
}
}
}
function gcType(x, kind) {
if (x >= types.length) bad('unknown type');
const e = canon[types[x]];
if (e.kind !== kind) bad(`not a ${kind} type`);
return e;
}
// -------------------------------------------------------------------- function bodies
function body(r, typeId) {
const ft = canon[typeId];
const locals = ft.params.slice();
let count = locals.length;
for (let g = r.u32(); g > 0; g--) {
const n = r.u32();
const at = r.at;
const t = valType(r, false);
valueItem(t, at);
count += n;
if (count > LIMIT.locals) bad('too many locals');
for (let i = 0; i < n; i++) locals.push(t);
}
const inited = new Uint8Array(locals.length);
for (let i = 0; i < locals.length; i++) inited[i] = i < ft.params.length || defaultable(locals[i]) ? 1 : 0;
const initLog = [];
const vals = [], ctrls = [];
const pushCtrl = (kind, start, end) => {
ctrls.push({ kind, start, end, height: vals.length, unreachable: false, init: initLog.length });
for (const t of start) vals.push(t);
};
const popVal = () => {
const f = ctrls[ctrls.length - 1];
if (vals.length === f.height) { if (f.unreachable) return 'bot'; bad('type mismatch: empty stack'); }
return vals.pop();
};
const pop = (t) => { const v = popVal(); if (!sub(v, t)) bad('type mismatch'); return v; };
const popVals = (ts) => { const out = new Array(ts.length); for (let i = ts.length - 1; i >= 0; i--) out[i] = pop(ts[i]); return out; };
const pushVals = (ts) => { for (const t of ts) vals.push(t); };
const popRef = () => { const v = popVal(); if (v === 'bot') return BOTREF; if (!isRef(v)) bad('type mismatch: not a reference'); return v; };
const popCtrl = () => {
const f = ctrls[ctrls.length - 1];
popVals(f.end);
if (vals.length !== f.height) bad('type mismatch: values left at the end of a block');
ctrls.pop();
while (initLog.length > f.init) inited[initLog.pop()] = 0;
return f;
};
const labelTypes = (l) => {
if (l >= ctrls.length) bad('unknown label');
const f = ctrls[ctrls.length - 1 - l];
return f.kind === 'loop' ? f.start : f.end;
};
const unreachable = () => { const f = ctrls[ctrls.length - 1]; vals.length = f.height; f.unreachable = true; };
const blockType = () => {
const at = r.at, c = r.peek();
if (c === 0x40) { r.at++; return [[], []]; }
if (isValTypeByte(c)) {
const t = valType(r, false);
if (isFloat(t)) findings.push({ at, rule: 3, int: true });
return [[], [t]];
}
const x = r.s33();
if (x < 0) bad('bad block type');
const e = funcType(x);
return [e.params, e.results];
};
const resultsMatch = (from, to) => from.length === to.length && from.every((t, i) => sub(t, to[i]));
const memarg = (natural) => {
let flags = r.u32();
let mem = 0;
if ((flags & 0x40) !== 0) { flags -= 0x40; mem = r.u32(); }
if (flags >= 64) bad('alignment too large');
const offset = r.u64n();
if (mem >= mems.length) bad('unknown memory');
if (flags > natural) bad('alignment above natural');
const at = mems[mem].at;
if (at === 'i32' && offset > 0xffffffff) bad('offset out of range');
return at;
};
const memIdx = () => { const m = r.u32(); if (m >= mems.length) bad('unknown memory'); return mems[m].at; };
const table = () => { const t = r.u32(); if (t >= tables.length) bad('unknown table'); return tables[t]; };
const dataIdx = () => { const d = r.u32(); if (dataCount === null) bad('data count section required'); if (d >= dataCount) bad('unknown data segment'); };
const elemIdx = () => { const e = r.u32(); if (e >= elems.length) bad('unknown element segment'); return elems[e]; };
const tag = () => { const x = r.u32(); if (x >= tags.length) bad('unknown tag'); return canon[tags[x]]; };
const local = () => { const x = r.u32(); if (x >= locals.length) bad('unknown local'); return x; };
const global = () => { const x = r.u32(); if (x >= globals.length) bad('unknown global'); return globals[x]; };
const callee = () => { const f = r.u32(); if (f >= funcs.length) bad('unknown function'); return canon[funcs[f]]; };
const labelMatch = (ts, l) => { const lt = labelTypes(l); if (!resultsMatch(ts, lt)) bad('type mismatch: catch'); };
const minAt = (a, c) => (a === 'i64' && c === 'i64' ? 'i64' : 'i32');
pushCtrl('func', [], ft.results);
for (;;) {
if (r.eof()) bad('function body ends before its end');
const at = r.at, op = r.byte();
if (op >= 0x45 && op <= 0xc4) {
const n = NUMERIC[op];
if (!n) bad('unknown opcode');
if (n[3]) findings.push({ at, rule: 3, int: true });
popVals(n[1]); pushVals(n[2]);
} else if (op >= 0x28 && op <= 0x3e) {
const [, t, natural, store] = LOADSTORE[op - 0x28];
if (t === 'f32' || t === 'f64') findings.push({ at, rule: 3, int: true });
const a = memarg(natural);
if (store) { pop(t); pop(a); } else { pop(a); vals.push(t); }
} else switch (op) {
case 0x00: unreachable(); break;
case 0x01: break;
case 0x02: case 0x03: { const [p, q] = blockType(); popVals(p); pushCtrl(op === 0x02 ? 'block' : 'loop', p, q); break; }
case 0x04: { const [p, q] = blockType(); pop('i32'); popVals(p); pushCtrl('if', p, q); break; }
case 0x05: { const f = popCtrl(); if (f.kind !== 'if') bad('else without if'); pushCtrl('else', f.start, f.end); break; }
case 0x08: { findings.push({ at, rule: 12 }); const e = tag(); popVals(e.params); unreachable(); break; }
case 0x0a: { findings.push({ at, rule: 12 }); pop({ nullable: true, heap: 'exn' }); unreachable(); break; }
case 0x0b: {
let f = ctrls[ctrls.length - 1];
if (f.kind === 'if') { popCtrl(); pushCtrl('else', f.start, f.end); }
f = popCtrl();
pushVals(f.end);
break;
}
case 0x0c: { popVals(labelTypes(r.u32())); unreachable(); break; }
case 0x0d: { const lt = labelTypes(r.u32()); pop('i32'); popVals(lt); pushVals(lt); break; }
case 0x0e: {
const n = r.u32();
const ls = [];
for (let i = 0; i < n; i++) ls.push(r.u32());
const dflt = r.u32();
pop('i32');
const arity = labelTypes(dflt).length;
for (const l of ls) {
const lt = labelTypes(l);
if (lt.length !== arity) bad('br_table arity');
pushVals(popVals(lt));
}
popVals(labelTypes(dflt));
unreachable();
break;
}
case 0x0f: popVals(ft.results); unreachable(); break;
case 0x10: { const e = callee(); popVals(e.params); pushVals(e.results); break; }
case 0x11: case 0x13: {
if (op === 0x13) findings.push({ at, rule: 12 });
const e = funcType(r.u32()), t = table();
if (!sub(t.elem, FUNCREF)) bad('call_indirect of a table not of functions');
pop(t.at); popVals(e.params);
if (op === 0x11) pushVals(e.results);
else { if (!resultsMatch(e.results, ft.results)) bad('return_call_indirect results'); unreachable(); }
break;
}
case 0x12: {
findings.push({ at, rule: 12 });
const e = callee(); popVals(e.params);
if (!resultsMatch(e.results, ft.results)) bad('return_call results');
unreachable(); break;
}
case 0x14: case 0x15: {
findings.push({ at, rule: 12 });
const x = r.u32(), e = funcType(x);
pop({ nullable: true, heap: types[x] }); popVals(e.params);
if (op === 0x14) pushVals(e.results);
else { if (!resultsMatch(e.results, ft.results)) bad('return_call_ref results'); unreachable(); }
break;
}
case 0x1a: popVal(); break;
case 0x1b: {
pop('i32');
const t1 = popVal(), t2 = popVal();
const num = (t) => t === 'bot' || t === 'i32' || t === 'i64' || t === 'f32' || t === 'f64';
const vec = (t) => t === 'bot' || t === 'v128';
if (!((num(t1) && num(t2)) || (vec(t1) && vec(t2)))) bad('select of references');
if (t1 !== t2 && t1 !== 'bot' && t2 !== 'bot') bad('select type mismatch');
vals.push(t1 === 'bot' ? t2 : t1);
break;
}
case 0x1c: {
if (r.u32() !== 1) bad('select with other than one type');
const tat = r.at;
const t = valType(r, false);
if (isFloat(t)) findings.push({ at: tat, rule: 3, int: true });
pop('i32'); pop(t); pop(t); vals.push(t);
break;
}
case 0x1f: {
findings.push({ at, rule: 12 });
const [p, q] = blockType();
const n = r.u32();
if (n > LIMIT.catches) bad('too many catches');
for (let i = 0; i < n; i++) {
const kind = r.byte();
if (kind > 3) bad('bad catch');
const e = kind < 2 ? tag() : null;
const l = r.u32();
const ts = e ? e.params.slice() : [];
if (kind === 1 || kind === 3) ts.push({ nullable: false, heap: 'exn' });
labelMatch(ts, l);
}
popVals(p); pushCtrl('block', p, q);
break;
}
case 0x20: { const x = local(); if (!inited[x]) bad('uninitialized local'); vals.push(locals[x]); break; }
case 0x21: case 0x22: {
const x = local();
pop(locals[x]);
if (!inited[x]) { inited[x] = 1; initLog.push(x); }
if (op === 0x22) vals.push(locals[x]);
break;
}
case 0x23: vals.push(global().type); break;
case 0x24: { const g = global(); if (!g.mut) bad('global.set of an immutable global'); pop(g.type); break; }
case 0x25: { const t = table(); pop(t.at); vals.push(t.elem); break; }
case 0x26: { findings.push({ at, rule: 7 }); const t = table(); pop(t.elem); pop(t.at); break; }
case 0x3f: vals.push(memIdx()); break;
case 0x40: { const a = memIdx(); pop(a); vals.push(a); break; }
case 0x41: r.s32(); vals.push('i32'); break;
case 0x42: r.s64(); vals.push('i64'); break;
case 0x43: r.skip(4); findings.push({ at, rule: 3, int: true }); vals.push('f32'); break;
case 0x44: r.skip(8); findings.push({ at, rule: 3, int: true }); vals.push('f64'); break;
case 0xd0: vals.push({ nullable: true, heap: heapType(r, false) }); break;
case 0xd1: popRef(); vals.push('i32'); break;
case 0xd2: {
const x = r.u32();
if (x >= funcs.length) bad('unknown function');
if (!refs.has(x)) bad('undeclared function reference');
vals.push({ nullable: false, heap: funcs[x] });
break;
}
case 0xd3: { findings.push({ at, rule: 12 }); const eq = { nullable: true, heap: 'eq' }; pop(eq); pop(eq); vals.push('i32'); break; }
case 0xd4: { findings.push({ at, rule: 12 }); const v = popRef(); vals.push({ nullable: false, heap: v.heap }); break; }
case 0xd5: {
findings.push({ at, rule: 12 });
const lt = labelTypes(r.u32());
const v = popRef();
popVals(lt); pushVals(lt);
vals.push({ nullable: false, heap: v.heap });
break;
}
case 0xd6: {
findings.push({ at, rule: 12 });
const lt = labelTypes(r.u32());
const last = lt[lt.length - 1];
if (!lt.length || !isRef(last)) bad('br_on_non_null label');
pop({ nullable: true, heap: last.heap });
const rest = lt.slice(0, -1);
popVals(rest); pushVals(rest);
break;
}
case 0xfb: gcOp(at); break;
case 0xfc: miscOp(at); break;
case 0xfd: simdOp(at); break;
default: bad('unknown opcode');
}
if (ctrls.length === 0) {
if (!r.eof()) bad('instructions after the end of the function');
return;
}
}
function miscOp(at) {
const s = r.u32();
if (s <= 7) {
findings.push({ at, rule: 3, int: true });
pop(s < 2 || s === 4 || s === 5 ? 'f32' : 'f64');
vals.push(s < 4 ? 'i32' : 'i64');
return;
}
switch (s) {
case 8: { dataIdx(); const a = memIdx(); pop('i32'); pop('i32'); pop(a); return; }
case 9: dataIdx(); return;
case 10: { const d = memIdx(), c = memIdx(); pop(minAt(d, c)); pop(c); pop(d); return; }
case 11: { const a = memIdx(); pop(a); pop('i32'); pop(a); return; }
case 12: { const e = elemIdx(), t = table(); if (!sub(e, t.elem)) bad('table.init type'); pop('i32'); pop('i32'); pop(t.at); return; }
case 13: elemIdx(); return;
case 14: { const d = table(), c = table(); if (!sub(c.elem, d.elem)) bad('table.copy type'); pop(minAt(d.at, c.at)); pop(c.at); pop(d.at); return; }
case 15: { findings.push({ at, rule: 7 }); const t = table(); pop(t.at); pop(t.elem); vals.push(t.at); return; }
case 16: vals.push(table().at); return;
case 17: { findings.push({ at, rule: 7 }); const t = table(); pop(t.at); pop(t.elem); pop(t.at); return; }
default: bad('unknown opcode');
}
}
function gcOp(at) {
const s = r.u32();
findings.push({ at, rule: 12 });
const ref = (x, nullable = true) => ({ nullable, heap: types[x] });
const field = (e, i) => { if (i >= e.fields.length) bad('unknown field'); return e.fields[i]; };
const packed = (f) => f.st === 'i8' || f.st === 'i16';
switch (s) {
case 0: { const x = r.u32(), e = gcType(x, 'struct'); for (let i = e.fields.length - 1; i >= 0; i--) pop(unpack(e.fields[i].st)); vals.push(ref(x, false)); return; }
case 1: { const x = r.u32(), e = gcType(x, 'struct'); if (!e.fields.every((f) => defaultable(unpack(f.st)))) bad('not defaultable'); vals.push(ref(x, false)); return; }
case 2: case 3: case 4: {
const x = r.u32(), e = gcType(x, 'struct'), f = field(e, r.u32());
if ((s === 2) === packed(f)) bad('struct.get of a packed field, or get_s of an unpacked one');
pop(ref(x)); vals.push(unpack(f.st)); return;
}
case 5: { const x = r.u32(), e = gcType(x, 'struct'), f = field(e, r.u32()); if (!f.mut) bad('immutable field'); pop(unpack(f.st)); pop(ref(x)); return; }
case 6: { const x = r.u32(), e = gcType(x, 'array'); pop('i32'); pop(unpack(e.fields[0].st)); vals.push(ref(x, false)); return; }
case 7: { const x = r.u32(), e = gcType(x, 'array'); if (!defaultable(unpack(e.fields[0].st))) bad('not defaultable'); pop('i32'); vals.push(ref(x, false)); return; }
case 8: { const x = r.u32(), e = gcType(x, 'array'), n = r.u32(); for (let i = 0; i < n; i++) pop(unpack(e.fields[0].st)); vals.push(ref(x, false)); return; }
case 9: case 18: {
const x = r.u32(), e = gcType(x, 'array');
dataIdx();
if (isRef(e.fields[0].st)) bad('array of references from data');
if (s === 18 && !e.fields[0].mut) bad('immutable array');
if (s === 9) { pop('i32'); pop('i32'); vals.push(ref(x, false)); } else { pop('i32'); pop('i32'); pop('i32'); pop(ref(x)); }
return;
}
case 10: case 19: {
const x = r.u32(), e = gcType(x, 'array'), et = elemIdx();
const st = e.fields[0].st;
if (!isRef(st) || !sub(et, st)) bad('element type');
if (s === 19 && !e.fields[0].mut) bad('immutable array');
if (s === 10) { pop('i32'); pop('i32'); vals.push(ref(x, false)); } else { pop('i32'); pop('i32'); pop('i32'); pop(ref(x)); }
return;
}
case 11: case 12: case 13: {
const x = r.u32(), f = gcType(x, 'array').fields[0];
if ((s === 11) === packed(f)) bad('array.get of a packed element, or get_s of an unpacked one');
pop('i32'); pop(ref(x)); vals.push(unpack(f.st)); return;
}
case 14: { const x = r.u32(), f = gcType(x, 'array').fields[0]; if (!f.mut) bad('immutable array'); pop(unpack(f.st)); pop('i32'); pop(ref(x)); return; }
case 15: pop({ nullable: true, heap: 'array' }); vals.push('i32'); return;
case 16: { const x = r.u32(), f = gcType(x, 'array').fields[0]; if (!f.mut) bad('immutable array'); pop('i32'); pop(unpack(f.st)); pop('i32'); pop(ref(x)); return; }
case 17: {
const x = r.u32(), d = gcType(x, 'array').fields[0], y = r.u32(), c = gcType(y, 'array').fields[0];
if (!d.mut) bad('immutable array');
if (!subStorage(c.st, d.st)) bad('array.copy types');
pop('i32'); pop('i32'); pop(ref(y)); pop('i32'); pop(ref(x)); return;
}
case 20: case 21: case 22: case 23: {
const h = heapType(r, false);
pop({ nullable: true, heap: top(h) });
vals.push(s < 22 ? 'i32' : { nullable: s === 23, heap: h });
return;
}
case 24: case 25: {
const flags = r.byte();
if (flags > 3) bad('bad cast flags');
const lt = labelTypes(r.u32());
const rt1 = { nullable: (flags & 1) !== 0, heap: heapType(r, false) };
const rt2 = { nullable: (flags & 2) !== 0, heap: heapType(r, false) };
if (!sub(rt2, rt1)) bad('br_on_cast types');
const diff = { nullable: rt2.nullable ? false : rt1.nullable, heap: rt1.heap };
const last = lt[lt.length - 1];
if (!lt.length || !isRef(last)) bad('br_on_cast label');
if (!sub(s === 24 ? rt2 : diff, last)) bad('br_on_cast label type');
pop(rt1);
const rest = lt.slice(0, -1);
popVals(rest); pushVals(rest);
vals.push(s === 24 ? diff : rt2);
return;
}
case 26: case 27: {
const v = pop({ nullable: true, heap: s === 26 ? 'extern' : 'any' });
const to = s === 26 ? 'any' : 'extern';
vals.push(v === 'bot' || v.heap === 'bot' ? { nullable: false, heap: to } : { nullable: v.nullable, heap: to });
return;
}
case 28: pop('i32'); vals.push({ nullable: false, heap: 'i31' }); return;
case 29: case 30: pop({ nullable: true, heap: 'i31' }); vals.push('i32'); return;
default: bad('unknown opcode');
}
}
function simdOp(at) {
const s = r.u32();
const e = SIMD[s];
if (!e) bad('unknown vector opcode');
const [name, sig, a1, a2] = e;
if (name.includes('f32') || name.includes('f64')) findings.push({ at, rule: 3, int: true });
if (s >= 0x100) findings.push({ at, rule: 4 });
const lane = (n) => { if (r.byte() >= n) bad('lane index out of range'); };
switch (sig) {
case 'load': { const a = memarg(a1); pop(a); vals.push('v128'); return; }
case 'store': { const a = memarg(a1); pop('v128'); pop(a); return; }
case 'const': r.skip(16); vals.push('v128'); return;
case 'shuffle': for (let i = 0; i < 16; i++) lane(32); pop('v128'); pop('v128'); vals.push('v128'); return;
case 'splat': pop(a1); vals.push('v128'); return;
case 'extract': lane(a2); pop('v128'); vals.push(a1); return;
case 'replace': lane(a2); pop(a1); pop('v128'); vals.push('v128'); return;
case 'loadlane': { const a = memarg(a1); lane(a2); pop('v128'); pop(a); vals.push('v128'); return; }
case 'storelane': { const a = memarg(a1); lane(a2); pop('v128'); pop(a); return; }
case 'un': pop('v128'); vals.push('v128'); return;
case 'bin': pop('v128'); pop('v128'); vals.push('v128'); return;
case 'tern': pop('v128'); pop('v128'); pop('v128'); vals.push('v128'); return;
case 'test': pop('v128'); vals.push('i32'); return;
case 'shift': pop('i32'); pop('v128'); vals.push('v128'); return;
default: bad('unknown vector opcode');
}
}
}
// -------------------------------------------------------------------- the sections
const r = new Reader(b, 0, b.length);
if (b.length < 8 || b[0] !== 0 || b[1] !== 0x61 || b[2] !== 0x73 || b[3] !== 0x6d) bad('no preamble');
if (b[4] !== 1 || b[5] !== 0 || b[6] !== 0 || b[7] !== 0) bad('not version 1 of the binary format');
r.at = 8;
// The order of the sections of WebAssembly 3.0, by id.
const ORDER = { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 13: 6, 6: 7, 7: 8, 8: 9, 9: 10, 12: 11, 10: 12, 11: 13 };
let last = 0;
while (!r.eof()) {
const idAt = r.at, id = r.byte(), size = r.u32();
if (size > r.end - r.at) bad('section beyond the module');
const s = new Reader(b, r.at, r.at + size);
r.at += size;
if (id === 0) { s.name(); continue; }
if (!ORDER[id]) bad('unknown section');
if (ORDER[id] <= last) bad('section out of order');
last = ORDER[id];
switch (id) {
case 1: {
const n = s.u32();
if (n > LIMIT.types) bad('too many types');
for (let i = 0; i < n; i++) {
const at = s.at, first = s.peek();
let subs;
if (first === 0x4e) {
s.at++;
const k = s.u32();
if (types.length + k > LIMIT.types) bad('too many types');
subs = [];
for (let j = 0; j < k; j++) subs.push(subType(s));
} else {
if (types.length + 1 > LIMIT.types) bad('too many types');
subs = [subType(s)];
}
if (first !== 0x60) findings.push({ at, rule: 12 });
else for (const [t, tat] of subs[0].items) valueItem(t, tat);
defineGroup(subs);
}
break;
}
case 2: {
for (let n = s.u32(); n > 0; n--) {
const at = s.at;
s.name(); s.name();
findings.push({ at, rule: 5 });
const kind = s.byte();
if (kind === 0x00) {
const x = s.u32(), e = funcType(x);
funcs.push(types[x]);
nImportedFuncs++;
if (funcs.length > LIMIT.functions) bad('too many functions');
addInterface(2 + e.params.length + e.results.length);
} else if (kind === 0x01) {
tables.push(tableType(s));
if (tables.length > LIMIT.tables) bad('too many tables');
addInterface(1);
} else if (kind === 0x02) {
mems.push(memType(s));
if (mems.length > LIMIT.memories) bad('too many memories');
addInterface(1);
} else if (kind === 0x03) {
globals.push(globalType(s));
if (globals.length > LIMIT.globals) bad('too many globals');
addInterface(1);
} else if (kind === 0x04) {
if (s.byte() !== 0) bad('bad tag attribute');
const x = s.u32(), e = funcType(x);
if (e.results.length) bad('tag type with results');
tags.push(types[x]);
if (tags.length > LIMIT.tags) bad('too many tags');
addInterface(2 + e.params.length);
} else bad('bad import kind');
}
break;
}
case 3: {
const n = s.u32();
if (funcs.length + n > LIMIT.functions) bad('too many functions');
findings.push({ at: idAt, rule: 11, cap: 'functions', value: n });
for (let i = 0; i < n; i++) { const x = s.u32(); funcType(x); funcs.push(types[x]); }
definedFuncs = n;
break;
}
case 4: {
const n = s.u32();
if (tables.length + n > LIMIT.tables) bad('too many tables');
for (let i = 0; i < n; i++) {
const at = s.at;
let init = false;
if (s.peek() === 0x40) { s.at++; if (s.byte() !== 0) bad('bad table encoding'); init = true; }
const t = tableType(s);
if (init) constExpr(s, t.elem);
else if (!t.elem.nullable) bad('table of a non-defaultable type without an initialiser');
const fixed = i === 0 && t.elem.nullable && t.elem.heap === 'func' && t.at === 'i32' && t.max !== null && t.max === t.min;
if (!fixed) findings.push({ at, rule: 7 });
tables.push(t);
}
break;
}
case 5: {
const n = s.u32();
if (mems.length + n > LIMIT.memories) bad('too many memories');
for (let i = 0; i < n; i++) {
const at = s.at, m = memType(s);
mems.push(m);
definedMems++;
if (i > 0 || m.at === 'i64' || m.max === null) findings.push({ at, rule: 6 });
else findings.push({ at, rule: 6, cap: 'memory_pages', value: Number(m.max) });
}
break;
}
case 13: {
findings.push({ at: idAt, rule: 12 });
const n = s.u32();
if (tags.length + n > LIMIT.tags) bad('too many tags');
for (let i = 0; i < n; i++) {
if (s.byte() !== 0) bad('bad tag attribute');
const x = s.u32();
if (funcType(x).results.length) bad('tag type with results');
tags.push(types[x]);
}
break;
}
case 6: {
const n = s.u32();
if (globals.length + n > LIMIT.globals) bad('too many globals');
for (let i = 0; i < n; i++) {
const at = s.at, g = globalType(s);
valueItem(g.type, at);
constExpr(s, g.type);
globals.push(g);
}
break;
}
case 7: {
for (let n = s.u32(); n > 0; n--) {
const at = s.at, nm = s.name(), kind = s.byte(), x = s.u32();
if (exported.has(nm)) bad('duplicate export name');
exported.add(nm);
let ty = null;
if (kind === 0) { if (x >= funcs.length) bad('unknown function'); refs.add(x); ty = canon[funcs[x]]; addInterface(2 + ty.params.length + ty.results.length); }
else if (kind === 1) { if (x >= tables.length) bad('unknown table'); addInterface(1); }
else if (kind === 2) { if (x >= mems.length) bad('unknown memory'); addInterface(1); }
else if (kind === 3) { if (x >= globals.length) bad('unknown global'); addInterface(1); }
else if (kind === 4) { if (x >= tags.length) bad('unknown tag'); const e = canon[tags[x]]; addInterface(2 + e.params.length + e.results.length); }
else bad('bad export kind');
const abi = ABI.find(([name]) => name === nm);
let ok = false;
if (abi) {
abiSeen.add(nm);
ok = kind === abi[1] && (abi[2] === null || (ty.final && ty.sup === null
&& ty.params.length === abi[2][0].length && ty.params.every((t, i) => t === abi[2][0][i])
&& ty.results.length === abi[2][1].length && ty.results.every((t, i) => t === abi[2][1][i])));
}
if (!ok) findings.push({ at, rule: 10 });
}
break;
}
case 8: {
findings.push({ at: idAt, rule: 8 });
const x = s.u32();
if (x >= funcs.length) bad('unknown function');
const e = canon[funcs[x]];
if (e.params.length || e.results.length) bad('start function type');
break;
}
case 9: {
const n = s.u32();
if (n > LIMIT.elems) bad('too many element segments');
for (let i = 0; i < n; i++) {
const flags = s.u32();
if (flags > 7) bad('bad element segment flags');
const active = (flags & 1) === 0, exprs = (flags & 4) !== 0;
let t = null;
if (active) {
const ti = flags & 2 ? s.u32() : 0;
if (ti >= tables.length) bad('unknown table');
t = tables[ti];
constExpr(s, t.at);
}
let type;
if (!exprs) {
if (flags & 3) { if (s.byte() !== 0) bad('bad element kind'); }
type = { nullable: false, heap: 'func' };
const k = s.u32();
if (k > LIMIT.segmentItems) bad('too many segment items');
for (let j = 0; j < k; j++) { const x = s.u32(); if (x >= funcs.length) bad('unknown function'); refs.add(x); }
} else {
type = flags & 3 ? refType(s) : FUNCREF;
const k = s.u32();
if (k > LIMIT.segmentItems) bad('too many segment items');
for (let j = 0; j < k; j++) constExpr(s, type);
}
if (t && !sub(type, t.elem)) bad('element segment type');
elems.push(type);
}
break;
}
case 12: {
dataCount = s.u32();
if (dataCount > LIMIT.datas) bad('too many data segments');
break;
}
case 10: {
const n = s.u32();
if (n !== (definedFuncs ?? 0)) bad('function and code sections differ');
codeEntries = n;
for (let i = 0; i < n; i++) {
const size = s.u32();
if (size > s.end - s.at) bad('unexpected end');
if (size > LIMIT.body) bad('function body too large');
body(new Reader(b, s.at, s.at + size), funcs[nImportedFuncs + i]);
s.at += size;
}
break;
}
case 11: {
const n = s.u32();
if (n > LIMIT.datas) bad('too many data segments');
if (dataCount !== null && dataCount !== n) bad('data count and data section differ');
dataSegments = n;
for (let i = 0; i < n; i++) {
const flags = s.u32();
if (flags === 0 || flags === 2) {
const m = flags === 2 ? s.u32() : 0;
if (m >= mems.length) bad('unknown memory');
constExpr(s, mems[m].at);
} else if (flags !== 1) bad('bad data segment flags');
s.skip(s.u32());
}
break;
}
default: bad('unknown section');
}
if (!s.eof()) bad('section size mismatch');
}
if ((definedFuncs ?? 0) !== (codeEntries ?? 0)) bad('function and code sections differ');
if (dataCount !== null && dataSegments === null && dataCount !== 0) bad('data count without data');
if (definedMems === 0) findings.push({ at: b.length, rule: 6 });
if (abiSeen.size < ABI.length) findings.push({ at: b.length, rule: 10 });
return findings;
}
// ------------------------------------------------------------------------------ tables
// Scalar numeric instructions 0x45..0xc4: [name, params, results, float].
const NUMERIC = [];
{
const def = (op, name, p, q) => { NUMERIC[op] = [name, p, q, name.includes('f32') || name.includes('f64')]; };
const rel = ['eq', 'ne', 'lt_s', 'lt_u', 'gt_s', 'gt_u', 'le_s', 'le_u', 'ge_s', 'ge_u'];
const frel = ['eq', 'ne', 'lt', 'gt', 'le', 'ge'];
const ibin = ['add', 'sub', 'mul', 'div_s', 'div_u', 'rem_s', 'rem_u', 'and', 'or', 'xor', 'shl', 'shr_s', 'shr_u', 'rotl', 'rotr'];
const fun = ['abs', 'neg', 'ceil', 'floor', 'trunc', 'nearest', 'sqrt'];
const fbin = ['add', 'sub', 'mul', 'div', 'min', 'max', 'copysign'];
def(0x45, 'i32.eqz', ['i32'], ['i32']);
rel.forEach((n, i) => def(0x46 + i, 'i32.' + n, ['i32', 'i32'], ['i32']));
def(0x50, 'i64.eqz', ['i64'], ['i32']);
rel.forEach((n, i) => def(0x51 + i, 'i64.' + n, ['i64', 'i64'], ['i32']));
frel.forEach((n, i) => def(0x5b + i, 'f32.' + n, ['f32', 'f32'], ['i32']));
frel.forEach((n, i) => def(0x61 + i, 'f64.' + n, ['f64', 'f64'], ['i32']));
['clz', 'ctz', 'popcnt'].forEach((n, i) => def(0x67 + i, 'i32.' + n, ['i32'], ['i32']));
ibin.forEach((n, i) => def(0x6a + i, 'i32.' + n, ['i32', 'i32'], ['i32']));
['clz', 'ctz', 'popcnt'].forEach((n, i) => def(0x79 + i, 'i64.' + n, ['i64'], ['i64']));
ibin.forEach((n, i) => def(0x7c + i, 'i64.' + n, ['i64', 'i64'], ['i64']));
fun.forEach((n, i) => def(0x8b + i, 'f32.' + n, ['f32'], ['f32']));
fbin.forEach((n, i) => def(0x92 + i, 'f32.' + n, ['f32', 'f32'], ['f32']));
fun.forEach((n, i) => def(0x99 + i, 'f64.' + n, ['f64'], ['f64']));
fbin.forEach((n, i) => def(0xa0 + i, 'f64.' + n, ['f64', 'f64'], ['f64']));
const conv = [
[0xa7, 'i32.wrap_i64', 'i64', 'i32'], [0xa8, 'i32.trunc_f32_s', 'f32', 'i32'], [0xa9, 'i32.trunc_f32_u', 'f32', 'i32'],
[0xaa, 'i32.trunc_f64_s', 'f64', 'i32'], [0xab, 'i32.trunc_f64_u', 'f64', 'i32'], [0xac, 'i64.extend_i32_s', 'i32', 'i64'],
[0xad, 'i64.extend_i32_u', 'i32', 'i64'], [0xae, 'i64.trunc_f32_s', 'f32', 'i64'], [0xaf, 'i64.trunc_f32_u', 'f32', 'i64'],
[0xb0, 'i64.trunc_f64_s', 'f64', 'i64'], [0xb1, 'i64.trunc_f64_u', 'f64', 'i64'], [0xb2, 'f32.convert_i32_s', 'i32', 'f32'],
[0xb3, 'f32.convert_i32_u', 'i32', 'f32'], [0xb4, 'f32.convert_i64_s', 'i64', 'f32'], [0xb5, 'f32.convert_i64_u', 'i64', 'f32'],
[0xb6, 'f32.demote_f64', 'f64', 'f32'], [0xb7, 'f64.convert_i32_s', 'i32', 'f64'], [0xb8, 'f64.convert_i32_u', 'i32', 'f64'],
[0xb9, 'f64.convert_i64_s', 'i64', 'f64'], [0xba, 'f64.convert_i64_u', 'i64', 'f64'], [0xbb, 'f64.promote_f32', 'f32', 'f64'],
[0xbc, 'i32.reinterpret_f32', 'f32', 'i32'], [0xbd, 'i64.reinterpret_f64', 'f64', 'i64'], [0xbe, 'f32.reinterpret_i32', 'i32', 'f32'],
[0xbf, 'f64.reinterpret_i64', 'i64', 'f64'], [0xc0, 'i32.extend8_s', 'i32', 'i32'], [0xc1, 'i32.extend16_s', 'i32', 'i32'],
[0xc2, 'i64.extend8_s', 'i64', 'i64'], [0xc3, 'i64.extend16_s', 'i64', 'i64'], [0xc4, 'i64.extend32_s', 'i64', 'i64'],
];
for (const [op, name, p, q] of conv) def(op, name, [p], [q]);
}
// Loads and stores 0x28..0x3e: [name, value type, natural alignment, store].
const LOADSTORE = [
['i32.load', 'i32', 2, false], ['i64.load', 'i64', 3, false], ['f32.load', 'f32', 2, false], ['f64.load', 'f64', 3, false],
['i32.load8_s', 'i32', 0, false], ['i32.load8_u', 'i32', 0, false], ['i32.load16_s', 'i32', 1, false], ['i32.load16_u', 'i32', 1, false],
['i64.load8_s', 'i64', 0, false], ['i64.load8_u', 'i64', 0, false], ['i64.load16_s', 'i64', 1, false], ['i64.load16_u', 'i64', 1, false],
['i64.load32_s', 'i64', 2, false], ['i64.load32_u', 'i64', 2, false], ['i32.store', 'i32', 2, true], ['i64.store', 'i64', 3, true],
['f32.store', 'f32', 2, true], ['f64.store', 'f64', 3, true], ['i32.store8', 'i32', 0, true], ['i32.store16', 'i32', 1, true],
['i64.store8', 'i64', 0, true], ['i64.store16', 'i64', 1, true], ['i64.store32', 'i64', 2, true],
];
// Vector instructions, by their opcode after 0xfd: [name, signature, argument, argument].
const SIMD = [];
{
const d = (op, name, sig, a1, a2) => { SIMD[op] = [name, sig, a1, a2]; };
[[0x00, 'v128.load', 4], [0x01, 'v128.load8x8_s', 3], [0x02, 'v128.load8x8_u', 3], [0x03, 'v128.load16x4_s', 3],
[0x04, 'v128.load16x4_u', 3], [0x05, 'v128.load32x2_s', 3], [0x06, 'v128.load32x2_u', 3], [0x07, 'v128.load8_splat', 0],
[0x08, 'v128.load16_splat', 1], [0x09, 'v128.load32_splat', 2], [0x0a, 'v128.load64_splat', 3],
[0x5c, 'v128.load32_zero', 2], [0x5d, 'v128.load64_zero', 3]].forEach(([op, n, a]) => d(op, n, 'load', a));
d(0x0b, 'v128.store', 'store', 4);
d(0x0c, 'v128.const', 'const');
d(0x0d, 'i8x16.shuffle', 'shuffle');
d(0x0e, 'i8x16.swizzle', 'bin');
[['i8x16', 'i32'], ['i16x8', 'i32'], ['i32x4', 'i32'], ['i64x2', 'i64'], ['f32x4', 'f32'], ['f64x2', 'f64']]
.forEach(([s, t], i) => d(0x0f + i, s + '.splat', 'splat', t));
[[0x15, 'i8x16.extract_lane_s', 'extract', 'i32', 16], [0x16, 'i8x16.extract_lane_u', 'extract', 'i32', 16],
[0x17, 'i8x16.replace_lane', 'replace', 'i32', 16], [0x18, 'i16x8.extract_lane_s', 'extract', 'i32', 8],
[0x19, 'i16x8.extract_lane_u', 'extract', 'i32', 8], [0x1a, 'i16x8.replace_lane', 'replace', 'i32', 8],
[0x1b, 'i32x4.extract_lane', 'extract', 'i32', 4], [0x1c, 'i32x4.replace_lane', 'replace', 'i32', 4],
[0x1d, 'i64x2.extract_lane', 'extract', 'i64', 2], [0x1e, 'i64x2.replace_lane', 'replace', 'i64', 2],
[0x1f, 'f32x4.extract_lane', 'extract', 'f32', 4], [0x20, 'f32x4.replace_lane', 'replace', 'f32', 4],
[0x21, 'f64x2.extract_lane', 'extract', 'f64', 2], [0x22, 'f64x2.replace_lane', 'replace', 'f64', 2],
].forEach(([op, n, sig, t, l]) => d(op, n, sig, t, l));
const irel = ['eq', 'ne', 'lt_s', 'lt_u', 'gt_s', 'gt_u', 'le_s', 'le_u', 'ge_s', 'ge_u'];
irel.forEach((n, i) => d(0x23 + i, 'i8x16.' + n, 'bin'));
irel.forEach((n, i) => d(0x2d + i, 'i16x8.' + n, 'bin'));
irel.forEach((n, i) => d(0x37 + i, 'i32x4.' + n, 'bin'));
['eq', 'ne', 'lt', 'gt', 'le', 'ge'].forEach((n, i) => { d(0x41 + i, 'f32x4.' + n, 'bin'); d(0x47 + i, 'f64x2.' + n, 'bin'); });
d(0x4d, 'v128.not', 'un');
['and', 'andnot', 'or', 'xor'].forEach((n, i) => d(0x4e + i, 'v128.' + n, 'bin'));
d(0x52, 'v128.bitselect', 'tern');
d(0x53, 'v128.any_true', 'test');
[8, 16, 32, 64].forEach((w, i) => {
d(0x54 + i, `v128.load${w}_lane`, 'loadlane', i, 16 >> i);
d(0x58 + i, `v128.store${w}_lane`, 'storelane', i, 16 >> i);
});
const rows = [
[0x5e, 'f32x4.demote_f64x2_zero', 'un'], [0x5f, 'f64x2.promote_low_f32x4', 'un'],
[0x60, 'i8x16.abs', 'un'], [0x61, 'i8x16.neg', 'un'], [0x62, 'i8x16.popcnt', 'un'], [0x63, 'i8x16.all_true', 'test'],
[0x64, 'i8x16.bitmask', 'test'], [0x65, 'i8x16.narrow_i16x8_s', 'bin'], [0x66, 'i8x16.narrow_i16x8_u', 'bin'],
[0x67, 'f32x4.ceil', 'un'], [0x68, 'f32x4.floor', 'un'], [0x69, 'f32x4.trunc', 'un'], [0x6a, 'f32x4.nearest', 'un'],
[0x6b, 'i8x16.shl', 'shift'], [0x6c, 'i8x16.shr_s', 'shift'], [0x6d, 'i8x16.shr_u', 'shift'],
[0x6e, 'i8x16.add', 'bin'], [0x6f, 'i8x16.add_sat_s', 'bin'], [0x70, 'i8x16.add_sat_u', 'bin'], [0x71, 'i8x16.sub', 'bin'],
[0x72, 'i8x16.sub_sat_s', 'bin'], [0x73, 'i8x16.sub_sat_u', 'bin'], [0x74, 'f64x2.ceil', 'un'], [0x75, 'f64x2.floor', 'un'],
[0x76, 'i8x16.min_s', 'bin'], [0x77, 'i8x16.min_u', 'bin'], [0x78, 'i8x16.max_s', 'bin'], [0x79, 'i8x16.max_u', 'bin'],
[0x7a, 'f64x2.trunc', 'un'], [0x7b, 'i8x16.avgr_u', 'bin'],
[0x7c, 'i16x8.extadd_pairwise_i8x16_s', 'un'], [0x7d, 'i16x8.extadd_pairwise_i8x16_u', 'un'],
[0x7e, 'i32x4.extadd_pairwise_i16x8_s', 'un'], [0x7f, 'i32x4.extadd_pairwise_i16x8_u', 'un'],
[0x80, 'i16x8.abs', 'un'], [0x81, 'i16x8.neg', 'un'], [0x82, 'i16x8.q15mulr_sat_s', 'bin'], [0x83, 'i16x8.all_true', 'test'],
[0x84, 'i16x8.bitmask', 'test'], [0x85, 'i16x8.narrow_i32x4_s', 'bin'], [0x86, 'i16x8.narrow_i32x4_u', 'bin'],
[0x87, 'i16x8.extend_low_i8x16_s', 'un'], [0x88, 'i16x8.extend_high_i8x16_s', 'un'],
[0x89, 'i16x8.extend_low_i8x16_u', 'un'], [0x8a, 'i16x8.extend_high_i8x16_u', 'un'],
[0x8b, 'i16x8.shl', 'shift'], [0x8c, 'i16x8.shr_s', 'shift'], [0x8d, 'i16x8.shr_u', 'shift'],
[0x8e, 'i16x8.add', 'bin'], [0x8f, 'i16x8.add_sat_s', 'bin'], [0x90, 'i16x8.add_sat_u', 'bin'], [0x91, 'i16x8.sub', 'bin'],
[0x92, 'i16x8.sub_sat_s', 'bin'], [0x93, 'i16x8.sub_sat_u', 'bin'], [0x94, 'f64x2.nearest', 'un'], [0x95, 'i16x8.mul', 'bin'],
[0x96, 'i16x8.min_s', 'bin'], [0x97, 'i16x8.min_u', 'bin'], [0x98, 'i16x8.max_s', 'bin'], [0x99, 'i16x8.max_u', 'bin'],
[0x9b, 'i16x8.avgr_u', 'bin'], [0x9c, 'i16x8.extmul_low_i8x16_s', 'bin'], [0x9d, 'i16x8.extmul_high_i8x16_s', 'bin'],
[0x9e, 'i16x8.extmul_low_i8x16_u', 'bin'], [0x9f, 'i16x8.extmul_high_i8x16_u', 'bin'],
[0xa0, 'i32x4.abs', 'un'], [0xa1, 'i32x4.neg', 'un'], [0xa3, 'i32x4.all_true', 'test'], [0xa4, 'i32x4.bitmask', 'test'],
[0xa7, 'i32x4.extend_low_i16x8_s', 'un'], [0xa8, 'i32x4.extend_high_i16x8_s', 'un'],
[0xa9, 'i32x4.extend_low_i16x8_u', 'un'], [0xaa, 'i32x4.extend_high_i16x8_u', 'un'],
[0xab, 'i32x4.shl', 'shift'], [0xac, 'i32x4.shr_s', 'shift'], [0xad, 'i32x4.shr_u', 'shift'], [0xae, 'i32x4.add', 'bin'],
[0xb1, 'i32x4.sub', 'bin'], [0xb5, 'i32x4.mul', 'bin'], [0xb6, 'i32x4.min_s', 'bin'], [0xb7, 'i32x4.min_u', 'bin'],
[0xb8, 'i32x4.max_s', 'bin'], [0xb9, 'i32x4.max_u', 'bin'], [0xba, 'i32x4.dot_i16x8_s', 'bin'],
[0xbc, 'i32x4.extmul_low_i16x8_s', 'bin'], [0xbd, 'i32x4.extmul_high_i16x8_s', 'bin'],
[0xbe, 'i32x4.extmul_low_i16x8_u', 'bin'], [0xbf, 'i32x4.extmul_high_i16x8_u', 'bin'],
[0xc0, 'i64x2.abs', 'un'], [0xc1, 'i64x2.neg', 'un'], [0xc3, 'i64x2.all_true', 'test'], [0xc4, 'i64x2.bitmask', 'test'],
[0xc7, 'i64x2.extend_low_i32x4_s', 'un'], [0xc8, 'i64x2.extend_high_i32x4_s', 'un'],
[0xc9, 'i64x2.extend_low_i32x4_u', 'un'], [0xca, 'i64x2.extend_high_i32x4_u', 'un'],
[0xcb, 'i64x2.shl', 'shift'], [0xcc, 'i64x2.shr_s', 'shift'], [0xcd, 'i64x2.shr_u', 'shift'], [0xce, 'i64x2.add', 'bin'],
[0xd1, 'i64x2.sub', 'bin'], [0xd5, 'i64x2.mul', 'bin'], [0xd6, 'i64x2.eq', 'bin'], [0xd7, 'i64x2.ne', 'bin'],
[0xd8, 'i64x2.lt_s', 'bin'], [0xd9, 'i64x2.gt_s', 'bin'], [0xda, 'i64x2.le_s', 'bin'], [0xdb, 'i64x2.ge_s', 'bin'],
[0xdc, 'i64x2.extmul_low_i32x4_s', 'bin'], [0xdd, 'i64x2.extmul_high_i32x4_s', 'bin'],
[0xde, 'i64x2.extmul_low_i32x4_u', 'bin'], [0xdf, 'i64x2.extmul_high_i32x4_u', 'bin'],
[0xe0, 'f32x4.abs', 'un'], [0xe1, 'f32x4.neg', 'un'], [0xe3, 'f32x4.sqrt', 'un'], [0xe4, 'f32x4.add', 'bin'],
[0xe5, 'f32x4.sub', 'bin'], [0xe6, 'f32x4.mul', 'bin'], [0xe7, 'f32x4.div', 'bin'], [0xe8, 'f32x4.min', 'bin'],
[0xe9, 'f32x4.max', 'bin'], [0xea, 'f32x4.pmin', 'bin'], [0xeb, 'f32x4.pmax', 'bin'],
[0xec, 'f64x2.abs', 'un'], [0xed, 'f64x2.neg', 'un'], [0xef, 'f64x2.sqrt', 'un'], [0xf0, 'f64x2.add', 'bin'],
[0xf1, 'f64x2.sub', 'bin'], [0xf2, 'f64x2.mul', 'bin'], [0xf3, 'f64x2.div', 'bin'], [0xf4, 'f64x2.min', 'bin'],
[0xf5, 'f64x2.max', 'bin'], [0xf6, 'f64x2.pmin', 'bin'], [0xf7, 'f64x2.pmax', 'bin'],
[0xf8, 'i32x4.trunc_sat_f32x4_s', 'un'], [0xf9, 'i32x4.trunc_sat_f32x4_u', 'un'],
[0xfa, 'f32x4.convert_i32x4_s', 'un'], [0xfb, 'f32x4.convert_i32x4_u', 'un'],
[0xfc, 'i32x4.trunc_sat_f64x2_s_zero', 'un'], [0xfd, 'i32x4.trunc_sat_f64x2_u_zero', 'un'],
[0xfe, 'f64x2.convert_low_i32x4_s', 'un'], [0xff, 'f64x2.convert_low_i32x4_u', 'un'],
// The relaxed vector instructions (R4), 0x100 to 0x113.
[0x100, 'i8x16.relaxed_swizzle', 'bin'], [0x101, 'i32x4.relaxed_trunc_f32x4_s', 'un'],
[0x102, 'i32x4.relaxed_trunc_f32x4_u', 'un'], [0x103, 'i32x4.relaxed_trunc_f64x2_s_zero', 'un'],
[0x104, 'i32x4.relaxed_trunc_f64x2_u_zero', 'un'], [0x105, 'f32x4.relaxed_madd', 'tern'],
[0x106, 'f32x4.relaxed_nmadd', 'tern'], [0x107, 'f64x2.relaxed_madd', 'tern'], [0x108, 'f64x2.relaxed_nmadd', 'tern'],
[0x109, 'i8x16.relaxed_laneselect', 'tern'], [0x10a, 'i16x8.relaxed_laneselect', 'tern'],
[0x10b, 'i32x4.relaxed_laneselect', 'tern'], [0x10c, 'i64x2.relaxed_laneselect', 'tern'],
[0x10d, 'f32x4.relaxed_min', 'bin'], [0x10e, 'f32x4.relaxed_max', 'bin'], [0x10f, 'f64x2.relaxed_min', 'bin'],
[0x110, 'f64x2.relaxed_max', 'bin'], [0x111, 'i16x8.relaxed_q15mulr_s', 'bin'],
[0x112, 'i16x8.relaxed_dot_i8x16_i7x16_s', 'bin'], [0x113, 'i32x4.relaxed_dot_i8x16_i7x16_add_s', 'tern'],
];
for (const [op, n, sig] of rows) d(op, n, sig);
}
/** The names of every instruction this file decodes, for a reader that compares tables. */
export function instructionNames() {
return {
numeric: NUMERIC.map((e, op) => e && [op, e[0]]).filter(Boolean),
loadstore: LOADSTORE.map((e, i) => [0x28 + i, e[0]]),
simd: SIMD.map((e, op) => e && [op, e[0]]).filter(Boolean),
};
}
// ------------------------------------------------------------------------------ the verdict
/**
* The analysis of a module's bytes: `null` where it breaks R2, else every item and absence
* that breaks one of R3 to R12, each with the level and the cap it depends on.
*/
export function analyse(bytes) {
try {
return analyze(bytes);
} catch (e) {
if (e instanceof Refused2) return null;
throw e;
}
}
/** The verdict from an analysis, for a module of `length` bytes, at `level` under `caps`. */
export function verdictOf(analysis, length, level, caps = PROFILE_CAPS) {
if (length > caps.module_bytes) return 'refused R1 0';
if (analysis === null) return 'refused R2 -';
let best = null;
for (const f of analysis) {
if (f.int && level !== 'integer') continue;
if (f.cap && !(f.value > caps[f.cap])) continue;
if (!best || f.at < best.at || (f.at === best.at && f.rule < best.rule)) best = f;
}
return best ? `refused R${best.rule} ${best.at}` : 'held';
}
/** The module's verdict at `level` ('integer' or 'float') under `caps` (§4.3). */
export function check(bytes, level, caps = PROFILE_CAPS) {
if (level !== 'integer' && level !== 'float') throw new Error(`no level ${level}`);
if (bytes.length > caps.module_bytes) return 'refused R1 0';
return verdictOf(analyse(bytes), bytes.length, level, caps);
}
/**
* The level a sink holds the module at (§6, check 9b): { level: 'integer' | 'float' | null,
* verdict }, the verdict being that of the last level checked.
*/
export function levelOf(bytes, caps = PROFILE_CAPS) {
const a = bytes.length > caps.module_bytes ? null : analyse(bytes);
const int = verdictOf(a, bytes.length, 'integer', caps);
if (int === 'held') return { level: 'integer', verdict: int };
if (!int.startsWith('refused R3 ')) return { level: null, verdict: int };
const float = verdictOf(a, bytes.length, 'float', caps);
return { level: float === 'held' ? 'float' : null, verdict: float };
}
/** The profile's caps with `name=value` changes, comma-joined; `-` for none. */
export function parseCaps(text) {
const caps = { ...PROFILE_CAPS };
if (text === '-') return caps;
for (const part of text.split(',')) {
const [name, value] = part.split('=');
if (!(name in caps) || !/^[0-9]+$/.test(value ?? '')) return null;
caps[name] = Number(value);
}
return caps;
}
const LEVELS = { integer: 'integer', float: 'float', 'waism/1-int': 'integer', 'waism/1-det': 'float' };
// ------------------------------------------------------------------------------ from a shell
async function main(argv) {
const { readFileSync } = await import('node:fs');
const { join } = await import('node:path');
const read = (p) => {
try { return new Uint8Array(readFileSync(p)); } catch (e) { console.error(`check-module: ${p}: ${e.message}`); process.exit(2); }
};
const usage = () => {
console.error('usage: node check-module.mjs [--level integer|float] [--caps NAME=VALUE,...] FILE...\n node check-module.mjs --cases DIR');
process.exit(2);
};
if (argv.length === 2 && argv[0] === '--cases') {
const list = join(argv[1], 'cases.txt');
let text;
try { text = readFileSync(list, 'utf8'); } catch (e) { console.error(`check-module: ${list}: ${e.message}`); process.exit(2); }
let checked = 0, wrong = 0;
const cache = new Map();
for (const raw of text.split('\n')) {
const line = raw.split(' #')[0].trim();
if (!line || line.startsWith('#')) continue;
const m = /^(\S+) (\S+) (\S+) (.+)$/.exec(line);
const level = m && LEVELS[m[2]];
const caps = m && parseCaps(m[3]);
if (!m || !level || !caps) { console.error(`check-module: ${list}: not a case: ${line}`); process.exit(2); }
if (!cache.has(m[1])) {
const bytes = read(join(argv[1], `${m[1]}.wasm`));
cache.set(m[1], { length: bytes.length, analysis: analyse(bytes) });
}
const { length, analysis } = cache.get(m[1]);
const got = verdictOf(analysis, length, level, caps);
checked++;
if (got !== m[4]) { wrong++; console.log(`${m[1]} ${level} ${m[3]}: ${got}, not ${m[4]}`); }
}
console.log(wrong ? `check-module: ${wrong} of ${checked} verdicts differ from ${list}`
: `check-module: ${checked} verdicts, each the one ${list} states`);
process.exit(wrong ? 1 : 0);
}
let level = null, caps = PROFILE_CAPS;
const files = [];
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--level') { level = LEVELS[argv[++i]]; if (!level) usage(); }
else if (argv[i] === '--caps') { caps = parseCaps(argv[++i] ?? ''); if (!caps) usage(); }
else if (argv[i].startsWith('--')) usage();
else files.push(argv[i]);
}
if (!files.length) usage();
let allHeld = true;
for (const f of files) {
const bytes = read(f);
let line;
if (level) {
line = check(bytes, level, caps);
allHeld &&= line === 'held';
} else {
const { level: held, verdict } = levelOf(bytes, caps);
if (held) line = `held at the ${held} level (${held === 'integer' ? 'waism/1-int' : 'waism/1-det'})`;
else { line = verdict; allHeld = false; }
}
console.log(files.length > 1 ? `${f}: ${line}` : line);
}
process.exit(allHeld ? 0 : 1);
}
if (typeof process === 'object' && process.argv && process.argv[1]) {
const { pathToFileURL } = await import('node:url');
const { realpathSync } = await import('node:fs');
let self;
try { self = pathToFileURL(realpathSync(process.argv[1])).href; } catch { self = null; }
if (self === import.meta.url) await main(process.argv.slice(2));
}