lumbda/wasm/asm/lumbda-asm.loader.js
russell@unturf.com 7e32eefd6b
playground+repl: bend dispatch from WASM + 1.33 zoom + free-form runner
bend!-call from the WAT tier
  - New WAT import: (import "env" "bend_call"). The host loader supplies
    a sync XMLHttpRequest that POSTs the payload to a configured URL
    (workers only — sync XHR isn't allowed on main thread).
  - New primitive (bend!-call "<payload>") returns the response as a
    lumbda string. Works in playground and REPL once the bend URL is
    saved in the new top bar.
  - bendUrl persists in plain localStorage (not encrypted — it's a
    server address, not a secret).
  - Tests stub the import with a no-op so unit / integration / functional
    suites keep instantiating cleanly.

Playground + REPL zoom 133% by default
  - html { zoom: 1.33 } so the styleguide sizes read comfortably without
    requiring browser-level zoom.

Free-form default = cross-tier assertion runner in Lisp
  - The default editor content for "free form" is now a small assertion
    framework matching tests/functional.lsp's PASS/FAIL convention. A
    starter the user can extend, runs identically on the three tiers.

C tier + Python tier (bend) are wired through the runner stub; full
bend integration in those tiers comes next once their loaders learn
about setBendUrl.
2026-06-14 13:05:06 -04:00

85 lines
3.4 KiB
JavaScript

// wasm/asm/lumbda-asm.loader.js
// Asm tier loader — instantiates lumbda-asm.wasm and exposes evalLisp.
//
// Memory layout (mirrors lumbda.wat):
// 0x10000 output buffer (read after each call)
// 0x20000 source buffer (write before each call)
// 0x40000 bend response scratch (host writes here when JS dispatches)
async function _bootstrap() {
const wasmURL = new URL("./lumbda-asm.wasm", import.meta.url).href;
const resp = await fetch(wasmURL);
const bytes = await resp.arrayBuffer();
// Closure-captured holder so the imported function can see the
// instance's memory after instantiation completes.
const refs = { instance: null, bendUrl: null };
const importObj = {
env: {
// bend_call(src_ptr, src_len, resp_ptr) -> resp_len.
// Synchronous XHR is the only sync HTTP available in workers;
// perfect for the WAT tier's blocking eval model.
bend_call(srcPtr, srcLen, respPtr) {
if (!refs.bendUrl) {
const msg = "no bend URL configured";
const mem = new Uint8Array(refs.instance.exports.memory.buffer);
mem.set(new TextEncoder().encode(msg), respPtr);
return msg.length;
}
const mem = new Uint8Array(refs.instance.exports.memory.buffer);
const payload = new TextDecoder().decode(mem.slice(srcPtr, srcPtr + srcLen));
try {
const xhr = new XMLHttpRequest();
xhr.open("POST", refs.bendUrl, false); // sync
xhr.setRequestHeader("Content-Type", "text/plain; charset=utf-8");
xhr.send(payload);
const respText = xhr.responseText || "";
const respBytes = new TextEncoder().encode(respText);
new Uint8Array(refs.instance.exports.memory.buffer).set(respBytes, respPtr);
return respBytes.length;
} catch (e) {
const err = "bend error: " + (e.message || String(e));
new Uint8Array(refs.instance.exports.memory.buffer)
.set(new TextEncoder().encode(err), respPtr);
return err.length;
}
},
},
};
const { instance } = await WebAssembly.instantiate(bytes, importObj);
refs.instance = instance;
instance.exports.lumbda_init();
const enc = new TextEncoder();
const dec = new TextDecoder();
const exp = instance.exports;
return {
async evalLisp(src) {
const srcBytes = enc.encode(src);
const srcPtr = exp.lumbda_source_ptr();
const mem = new Uint8Array(exp.memory.buffer);
mem.set(srcBytes, srcPtr);
try {
exp.lumbda_eval(srcBytes.length);
} catch (e) {
return `error: ${e.message}`;
}
const outPtr = exp.lumbda_output_ptr();
const outLen = exp.lumbda_output_len();
return dec.decode(new Uint8Array(exp.memory.buffer, outPtr, outLen));
},
setBendUrl(url) { refs.bendUrl = url || null; },
};
}
// Closure-encapsulated singleton (see lumbda-c.loader.js for rationale).
export const createAsmTier = (() => {
let tier = null;
return async () => {
if (!tier) tier = await _bootstrap();
return tier;
};
})();