asm tier: read-line for subprocess pipes

Closes the last asm-tier gap for hosting bend workers:

  (read-line port) → string or #f
    Reads bytes one at a time from the port's fd until '\n' or EOF.
    Strips the trailing newline. Returns #f when no bytes were
    available (peer closed / pipe drained).

Verified end-to-end on 3090-ai:

  λ> (define p (spawn-process-stdio "./shake256-fanout" (quote (--daemon))))
  λ> (display (read-line (cdr p))) (newline)
  ready
  λ> (display "quit\n" (car p))
  λ> (flush-port (car p))
  λ> (display (read-line (cdr p))) (newline)
  bye

asm tier can now spawn, write, flush, read line — the full
subprocess capability gpu-worker.lsp's daemon pool needs.

Implementation:
  - Stack scratch buffer: 4096 bytes via %rbp (heap-safe; %r15 is
    lumbda's heap pointer, do not touch)
  - One-byte-at-a-time SYS_READ via fd from decode_port
  - Max line: 4094 bytes (fits the daemon protocol — "ready",
    "done /path", "bye" all under 100)
  - heap_alloc(8 + len) + length header + byte copy + TAG_STRING
    matches the Python/C tier string format byte-for-byte
  - GC_NAIVE path stamps HT_STRING header for the GC pass

BI_READLINE = 123; GC_* renumbered to 124..128; BI_COUNT = 129
(GC_NAIVE) / 124. bn_readline added to symbol table + name array.

Remaining asm-tier gaps for full gpu-worker.lsp hosting:
  - *argv* binding (used to parse --port)
  - define-syntax + syntax-rules (used by bend.lsp macros — handled
    by splitting bend-macros.lsp out in a prior commit)
  - error builtin (also handled by portable bend-error wrapper)

Per-tier matrix:
  Python tier ✓ macro + function client; full worker host
  C tier      ✓ macro + function client; full worker host
  asm tier    ✓ function client; subprocess primitives complete;
               gpu-worker.lsp needs *argv* + a few other helpers
               before pure-asm hosting is fully working
This commit is contained in:
russell@unturf.com 2026-06-04 20:58:10 -04:00
parent ceb497ac96
commit 6262a24d47
No known key found for this signature in database

View file

@ -212,15 +212,16 @@
.equ BI_CWV, 120
.equ BI_SPAWNPROC, 121
.equ BI_FLUSHPORT, 122
.equ BI_READLINE, 123
.ifdef GC_NAIVE
.equ BI_GC_COLLECT, 123
.equ BI_GC_STATS, 124
.equ BI_WITH_ARENA, 125
.equ BI_ARENA_STATS, 126
.equ BI_ARENA_SET_MODE, 127
.equ BI_COUNT, 128
.equ BI_GC_COLLECT, 124
.equ BI_GC_STATS, 125
.equ BI_WITH_ARENA, 126
.equ BI_ARENA_STATS, 127
.equ BI_ARENA_SET_MODE, 128
.equ BI_COUNT, 129
.else
.equ BI_COUNT, 123
.equ BI_COUNT, 124
.endif
# ============================================================
@ -351,6 +352,7 @@ bn_tcpclose: .byte 9; .ascii "tcp-close"
bn_tcpsendfile: .byte 12; .ascii "tcp-sendfile"
bn_spawnproc: .byte 19; .ascii "spawn-process-stdio"
bn_flushport: .byte 10; .ascii "flush-port"
bn_readline: .byte 9; .ascii "read-line"
bn_heapsnap: .byte 13; .ascii "heap-snapshot"
bn_heaprest: .byte 12; .ascii "heap-restore"
bn_curtime: .byte 15; .ascii "current-time-ms"
@ -495,7 +497,7 @@ bi_names:
.quad bn_randomseed, bn_randomint, bn_randomstate, bn_randomstateset
.quad bn_randomseedfromos
.quad bn_cadr, bn_sort, bn_gensym, bn_exit, bn_values, bn_cwv
.quad bn_spawnproc, bn_flushport
.quad bn_spawnproc, bn_flushport, bn_readline
.ifdef GC_NAIVE
.quad bn_gccollect, bn_gcstats, bn_witharena, bn_arenastats, bn_arenamode
.endif
@ -4286,6 +4288,8 @@ eval_list:
je bi_spawn_process_stdio
cmpq $BI_FLUSHPORT, %rax
je bi_flush_port
cmpq $BI_READLINE, %rax
je bi_read_line
.ifdef GC_NAIVE
cmpq $BI_GC_COLLECT, %rax
je bi_gc_collect_user
@ -8838,6 +8842,92 @@ bi_flush_port:
movq $VAL_VOID, %rax
RET_VAL
# ============================================================
# bi_read_line: (read-line port) string or #f on EOF
#
# Reads bytes one at a time from the port's fd until '\n' or EOF.
# Strips the trailing '\n'. Returns a Scheme string, or #f if no
# bytes were available (peer closed / pipe drained).
#
# Used by gpu-worker.lsp to consume daemon responses ("ready",
# "done /path", "bye") line by line.
#
# Max line length: 4095 bytes (fits in a 4 KB scratch). Longer lines
# truncated fine for our daemon protocol.
# ============================================================
bi_read_line:
pushq %r13
pushq %r14
subq $4104, %rsp
movq %rsp, %rbp # %rbp = byte buffer
# Get port fd in %r13
GETARG %rdi
call decode_port
testq %rax, %rax
js .rl_fail
movq %rax, %r13 # %r13 = fd
xorq %r14, %r14 # %r14 = byte count
.rl_loop:
cmpq $4094, %r14 # leave room for final byte
jge .rl_done
movq $SYS_READ, %rax
movq %r13, %rdi
leaq (%rbp,%r14), %rsi
movq $1, %rdx
syscall
cmpq $1, %rax
jne .rl_done # EOF or error
movb (%rbp,%r14), %al
incq %r14
cmpb $10, %al # '\n'
je .rl_strip_nl
jmp .rl_loop
.rl_strip_nl:
decq %r14 # exclude the newline
.rl_done:
testq %r14, %r14
jz .rl_eof # 0 bytes & EOF #f
# heap_alloc(8 + len)
movq %r14, %rdi
addq $8, %rdi
call heap_alloc
.ifdef GC_NAIVE
movb $HT_STRING, -7(%rax)
.endif
movq %r14, (%rax) # length header
xorq %rcx, %rcx
.rl_copy:
cmpq %r14, %rcx
jge .rl_copy_done
movb (%rbp,%rcx), %dl
movb %dl, 8(%rax,%rcx)
incq %rcx
jmp .rl_copy
.rl_copy_done:
orq $TAG_STRING, %rax
addq $4104, %rsp
popq %r14
popq %r13
RET_VAL
.rl_eof:
movq $VAL_FALSE, %rax
addq $4104, %rsp
popq %r14
popq %r13
RET_VAL
.rl_fail:
movq $VAL_FALSE, %rax
addq $4104, %rsp
popq %r14
popq %r13
RET_VAL
# ============================================================
# list_reverse: %rdi = list -> %rax = reversed list
# ============================================================