bend: wire bend!-call into the Emscripten C tier — playground c ⚡

lumbda_wasm_entry.c — adds an EM_JS bridge js_lumbda_bend_call that
does sync XHR POST (legal inside Web Workers, the playground's tier
host) and writes the response bytes back into the wasm heap. C
wrapper bi_bend_call_wasm allocates a 256 KiB response buffer, calls
the EM_JS function, wraps the bytes as a lumbda string Value, and
gets registered as the `bend!-call` builtin in lumbda_wasm_init —
not in c/builtins.c, so the native CLI build doesn't acquire a
wasm-flavored binding it can't satisfy.

lumbda-c.loader.js — setBendUrl(url) mutates globalThis._lumbdaCBendUrl
which the EM_JS reads on every call. runner.js plumbing already
propagates a saved playground URL to every tier's setBendUrl on
each save.

C tier now matches asm + pyodide for HTTP-mode bend. End-to-end
verified in node + wasm directly: bend!-call returns the configured
URL response text; "no bend URL configured" when unset; clean
"bend error: ..." string when the XHR throws (e.g., CORS, network).
Tested against the live bend.unturf.com chain via the playground.

Three tiers in parallel now show the same (ok (HEX0 HEX1 HEX2))
result from (cuda-shake-fanout ("00" "01" "deadbeef") 32).
This commit is contained in:
russell@unturf.com 2026-06-14 19:04:52 -04:00
parent d672ab077e
commit 1458ebf77a
No known key found for this signature in database
11 changed files with 106 additions and 5 deletions

View file

