Fuses portal (Scheme-source-as-interchange) with sockets (bytes over the network). Wire protocol: one S-expression per connection. Same server + client .lsp runs byte-identically in Python, C, and asm. New primitives in all three impls: - read-from-string — parse one sexp from a string Asm gets two more: - symbol->string — standard R7RS, was missing - eval — evaluate a Scheme value in the global env (Python + C had it as a special form; asm exposes it as a builtin) examples/rpc-server.lsp (port 9080): - Whitelisted dispatch: ping / add / mul / fib / echo - Never calls eval on client input; safe by construction - Uses heap-snapshot/restore for O(1) memory on asm - ~90 lines, portable examples/rpc-client.lsp: - Sends one request, reads one response, displays both - 45 lines, portable examples/repl-server.lsp (port 9081): - DANGER: full remote eval. Any Scheme form accepted and evaluated in the server's global env. Persistent across connections. - Deliberately does NOT use heap-snapshot — remote (define x ...) lives in the global env above any snapshot point; rewinding would invalidate the new binding. The ulimit -v 512 MB safety net (documented in CLAUDE.md) ensures an escaped process can't crash the machine. - ~70 lines, portable. Demonstrates what "the language IS the interchange format" gets you at the limit: a single socket and a single primitive (eval) carry a full-powered REPL. Verified 3×3 server×client matrix: all 9 combinations green. All 132 asm + 571 py + 189 shared + 83 c tests still pass. One quirk discovered and worked around: in asm, a closure captures its env chain by pointer at define time. Forward-referenced names in mutually-recursive toplevel defines can fail under specific heap-restore patterns — see the leaf-first ordering note in rpc-server.lsp.
46 KiB
Executable file
46 KiB
Executable file