asm: argv script-mode + always-on eq? prelude (defects #28, #30)

Two coupled defects surfaced during ecdsa cross-tier validation against
the asm tier.

Defect #28 — _start ignored argv. Invoking `asm/lumbda-gc file.lsp`
silently discarded argv[1] and dropped into a REPL that blocked on a
pty when run under SSH. Walk argc/argv after init_builtins + prelude
load and before repl_top: for each argv[i] starting at i=1, skip
arg if it begins with '-' (flag stub), otherwise allocate a Scheme
string from the C string, wrap in a 1-element arg list, dispatch
through apply_proc_raw on the BI_LOAD builtin. If any non-flag arg
ran, jump to repl_exit instead of entering the REPL. Mirrors the
c/main.c script-mode semantics. The RET_VAL macro on the builtin
return path pops r12/rbp/rbx in an order that corrupts %rbp (it
restores the pre-call %r12 into rbp), so the loop counter saves
%rbp around the apply_proc_raw call.

Defect #30 — eq? was only present under CL_FULL. The plain `lumbda`
and `lumbda-gc` binaries shipped without the alias `(define eq? eqv?)`,
so any .lsp expecting eq? (every cross-tier file we own) hit
"unbound variable: eq?" the moment it tried a status check. Lift
that single alias into a new always-on `default_prelude` block with
its own `load_default_prelude` loader (modelled after
load_cl_full_prelude), and call it unconditionally from _start
between rng_seed and the CL_FULL block.

Verification:
- `make asm-build` clean
- `make asm-test`: 158 passed, 0 failed (full suite green)
- `(eq? 1 1)` -> #t on all three tiers via stdin pipe AND file arg
- `~/git/lumbda/asm/lumbda-gc /tmp/asm-test.lsp` exits 0 with #t printed
This commit is contained in:
russell@unturf.com 2026-06-04 12:49:13 -04:00
parent 98d0e35a4e
commit 297ae976e2
No known key found for this signature in database
7 changed files with 155 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -385,6 +385,17 @@ s_hashset: .ascii "#<hash-set>"
err_ht_miss: .ascii "Error: hash-table-ref: missing key\n"
.equ err_ht_miss_len, . - err_ht_miss
# Default prelude always loaded, every binary tier
# Defines the Scheme-side aliases that cross-tier .lsp code expects
# to exist on the plain `lumbda` and `lumbda-gc` builds (not just
# `lumbda-full`). Kept minimal: every entry here ships in every
# binary, so add only universally-needed forms. (Defect #30 in our
# ecdsa cross-tier validation: `eq?` was only present under CL_FULL,
# leaving `lumbda` and `lumbda-gc` with `eqv?` but no `eq?`.)
default_prelude:
.ascii "(define eq? eqv?)\n"
.equ default_prelude_len, . - default_prelude
.ifdef CL_FULL
# CL_FULL prelude auto-loaded Scheme definitions
# Runs once at init, after builtins + RNG seed, before the REPL.
@ -720,12 +731,105 @@ _start:
xorq %rdi, %rdi
call rng_seed
# Always-on prelude defines `eq?` (and any future cross-tier
# aliases). Required for parity with our Python + C tiers, which
# carry these via their own preludes / native builtins.
call load_default_prelude
.ifdef CL_FULL
# Auto-load the CL_FULL prelude (caddr, 1+, list-ref, assq, ).
# Runs before the REPL so user code sees the helpers immediately.
call load_cl_full_prelude
.endif
# argv script-mode handling
# Stack layout at _start entry (preserved through here the
# only %rsp mutations above are paired sub/add 256 around ioctl):
# 0(%rsp) = argc, 8(%rsp) = argv[0], 16(%rsp) = argv[1],
# For each argv[i] (i >= 1):
# * Skip args starting with '-' (flag stubs, no-op for now).
# * Otherwise treat as a script path: build a Scheme string,
# wrap in a 1-element arg list, dispatch (load "path") via
# apply_proc_raw on the BI_LOAD builtin.
# If any non-flag arg was consumed, exit after processing all
# args instead of entering the REPL. Mirrors c/main.c:234-252.
#
# %rbx = argc (callee-saved across apply_proc_raw).
# %rbp = current argv index (1-based).
# %r13 is the heap limit, so we cannot use it here use a stack
# slot for the "any-script-loaded" flag instead.
movq 0(%rsp), %rbx # argc
cmpq $1, %rbx
jle .argv_done # argc <= 1 no args, fall through
pushq $0 # [rsp+0] = any-script-loaded flag (0 = none yet)
movq $1, %rbp # i = 1 (skip argv[0])
.argv_loop:
cmpq %rbx, %rbp
jge .argv_loop_done
# argv pointer table sits at 8(%rsp_orig). We pushed one qword
# (the flag), so add 8 to compensate.
movq 16(%rsp,%rbp,8), %rdi # argv[i] = C string pointer
# Skip if starts with '-'
cmpb $'-', (%rdi)
je .argv_next
# Compute strlen of argv[i] %rcx
movq %rdi, %rcx
.argv_strlen:
cmpb $0, (%rcx)
je .argv_strlen_done
incq %rcx
jmp .argv_strlen
.argv_strlen_done:
subq %rdi, %rcx # %rcx = length
# Allocate Scheme string: heap_alloc(8 + length)
pushq %rdi # save C string pointer
pushq %rcx # save length
leaq 8(%rcx), %rdi
call heap_alloc
.ifdef GC_NAIVE
movb $HT_STRING, -7(%rax)
.endif
popq %rcx # restore length
popq %rdi # restore C string pointer
movq %rcx, (%rax) # store 8-byte length header
# Copy bytes from argv[i] into payload at 8(%rax)
xorq %rdx, %rdx
.argv_copy:
cmpq %rcx, %rdx
jge .argv_copy_done
movb (%rdi,%rdx), %sil
movb %sil, 8(%rax,%rdx)
incq %rdx
jmp .argv_copy
.argv_copy_done:
orq $TAG_STRING, %rax # tag as Scheme string
# Build 1-element arg list: (cons str NIL)
movq %rax, %rdi
movq $VAL_NIL, %rsi
call make_pair
movq %rax, %rsi # args list
# BI_LOAD builtin value = (BI_LOAD << 3) | TAG_BUILTIN
movq $((BI_LOAD << 3) | TAG_BUILTIN), %rdi
# apply_proc_raw bi_load returns via RET_VAL, which pops
# r12/rbp/rbx in an order that leaves %rbp corrupted (it gets
# the pre-call %r12 instead of pre-call %rbp). Save %rbp ourselves
# across the call so our loop counter survives. %rbx (argc) is
# restored cleanly.
pushq %rbp
call apply_proc_raw
popq %rbp
# Mark that at least one script ran.
movq $1, 0(%rsp)
.argv_next:
incq %rbp
jmp .argv_loop
.argv_loop_done:
# Pop flag; if non-zero, exit (don't enter REPL).
popq %rax
testq %rax, %rax
jnz repl_exit
.argv_done:
# Set up a generous stack area (16 KB stack frame for deep recursion)
# Actually the OS already gave us a stack, so we're fine.
@ -6525,6 +6629,57 @@ bi_portal_resume:
RET_VAL
.endif
# ============================================================
# load_default_prelude parse and eval the always-on prelude.
# Mirrors load_cl_full_prelude but applies to every binary tier
# (default, GC, FULL). Defines `eq?` and any other always-on
# aliases listed at default_prelude. Saves + restores reader input
# state so neither REPL nor argv scripts see the prelude bytes.
# Called from _start after init_builtins / rng_seed, unconditionally.
load_default_prelude:
pushq %rbp
# Save current input state.
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
# Install prelude string as input.
leaq default_prelude(%rip), %rax
movq %rax, input_buf_ptr(%rip)
movq $0, input_pos(%rip)
movq $default_prelude_len, input_end(%rip)
movq $1, input_is_file(%rip) # EOF at end; do not refill
.ldp_loop:
call scheme_read
testq %rax, %rax
jz .ldp_done
movq %rax, %rdi
movq %r14, %rsi # global env
call eval
# .ev_define mutates %r14 directly; no post-eval env update needed.
jmp .ldp_loop
.ldp_done:
# Restore input state.
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)
popq %rbp
ret
# ============================================================
.ifdef CL_FULL
# load_cl_full_prelude parse and eval the embedded prelude string.