From ebcf1625c71036afbaf0cf80341e7d3eaefffead Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Thu, 4 Jun 2026 13:04:34 -0400 Subject: [PATCH] asm: case as always-on special form (defect #32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ecdsa cross-tier validation on lumbda-gc and bump-only lumbda blocked on `case` being unbound. R7RS standard control form — present on Python (lumbda.py) and C (c/) tiers, but on asm reachable only via cl_full_prelude's define-macro form (carved into lumbda-full only). Mirrors commit 865be28 (when/unless via Path B dispatch table). Path B (special-form dispatch table extension): - sf_case length-prefixed symbol name - sym_case_val interned at init_special_forms - dispatch case in .eval_top alongside .ev_when / .ev_unless (placed before the .ifdef CL_FULL macro-lookup block so the dispatch shadow takes precedence over the cl_full_prelude macro on lumbda-full — no conflict, the macro just becomes dead code) - .ev_case evaluator: eval key once, push on stack, walk clauses; each clause's datum list compared by pointer equality (eqv? on the asm tier — fixnums, symbols, booleans, characters, nil are all interned/unboxed to unique values). `else` matches uncondi- tionally. Match → .ev_begin (TCO). No match → .ev_begin_void. Available on every asm tier (plain `lumbda`, `lumbda-gc`, `lumbda-full`). Binary size impact: lumbda 60768 → 61040 (+272, +0.45%) lumbda-gc 69496 → 69768 (+272, +0.39%) lumbda-full 72000 → 72264 (+264, +0.37%) All 158 asm tests still pass. Sanity tests: single-datum, multi-datum, symbol key (eqv?), else, empty body, no-match, nested case — all correct on all three tiers. ecdsa search.lsp now produces byte-identical winner (v3-clifford-only score 0) across five tiers: Python, C, asm-bump, asm-gc, asm-full. --- asm/lumbda.s | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/asm/lumbda.s b/asm/lumbda.s index b66b39c..c258ead 100644 --- a/asm/lumbda.s +++ b/asm/lumbda.s @@ -248,6 +248,7 @@ sf_or: .byte 2; .ascii "or" sf_else: .byte 4; .ascii "else" sf_when: .byte 4; .ascii "when" sf_unless: .byte 6; .ascii "unless" +sf_case: .byte 4; .ascii "case" # Builtin names (length-prefixed) bn_add: .byte 1; .ascii "+" @@ -558,6 +559,7 @@ sym_or_val: .quad 0 sym_else_val: .quad 0 sym_when_val: .quad 0 sym_unless_val: .quad 0 +sym_case_val: .quad 0 # ============================================================ .bss @@ -2819,6 +2821,8 @@ eval: je .ev_when cmpq sym_unless_val(%rip), %rbx je .ev_unless + cmpq sym_case_val(%rip), %rbx + je .ev_case .ifdef CL_FULL cmpq sym_quasiquote_val(%rip), %rbx je .ev_quasiquote @@ -3571,6 +3575,68 @@ env_set_both: .ev_unless_run: jmp .ev_begin +# ---- case ---- +# (case key clause ...) where each clause is +# ((datum ...) body...) or (else body...) +# Evaluate `key` once, then walk clauses; for the first clause whose +# datum list contains a value `eqv?` to key, evaluate body via begin +# (TCO). `else` matches unconditionally and must be last. If no clause +# matches, return void. R7RS standard form — Path B mirrors the just- +# shipped `when`/`unless` fix (commit 865be28); previously case lived +# only in cl_full_prelude as a define-macro form, leaving lumbda-gc +# and bump-only lumbda unable to run ecdsa search.lsp. +# eqv? on the asm tier is pointer equality (fixnums, symbols, bools, +# chars, nil are all interned/unboxed to unique values). +.ev_case: + # %r12 = (key . clauses) + cmpq $VAL_NIL, %r12 + je .ev_begin_void # (case) → void (degenerate) + movq %r12, %rax + andq $-8, %rax + movq (%rax), %rdi # key expr + movq 8(%rax), %r12 # clauses + pushq %r12 # save clauses + movq %rbp, %rsi + call eval + popq %r12 # restore clauses + pushq %rax # save key value on stack + # Loop over clauses; key sits at (%rsp). +.ev_case_loop: + cmpq $VAL_NIL, %r12 + je .ev_case_void + movq %r12, %rax + andq $-8, %rax + movq (%rax), %rbx # current clause + movq 8(%rax), %r12 # rest clauses + # Clause shape: (datums . body) or (else . body) + movq %rbx, %rax + andq $-8, %rax + movq (%rax), %rdx # datums (or `else` symbol) + movq 8(%rax), %rcx # body forms + # else clause matches unconditionally + cmpq sym_else_val(%rip), %rdx + je .ev_case_match + # Walk datum list; key is at (%rsp) + movq (%rsp), %rdi +.ev_case_dloop: + cmpq $VAL_NIL, %rdx + je .ev_case_loop # exhausted datums → next clause + movq %rdx, %rax + andq $-8, %rax + movq (%rax), %rsi # datum + movq 8(%rax), %rdx # rest datums + cmpq %rdi, %rsi # eqv? — pointer compare + je .ev_case_match + jmp .ev_case_dloop +.ev_case_match: + # %rcx = body forms. Discard saved key, run body via begin (TCO). + addq $8, %rsp + movq %rcx, %r12 + jmp .ev_begin +.ev_case_void: + addq $8, %rsp # discard saved key + jmp .ev_begin_void + .ifdef CL_FULL # ---- quasiquote (CL_FULL only) ---- # (quasiquote template) walks `template`: @@ -8668,6 +8734,10 @@ init_special_forms: call intern_static movq %rax, sym_unless_val(%rip) + leaq sf_case(%rip), %rdi + call intern_static + movq %rax, sym_case_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