asm: values + call-with-values + #(...) reader + exit + vector equal?

Closes the remaining asm-side gaps from ticket 0005's follow-up
discussion. Every test in tests/cl-compat.lsp and tests/ursa.lsp
now runs unmodified on default asm (Scheme port) and asm-full (full
CL path) — no more commented-out tests or shim syntax.

Landed (all in default asm — useful beyond cl-compat):

  * (values . xs) / (call-with-values producer consumer). values
    packs a tagged pair (mval_marker . xs) when multiple; a lone arg
    passes through unchanged so legacy single-value code is
    undisturbed. call-with-values invokes the producer, destructures
    the multi-value packet if present, applies consumer positionally.
    The marker is a gensymed symbol interned once at init, so no
    user-constructed pair can masquerade as a multi-value packet.

  * (exit [code]) builtin. Default code is 0 when called with no
    args. Passes through to the SYS_EXIT syscall.

  * #(...) vector literal in the reader. .sr_hash now dispatches on
    '(' as a vector literal alongside 't' and 'f'. list_to_vector_
    reader is a standalone helper callable from the reader (separate
    from bi_listtovec which uses the GETARG builtin convention).
    Matches R7RS vector literal syntax. Existing vector builtins
    already handled construction; this just teaches the reader.

  * deep_equal extended to vectors. equal? now descends into vectors
    (length + elementwise recursive compare), matching R7RS.
    Previously only strings and pairs were handled; vectors fell
    through to shallow pointer compare which only matched identical
    heap objects.

Test file reverts (picking up the new capabilities):

  * tests/cl-compat.lsp — multiple-value-bind test restored
    (previously commented out because asm lacked values /
    call-with-values).
  * tests/ursa-scheme.lsp — #(1 0 1 0 1 0) literal restored
    (previously worked around with (vector->list (digits ...)));
    (exit 1) failure trailer restored (previously removed because
    asm had no exit builtin).
  * tests/ursa.lsp — same digits literal restoration.

Verified:
  * asm regression: 158/158.
  * asm-full regression: 158/158.
  * Zoë-favorites across Python + C + asm + asm-full: all suites
    green with native reader syntax and multi-value tests.
  * make test-all stays green.
This commit is contained in:
russell@unturf.com 2026-04-24 12:38:47 -04:00
parent 2061cb169a
commit 99b0622520
10 changed files with 202 additions and 23 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

