C tier: spawn-process-stdio + flush-port for bend cross-tier

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.
This commit is contained in:
russell@unturf.com 2026-06-04 19:55:48 -04:00
parent 494ae3193c
commit b890e3641f
No known key found for this signature in database

View file

@ -1223,6 +1223,86 @@ static Value bi_tcp_close(Value *a, int n, Env *e) {
return VAL_VOID;
}
#include <sys/wait.h>
/* 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"))
* (#<port> . #<port>)
*/
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);