Become the compiler's back end
Compilers end by writing bytes. Write buildModule: given a list of functions (all i32) and their instruction bytes, emit the type, function, export and code sections — then the browser compiles your binary.
The builder
const nameBytes = (s) => [...encodeULEB128(s.length), ...[...s].map((c) => c.charCodeAt(0))];
const section = (id, content) => [id, ...encodeULEB128(content.length), ...content];
// funcs: [{ name, params, locals = 0, body }] — every param, local and result is i32.
function buildModule(funcs) {
const types = funcs.map((f) => [0x60, ...encodeULEB128(f.params), ...Array(f.params).fill(0x7f), 1, 0x7f]);
const typeSec = section(1, [...encodeULEB128(funcs.length), ...types.flat()]);
const funcSec = section(3, [...encodeULEB128(funcs.length), ...funcs.flatMap((_, i) => encodeULEB128(i))]);
const exportSec = section(7, [...encodeULEB128(funcs.length), ...funcs.flatMap((f, i) => [...nameBytes(f.name), 0x00, ...encodeULEB128(i)])]);
const bodies = funcs.flatMap((f) => {
const locals = f.locals ? [1, ...encodeULEB128(f.locals), 0x7f] : [0];
const body = [...locals, ...f.body, 0x0b]; // every body ends with "end"
return [...encodeULEB128(body.length), ...body];
});
const codeSec = section(10, [...encodeULEB128(funcs.length), ...bodies]);
return new Uint8Array([0x00, 0x61, 0x73, 0x6d, 1, 0, 0, 0, ...typeSec, ...funcSec, ...exportSec, ...codeSec]);
}- 0x20 n local.get, 0x21 n local.set
- 0x41 n i32.const (signed LEB128)
- 0x6a i32.add, 0x6b i32.sub, 0x6c i32.mul
- 0x02 0x40 … 0x0b block, 0x03 0x40 … 0x0b loop, 0x0d n br_if, 0x0c n br, 0x4c i32.le_s