# ═══════════════════════════════════════════════════════════════════ # wasm/Makefile — three-tier Lumbda to WebAssembly # ═══════════════════════════════════════════════════════════════════ # # Builds: # python/ — Pyodide loader + lumbda.py (CPython-in-WASM) # c/ — Emscripten build of c/ (tree-walker + bytecode VM, no JIT) # asm/ — Hand-written WAT compiled to wasm (parallel "asm" tier) # app/ — Single-page app shell (HTML + CodeMirror + JS glue) # # Targets: # make build all three tiers + SPA bundle in dist/ # make test unit (node) + integration (node, cross-tier diff) # make test-fn functional (playwright headless browser) # make serve local dev server on :8080 # make deploy copy dist/ -> ../www/playground/ # make clean EMSDK_DIR := $(HOME)/git/emsdk EMSDK_ENV := $(EMSDK_DIR)/emsdk_env.sh EMCC := $(EMSDK_DIR)/upstream/emscripten/emcc WAT2WASM := $(HOME)/git/wabt/bin/wat2wasm WASM_VALIDATE := $(HOME)/git/wabt/bin/wasm-validate REPO_ROOT := $(abspath ..) C_SRC_DIR := $(REPO_ROOT)/c PY_SRC := $(REPO_ROOT)/lumbda.py STDLIB := $(REPO_ROOT)/stdlib.lsp DIST := dist # Pyodide pinned version PYODIDE_VER := 0.27.2 PYODIDE_CDN := https://cdn.jsdelivr.net/pyodide/v$(PYODIDE_VER)/full # ─── Top-level ───────────────────────────────────────────────────── .PHONY: all build test test-unit test-integration test-fn serve deploy clean \ python c asm app dist-bundle check-tools all: build build: check-tools python c asm app dist-bundle check-tools: @test -x $(EMCC) || { echo "emcc missing at $(EMCC)"; exit 1; } @test -x $(WAT2WASM) || { echo "wat2wasm missing at $(WAT2WASM)"; exit 1; } @command -v node >/dev/null || { echo "node missing"; exit 1; } # ─── Python tier (Pyodide) ───────────────────────────────────────── # # Pyodide loads CPython into WASM. We ship a small JS loader that: # 1. Imports Pyodide from CDN (pinned version) # 2. Drops lumbda.py + stdlib.lsp into Pyodide's virtual FS # 3. Exposes evalLisp(src) -> string python: $(DIST)/python/lumbda-py.js $(DIST)/python/lumbda.py $(DIST)/python/stdlib.lsp $(DIST)/python/lumbda-py.js: python/lumbda-py.js @mkdir -p $(@D) @cp $< $@ $(DIST)/python/lumbda.py: $(PY_SRC) @mkdir -p $(@D) @cp $< $@ $(DIST)/python/stdlib.lsp: $(STDLIB) @mkdir -p $(@D) @cp $< $@ # ─── C tier (Emscripten) ─────────────────────────────────────────── # # Build the C tier under emcc. Drops: # * jit.c — x86_64 machine-code emitter, no WASM equivalent # * gc.c — Boehm-specific tracing (replaced with malloc-no-free) # # Keeps tree-walker + bytecode VM + reader + printer + portal. # # -DLUMBDA_WASM gates conditional code paths in main.c / eval.c. C_SOURCES := $(addprefix $(C_SRC_DIR)/, \ reader.c printer.c eval.c vm.c types.c builtins.c bignum.c portal.c gc.c) # jit.c is included but g_jit_enabled stays false at runtime so emit paths # are never executed. mmap PROT_EXEC would fail in WASM anyway; harmless. C_JIT_STUB := c/jit-stub.c # bdwgc integration attempt — DEFERRED. See c/lumbda_wasm_entry.c # header comment for the full status: bdwgc builds cleanly with emcc # via CMake, but option-1 (binaryen --spill-pointers) crashes in our # emsdk version and option-2 (GC_disable + manual gcollect at safe # points) compiles but triggers a runtime fault on the first eval. # Likely a missed root somewhere in the eval call chain. Leaving the # pieces in place (BDWGC_DIR check + commented include) so the next # session can iterate without rebuilding bdwgc. C_CFLAGS := -O2 -DLUMBDA_WASM -std=c11 \ -D_POSIX_C_SOURCE=200809L -D_GNU_SOURCE \ -I$(C_SRC_DIR) -Wno-everything C_LDFLAGS := -s WASM=1 -s MODULARIZE=1 -s EXPORT_ES6=1 \ -s EXPORT_NAME=createLumbdaC \ -s EXPORTED_FUNCTIONS='["_lumbda_wasm_init","_lumbda_wasm_eval","_lumbda_wasm_free_result","_malloc","_free"]' \ -s EXPORTED_RUNTIME_METHODS='["cwrap","ccall","UTF8ToString","stringToUTF8","lengthBytesUTF8","FS"]' \ -s ALLOW_MEMORY_GROWTH=1 -s INITIAL_MEMORY=32MB \ -s STACK_SIZE=8MB \ -s ENVIRONMENT=web,worker,node \ -s SINGLE_FILE=0 \ --js-library c/bend-call-library.js c: $(DIST)/c/lumbda-c.js $(DIST)/c/lumbda-c.wasm $(DIST)/c/lumbda-c.loader.js $(DIST)/c/lumbda-c.js $(DIST)/c/lumbda-c.wasm: $(C_SOURCES) $(C_JIT_STUB) c/lumbda_wasm_entry.c c/bend-call-library.js @mkdir -p $(@D) EMSDK=$(EMSDK_DIR) EMSDK_NODE=$(EMSDK_DIR)/node/22.16.0_64bit/bin/node \ PATH=$(EMSDK_DIR)/node/22.16.0_64bit/bin:$(EMSDK_DIR)/upstream/emscripten:$$PATH \ $(EMCC) $(C_CFLAGS) $(C_SOURCES) $(C_JIT_STUB) c/lumbda_wasm_entry.c \ $(C_LDFLAGS) -o $(DIST)/c/lumbda-c.js $(DIST)/c/lumbda-c.loader.js: c/lumbda-c.loader.js @mkdir -p $(@D) @cp $< $@ # ─── Asm tier (hand-written WAT) ─────────────────────────────────── # # asm/lumbda.wat is a Lisp interpreter written directly in WebAssembly # Text format. Stack-machine assembly, no libc, raw linear memory. # Parallel implementation to asm/lumbda.s (x86_64). Same tier semantics. asm: $(DIST)/asm/lumbda-asm.wasm $(DIST)/asm/lumbda-asm.loader.js $(DIST)/asm/lumbda-asm.wasm: asm/lumbda.wat @mkdir -p $(@D) $(WAT2WASM) --enable-tail-call $< -o $@ $(WASM_VALIDATE) --enable-tail-call $@ $(DIST)/asm/lumbda-asm.loader.js: asm/lumbda-asm.loader.js @mkdir -p $(@D) @cp $< $@ # ─── SPA shell ───────────────────────────────────────────────────── APP_SRC := $(wildcard app/*.html app/*.css app/*.js app/*.mjs app/demos/*.lsp) app: $(DIST)/index.html $(DIST)/style.css $(DIST)/app.js $(DIST)/runner.js $(DIST)/worker.mjs \ $(DIST)/crypto.js \ $(DIST)/lumbda-logo-green.png $(DIST)/fonts/chunkfive/chunkfive-regular-webfont.woff2 \ $(DIST)/fonts/chunkfive/chunkfive-regular-webfont.woff $(DIST)/%: app/% @mkdir -p $(@D) @cp $< $@ dist-bundle: app @mkdir -p $(DIST)/demos @cp app/demos/*.lsp $(DIST)/demos/ 2>/dev/null || true # ─── REPL bundle (separate page sharing tier assets) ─────────────── # The REPL deploys to www/repl/ and shares /playground/'s tier loaders # via copied symlinks so we don't ship the WASM bytes twice. REPL_DIST := $(DIST)-repl repl: build @mkdir -p $(REPL_DIST)/c $(REPL_DIST)/asm $(REPL_DIST)/python $(REPL_DIST)/fonts/chunkfive @cp $(DIST)/c/lumbda-c.js $(REPL_DIST)/c/ @cp $(DIST)/c/lumbda-c.wasm $(REPL_DIST)/c/ @cp $(DIST)/c/lumbda-c.loader.js $(REPL_DIST)/c/ @cp $(DIST)/asm/lumbda-asm.wasm $(REPL_DIST)/asm/ @cp $(DIST)/asm/lumbda-asm.loader.js $(REPL_DIST)/asm/ @cp $(DIST)/python/lumbda.py $(REPL_DIST)/python/ @cp $(DIST)/python/stdlib.lsp $(REPL_DIST)/python/ @cp $(DIST)/python/lumbda-py.js $(REPL_DIST)/python/ @cp $(DIST)/runner.js $(REPL_DIST)/ @cp $(DIST)/worker.mjs $(REPL_DIST)/ @cp repl/index.html repl/repl.css repl/repl.js repl/crypto.js $(REPL_DIST)/ @cp $(DIST)/style.css $(REPL_DIST)/style.css @cp $(DIST)/lumbda-logo-green.png $(REPL_DIST)/lumbda-logo-green.png @cp $(DIST)/fonts/chunkfive/*.woff* $(REPL_DIST)/fonts/chunkfive/ # ─── Tests ───────────────────────────────────────────────────────── test: test-unit test-integration test-parity test-functional-cross test-unit: build @echo "── wasm unit tests ──" node tests/unit.mjs test-integration: build @echo "── wasm integration tests (cross-tier diff) ──" node tests/integration.mjs # Cross-tier parity probe — every expression in parity-corpus.mjs runs on # native python (reference), c-wasm, and asm-wasm. Fails on any unknown # divergence so a regression against whitepaper § normative claims gets # caught before merge. Known gaps (bignums on asm-wasm today) live in # KNOWN_DIVERGE; trim that list as the gaps close. test-parity: build @echo "── wasm cross-tier parity probe ──" node tests/parity-cross-tier.mjs # Cross-tier functional.lsp runner. C-wasm reaches full parity (205/205); # asm-wasm passes a documented growing subset and is exit-soft. test-functional-cross: build @echo "── wasm cross-tier functional.lsp ──" node tests/functional-cross.mjs test-fn: build @echo "── wasm functional tests (headless browser) ──" @test -d node_modules/playwright || { \ echo "SKIP: node_modules/playwright not present (symlink or npm install)"; exit 0; } node tests/functional.mjs # ─── Dev server ──────────────────────────────────────────────────── serve: build @echo "Serving $(DIST) on http://localhost:8080" @cd $(DIST) && python3 -m http.server 8080 # REPL dev server with the COOP/COEP headers SharedArrayBuffer needs. # Pause-and-snapshot relies on a SAB-backed atomic flag the C tier's # eval loop polls — the browser refuses to create a SAB without # cross-origin-isolation headers in the response. serve-repl: repl @python3 serve-coop.py 8090 $(REPL_DIST) # ─── Deploy ──────────────────────────────────────────────────────── deploy: build repl @mkdir -p $(REPO_ROOT)/www/playground $(REPO_ROOT)/www/repl @cp -r $(DIST)/* $(REPO_ROOT)/www/playground/ @cp -r $(REPL_DIST)/* $(REPO_ROOT)/www/repl/ @echo "Deployed to www/playground/ and www/repl/" # ─── Clean ───────────────────────────────────────────────────────── clean: rm -rf $(DIST)