// wasm/tests/integration.mjs // Cross-tier integration — each demo runs on c-WASM and asm-WASM (Node // hosts), output is compared to the canonical native Python lumbda run. // Python tier (Pyodide) loads from a CDN inside a browser; covered in the // functional/Playwright test suite. import fs from "node:fs"; import path from "node:path"; import { execFileSync } from "node:child_process"; 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 demosDir = path.join(here, "..", "app", "demos"); const DEMOS = ["mandelbrot", "fib-ack", "sieve", "self-interp"]; const state = { pass: 0, fail: 0, failures: [] }; function check(name, cond, detail) { if (cond) { state.pass++; console.log(` ✓ ${name}`); } else { state.fail++; console.log(` ✗ ${name}`); if (detail) { console.log(detail); } state.failures.push({ name, detail }); } } function nativePython(demoPath) { return execFileSync("python3", [path.join(repoRoot, "lumbda.py"), "--fast", demoPath], { encoding: "utf8", timeout: 120000, }); } async function runAsm(src) { const wasmBytes = fs.readFileSync(path.join(dist, "asm", "lumbda-asm.wasm")); const { instance } = await WebAssembly.instantiate(wasmBytes); const exp = instance.exports; exp.lumbda_init(); const bytes = new TextEncoder().encode(src); new Uint8Array(exp.memory.buffer).set(bytes, exp.lumbda_source_ptr()); exp.lumbda_eval(bytes.length); return new TextDecoder().decode( new Uint8Array(exp.memory.buffer, exp.lumbda_output_ptr(), exp.lumbda_output_len())); } async function runC(src) { const factoryURL = "file://" + path.join(dist, "c", "lumbda-c.js"); const createLumbdaC = (await import(factoryURL)).default; let out = []; const m = await createLumbdaC({ locateFile: (p) => path.join(dist, "c", p), print: (line) => out.push(line), printErr: (line) => out.push("ERR: " + line), }); m.cwrap("lumbda_wasm_init", null, [])(); const r = m.cwrap("lumbda_wasm_eval", "number", ["string"])(src); let errMsg = ""; if (r) errMsg = m.UTF8ToString(r); return out.join("\n") + (errMsg ? "\n" + errMsg : "") + (out.length ? "\n" : ""); } function diffSnippet(a, b) { const aL = a.split("\n"); const bL = b.split("\n"); const n = Math.max(aL.length, bL.length); const lines = []; for (let i = 0; i < n; i++) { if (aL[i] !== bL[i]) { lines.push(` line ${i + 1}:`); lines.push(` canonical: ${JSON.stringify(aL[i])}`); lines.push(` got: ${JSON.stringify(bL[i])}`); if (lines.length > 10) { lines.push(" ... (truncated)"); break; } } } return lines.join("\n"); } (async () => { for (const demo of DEMOS) { console.log(`── ${demo} ──`); const demoPath = path.join(demosDir, demo + ".lsp"); const src = fs.readFileSync(demoPath, "utf8"); let canonical; try { canonical = nativePython(demoPath); } catch (e) { console.log(" ✗ canonical (native python lumbda) FAILED:", e.message.slice(0, 200)); state.fail++; continue; } try { const asmOut = await runAsm(src); check(`${demo}: asm tier matches canonical`, asmOut === canonical, diffSnippet(canonical, asmOut)); } catch (e) { check(`${demo}: asm tier`, false, " threw: " + e.message); } try { const cOut = await runC(src); check(`${demo}: c tier matches canonical`, cOut === canonical, diffSnippet(canonical, cOut)); } catch (e) { check(`${demo}: c tier`, false, " threw: " + e.message); } } console.log(`\n${state.pass} passed, ${state.fail} failed`); process.exit(state.fail ? 1 : 0); })();