asm tier: fork-self + waitpid-nonblock + exit-immediate + sleep primitives

Cross-tier API parity with c-tier (81ac49e) + python-tier — all three
lumbda runtimes now share the substrate for fork-per-accept patterns.

Implementation: direct syscalls (no libc):
- SYS_FORK=57 → bi_forkself, returns 0/pid via make_int
- SYS_WAIT4=61 + WNOHANG=1 → bi_waitpid_nonblock, returns pid or 0
- SYS_EXIT=60 → bi_exit_immediate (same as bi_exit on asm — no atexit
  to bypass; present for cross-tier API parity)
- SYS_NANOSLEEP=35 → bi_sleep, stack-allocated timespec (tv_sec=N,
  tv_nsec=0), returns VAL_VOID

Built + tested in vm-runner.sh VM (Ubuntu 2G/2vCPU): both lumbda
and lumbda-gc + fork-cycle test = 3/3 children reap clean, exit-
immediate returns to parent waitpid correctly. Same behavioral
contract as c-tier (commit 81ac49e) and python-tier.
This commit is contained in:
russell@unturf.com 2026-06-11 09:30:31 -04:00
parent 092a8ed741
commit 7c99df99cf
No known key found for this signature in database

View file

@ -43,6 +43,9 @@
.equ SYS_EXECVE,59
.equ SYS_DUP2, 33
.equ SYS_UNLINK, 87
.equ SYS_WAIT4, 61
.equ SYS_NANOSLEEP, 35
.equ WNOHANG, 1
.equ AF_INET, 2
.equ SOCK_STREAM,1
@ -227,11 +230,19 @@
.equ BI_ARENA_SET_MODE, 128
.equ BI_STRTOSYM, 129
.equ BI_DELETEFILE, 130
.equ BI_COUNT, 131
.equ BI_FORKSELF, 131
.equ BI_WAITPIDNB, 132
.equ BI_EXITIMM, 133
.equ BI_SLEEP, 134
.equ BI_COUNT, 135
.else
.equ BI_STRTOSYM, 124
.equ BI_DELETEFILE, 125
.equ BI_COUNT, 126
.equ BI_FORKSELF, 126
.equ BI_WAITPIDNB, 127
.equ BI_EXITIMM, 128
.equ BI_SLEEP, 129
.equ BI_COUNT, 130
.endif
# ============================================================
@ -394,6 +405,10 @@ bn_gensym: .byte 6; .ascii "gensym"
bn_exit: .byte 4; .ascii "exit"
bn_values: .byte 6; .ascii "values"
bn_cwv: .byte 16; .ascii "call-with-values"
bn_forkself: .byte 9; .ascii "fork-self"
bn_waitpidnb: .byte 16; .ascii "waitpid-nonblock"
bn_exitimm: .byte 14; .ascii "exit-immediate"
bn_sleep: .byte 5; .ascii "sleep"
.ifdef GC_NAIVE
bn_gccollect: .byte 10; .ascii "gc-collect"
bn_gcstats: .byte 8; .ascii "gc-stats"
@ -514,6 +529,7 @@ bi_names:
.quad bn_gccollect, bn_gcstats, bn_witharena, bn_arenastats, bn_arenamode
.endif
.quad bn_strtosym, bn_deletefile
.quad bn_forkself, bn_waitpidnb, bn_exitimm, bn_sleep
# Error messages
err_unbound: .ascii "Error: unbound variable: "
@ -4260,6 +4276,14 @@ eval_list:
je bi_gensym
cmpq $BI_EXIT, %rax
je bi_exit
cmpq $BI_FORKSELF, %rax
je bi_forkself
cmpq $BI_WAITPIDNB, %rax
je bi_waitpid_nonblock
cmpq $BI_EXITIMM, %rax
je bi_exit_immediate
cmpq $BI_SLEEP, %rax
je bi_sleep
cmpq $BI_VALUES, %rax
je bi_values
cmpq $BI_CWV, %rax
@ -5148,6 +5172,71 @@ bi_exit:
movq $SYS_EXIT, %rax
syscall
# (fork-self) — fork() wrapper. Child sees 0, parent sees pid, -1/#f on fail.
# Caller MUST use exit-immediate in child to avoid dual-cleanup hang.
bi_forkself:
movq $SYS_FORK, %rax
syscall
# %rax = -errno on fail, 0 in child, pid in parent. asm tier has
# no #f-on-fail convention here; return -errno as a fixnum (will
# be negative — caller can check). For our gpu-worker pattern,
# success path (0/pid) is the only path we exercise.
movq %rax, %rdi
call make_int
RET_VAL
# (waitpid-nonblock) — reap one completed child non-blocking. Returns pid or 0.
bi_waitpid_nonblock:
movq $-1, %rdi # pid = -1 (any child)
xorq %rsi, %rsi # status* = NULL
movq $WNOHANG, %rdx # options
xorq %r10, %r10 # rusage* = NULL
movq $SYS_WAIT4, %rax
syscall
# %rax <= 0 means no child reaped (or error) — return 0.
testq %rax, %rax
jg .bwn_ok
xorq %rax, %rax
.bwn_ok:
movq %rax, %rdi
call make_int
RET_VAL
# (exit-immediate [code]) — _exit() wrapper, identical to bi_exit on asm
# tier (no atexit handlers / stdio buffers to bypass). Present for
# cross-tier API parity with c-tier / python-tier where the distinction
# matters.
bi_exit_immediate:
cmpq $VAL_NIL, %r12
je .beim_zero
GETARG %rax
sarq $3, %rax
movq %rax, %rdi
movq $SYS_EXIT, %rax
syscall
.beim_zero:
xorq %rdi, %rdi
movq $SYS_EXIT, %rax
syscall
# (sleep N) — wall-clock sleep for N seconds via nanosleep. Returns void.
# Uses stack-allocated timespec.
bi_sleep:
GETARG %rax
sarq $3, %rax # untag fixnum
# Build timespec on stack: tv_sec (8 bytes), tv_nsec (8 bytes)
subq $16, %rsp
movq %rax, 0(%rsp) # tv_sec = N
xorq %rax, %rax
movq %rax, 8(%rsp) # tv_nsec = 0
movq %rsp, %rdi # req timespec
xorq %rsi, %rsi # rem = NULL
movq $SYS_NANOSLEEP, %rax
syscall
addq $16, %rsp
movq $VAL_VOID, %rax
RET_VAL
# (values . xs) — packs its args as a multi-value packet.
# Calling convention: if xs has exactly one element, that element is
# returned directly (so code that ignores multi-value semantics still