lumbda/wasm/tests/functional-cross.mjs
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

97 lines
3.9 KiB
JavaScript

// wasm/tests/functional-cross.mjs
// Run tests/functional.lsp on each WASM tier, count PASS / FAIL lines.
// Documents how close each tier is to passing the shared suite.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const here = path.dirname(fileURLToPath(import.meta.url));
const dist = path.resolve(here, "..", "dist");
const repoRoot = path.resolve(here, "..", "..");
const functionalLsp = fs.readFileSync(path.join(repoRoot, "tests", "functional.lsp"), "utf8");
function countPassFail(out) {
const pass = (out.match(/^PASS: /gm) || []).length;
const fail = (out.match(/^FAIL: /gm) || []).length;
return { pass, fail };
}
async function runAsm() {
const wasmBytes = fs.readFileSync(path.join(dist, "asm", "lumbda-asm.wasm"));
const importObj = { env: { bend_call(_p, _l, _r) { return 0; } } };
const { instance } = await WebAssembly.instantiate(wasmBytes, importObj);
const exp = instance.exports;
exp.lumbda_init();
const bytes = new TextEncoder().encode(functionalLsp);
if (bytes.length > 65536) throw new Error(`source too big for asm tier source buffer (${bytes.length} > 65536)`);
new Uint8Array(exp.memory.buffer).set(bytes, exp.lumbda_source_ptr());
try { exp.lumbda_eval(bytes.length); } catch (e) {
const out = new TextDecoder().decode(
new Uint8Array(exp.memory.buffer, exp.lumbda_output_ptr(), exp.lumbda_output_len()));
return { out, threw: e.message };
}
const out = new TextDecoder().decode(
new Uint8Array(exp.memory.buffer, exp.lumbda_output_ptr(), exp.lumbda_output_len()));
return { out, threw: null };
}
async function runC() {
const factoryURL = "file://" + path.join(dist, "c", "lumbda-c.js");
const createLumbdaC = (await import(factoryURL)).default;
let outBuf = [];
const m = await createLumbdaC({
locateFile: (p) => path.join(dist, "c", p),
print: (line) => outBuf.push(line),
printErr: () => {},
});
m.cwrap("lumbda_wasm_init", null, [])();
const r = m.cwrap("lumbda_wasm_eval", "number", ["string"])(functionalLsp);
let errMsg = "";
if (r) errMsg = m.UTF8ToString(r);
return { out: outBuf.join("\n"), threw: errMsg || null };
}
(async () => {
console.log("─".repeat(60));
console.log("Cross-tier functional.lsp runner");
console.log(`Source: ${functionalLsp.length} bytes`);
const results = {};
console.log("\n── asm tier ──");
try {
const { out, threw } = await runAsm();
const { pass, fail } = countPassFail(out);
results.asm = { pass, fail, threw, lines: out.split("\n").length };
console.log(` PASS: ${pass}`);
console.log(` FAIL: ${fail}`);
if (threw) console.log(` threw: ${threw}`);
} catch (e) {
console.log(` fatal: ${e.message}`);
results.asm = { pass: 0, fail: 0, threw: e.message };
}
console.log("\n── c tier ──");
try {
const { out, threw } = await runC();
const { pass, fail } = countPassFail(out);
results.c = { pass, fail, threw, lines: out.split("\n").length };
console.log(` PASS: ${pass}`);
console.log(` FAIL: ${fail}`);
if (threw) console.log(` threw: ${threw.slice(0, 200)}`);
} catch (e) {
console.log(` fatal: ${e.message}`);
results.c = { pass: 0, fail: 0, threw: e.message };
}
console.log("\n" + "─".repeat(60));
const total = Math.max(results.asm?.pass + results.asm?.fail || 0,
results.c?.pass + results.c?.fail || 0);
console.log(`Summary (out of ~${total} reached assertions):`);
console.log(` asm-wasm: ${results.asm.pass}/${results.asm.pass + results.asm.fail} pass`);
console.log(` c-wasm: ${results.c.pass}/${results.c.pass + results.c.fail} pass`);
// Soft exit — this is a measurement target, not a pass/fail gate.
process.exit(0);
})();