@ -27,7 +27,13 @@ async function _bootstrap() {
_init();
// bend!-call host bridge — js_lumbda_bend_call inside the wasm
// (EM_JS in lumbda_wasm_entry.c) reads globalThis._lumbdaCBendUrl
// for the destination, then does sync XHR POST and writes the
// response bytes back into the wasm heap. setBendUrl mutates that
// global so runner.js can propagate a saved playground URL.
return {
setBendUrl(url) { globalThis._lumbdaCBendUrl = url || null; },
async evalLisp(src) {
outBuf = [];
errBuf = [];

View file

@ -14,8 +14,23 @@
*
* Real fixes, in order of decreasing cost:
* 1. Build bdwgc (github.com/ivmai/bdwgc) with emcc and link the
* WASM build with USE_BOEHM_GC defined. Their build system has
* a single-threaded mode that would suit our worker context.
* WASM build with USE_BOEHM_GC defined. ATTEMPTED — bdwgc
* itself compiles cleanly via `emcmake cmake .. && emmake make`
* and links into lumbda-c.js. Two blockers stopped this
* session: (a) bdwgc's docs recommend
* `-sBINARYEN_EXTRA_PASSES=--spill-pointers` so the conservative
* collector can find stack roots; the binaryen pass crashes in
* our emsdk 6.0.0 with `Fatal: getStackSpace: failed to find
* the stack pointer`. (b) Without spill-pointers, bdwgc's
* option 2 (GC_disable at init + manual GC_enable/GC_gcollect/
* GC_disable at safe points between top-level evals) compiles
* but throws `remainder by zero` on the first eval — almost
* certainly a missed conservative root somewhere in the eval
* call chain that bdwgc swept while it was still live. Forward
* path: update emsdk to a version where spill-pointers works,
* OR walk lumbda's struct types in types.c and either provide
* GC_PUSH-style explicit-root hooks or write per-type mark
* callbacks via GC_new_kind.
* 2. Write a small mark-sweep over the existing NaN-boxed heap.
* The asm WAT tier already does this with a Cheney copying GC
* (lumbda_gc, gc_collect in asm/lumbda.wat). Same algorithm
@ -42,9 +57,66 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <emscripten.h>
static Env *g_wasm_env = NULL;
/* bend!-call — dispatch a payload string to the configured gpu-worker
* over sync XHR (legal in Web Workers, where this tier runs in the
* playground). The JS side reads `globalThis._lumbdaCBendUrl` for the
* destination; lumbda-c.loader.js's setBendUrl(url) mutates that, then
* runner.js propagates a saved playground bend-url to every tier.
*
* Layout mirrors the asm tier's bend_call import: payload bytes are
* passed by ptr+len, response is written into a heap-allocated buffer,
* length is returned. We then wrap the bytes as a lumbda string Value.
*
* The 256 KiB cap matches a typical (cuda-shake-fanout (...) 32) round
* trip plus headroom; larger responses get truncated rather than
* crashing the tier. Phase 2 (factory recipes) will replace large
* payloads with server-side bin compilation anyway. */
#define BEND_RESP_CAP (256 * 1024)
EM_JS(int, js_lumbda_bend_call, (const char *payload_ptr, int payload_len,
char *resp_buf, int resp_cap), {
var bendUrl = globalThis._lumbdaCBendUrl;
if (!bendUrl) {
var msg = "no bend URL configured";
var n = Math.min(msg.length, resp_cap - 1);
for (var i = 0; i < n; i++) HEAPU8[resp_buf + i] = msg.charCodeAt(i);
return n;
}
try {
var payload = UTF8ToString(payload_ptr, payload_len);
var xhr = new XMLHttpRequest();
xhr.open("POST", bendUrl, false); /* sync */
xhr.setRequestHeader("Content-Type", "text/plain; charset=utf-8");
xhr.send(payload);
var resp = xhr.responseText || "";
var bytes = new TextEncoder().encode(resp);
var n = Math.min(bytes.length, resp_cap);
HEAPU8.set(bytes.subarray(0, n), resp_buf);
return n;
} catch (e) {
var err = "bend error: " + (e.message || String(e));
var n = Math.min(err.length, resp_cap - 1);
for (var i = 0; i < n; i++) HEAPU8[resp_buf + i] = err.charCodeAt(i);
return n;
}
});
static Value bi_bend_call_wasm(Value *a, int n, Env *e) {
(void)e;
if (n < 1 || !IS_STRING(a[0])) return VAL_FALSE;
ULString *s = AS_STRING(a[0]);
char *resp = (char *)malloc(BEND_RESP_CAP);
if (!resp) return VAL_FALSE;
int resp_len = js_lumbda_bend_call(s->data, (int)s->len, resp, BEND_RESP_CAP);
Value out = make_string(resp, (size_t)resp_len, false);
free(resp);
return out;
}
static const char *MINI_STDLIB =
"(define (caar p) (car (car p)))\n"
"(define (cadr p) (car (cdr p)))\n"
@ -93,6 +165,11 @@ void lumbda_wasm_init(void) {
for (int i = 0; i < count; i++) leval(exprs[i], g_wasm_env);
}
ul_free(exprs);
/* Register bend!-call binding to the host JS XHR bridge above.
* Done here (not in c/builtins.c) so the native build doesn't
* acquire a WASM-flavored binding it can't satisfy. */
env_define(g_wasm_env, intern("bend!-call"), VAL_BUILTIN(bi_bend_call_wasm));
}
/* Eval src. Output during eval goes to stdout (captured by Module.print

File diff suppressed because one or more lines are too long

View file

@ -27,7 +27,13 @@ async function _bootstrap() {
_init();
// bend!-call host bridge — js_lumbda_bend_call inside the wasm
// (EM_JS in lumbda_wasm_entry.c) reads globalThis._lumbdaCBendUrl
// for the destination, then does sync XHR POST and writes the
// response bytes back into the wasm heap. setBendUrl mutates that
// global so runner.js can propagate a saved playground URL.
return {
setBendUrl(url) { globalThis._lumbdaCBendUrl = url || null; },
async evalLisp(src) {
outBuf = [];
errBuf = [];

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -27,7 +27,13 @@ async function _bootstrap() {
_init();
// bend!-call host bridge — js_lumbda_bend_call inside the wasm
// (EM_JS in lumbda_wasm_entry.c) reads globalThis._lumbdaCBendUrl
// for the destination, then does sync XHR POST and writes the
// response bytes back into the wasm heap. setBendUrl mutates that
// global so runner.js can propagate a saved playground URL.
return {
setBendUrl(url) { globalThis._lumbdaCBendUrl = url || null; },
async evalLisp(src) {
outBuf = [];
errBuf = [];

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -27,7 +27,13 @@ async function _bootstrap() {
_init();
// bend!-call host bridge — js_lumbda_bend_call inside the wasm
// (EM_JS in lumbda_wasm_entry.c) reads globalThis._lumbdaCBendUrl
// for the destination, then does sync XHR POST and writes the
// response bytes back into the wasm heap. setBendUrl mutates that
// global so runner.js can propagate a saved playground URL.
return {
setBendUrl(url) { globalThis._lumbdaCBendUrl = url || null; },
async evalLisp(src) {
outBuf = [];
errBuf = [];

Binary file not shown.