lumbda/wasm/c/lumbda_wasm_entry.c
russell@unturf.com 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

110 lines
3.6 KiB
C

/* lumbda_wasm_entry.c — Emscripten entry points for the C tier.
*
* The JS loader provides Module.print / Module.printErr callbacks that
* Emscripten routes stdout/stderr through, so output capture happens on
* the JS side. We only expose:
*
* lumbda_wasm_init() — set up symbols + env + stdlib
* lumbda_wasm_eval(src) — eval src, last value printed if non-void
* lumbda_wasm_free_result(p) — free a string returned to JS
*
* The env is module-global so successive eval calls preserve defines.
*/
#include "lumbda.h"
#include "jit.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static Env *g_wasm_env = NULL;
static const char *MINI_STDLIB =
"(define (caar p) (car (car p)))\n"
"(define (cadr p) (car (cdr p)))\n"
"(define (cdar p) (cdr (car p)))\n"
"(define (cddr p) (cdr (cdr p)))\n"
"(define (caddr p) (car (cdr (cdr p))))\n"
"(define (cadddr p) (car (cdr (cdr (cdr p)))))\n"
"(define (list . xs) xs)\n"
"(define (not x) (if x #f #t))\n"
"(define (map f xs) (if (null? xs) '() (cons (f (car xs)) (map f (cdr xs)))))\n"
"(define (length xs) (if (null? xs) 0 (+ 1 (length (cdr xs)))))\n"
"(define (reverse xs) (if (null? xs) '() (append (reverse (cdr xs)) (list (car xs)))))\n"
"(define (append a b) (if (null? a) b (cons (car a) (append (cdr a) b))))\n"
"(define (zero? n) (= n 0))\n"
"(define (positive? n) (> n 0))\n"
"(define (negative? n) (< n 0))\n"
"(define (abs n) (if (< n 0) (- 0 n) n))\n";
void lumbda_wasm_init(void) {
if (g_wasm_env) return;
init_symbols();
g_wasm_env = make_global_env();
/* Load PRELUDE (built into eval/builtins via make_global_env). */
int count = 0;
Value *exprs = read_all(PRELUDE, &count, false);
for (int i = 0; i < count; i++) leval(exprs[i], g_wasm_env);
ul_free(exprs);
/* Load a small stdlib subset embedded above. Full stdlib.lsp is too
* large to embed cleanly; demos only need the listed primitives. */
count = 0;
exprs = read_all(MINI_STDLIB, &count, false);
ErrorContext ctx_local;
ctx_local.call_stack_depth = 0;
ctx_local.error_obj = VAL_NIL;
ctx_local.source_line = 0;
g_error_ctx = &ctx_local;
if (setjmp(ctx_local.jmp) == 0) {
for (int i = 0; i < count; i++) leval(exprs[i], g_wasm_env);
}
ul_free(exprs);
}
/* Eval src. Output during eval goes to stdout (captured by Module.print
* on the JS side). The final non-void value's repr is appended to stdout
* via printf("%s\n", ...).
*
* Returns NULL on success, or a malloc'd error message on failure. JS
* frees via lumbda_wasm_free_result.
*/
char *lumbda_wasm_eval(const char *src) {
if (!g_wasm_env) lumbda_wasm_init();
ErrorContext ctx_local;
ctx_local.call_stack_depth = 0;
ctx_local.error_obj = VAL_NIL;
ctx_local.source_line = 0;
g_error_ctx = &ctx_local;
if (setjmp(ctx_local.jmp) != 0) {
const char *msg = ctx_local.message[0] ? ctx_local.message : "unknown error";
size_t n = strlen(msg) + 16;
char *err = (char *)malloc(n);
snprintf(err, n, "error: %s", msg);
return err;
}
int count = 0;
Value *exprs = read_all(src, &count, false);
Value last = VAL_VOID;
for (int i = 0; i < count; i++) {
last = leval(exprs[i], g_wasm_env);
}
ul_free(exprs);
if (!IS_VOID(last)) {
char *rep = show(last, false);
printf("%s\n", rep);
fflush(stdout);
ul_free(rep);
} else {
fflush(stdout);
}
return NULL;
}
void lumbda_wasm_free_result(char *p) {
if (p) free(p);
}