The qip contract in tiny, pure JavaScript. Write bytes to input_ptr, call render(input_size), read bytes from output_ptr.
Core utilities
Numeric conversion, MIME normalization, and export value reading all fail fast with explicit errors.
|
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder("utf-8", { fatal: true });
function toI32(value, label) {
const n = typeof value === "bigint" ? Number(value) : value;
if (typeof n !== "number" || !Number.isFinite(n)) {
throw Error(label + " returned non-finite numeric value");
}
return n | 0;
}
function normalizeMimeType(value) {
if (typeof value !== "string") return "";
const trimmed = value.trim().toLowerCase();
if (trimmed === "") return "";
const semi = trimmed.indexOf(";");
return semi === -1 ? trimmed : trimmed.slice(0, semi).trim();
}
function valueFromExport(exportsObj, name, required) {
const value = exportsObj[name];
if (typeof value === "function") return toI32(value(), name);
if (value instanceof WebAssembly.Global) return toI32(value.value, name);
if (typeof value === "number" || typeof value === "bigint") return toI32(value, name);
if (required) throw Error("module missing export " + name);
return null;
}
|
Memory safety helpers
Every memory read/write performs pointer and bounds checks.
|
function readSlice(memory, ptr, len, label) {
if (!(memory instanceof WebAssembly.Memory)) {
throw Error("module export memory must be WebAssembly.Memory");
}
if (ptr < 0 || len < 0) {
throw Error(label + " returned negative pointer/size");
}
const start = ptr >>> 0;
const size = len >>> 0;
const end = start + size;
const mem = new Uint8Array(memory.buffer);
if (end < start || end > mem.length) {
throw Error(label + " exceeds wasm memory bounds");
}
return mem.slice(start, end);
}
function writeSlice(memory, ptr, bytes, label) {
if (!(memory instanceof WebAssembly.Memory)) {
throw Error("module export memory must be WebAssembly.Memory");
}
const start = ptr >>> 0;
const end = start + bytes.length;
const mem = new Uint8Array(memory.buffer);
if (ptr < 0 || end < start || end > mem.length) {
throw Error(label + " exceeds wasm memory bounds");
}
mem.set(bytes, start);
}
|
Module loading
`wasmModule` can be an instance, compiled module, URL string, `Response`, `Uint8Array`, `ArrayBuffer`, or typed view.
|
async function instantiateWasm(wasmModule) {
if (wasmModule instanceof WebAssembly.Instance) return wasmModule;
if (wasmModule instanceof WebAssembly.Module) {
const out = await WebAssembly.instantiate(wasmModule, {});
return out instanceof WebAssembly.Instance ? out : out.instance;
}
if (typeof wasmModule === "string") {
const response = await fetch(wasmModule);
if (!response.ok) {
throw Error("failed to fetch wasm module: " + response.status + " " + response.statusText);
}
const bytes = await response.arrayBuffer();
const out = await WebAssembly.instantiate(bytes, {});
return out instanceof WebAssembly.Instance ? out : out.instance;
}
if (wasmModule && typeof wasmModule.arrayBuffer === "function") {
const bytes = await wasmModule.arrayBuffer();
const out = await WebAssembly.instantiate(bytes, {});
return out instanceof WebAssembly.Instance ? out : out.instance;
}
}
|
Contract parsing
The runner supports qip’s dual style exports: function or global for pointers/caps.
|
function parseInputSignature(exportsObj) {
const inputPtr = valueFromExport(exportsObj, "input_ptr", true);
const utf8Cap = valueFromExport(exportsObj, "input_utf8_cap", false);
const bytesCap = valueFromExport(exportsObj, "input_bytes_cap", false);
if (utf8Cap !== null) return { ptr: inputPtr, cap: utf8Cap, kind: "utf8" };
if (bytesCap !== null) return { ptr: inputPtr, cap: bytesCap, kind: "bytes" };
throw Error("module must export inpututf8cap or inputbytescap");
}
function parseOutputSignature(exportsObj) {
const outputPtr = valueFromExport(exportsObj, "output_ptr", false);
const utf8Cap = valueFromExport(exportsObj, "outpututf8cap", false);
const bytesCap = valueFromExport(exportsObj, "outputbytescap", false);
const i32Cap = valueFromExport(exportsObj, "outputi32cap", false);
if (outputPtr === null || (utf8Cap === null && bytesCap === null && i32Cap === null)) {
return { kind: "scalar" };
}
if (utf8Cap !== null) return { kind: "utf8", ptr: outputPtr, cap: utf8Cap, itemSize: 1 };
if (bytesCap !== null) return { kind: "bytes", ptr: outputPtr, cap: bytesCap, itemSize: 1 };
return { kind: "i32", ptr: outputPtr, cap: i32Cap, itemSize: 4 };
}
|
Stage execution (`renderDetailed`)
This is the qip loop: validate types, write input bytes, call `render`, read output.
|
async function renderDetailed(wasmModule, input, inputContentType) {
const instance = await instantiateWasm(wasmModule);
const exportsObj = instance.exports;
const renderExport = exportsObj.render;
if (typeof renderExport !== "function") {
throw Error("module missing export: render");
}
const inputSignature = parseInputSignature(exportsObj);
const outputSignature = parseOutputSignature(exportsObj);
const normalized = toInputBytes(input);
if (normalized.bytes.length > inputSignature.cap) {
throw Error("input is too large for module");
}
writeSlice(exportsObj.memory, inputSignature.ptr, normalized.bytes, "input_ptr");
const outputLen = toI32(renderExport(normalized.bytes.length), "render");
if (outputSignature.kind === "scalar") {
return outputLen;
}
const byteLen = outputLen * outputSignature.itemSize;
const outputBytes = readSlice(exportsObj.memory, outputSignature.ptr, byteLen, "output_ptr");
if (outputSignature.kind === "utf8") {
return textDecoder.decode(outputBytes);
}
if (outputSignature.kind === "bytes") {
return outputBytes;
}
return new Int32Array(outputBytes.buffer, outputBytes.byteOffset, outputLen);
}
|
Public `render`
This returns only the stage output value: `string`, `Uint8Array`, `Int32Array`, or scalar `number`.
|
export async function render(wasmModule, input) {
const result = await renderDetailed(wasmModule, input, "");
return result.value;
}
|
`Recipe` pipeline
`Recipe` composes multiple render modules in stage order and tracks final MIME/kind metadata.
|
export class Recipe {
constructor(inputMimeType, arrayOfWasmModules) {
if (!Array.isArray(arrayOfWasmModules) || arrayOfWasmModules.length === 0) {
throw Error("Recipe requires a non-empty array of wasm modules");
}
this.inputMimeType = normalizeMimeType(inputMimeType);
this.modules = arrayOfWasmModules.slice();
this.lastRender = null;
}
async render(input) {
let current = input;
let currentMimeType = this.inputMimeType;
for (let i = 0; i < this.modules.length; i += 1) {
const isLast = i === this.modules.length - 1;
const result = await renderDetailed(this.modules[i], current, currentMimeType);
if (!isLast && (result.kind === "scalar" || result.kind === "i32")) {
throw Error("only utf8/bytes can be piped to another stage");
}
current = result.value;
if (result.outputContentType !== "") currentMimeType = result.outputContentType;
}
this.lastRender = { outputMimeType: currentMimeType };
return current;
}
}
|