From b890e3641fd69ef0a79a5afad28492cf2ededb9b Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Thu, 4 Jun 2026 19:55:48 -0400 Subject: [PATCH] C tier: spawn-process-stdio + flush-port for bend cross-tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new primitives in builtins.c, paralleling the Python tier shipped in the previous commit. gpu-worker.lsp now runs on the C tier byte-identically to the Python tier. (spawn-process-stdio path args) → (stdin-port . stdout-port) fork + pipe + execvp; child's stdin & stdout wired back to parent as line-buffered FILE* ports. Accepts both strings and symbols in the args list (matches Python tier's permissive conversion). (flush-port port) fflush() on the port's FILE*. No-op when fp is null. End-to-end on 3090-ai with C-tier lumbda everywhere: shell A: ./lumbda /tmp/launch-c.lsp → gpu-worker: ready cuda-shake-fanout ← ./shake256-fanout → gpu-worker listening on port 9091 shell B: ./lumbda smoke-bend.lsp # run 3× === smoke-bend === 1. cost estimator picks local for 3 inputs: OK 2. worker available? #t 3. bend! (cuda-shake-fanout '("00" "01" "deadbeef") 32): (b8d01df855… 94da6280b2… fa094fa86e…) Three runs identical bytes. Same hashes as Python tier. Same hashes as hashlib.shake_256 host reference. Cross-tier matrix (proves wire protocol is tier-agnostic): client tier worker tier status ───────────────────────────────────── C tier C tier PASS — 2 sequential runs, byte-identical Python tier C tier PASS — same hashes C tier Python tier implicit by symmetry (same wire bytes both directions; Python-server tested against Python-client in prior commit) Per-tier status after this commit: Python tier ✓ end-to-end C tier ✓ end-to-end + cross-tier byte-identical to Python tier asm tier → still needs spawn-process-stdio via raw fork+pipe+ execve syscalls. Scheme files unchanged. --- c/builtins.c | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/c/builtins.c b/c/builtins.c index 8176c1c..28f7778 100644 --- a/c/builtins.c +++ b/c/builtins.c @@ -1223,6 +1223,86 @@ static Value bi_tcp_close(Value *a, int n, Env *e) { return VAL_VOID; } +#include +/* spawn-process-stdio: fork+exec a child with its stdin & stdout piped + * back to us; return (stdin-port . stdout-port). Lets gpu-worker.lsp + * (and any other lumbda code) hold a long-lived subprocess across many + * request cycles without spawning per request. + * + * (spawn-process-stdio "path/to/binary" '("arg1" "arg2")) + * ⇒ (# . #) + */ +static Value bi_spawn_process_stdio(Value *a, int n, Env *e) { + (void)e; CHECK_ARITY("spawn-process-stdio", 2); check_string(a[0]); + const char *path = AS_STRING(a[0])->data; + + /* count args */ + int argc = 1; + Value lst = a[1]; + while (IS_PAIR(lst)) { argc++; lst = CDR(lst); } + char **argv = (char**)ul_malloc(sizeof(char*) * (argc + 1)); + argv[0] = (char*)path; + int i = 1; lst = a[1]; + while (IS_PAIR(lst)) { + Value v = CAR(lst); + if (IS_STRING(v)) { + argv[i++] = AS_STRING(v)->data; + } else if (IS_SYM(v)) { + argv[i++] = (char*)sym_name(v); + } else { + ul_free(argv); + lisp_error("spawn-process-stdio: arg must be string or symbol"); + } + lst = CDR(lst); + } + argv[argc] = NULL; + + int in_pipe[2]; /* parent writes → child reads (child's stdin) */ + int out_pipe[2]; /* child writes → parent reads (child's stdout)*/ + if (pipe(in_pipe) < 0 || pipe(out_pipe) < 0) { + ul_free(argv); return VAL_FALSE; + } + pid_t pid = fork(); + if (pid < 0) { + close(in_pipe[0]); close(in_pipe[1]); + close(out_pipe[0]); close(out_pipe[1]); + ul_free(argv); return VAL_FALSE; + } + if (pid == 0) { + /* child */ + dup2(in_pipe[0], STDIN_FILENO); + dup2(out_pipe[1], STDOUT_FILENO); + close(in_pipe[0]); close(in_pipe[1]); + close(out_pipe[0]); close(out_pipe[1]); + execvp(path, argv); + _exit(127); + } + /* parent */ + close(in_pipe[0]); + close(out_pipe[1]); + ul_free(argv); + + /* line-buffer the parent's write end so daemon sees newlines promptly */ + FILE *win = fdopen(in_pipe[1], "w"); + FILE *rout = fdopen(out_pipe[0], "r"); + if (!win || !rout) { + if (win) fclose(win); else close(in_pipe[1]); + if (rout) fclose(rout); else close(out_pipe[0]); + return VAL_FALSE; + } + setvbuf(win, NULL, _IOLBF, 0); + Value pin = make_file_port(win, PORT_OUTPUT); + Value pout = make_file_port(rout, PORT_INPUT); + return cons(pin, pout); +} + +static Value bi_flush_port(Value *a, int n, Env *e) { + (void)e; CHECK_ARITY("flush-port", 1); + ULPort *p = AS_PORT(a[0]); + if (p->fp) fflush(p->fp); + return VAL_VOID; +} + /* heap-snapshot / heap-restore: asm-only arena primitives. The asm impl * has no GC; these let a server rewind its bump allocator between * requests. Python + C have real GCs — no-ops here so portable .lsp @@ -1906,6 +1986,8 @@ Env *make_global_env(void) { DEF("tcp-recv", bi_tcp_recv); DEF("tcp-send", bi_tcp_send); DEF("tcp-close", bi_tcp_close); + DEF("spawn-process-stdio", bi_spawn_process_stdio); + DEF("flush-port", bi_flush_port); DEF("heap-snapshot", bi_heap_snapshot); DEF("heap-restore", bi_heap_restore); DEF("current-time-ms", bi_current_time_ms);