// wasm/python/lumbda-py.js // Python tier loader — Pyodide (CPython-in-WASM) hosting lumbda.py. // // ES module form so it works in both window and Web Worker contexts. // Exports createPythonTier() -> Promise<{ evalLisp(src) -> Promise, // setBendUrl(url) -> void, // heapStats() -> {used,total} }>. const PYODIDE_VERSION = "0.27.2"; const PYODIDE_INDEX_URL = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`; async function _bootstrap() { // Dynamic ES-module import works in both window and Worker (module type) // contexts. The CDN ships pyodide.mjs alongside pyodide.js. const { loadPyodide } = await import(PYODIDE_INDEX_URL + "pyodide.mjs"); const pyodide = await loadPyodide({ indexURL: PYODIDE_INDEX_URL }); // Pull lumbda.py + stdlib.lsp into Pyodide's virtual FS. Paths resolve // relative to THIS loader (under python/) for both window and Worker. const pyURL = new URL("./lumbda.py", import.meta.url).href; const stdlibURL = new URL("./stdlib.lsp", import.meta.url).href; const lumbdaSrc = await (await fetch(pyURL)).text(); const stdlibSrc = await (await fetch(stdlibURL)).text(); pyodide.FS.writeFile("/home/pyodide/lumbda.py", lumbdaSrc); pyodide.FS.writeFile("/home/pyodide/stdlib.lsp", stdlibSrc); // bend dispatch closure. Browser can't open raw TCP, so we POST // the S-expression payload to the configured worker URL and read // the response text back. Sync XHR is the only sync HTTP available // in a Web Worker; perfect for the blocking eval model lumbda // primitives expect. const refs = { bendUrl: null }; globalThis._lumbdaPyBendCall = (payload) => { if (!refs.bendUrl) return "no bend URL configured"; try { const xhr = new XMLHttpRequest(); xhr.open("POST", refs.bendUrl, false); // sync xhr.setRequestHeader("Content-Type", "text/plain; charset=utf-8"); xhr.send(payload); return xhr.responseText || ""; } catch (e) { return `bend error: ${e.message || String(e)}`; } }; await pyodide.runPythonAsync(` import sys, io sys.path.insert(0, "/home/pyodide") import lumbda _env = lumbda.make_global_env() for _e in lumbda.read_all(lumbda.PRELUDE): lumbda.leval(_e, _env) # bend!-call primitive — bridges to JS XHR via globalThis._lumbdaPyBendCall. # Argument: a string payload (the S-expression text). Returns response text. from js import _lumbdaPyBendCall as _js_bend_call def _bend_call_prim(args, env): if not args: return "" payload = args[0] if not isinstance(payload, str): payload = lumbda.show(payload) return str(_js_bend_call(payload)) _env.define(lumbda.S('bend!-call'), _bend_call_prim) def _lumbda_eval(src): buf = io.StringIO() old = sys.stdout sys.stdout = buf last = None try: for e in lumbda.read_all(src): last = lumbda.leval(e, _env) except Exception as ex: sys.stdout = old return f"{buf.getvalue()}error: {ex}" sys.stdout = old out = buf.getvalue() if last is not None and not isinstance(last, lumbda._Void): rep = lumbda.show(last) if out and not out.endswith("\\n"): out += "\\n" out += rep return out `); return { async evalLisp(src) { pyodide.globals.set("_src_in", src); return await pyodide.runPythonAsync("_lumbda_eval(_src_in)"); }, setBendUrl(url) { refs.bendUrl = url || null; }, heapStats() { // Pyodide's runtime memory is the Emscripten linear memory. // CPython's GC reclaims behind the scenes, so this number // rises and falls naturally as objects die. const total = pyodide._module.HEAPU8.byteLength; return { used: total, total }; }, }; } // Closure-encapsulated singleton (see lumbda-c.loader.js for rationale). export const createPythonTier = (() => { let tier = null; return async () => { if (!tier) tier = await _bootstrap(); return tier; }; })();