Commit graph

10 commits

Author SHA1 Message Date
df154b9a56
repl: auto-pause + portal-save when leaving a tab mid-eval, resume on return
Tab switching during a long-running eval used to silently abandon
the calc — output stopped streaming, no snapshot, nothing to come
back to. Now setActiveTab pauses the outgoing tab's eval (and
optionally portal-saves the env), terminates the worker, and on
re-entry hydrates + re-fires the original input.

Pieces:

* serve-coop.py + make serve-repl — dev server that emits
  Cross-Origin-Opener-Policy: same-origin and
  Cross-Origin-Embedder-Policy: require-corp so SharedArrayBuffer
  is constructable in the browser. Same headers production needs.

* C tier eval-loop pause poll — c/eval.c grows lumbda_check_pause(),
  guarded by #ifdef LUMBDA_WASM. Called at the top of leval()'s
  while(1); masked to every 1024th iteration so the polling cost
  stays under noise floor. When the JS-library import
  js_lumbda_pause_requested returns 1, lisp_error("paused")
  longjmps out so module-global env survives intact for the
  portal-snapshot that follows.

* SAB plumbing — main thread allocates new SharedArrayBuffer(4),
  hands it through worker config → runner.setPauseFlag →
  lumbda-c.loader.setPauseFlag → globalThis._lumbdaCPauseFlag.
  Atomics.store / Atomics.load on index 0 is the signalling
  channel. Falls back to null when COOP/COEP isn't isolated, in
  which case pause degrades to a hard worker.terminate().

* autoPauseTab() — on setActiveTab away, snapshots the tier
  (C tier with SAB) or hard-cancels (other tiers / no SAB),
  stashes tab.autoPause = {tier, blob, inputSrc, savedAt},
  terminates the workers so the heap is reclaimed.

* autoResumeTab() — on setActiveTab into a tab with autoPause,
  reboots the tier, hydrates MEMFS, runs (portal-load! ...), then
  re-fires the original input via sendInput so the eval restarts
  from the saved state. Asm + Python paths re-run from scratch
  until their poll sites land.

Also closes two UX papercuts from fox: chip ⇣ export icon bumped
from 0.85em muted to 1em green so it's actually discoverable; the
scope toggle now reads "scope: this tab" / "scope: all tabs" so the
button label describes the state rather than a target.
2026-06-15 09:02:53 -04:00
d4380c64c7
repl: portal save/resume — vault-backed tier checkpoints
Adds a portal-bar to the REPL between tabbar and transcript: a save
button + chip strip showing all saved checkpoints for the active tab.
Click a chip to restore, click × to delete.

Per-tier strategy:
  * c, python — call the tier's (portal-snapshot! NAME), then read the
    JSON blob out of MEMFS (Emscripten/Pyodide FS) and stash it in the
    encrypted vault entry. Restore reverses: hydrate MEMFS, then
    (portal-load! NAME) merges the bindings into the live env.
  * asm — no portal serializer in the WAT tier yet (would need a
    Cheney-aware walk). Falls back to transcript replay: save snapshots
    every successful prior input, restore reboots the tier and re-evals
    them in order.

