S-expressions over sockets — RPC + remote REPL in portable Scheme
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.
This commit is contained in:
parent
fb41b73274
commit
574ddc50d7
8 changed files with 370 additions and 1 deletions
BIN
asm/uncommonlisp
BIN
asm/uncommonlisp
Binary file not shown.
Binary file not shown.
|
|
@ -165,7 +165,10 @@
|
|||
.equ BI_HEAPSNAP, 85
|
||||
.equ BI_HEAPREST, 86
|
||||
.equ BI_CURTIME, 87
|
||||
.equ BI_COUNT, 88
|
||||
.equ BI_READSTR, 88
|
||||
.equ BI_EVAL, 89
|
||||
.equ BI_SYMTOSTR, 90
|
||||
.equ BI_COUNT, 91
|
||||
|
||||
# ============================================================
|
||||
.data
|
||||
|
|
@ -279,6 +282,9 @@ bn_tcpclose: .byte 9; .ascii "tcp-close"
|
|||
bn_heapsnap: .byte 13; .ascii "heap-snapshot"
|
||||
bn_heaprest: .byte 12; .ascii "heap-restore"
|
||||
bn_curtime: .byte 15; .ascii "current-time-ms"
|
||||
bn_readstr: .byte 16; .ascii "read-from-string"
|
||||
bn_eval: .byte 4; .ascii "eval"
|
||||
bn_symtostr: .byte 14; .ascii "symbol->string"
|
||||
|
||||
portal_magic: .ascii "ULPORTAL"
|
||||
.equ PORTAL_MAGIC_LEN, 8
|
||||
|
|
@ -309,6 +315,7 @@ bi_names:
|
|||
.quad bn_tcplisten, bn_tcpaccept, bn_tcpconnect
|
||||
.quad bn_tcprecv, bn_tcpsend, bn_tcpclose
|
||||
.quad bn_heapsnap, bn_heaprest, bn_curtime
|
||||
.quad bn_readstr, bn_eval, bn_symtostr
|
||||
|
||||
# Error messages
|
||||
err_unbound: .ascii "Error: unbound variable: "
|
||||
|
|
@ -2503,6 +2510,12 @@ eval_list:
|
|||
je bi_heap_restore
|
||||
cmpq $BI_CURTIME, %rax
|
||||
je bi_current_time_ms
|
||||
cmpq $BI_READSTR, %rax
|
||||
je bi_read_from_string
|
||||
cmpq $BI_EVAL, %rax
|
||||
je bi_eval
|
||||
cmpq $BI_SYMTOSTR, %rax
|
||||
je bi_symbol_to_string
|
||||
|
||||
movq $VAL_VOID, %rax
|
||||
popq %r12
|
||||
|
|
@ -4454,6 +4467,93 @@ bi_heap_restore:
|
|||
movq $VAL_VOID, %rax
|
||||
RET_VAL
|
||||
|
||||
# bi_read_from_string: (read-from-string "sexp") → value
|
||||
# Saves the current input-buffer state, swaps in the caller's string
|
||||
# as the input source, reads one expression, restores input state.
|
||||
bi_read_from_string:
|
||||
GETARG %rdi
|
||||
andq $-8, %rdi
|
||||
movq (%rdi), %rcx # length
|
||||
leaq 8(%rdi), %r8 # pointer to string bytes
|
||||
|
||||
# Save input state (buf_ptr, pos, end, is_file) to stack
|
||||
movq input_buf_ptr(%rip), %rax
|
||||
pushq %rax
|
||||
movq input_pos(%rip), %rax
|
||||
pushq %rax
|
||||
movq input_end(%rip), %rax
|
||||
pushq %rax
|
||||
movq input_is_file(%rip), %rax
|
||||
pushq %rax
|
||||
|
||||
# Swap input to the string. Mark as "file" so no stdin refill.
|
||||
movq %r8, input_buf_ptr(%rip)
|
||||
movq $0, input_pos(%rip)
|
||||
movq %rcx, input_end(%rip)
|
||||
movq $1, input_is_file(%rip)
|
||||
|
||||
call scheme_read # rax = value (or 0 for EOF)
|
||||
movq %rax, %rbx # stash result in callee-save
|
||||
|
||||
# Restore input state (pop reversed order of push)
|
||||
popq %rax
|
||||
movq %rax, input_is_file(%rip)
|
||||
popq %rax
|
||||
movq %rax, input_end(%rip)
|
||||
popq %rax
|
||||
movq %rax, input_pos(%rip)
|
||||
popq %rax
|
||||
movq %rax, input_buf_ptr(%rip)
|
||||
|
||||
# Translate EOF (0) into VAL_FALSE so callers can distinguish
|
||||
movq %rbx, %rax
|
||||
testq %rax, %rax
|
||||
jnz .brfs_done
|
||||
movq $VAL_FALSE, %rax
|
||||
.brfs_done:
|
||||
RET_VAL
|
||||
|
||||
# bi_eval: (eval expr) → value (evaluates in global env r14)
|
||||
bi_eval:
|
||||
GETARG %rdi # expr
|
||||
movq %r14, %rsi # env
|
||||
call eval
|
||||
RET_VAL
|
||||
|
||||
# bi_symbol_to_string: (symbol->string 'foo) → "foo"
|
||||
# Symbols are stored as [length-byte, bytes...] at ptr (low 3 bits = TAG_SYM).
|
||||
# Return a fresh string cell [8-byte length, bytes...].
|
||||
bi_symbol_to_string:
|
||||
GETARG %rdi # symbol value
|
||||
andq $-8, %rdi # untag
|
||||
movzbq (%rdi), %rcx # length (one byte)
|
||||
leaq 1(%rdi), %r8 # byte pointer
|
||||
|
||||
pushq %rcx
|
||||
pushq %r8
|
||||
movq %rcx, %rdi
|
||||
addq $8, %rdi # cell size: 8-byte length + bytes
|
||||
call heap_alloc
|
||||
popq %r8
|
||||
popq %rcx
|
||||
|
||||
movq %rcx, (%rax) # store length as 8 bytes
|
||||
movq %rax, %rbx # save result pointer
|
||||
leaq 8(%rax), %rdx # dest
|
||||
.b_s2s_copy:
|
||||
testq %rcx, %rcx
|
||||
jz .b_s2s_done
|
||||
movb (%r8), %r9b
|
||||
movb %r9b, (%rdx)
|
||||
incq %r8
|
||||
incq %rdx
|
||||
decq %rcx
|
||||
jmp .b_s2s_copy
|
||||
.b_s2s_done:
|
||||
movq %rbx, %rax
|
||||
orq $TAG_STRING, %rax
|
||||
RET_VAL
|
||||
|
||||
# bi_current_time_ms: (current-time-ms) → int ms since epoch
|
||||
# struct timespec is { int64_t tv_sec; int64_t tv_nsec; } — 16 bytes.
|
||||
bi_current_time_ms:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue