From 7e32eefd6ba846b3476ac0afaa4cabee2fe3e592 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sun, 14 Jun 2026 13:05:06 -0400 Subject: [PATCH] playground+repl: bend dispatch from WASM + 1.33 zoom + free-form runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 "") 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. --- wasm/app/app.js | 62 +++++++++++++++++++++++- wasm/app/index.html | 9 ++++ wasm/app/runner.js | 10 ++++ wasm/app/style.css | 46 ++++++++++++++++++ wasm/app/worker.mjs | 9 +++- wasm/asm/lumbda-asm.loader.js | 46 ++++++++++++++++-- wasm/asm/lumbda.wat | 44 +++++++++++++++++ wasm/dist-repl/asm/lumbda-asm.loader.js | 46 ++++++++++++++++-- wasm/dist-repl/asm/lumbda-asm.wasm | Bin 16549 -> 16704 bytes wasm/dist-repl/runner.js | 10 ++++ wasm/dist-repl/style.css | 46 ++++++++++++++++++ wasm/dist-repl/worker.mjs | 9 +++- wasm/tests/functional-cross.mjs | 3 +- wasm/tests/integration.mjs | 3 +- wasm/tests/unit.mjs | 3 +- www/playground/app.js | 62 +++++++++++++++++++++++- www/playground/asm/lumbda-asm.loader.js | 46 ++++++++++++++++-- www/playground/asm/lumbda-asm.wasm | Bin 16549 -> 16704 bytes www/playground/index.html | 9 ++++ www/playground/runner.js | 10 ++++ www/playground/style.css | 46 ++++++++++++++++++ www/playground/worker.mjs | 9 +++- www/repl/asm/lumbda-asm.loader.js | 46 ++++++++++++++++-- www/repl/asm/lumbda-asm.wasm | Bin 16549 -> 16704 bytes www/repl/runner.js | 10 ++++ www/repl/style.css | 46 ++++++++++++++++++ www/repl/worker.mjs | 9 +++- 27 files changed, 612 insertions(+), 27 deletions(-) diff --git a/wasm/app/app.js b/wasm/app/app.js index a739eac..bd26c0a 100644 --- a/wasm/app/app.js +++ b/wasm/app/app.js @@ -12,7 +12,35 @@ import { oneDark } from "@codemirror/theme-one-dark"; import { openVault } from "./crypto.js"; const TIERS = { python: "python (pyodide)", c: "c (emcc)", asm: "asm (wat)" }; -const FREE_FORM_DEFAULT = "; free-form mode — unlock the vault below to persist this code\n; encrypted in localStorage with your password\n\n(+ 1 2)\n"; +const FREE_FORM_DEFAULT = `; free-form mode — write any lisp, race all three tiers. +; unlock the vault below to persist this code (encrypted localStorage). +; +; below: a tiny version of the cross-tier functional.lsp runner. + +(define pass 0) +(define fail 0) +(define (check name got expected) + (cond ((equal? got expected) + (set! pass (+ pass 1)) + (display "PASS: ") (display name) (newline)) + (else + (set! fail (+ fail 1)) + (display "FAIL: ") (display name) + (display " got=") (display got) + (display " expected=") (display expected) (newline)))) + +(check "arithmetic" (+ 1 2) 3) +(check "division" (/ 42 6) 7) +(check "string" (string-append "foo" "bar") "foobar") +(check "list reverse" (reverse (list 1 2 3)) (list 3 2 1)) +(check "vector len" (vector-length (vector 1 2 3)) 3) +(check "char" (char->integer #\\A) 65) +(check "let loop" (let loop ((i 0) (n 0)) + (if (= i 10) n (loop (+ i 1) (+ n i)))) 45) + +(display pass) (display " pass, ") (display fail) (display " fail") +(newline) +`; const demoSources = {}; // Vault state. When unlocked, free-form code auto-saves on every edit. @@ -166,7 +194,9 @@ const workerState = { }; function spawnWorker() { - return new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" }); + const w = new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" }); + if (bendState.url) w.postMessage({ kind: "config", bendUrl: bendState.url }); + return w; } function ensureWorker(tier) { @@ -174,6 +204,34 @@ function ensureWorker(tier) { return workerState.workers[tier]; } +// ─── Bend URL ────────────────────────────────────────────────────── +const bendState = { url: localStorage.getItem("lumbda_bend_url") || "" }; +const bendUrlEl = document.getElementById("bend-url"); +const bendSaveBtn = document.getElementById("bend-save"); +const bendStateEl = document.getElementById("bend-state"); +if (bendState.url) { + bendUrlEl.value = bendState.url; + bendStateEl.textContent = "saved"; +} +function saveBendUrl() { + bendState.url = bendUrlEl.value.trim(); + if (bendState.url) { + localStorage.setItem("lumbda_bend_url", bendState.url); + bendStateEl.textContent = "saved"; + } else { + localStorage.removeItem("lumbda_bend_url"); + bendStateEl.textContent = ""; + } + // Push config to any already-spawned workers. + for (const w of Object.values(workerState.workers)) { + if (w) w.postMessage({ kind: "config", bendUrl: bendState.url }); + } +} +bendSaveBtn.addEventListener("click", saveBendUrl); +bendUrlEl.addEventListener("keydown", (e) => { + if (e.key === "Enter") { e.preventDefault(); saveBendUrl(); } +}); + function runOnTierInWorker(tier, src, onLoading) { return new Promise((resolve, reject) => { const w = ensureWorker(tier); diff --git a/wasm/app/index.html b/wasm/app/index.html index 03a27f3..b664386 100644 --- a/wasm/app/index.html +++ b/wasm/app/index.html @@ -59,6 +59,15 @@ +
+ + + + +
+ +
+ + + + +
+