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)
110 lines
3.6 KiB
C
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);
|
|
}
|