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]>
119 lines
4.9 KiB
JavaScript
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 };
|
|
}
|