/** * Hand-encoded WASM fixture builder for the wasm host bridge's tests + * conformance harness. Produces deterministic, validator-clean WASM * modules implementing the `__suaveplan_*` ABI surface with configurable * behaviour per export so the test suite can exercise every bridge * production path WITHOUT pulling in `wabt` or any other external WASM * build tool. * * Memory layout (single 1-page memory): * [ 0, 1024) — reserved scratch (cleared on init). * [1024, 1024 + routesBytes.length) — routes JSON. * [2048, 2048 + responseBytes.length) — canned response JSON. * [3072, 3072 + WITNESS_CAPACITY) — request-byte witness. * [4096, 65536) — bump allocator arena. * * Global 0: mut i32 = 4096 (bump pointer). */ import { concat, encodeI32, encodeName, encodeU32, section, vec, } from "./leb128.js"; import { BLOCK_TYPE_EMPTY, funcBody, OP_BLOCK, OP_END, OP_I32_ADD, OP_I32_CONST, OP_I32_EQZ, OP_I32_LT_U, OP_I64_CONST, OP_LOOP, OP_UNREACHABLE, opBr, opBrIf, opCall, opGlobalGet, opGlobalSet, opI32Const, opI32Load8U, opI32Store8, opLocalGet, opLocalSet, VT_I32, VT_I64, } from "./wasm-opcodes.js"; const ROUTES_OFFSET = 1024; const RESPONSE_OFFSET = 2048; export const WITNESS_OFFSET = 3072; export const WITNESS_CAPACITY = 1024; const HEAP_START = 4096; const MAGIC = new Uint8Array([0x00, 0x61, 0x73, 0x6d]); const VERSION = new Uint8Array([0x01, 0x00, 0x00, 0x00]); /** Configurable behaviour per ABI export. */ export interface BuildFixtureWasmOptions { /** * The JSON-encoded `SerializedRoute[]` the guest's * `__suaveplan_routes` returns. Defaults to a single-route catalog. */ readonly routesJson?: string; /** * The JSON-encoded `SerializedResponse` the guest's * `__suaveplan_handle_request` returns. Defaults to a 200 with an * empty body. */ readonly responseJson?: string; /** Force `__suaveplan_alloc` to return 0 (OOM). */ readonly allocAlwaysZero?: boolean; /** Force `__suaveplan_handle_request` to execute `unreachable`. */ readonly handlerTraps?: boolean; /** Force `__suaveplan_start` to execute `unreachable`. */ readonly startTraps?: boolean; /** Force `__suaveplan_dispose` to execute `unreachable`. */ readonly disposeTraps?: boolean; /** Force `__suaveplan_routes` to execute `unreachable`. */ readonly routesTraps?: boolean; /** Force `__suaveplan_routes` to return `(0n, 0n)` packed. */ readonly routesEmpty?: boolean; /** Force `__suaveplan_handle_request` to return `(0n, 0n)` packed. */ readonly handlerEmptyResponse?: boolean; /** * Force `__suaveplan_handle_request` to return `(ptr, 0)` packed * with the supplied non-zero pointer. Used to test the * `(nonzero, 0)` empty-response branch where the bridge must free * the dangling pointer. */ readonly handlerZeroLengthResponse?: number; /** Drop one or more required exports — exercises the ABI validator. */ readonly dropExports?: readonly ( | "memory" | "__suaveplan_alloc" | "__suaveplan_free" | "__suaveplan_handle_request" | "__suaveplan_routes" | "__suaveplan_start" | "__suaveplan_dispose" )[]; } const ENCODER = new TextEncoder(); const DEFAULT_ROUTES_JSON = JSON.stringify([{ method: "GET", path: "/hello" }]); const DEFAULT_RESPONSE_JSON = JSON.stringify({ status: 200, headers: [["content-type", "text/plain"]], bodyBase64: btoa("ok") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""), }); /** * Build a validator-clean WASM module. The returned bytes are accepted * by `WebAssembly.compile` and instantiate against an empty imports * object (no WASI, no capability imports — the fixture is a pure * compute module). */ export function buildFixtureWasm( options: BuildFixtureWasmOptions = {} ): Uint8Array { const routesBytes = ENCODER.encode( options.routesJson ?? DEFAULT_ROUTES_JSON ); const responseBytes = ENCODER.encode( options.responseJson ?? DEFAULT_RESPONSE_JSON ); if (routesBytes.length > RESPONSE_OFFSET - ROUTES_OFFSET) { throw new RangeError("routesJson too large for fixture memory layout"); } if (responseBytes.length > WITNESS_OFFSET - RESPONSE_OFFSET) { throw new RangeError( "responseJson too large for fixture memory layout" ); } const dropExports = new Set(options.dropExports ?? []); // --- Type section --------------------------------------------------- // type 0: () -> () // type 1: (i32) -> i32 // type 2: (i32) -> () // type 3: (i32, i32) -> i64 // type 4: () -> i64 // type 5: (i32, i32, i32) -> () [memcpy helper] const typeSection = section( 0x01, vec([ new Uint8Array([0x60, 0x00, 0x00]), new Uint8Array([0x60, 0x01, VT_I32, 0x01, VT_I32]), new Uint8Array([0x60, 0x01, VT_I32, 0x00]), new Uint8Array([0x60, 0x02, VT_I32, VT_I32, 0x01, VT_I64]), new Uint8Array([0x60, 0x00, 0x01, VT_I64]), new Uint8Array([0x60, 0x03, VT_I32, VT_I32, VT_I32, 0x00]), ]) ); // --- Function section (declares the type index of each defined fn) - // Function indices (no imports): // 0: __suaveplan_alloc — type 1 // 1: __suaveplan_free — type 2 // 2: __suaveplan_handle_request — type 3 // 3: __suaveplan_routes — type 4 // 4: __suaveplan_start — type 0 // 5: __suaveplan_dispose — type 0 // 6: memcpy_internal — type 5 const funcSection = section( 0x03, vec([ encodeU32(1), encodeU32(2), encodeU32(3), encodeU32(4), encodeU32(0), encodeU32(0), encodeU32(5), ]) ); // --- Memory section ------------------------------------------------- // One memory with initial=1 page, no max. const memorySection = section(0x05, vec([new Uint8Array([0x00, 0x01])])); // --- Global section ------------------------------------------------- // Global 0: (mut i32) = HEAP_START const globalSection = section( 0x06, vec([ concat( new Uint8Array([VT_I32, 0x01]), new Uint8Array([OP_I32_CONST]), encodeI32(HEAP_START), new Uint8Array([OP_END]) ), ]) ); // --- Export section ------------------------------------------------- const exportEntries: Uint8Array[] = []; const pushExport = (name: string, kind: number, index: number): void => { if (dropExports.has(name as never)) return; exportEntries.push( concat(encodeName(name), new Uint8Array([kind, index])) ); }; // 0x00 = function, 0x02 = memory. pushExport("memory", 0x02, 0); pushExport("__suaveplan_alloc", 0x00, 0); pushExport("__suaveplan_free", 0x00, 1); pushExport("__suaveplan_handle_request", 0x00, 2); pushExport("__suaveplan_routes", 0x00, 3); pushExport("__suaveplan_start", 0x00, 4); pushExport("__suaveplan_dispose", 0x00, 5); const exportSection = section(0x07, vec(exportEntries)); // --- Code section --------------------------------------------------- const codeSection = section( 0x0a, vec([ buildAllocBody(options.allocAlwaysZero === true), buildFreeBody(), buildHandleRequestBody({ trap: options.handlerTraps === true, emptyResponse: options.handlerEmptyResponse === true, zeroLengthPtr: options.handlerZeroLengthResponse, responseBytesLen: responseBytes.length, }), buildRoutesBody({ trap: options.routesTraps === true, empty: options.routesEmpty === true, routesLen: routesBytes.length, }), buildLifecycleBody(options.startTraps === true), buildLifecycleBody(options.disposeTraps === true), buildMemcpyBody(), ]) ); // --- Data section --------------------------------------------------- const dataSection = section( 0x0b, vec([ buildDataSegment(ROUTES_OFFSET, routesBytes), buildDataSegment(RESPONSE_OFFSET, responseBytes), ]) ); return concat( MAGIC, VERSION, typeSection, funcSection, memorySection, globalSection, exportSection, codeSection, dataSection ); } function buildDataSegment(offset: number, payload: Uint8Array): Uint8Array { // Active data segment, memory index 0, init expr (i32.const offset, end), bytes. return concat( encodeU32(0), // memory index new Uint8Array([OP_I32_CONST]), encodeI32(offset), new Uint8Array([OP_END]), encodeU32(payload.length), payload ); } function buildAllocBody(zero: boolean): Uint8Array { // (param i32) -> i32 // local 0 = len if (zero) { return funcBody([], concat(opI32Const(0))); } // ptr = global; global = ptr + len; return ptr // locals: ptr (local 1) const body = concat( opGlobalGet(0), opLocalSet(1), opLocalGet(1), opLocalGet(0), new Uint8Array([OP_I32_ADD]), opGlobalSet(0), opLocalGet(1) ); return funcBody([{ count: 1, type: VT_I32 }], body); } function buildFreeBody(): Uint8Array { // (param i32) -> () // No-op — emit just end (handled by funcBody). return funcBody([], new Uint8Array()); } function buildHandleRequestBody(options: { readonly trap: boolean; readonly emptyResponse: boolean; readonly zeroLengthPtr: number | undefined; readonly responseBytesLen: number; }): Uint8Array { // (param i32 i32) -> i64 // local 0 = reqPtr, local 1 = reqLen if (options.trap) { return funcBody([], new Uint8Array([OP_UNREACHABLE])); } if (options.emptyResponse) { return funcBody( [], concat(new Uint8Array([OP_I64_CONST]), encodeI32(0)) ); } if (options.zeroLengthPtr !== undefined) { const packed = BigInt(options.zeroLengthPtr) << 32n; return funcBody( [], concat(new Uint8Array([OP_I64_CONST]), encodeI64(packed)) ); } // 1. Clamp reqLen to WITNESS_CAPACITY. // 2. Call memcpy_internal(WITNESS, reqPtr, reqLen). // 3. Return packed (RESPONSE_OFFSET << 32) | responseBytesLen as i64. const clipBlock = concat( new Uint8Array([OP_BLOCK, BLOCK_TYPE_EMPTY]), opLocalGet(1), opI32Const(WITNESS_CAPACITY), new Uint8Array([OP_I32_LT_U]), opBrIf(0), opI32Const(WITNESS_CAPACITY), opLocalSet(1), new Uint8Array([OP_END]) ); const callMemcpy = concat( opI32Const(WITNESS_OFFSET), // dst opLocalGet(0), // src = reqPtr opLocalGet(1), // len (possibly clamped) opCall(6) // memcpy_internal ); const packed = (BigInt(RESPONSE_OFFSET) << 32n) | BigInt(options.responseBytesLen); const returnPacked = concat( new Uint8Array([OP_I64_CONST]), encodeI64(packed) ); return funcBody([], concat(clipBlock, callMemcpy, returnPacked)); } function buildRoutesBody(options: { readonly trap: boolean; readonly empty: boolean; readonly routesLen: number; }): Uint8Array { // () -> i64 if (options.trap) { return funcBody([], new Uint8Array([OP_UNREACHABLE])); } if (options.empty) { return funcBody( [], concat(new Uint8Array([OP_I64_CONST]), encodeI64(0n)) ); } const packed = (BigInt(ROUTES_OFFSET) << 32n) | BigInt(options.routesLen); return funcBody( [], concat(new Uint8Array([OP_I64_CONST]), encodeI64(packed)) ); } function buildLifecycleBody(trap: boolean): Uint8Array { if (trap) { return funcBody([], new Uint8Array([OP_UNREACHABLE])); } return funcBody([], new Uint8Array()); } function buildMemcpyBody(): Uint8Array { // (param dst i32, src i32, len i32) -> () // local 3 = i // block // loop // local.get i // local.get len // i32.lt_u // i32.eqz // br_if 1 // ;; memory[dst+i] = memory[src+i] // local.get dst // local.get i // i32.add // local.get src // local.get i // i32.add // i32.load8_u // i32.store8 // local.get i // i32.const 1 // i32.add // local.set i // br 0 // end // end const body = concat( opI32Const(0), opLocalSet(3), new Uint8Array([OP_BLOCK, BLOCK_TYPE_EMPTY]), new Uint8Array([OP_LOOP, BLOCK_TYPE_EMPTY]), opLocalGet(3), opLocalGet(2), new Uint8Array([OP_I32_LT_U]), new Uint8Array([OP_I32_EQZ]), opBrIf(1), opLocalGet(0), opLocalGet(3), new Uint8Array([OP_I32_ADD]), opLocalGet(1), opLocalGet(3), new Uint8Array([OP_I32_ADD]), opI32Load8U(), opI32Store8(), opLocalGet(3), opI32Const(1), new Uint8Array([OP_I32_ADD]), opLocalSet(3), opBr(0), new Uint8Array([OP_END]), new Uint8Array([OP_END]) ); return funcBody([{ count: 1, type: VT_I32 }], body); } /** Encode a signed LEB128 i64 literal. */ function encodeI64(value: bigint): Uint8Array { const out: number[] = []; let v = value; while (true) { const byte = Number(v & 0x7fn); v >>= 7n; const signBit = (byte & 0x40) !== 0; if ((v === 0n && !signBit) || (v === -1n && signBit)) { out.push(byte); break; } out.push(byte | 0x80); } return new Uint8Array(out); }