@ -203,15 +203,18 @@
.equ BI_CADR, 115
.equ BI_SORT, 116
.equ BI_GENSYM, 117
.equ BI_EXIT, 118
.equ BI_VALUES, 119
.equ BI_CWV, 120
.ifdef GC_NAIVE
.equ BI_GC_COLLECT, 118
.equ BI_GC_STATS, 119
.equ BI_WITH_ARENA, 120
.equ BI_ARENA_STATS, 121
.equ BI_ARENA_SET_MODE, 122
.equ BI_COUNT, 123
.equ BI_GC_COLLECT, 121
.equ BI_GC_STATS, 122
.equ BI_WITH_ARENA, 123
.equ BI_ARENA_STATS, 124
.equ BI_ARENA_SET_MODE, 125
.equ BI_COUNT, 126
.else
.equ BI_COUNT, 118
.equ BI_COUNT, 121
.endif
# ============================================================
@ -363,6 +366,9 @@ bn_hslist: .byte 14; .ascii "hash-set->list"
bn_cadr: .byte 4; .ascii "cadr"
bn_sort: .byte 4; .ascii "sort"
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"
.ifdef GC_NAIVE
bn_gccollect: .byte 10; .ascii "gc-collect"
bn_gcstats: .byte 8; .ascii "gc-stats"
@ -423,6 +429,8 @@ cl_full_prelude:
.equ cl_full_prelude_len, . - cl_full_prelude
.endif
bn_mval_tag: .byte 17; .ascii "__values-marker__"
portal_magic: .ascii "LUMBDAB2"
.equ PORTAL_MAGIC_LEN, 8
# magic(8) + heap_size(8) + heap_base(8) + r14(8) + r15(8)
@ -464,7 +472,7 @@ bi_names:
.quad bn_isqrt
.quad bn_randomseed, bn_randomint, bn_randomstate, bn_randomstateset
.quad bn_randomseedfromos
.quad bn_cadr, bn_sort, bn_gensym
.quad bn_cadr, bn_sort, bn_gensym, bn_exit, bn_values, bn_cwv
.ifdef GC_NAIVE
.quad bn_gccollect, bn_gcstats, bn_witharena, bn_arenastats, bn_arenamode
.endif
@ -565,6 +573,7 @@ sym_hash_buckets: .skip 8192 # 1024 * 8 bytes
gensym_counter: .skip 8 # bumped each call to (gensym)
num_buf_lp: .skip 64 # scratch [len:byte][chars...] for intern
mval_marker: .skip 8 # interned symbol; marks multi-value packets
.ifdef CL_FULL
# macro_env_head linked list of (sym, closure, next) 24-byte nodes.
@ -1945,10 +1954,65 @@ scheme_read:
je .sr_true
cmpb $'f', %al
je .sr_false
cmpb $'(', %al
je .sr_hash_vector
movq $VAL_VOID, %rax
popq %r12
popq %rbx
ret
.sr_hash_vector:
# `(` already consumed. Read list of elements, convert to vector.
call .read_list_elems
movq %rax, %rdi # list (tagged)
call list_to_vector_reader
popq %r12
popq %rbx
ret
# list_to_vector_reader: %rdi = tagged list -> %rax = tagged vector.
# Separate from bi_listtovec (which uses GETARG) so the reader can
# call it directly.
list_to_vector_reader:
pushq %rbx
movq %rdi, %rbx # save list
xorq %rcx, %rcx
movq %rdi, %rax
.l2vr_count:
cmpq $VAL_NIL, %rax
je .l2vr_alloc
incq %rcx
movq %rax, %rdx
andq $-8, %rdx
movq 8(%rdx), %rax
jmp .l2vr_count
.l2vr_alloc:
pushq %rbx
pushq %rcx
leaq 8(,%rcx,8), %rdi
call heap_alloc
.ifdef GC_NAIVE
movb $HT_VECTOR, -7(%rax)
.endif
popq %rcx
popq %rbx
movq %rcx, (%rax)
leaq 8(%rax), %rdi
movq %rbx, %rdx
.l2vr_fill:
cmpq $VAL_NIL, %rdx
je .l2vr_done
movq %rdx, %rcx
andq $-8, %rcx
movq (%rcx), %rsi
movq %rsi, (%rdi)
addq $8, %rdi
movq 8(%rcx), %rdx
jmp .l2vr_fill
.l2vr_done:
orq $7, %rax
popq %rbx
ret
.sr_true:
movq $VAL_TRUE, %rax
popq %r12
@ -3901,6 +3965,12 @@ eval_list:
je bi_sort
cmpq $BI_GENSYM, %rax
je bi_gensym
cmpq $BI_EXIT, %rax
je bi_exit
cmpq $BI_VALUES, %rax
je bi_values
cmpq $BI_CWV, %rax
je bi_call_with_values
cmpq $BI_INTEGERP, %rax
je bi_integerp
cmpq $BI_PORTALSAVE, %rax
@ -4297,6 +4367,8 @@ deep_equal:
jne .deq_false
cmpq $TAG_STRING, %rax
je .deq_string
cmpq $7, %rax # vector tag
je .deq_vector
cmpq $TAG_PAIR, %rax
jne .deq_false
# Both pairs compare car then cdr
@ -4350,6 +4422,36 @@ deep_equal:
decq %rdx
jmp .deq_str_loop
.deq_vector:
# Both vectors. Compare lengths, then recurse elementwise.
movq %rdi, %rax
andq $-8, %rax
movq %rsi, %rcx
andq $-8, %rcx
movq (%rax), %rdx # len a
cmpq (%rcx), %rdx # len a vs len b
jne .deq_false
leaq 8(%rax), %r8 # a elements
leaq 8(%rcx), %r9 # b elements
.deq_vec_loop:
testq %rdx, %rdx
jz .deq_true
pushq %r8
pushq %r9
pushq %rdx
movq (%r8), %rdi
movq (%r9), %rsi
call deep_equal
popq %rdx
popq %r9
popq %r8
cmpq $VAL_FALSE, %rax
je .deq_false
addq $8, %r8
addq $8, %r9
decq %rdx
jmp .deq_vec_loop
bi_abs:
GETARG %rax
sarq $3, %rax
@ -4729,6 +4831,86 @@ bi_gensym:
call intern_static
RET_VAL
# (exit [code]) graceful termination. Missing arg exit 0.
bi_exit:
cmpq $VAL_NIL, %r12
je .bex_zero
GETARG %rax
sarq $3, %rax
movq %rax, %rdi
movq $SYS_EXIT, %rax
syscall
.bex_zero:
xorq %rdi, %rdi
movq $SYS_EXIT, %rax
syscall
# (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
# works). Otherwise we build a tagged pair (mval_marker . xs) that
# call-with-values destructures. mval_marker is a gensymed symbol set
# at init, so no user-written pair can masquerade as a packet.
bi_values:
cmpq $VAL_NIL, %r12
je .bv_empty
movq %r12, %rax
andq $-8, %rax
movq 8(%rax), %rcx # cdr
cmpq $VAL_NIL, %rcx
je .bv_single
# multi: build pair (mval_marker . args)
movq mval_marker(%rip), %rdi
movq %r12, %rsi
call make_pair
RET_VAL
.bv_single:
movq (%rax), %rax # car
RET_VAL
.bv_empty:
movq mval_marker(%rip), %rdi
movq $VAL_NIL, %rsi
call make_pair
RET_VAL
# (call-with-values producer consumer) invokes producer with no
# args, destructures the multi-value packet (if any), passes the
# values positionally to consumer.
bi_call_with_values:
GETARG %rbx # producer
GETARG %rcx # consumer (save in %rcx via stack)
pushq %rcx
movq %rbx, %rdi # producer
movq $VAL_NIL, %rsi # empty arg list
call apply_proc_raw
popq %rcx # consumer
# Is %rax a multi-value packet? (pair whose car = mval_marker)
movq %rax, %rdx
andq $TAG_MASK, %rdx
cmpq $TAG_PAIR, %rdx
jne .bcwv_single
movq %rax, %rdx
andq $-8, %rdx
movq (%rdx), %r8
cmpq mval_marker(%rip), %r8
jne .bcwv_single
# Multi-value args = cdr of packet
movq 8(%rdx), %rsi
movq %rcx, %rdi # consumer
call apply_proc_raw
RET_VAL
.bcwv_single:
# Single value wrap in a 1-element list and apply
pushq %rcx
movq %rax, %rdi
movq $VAL_NIL, %rsi
call make_pair
popq %rcx
movq %rax, %rsi # (result)
movq %rcx, %rdi # consumer
call apply_proc_raw
RET_VAL
# (cadr x) second element. Shortcut: (car (cdr x)).
bi_cadr:
GETARG %rdi
@ -8269,6 +8451,12 @@ init_special_forms:
call intern_static
movq %rax, sym_else_val(%rip)
# Intern the multi-value marker symbol used by (values) and
# (call-with-values). Stored once at init.
leaq bn_mval_tag(%rip), %rdi
call intern_static
movq %rax, mval_marker(%rip)
.ifdef CL_FULL
leaq sf_quasiquote(%rip), %rdi
call intern_static