open-standards/wai/wai-web/reach/tools/wasm.mjs
Transaction Science c4e592cfa4 wai-web: reach builds — L1 legalised, L2 MVP-lowered, J wasm2js, and an ES5 loader
The shipped wai-web package needs WebAssembly with sign-extension, non-trapping
float-to-int and bulk memory, and i64 crossing the JS boundary as BigInt.
reach/build.sh derives three more builds from that package with pinned tools
(binaryen version_123 by archive SHA-256; TypeScript, esbuild and acorn by
package-lock.json):

- L1: the shipped binary legalised at the JS interface, for engines where i64
  does not cross as BigInt;
- L2: MVP-lowered and legalised, at four profiles (full, verify_decode, verify,
  receipt), for MVP-only WebAssembly;
- J: the wasm2js translation, one ES5 script per profile, with an ES5-floor
  runtime and glue: memory views refreshed when the buffer changes, i64 values
  carried as Numbers where there is no BigInt.

wai-load.js (ES5) picks shipped, L1, L2 or J from feature probes alone and
says why; a rung can be forced for testing.

test/conformance.js (ES5) runs the workloads over the repository's corpora on
any rung and compares every output with the shipped build's; run-modern.mjs
forces every rung and profile on a modern Node and runs the demo verify
scripts against each full build.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 18:36:04 -04:00

119 lines
4.9 KiB
JavaScript

// A small reader for the parts of a WebAssembly binary the reach build needs: the
// function signatures of every import and export. It reads the type, import,
// function and export sections and skips the rest, so the build does not depend on
// any tool's text format.
//
// import { readModule } from './wasm.mjs';
// const m = readModule(bytes);
// m.imports [{ module, name, kind, params?, results? }]
// m.exports [{ name, kind, params?, results? }] kind: func | table | memory | global | tag
const VALTYPE = { 0x7f: 'i32', 0x7e: 'i64', 0x7d: 'f32', 0x7c: 'f64', 0x7b: 'v128', 0x70: 'funcref', 0x6f: 'externref' };
const KIND = ['func', 'table', 'memory', 'global', 'tag'];
export function readModule(bytes) {
const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
let p = 0;
const fail = (msg) => { throw new Error(`wasm: ${msg} at byte ${p}`); };
const u8 = () => (p < b.length ? b[p++] : fail('unexpected end'));
const leb = () => {
let result = 0, shift = 0, byte;
do {
byte = u8();
result += (byte & 0x7f) * 2 ** shift;
shift += 7;
if (shift > 35) fail('LEB128 too long');
} while (byte & 0x80);
return result;
};
const name = () => {
const n = leb();
if (p + n > b.length) fail('name runs past the end');
const s = new TextDecoder('utf-8', { fatal: true }).decode(b.subarray(p, p + n));
p += n;
return s;
};
const valtype = () => {
const t = u8();
return VALTYPE[t] ?? fail(`value type 0x${t.toString(16)} not read here`);
};
const limits = () => {
const flag = u8();
if (flag > 7) fail(`limits flag 0x${flag.toString(16)}`);
leb();
if (flag & 1) leb();
};
if (b.length < 8 || b[0] !== 0 || b[1] !== 0x61 || b[2] !== 0x73 || b[3] !== 0x6d) fail('not a WebAssembly binary');
if (b[4] !== 1 || b[5] !== 0 || b[6] !== 0 || b[7] !== 0) fail('not version 1');
p = 8;
const types = [];
const imports = [];
const funcTypes = []; // type index of every function, imported ones first
const exports = [];
while (p < b.length) {
const id = u8();
const size = leb();
const end = p + size;
if (end > b.length) fail(`section ${id} runs past the end`);
if (id === 1) {
const n = leb();
for (let i = 0; i < n; i++) {
const form = u8();
if (form !== 0x60) fail(`type form 0x${form.toString(16)} not read here`);
const params = Array.from({ length: leb() }, valtype);
const results = Array.from({ length: leb() }, valtype);
types.push({ params, results });
}
} else if (id === 2) {
const n = leb();
for (let i = 0; i < n; i++) {
const module = name(), field = name(), kind = u8();
if (kind === 0) {
const t = leb();
funcTypes.push(t);
imports.push({ module, name: field, kind: 'func', ...types[t] });
} else if (kind === 1) { u8(); limits(); imports.push({ module, name: field, kind: 'table' }); }
else if (kind === 2) { limits(); imports.push({ module, name: field, kind: 'memory' }); }
else if (kind === 3) { valtype(); u8(); imports.push({ module, name: field, kind: 'global' }); }
else if (kind === 4) { u8(); leb(); imports.push({ module, name: field, kind: 'tag' }); }
else fail(`import kind ${kind}`);
}
} else if (id === 3) {
const n = leb();
for (let i = 0; i < n; i++) funcTypes.push(leb());
} else if (id === 7) {
const n = leb();
for (let i = 0; i < n; i++) {
const field = name(), kind = u8(), index = leb();
const e = { name: field, kind: KIND[kind] ?? fail(`export kind ${kind}`), index };
if (kind === 0) Object.assign(e, types[funcTypes[index]] ?? fail(`export ${field}: no type for function ${index}`));
exports.push(e);
}
}
p = end;
}
return { types, imports, exports };
}
// The i64 boundary of a module: every function import and export with an i64
// parameter or result. The reach runtime splits and joins these values at the JS
// boundary (an i64 crosses as a (low, high) pair of i32 values, and a result's high
// half travels through env.setTempRet0). A multi-value result is refused: neither
// legalisation nor the runtime handles one.
export function i64Boundary(m) {
const sig = (f) => {
if (f.results.length > 1) throw new Error(`${f.name}: a multi-value result is not supported`);
return { params: f.params, result: f.results[0] ?? null };
};
const has64 = (f) => f.params.includes('i64') || f.results.includes('i64');
const exports = {}, imports = {};
for (const e of m.exports) if (e.kind === 'func' && has64(e)) exports[e.name] = sig(e);
for (const i of m.imports) {
if (i.kind !== 'func' || !has64(i)) continue;
if (imports[i.name]) throw new Error(`import ${i.name} appears under two modules`);
imports[i.name] = { module: i.module, ...sig(i) };
}
return { exports, imports };
}