From 865be28091eccdaf886fefcfe0eec303ecf5dd77 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Thu, 4 Jun 2026 12:58:52 -0400 Subject: [PATCH] asm: when/unless as always-on special forms (defect #31) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ecdsa cross-tier validation blocked on `when` and `unless` being unbound on the asm tier. R7RS standard control forms — present on Python (lumbda.py) and C (c/) tiers, but absent on asm because the existing macro facility (define-macro) only ships under CL_FULL. Path B chosen (special-form dispatch table extension): - sf_when / sf_unless length-prefixed symbol names - sym_when_val / sym_unless_val interned at init_special_forms - dispatch cases in .eval_top alongside .ev_and / .ev_or - .ev_when / .ev_unless evaluators reuse .ev_begin for the body branch and .ev_begin_void for the skip branch (TCO preserved) Available on every asm tier (plain `lumbda`, `lumbda-gc`, `lumbda-full`). Binary size impact: lumbda 60488 → 60768 (+280, +0.46%) lumbda-gc 69224 → 69496 (+272, +0.39%) lumbda-full 71720 → 72000 (+280, +0.39%) All 158 asm tests still pass. Tested truthy/falsy/multi-form bodies on all three tiers. ecdsa search.lsp now runs on asm-full with output byte-identical to Python tier (v3-clifford-only winner, score 0). Note: ecdsa search.lsp also depends on `case`, which is only present under CL_FULL (carved into cl_full_prelude as a define-macro form). That gap blocks lumbda-gc cross-tier validation and is out of scope for this commit. --- asm/lumbda.s | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/asm/lumbda.s b/asm/lumbda.s index 907c0c2..b66b39c 100644 --- a/asm/lumbda.s +++ b/asm/lumbda.s @@ -246,6 +246,8 @@ sf_define_mac: .byte 12; .ascii "define-macro" sf_and: .byte 3; .ascii "and" sf_or: .byte 2; .ascii "or" sf_else: .byte 4; .ascii "else" +sf_when: .byte 4; .ascii "when" +sf_unless: .byte 6; .ascii "unless" # Builtin names (length-prefixed) bn_add: .byte 1; .ascii "+" @@ -554,6 +556,8 @@ sym_define_mac_val: .quad 0 sym_and_val: .quad 0 sym_or_val: .quad 0 sym_else_val: .quad 0 +sym_when_val: .quad 0 +sym_unless_val: .quad 0 # ============================================================ .bss @@ -2811,6 +2815,10 @@ eval: je .ev_and cmpq sym_or_val(%rip), %rbx je .ev_or + cmpq sym_when_val(%rip), %rbx + je .ev_when + cmpq sym_unless_val(%rip), %rbx + je .ev_unless .ifdef CL_FULL cmpq sym_quasiquote_val(%rip), %rbx je .ev_quasiquote @@ -3517,6 +3525,52 @@ env_set_both: popq %rbx ret +# ---- when ---- +# (when test body...) — if test is truthy, eval body as begin; +# else return void. Available on every asm tier (defect #31 in our +# ecdsa cross-tier validation: when/unless are R7RS standard but +# previously only reachable on tiers carrying define-macro). +.ev_when: + # %r12 = (test body...) + cmpq $VAL_NIL, %r12 + je .ev_begin_void # (when) → void (degenerate) + movq %r12, %rax + andq $-8, %rax + movq (%rax), %rdi # test expr + movq 8(%rax), %r12 # body forms + pushq %r12 + movq %rbp, %rsi + call eval + popq %r12 + cmpq $VAL_FALSE, %rax + je .ev_begin_void + cmpq $VAL_NIL, %rax + je .ev_begin_void + # Truthy → run body via begin (TCO on last form) + jmp .ev_begin + +# ---- unless ---- +# (unless test body...) — inverse of when. +.ev_unless: + cmpq $VAL_NIL, %r12 + je .ev_begin_void # (unless) → void + movq %r12, %rax + andq $-8, %rax + movq (%rax), %rdi # test expr + movq 8(%rax), %r12 # body forms + pushq %r12 + movq %rbp, %rsi + call eval + popq %r12 + cmpq $VAL_FALSE, %rax + je .ev_unless_run + cmpq $VAL_NIL, %rax + je .ev_unless_run + # Truthy → skip body, return void + jmp .ev_begin_void +.ev_unless_run: + jmp .ev_begin + .ifdef CL_FULL # ---- quasiquote (CL_FULL only) ---- # (quasiquote template) walks `template`: @@ -8606,6 +8660,14 @@ init_special_forms: call intern_static movq %rax, sym_else_val(%rip) + leaq sf_when(%rip), %rdi + call intern_static + movq %rax, sym_when_val(%rip) + + leaq sf_unless(%rip), %rdi + call intern_static + movq %rax, sym_unless_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