Plumbing:
  * Worker bridge: new portal-save / portal-load message kinds wire
    MEMFS reads/writes to the main thread.
  * runner.js exposes portalSave / portalLoad — null when a tier
    hasn't implemented portals (asm stays grey).
  * C tier: replace EM_JS with extern + --js-library for js_lumbda_bend_call
    (EM_JS-generated declaration was unreachable from wasmImports at
    instantiate time, browsers threw "import object field ... not a
    Function"). FS added to EXPORTED_RUNTIME_METHODS so JS can reach
    pyodide.FS / Module.FS for MEMFS I/O.

Smoke-tested all three tiers headlessly: save → chip render → restore
round-trips clean on c / python / asm, zero console errors.
2026-06-15 07:43:07 -04:00
af0bf9cb35
bend: fix C tier LinkError — switch EM_JS to --js-library mergeInto
Browser instantiation of lumbda-c.wasm failed with:

  Aborted(LinkError: import object field 'js_lumbda_bend_call'
  is not a Function)

The wasm correctly required env.js_lumbda_bend_call as a function
import, and the EM_JS-generated function existed in the glue —
emcc placed the function declaration at depth 0 of createLumbdaC
where wasmImports lives, so it SHOULD have been hoisted into scope
at instantiate time. It worked in node test runs but threw on
browser load. The exact emcc-version cause is fuzzy; the fix is
to use the textbook mechanism instead of guessing.

New file wasm/c/bend-call-library.js — Emscripten JS library with
mergeInto(LibraryManager.library, { js_lumbda_bend_call: function(...) }).
mergeInto lands the function directly inside wasmImports under the
mangled name _js_lumbda_bend_call (auto-prefix), wired to the
js_lumbda_bend_call import. No scope guessing.

lumbda_wasm_entry.c — drops EM_JS, keeps the C wrapper bi_bend_call_wasm
and declares extern int js_lumbda_bend_call(...) so emcc emits the
env import that the library fills.

wasm/Makefile — adds --js-library c/bend-call-library.js to C_LDFLAGS
and lists the library as a dependency so changes trigger a rebuild.

Verified: deployed glue at lumbda.com now contains the mergeInto
binding (js_lumbda_bend_call:_js_lumbda_bend_call inside wasmImports).
End-to-end smoke via mocked XHR in node returns the expected
(ok pong) round-trip from the demo's first probe step.
2026-06-14 19:33:07 -04:00
7ea65dba21
parity probe: cross-tier corpus + fix python remainder + asm modulo
New: wasm/tests/parity-cross-tier.mjs runs the parity-corpus.mjs (216
test cases tagged by whitepaper section / R7RS concept) against three
tiers — native python (reference), c-wasm, asm-wasm — and fails on any
unknown divergence. Known gaps live in KNOWN_DIVERGE so the table stays
green while the bignum / call/cc / etc. work proceeds.

Wired into `make wasm-test` so a regression against any spec claim gets
caught before merge.

Bugs caught and fixed:
  - python remainder: was `signed_a % signed_b * sign(a)`, which double-
    applied the sign of a (python's % floors) — gave -3 for (-17, 5)
    instead of the R7RS-correct -2. Now uses abs() on both sides.
  - asm-wasm modulo: was i32.rem_s (truncated, remainder semantics)
    where R7RS modulo wants sign of divisor. Added the "if rem and
    divisor disagree on sign, add divisor" branch.

Cross-tier numbers after fix:
  216 passing
    3 known diverge: expt-2-100, expt-3-50, big-arith — all asm-wasm
      (no bignums on the asm tier yet; whitepaper §2.1 claim still open)
    0 fail

REPL layout: body is now the scroll container, prompt-bar is
position:fixed at the viewport bottom so it doesn't get pushed off
screen by a long transcript. Empty space above the prompt on a fresh
session reads like a terminal.

All other tests still pass: 20 unit, 8 integration, 11 functional.
2026-06-14 15:21:19 -04:00
ef8b9b5819
wat iter-5: TCO via return_call + dotted pairs + bend gpu demo
WASM tail-call instruction wired into every tail position:
  if branches, eval_begin last expression, all special-form dispatchers
  (cond/when/unless/case/let/let*/letrec/begin/and/or), function
  application's $apply, and apply's closure-body $eval_begin.
  wat2wasm + wasm-validate now use --enable-tail-call.

Reader fixes:
  - Dotted pair syntax: (a b . rest) parses as a real dotted list.
    Without this, variadic params and other dotted-cdr forms parsed
    as 4-element proper lists.
  - Stray ) at top level advances source_ptr instead of spinning
    forever. Found via bisection of functional.lsp under TCO.
  - eval_args guards against non-pair tails so a misplaced dotted
    argument (e.g. an unexpanded macro template) can't deref garbage.

Cross-tier numbers (wasm-test-functional-cross):
  before: 93/111 reached, stack overflow on (ack 3 4)
  now:    progresses through the full TCO section, deep let, named-let
          to 100k, mutual recursion to 200k. Still climbing.

Bend gpu demo:
  6th playground option ("bend (gpu dispatch) ") ships a SHAKE256
  fan-out at 1M inputs via (bend!-call ...). Run button is GUARDED:
  if no bend URL is configured, refuses with
  "set a bend URL first — this demo is GPU-only by design".
  Protects customer machines from burning minutes on a workload the
  local tiers cannot finish in reasonable time.
2026-06-14 14:18:41 -04:00
51b449a83d
wat: write primitive + cross-tier functional.lsp runner
Adds (write x) and (write-string s) primitives. write quotes strings
and #\-prefixes chars — what tests/functional.lsp's assert-equal
uses to print failures.

New target: make wasm-test-functional-cross (also rolled into wasm-test)
runs the 205-assertion tests/functional.lsp against each WASM tier and
reports pass counts:

  c-wasm:   205 / 205   (full parity with native c)
  asm-wasm:  93 / 111   reaches mid-suite before stack overflow on
                        a deeply recursive test; the 84% it reaches
                        passes. Documented progress toward full parity
                        with asm/lumbda.s.

The runner exit-soft on the asm tier — it's a measurement, not a gate.
2026-06-14 13:00:06 -04:00
3d6d5d1010
playground: free-form code option + encrypted vault, single-scroll layout
Free-form radio adds a 5th demo slot. When selected, a vault bar appears
under the controls: enter a password, "unlock" derives a per-device
vault and decrypts (or creates fresh). Edits in the editor auto-save
350ms after typing stops. Reload + same password restores the code.

Same Web Crypto stack as /repl/ (PBKDF2 + AES-GCM, vault id =
SHA-256(password || device-salt)).

Layout: one shared vertical scroller — code pane and output pane both
grow with content, the body scrolls. No more independent in-pane
scrollers fighting the page.

Home page split into "Demo" and "REPL" sections with their own CTAs.
2026-06-14 12:56:25 -04:00
d8ffab5ea6
repl: /repl/ page with encrypted multi-tab sessions
Interactive REPL at lumbda.com/repl with:
  - multi-tab sessions (click + to add, × to close, double-click to rename)
  - per-tab tier selector (python/c/asm/all-three race)
  - persistent transcripts encrypted in localStorage via Web Crypto
    (PBKDF2 + AES-GCM, vault id = SHA-256(password || device-salt) —
    same pattern as unsandbox's vault-encryption-design.md, native
    crypto.subtle API instead of CryptoJS)
  - ephemeral mode (skip vault, transcripts vanish on reload)
  - one worker per (tab × tier) — state persists across evals in a tab
  - reboot tier button (terminate this tab's worker, fresh state next eval)
  - cancel button (kills the running worker in active tab)

Home page now links to both /playground/ and /repl/.

Tier state itself does NOT persist across reloads — the transcript does,
but defines/set!/hash-tables vanish with the worker. Portal save/resume
in WAT (deferred) will let a tier session survive close+reopen.
2026-06-14 12:50:35 -04:00
1b7de2c9c6
wasm/playground: cancel button, asm state-leak fix, restyle to match homepage
User-visible changes
  - Cancel button — terminates the running worker. Pyodide's slow mandelbrot
    no longer freezes the UI; click cancel and the elapsed counter freezes
    at "(cancelled @ NNNN ms)".
  - Live ms counter ticks per animation frame while a tier is busy, so the
    Pyodide tier's ~5-15 s wait is visible instead of looking hung.
  - Restyled to match lumbda.com: chunkfive wordmark, --green: #227842
    (light) / #5ec07a (dark), lumbda-logo-green.png, lowercase "lumbda"
    everywhere. Pulls fonts/chunkfive locally so the playground stays
    self-contained.

Architecture
  - All tier evaluations now run inside a Web Worker (wasm/app/worker.mjs)
    so the main thread stays responsive. Cancel = worker.terminate(); next
    eval respawns a fresh worker.
  - Loaders use new URL("./...", import.meta.url) so paths resolve against
    the loader file's own location — works identically in window and
    worker contexts, no baseURL argument needed.
  - C tier Emscripten build flipped to EXPORT_ES6=1; loader uses dynamic
    `import()` of the factory module. Integration test updated accordingly.
  - Python loader uses `import("pyodide.mjs")` (ES module) instead of
    document.createElement, which doesn't exist in workers.

Bug fixes
  - Asm tier state leak: running the same demo twice on a cached WASM
    instance produced corrupted output (every other cell on row 2+ rendered
    as " " instead of the expected shade char). Root cause: top-level eval
    passed `global_env` as the env, so closures captured stale globals;
    fixed by passing NIL — env_lookup falls back to the CURRENT global_env
    via its existing two-pass walk. Multi-run regression added to the
    functional test suite.
  - fib-ack demo: (ack 3 4) was too heavy for Pyodide (minutes). Cut to
    (ack 3 3) + (fib 20) max so every tier finishes in seconds.

Test discipline
  - Root `make test-all` now includes `wasm-test`. Adding a language
    feature without exercising it on all six implementations is no longer
    possible by accident.
  - Functional suite: 11 assertions (was 10) — adds asm multi-run stability.
  - Integration + unit: still 20 + 8.
2026-06-14 12:13:20 -04:00
346b873247
wasm: three-tier Lumbda to WebAssembly + browser playground
Adds a parallel build of all three Lumbda implementations to WASM, a
single-page playground at www/playground/, and a verified test suite.

Tiers
  - Python: Pyodide (CPython-in-WASM) hosting lumbda.py
  - C:      Emscripten build of c/ (tree-walker + bytecode VM; jit.c
            stubbed, gc.c uses its existing no-Boehm fallback)
  - Asm:    hand-written asm/lumbda.wat — parallel impl to asm/lumbda.s.
            Reader, eval (lambda/define/if/cond/let/and/or/quote/set!),
            recursion across mutated top-level env, bump allocator with
            memory.grow, 24 primitives. ~1200 lines of raw WAT.

SPA (wasm/app/, deployed to www/playground/)
  - CodeMirror 6 editor (Scheme highlighting) on left, output on right
  - Radios: 4 demos (Mandelbrot, Fib+Ack, Sieve, self-interp meta-eval)
            x 4 tiers (Python | C | Asm | All three)
  - All-three mode renders the three tier outputs side by side with
    per-tier elapsed timing

Tests (38 verified assertions)
  - 20 unit (Node): per-tier module loads, eval smoke
  - 8 integration (Node): each demo on c+asm WASM byte-matches the
                          canonical native Python run
  - 10 functional (Playwright headless Chromium): page mounts, every
                          demo runs on every tier, all-three renders

Makefile
  - Root targets: wasm-build, wasm-test, wasm-test-fn, wasm-serve,
                  wasm-deploy, wasm-clean
  - wasm/Makefile orchestrates the three tier builds; deploy copies
    dist/ into www/playground/

Asm tier notes
  - WAT linear symbol intern + linear env lookup is MOAD-0001 at scale;
    documented in the asm/lumbda.wat header and in the SPA footer. The
    demos hit ~30 globals so the linear walks are cheap enough.
  - Bump allocator never frees (matches asm/lumbda.s heap discipline);
    memory.grow expands by 1 MB chunks. Browser tab tears down at unload.

Toolchain (developer prerequisites)
  - Emscripten 6.0.0 via emsdk at ~/git/emsdk
  - wabt 1.0.36 at ~/git/wabt
  - Playwright for functional tests (symlinked from ~/git/agnt)
2026-06-14 11:40:34 -04:00