asm/lumbda-full: Zoë's CL runs end-to-end (ticket 0005 follow-up)

Four fixes that turn the asm-full infrastructure from "loads cl-compat
but crashes on cl-loop-emit output" into "runs Zoë Trout's full CL
test suite (18/19) end-to-end." Zoë's original `examples/ursa.lisp.txt`
now produces matching answers to the Python and C tiers on asm-full.

1. asm/lumbda.s bi_apply — second arg was being clobbered. The
   previous impl did `GETARG %rbx; GETARG %rdi; movq %rbx, %rdi;
   ... movq %r12, %rsi` — so the args-list got overwritten by the
   proc, and %r12 (empty after two GETARGs) became the arg list
   instead. `(apply f '(1 2 3))` silently reduced to `(f)`. Fix:
   `GETARG %rbx; GETARG %rsi; movq %rbx, %rdi; call apply_proc_raw`.

2. asm/lumbda.s bi_expt — decrements rcx by 1 until zero. Negative
   exponents looped forever. cl-loop's look-ahead termination stages
   step values in a let* BEFORE the terminate check, so a range that
   ends at 0 ends up evaluating `(expt 2 -1)` on the last step. Fix:
   guard negative exponents, return 0. asm is integer-only; returning
   a rational would need a new type. Zero truncates the out-of-range
   iter's contribution, which the look-ahead termination discards
   anyway — the result is correct.

3. asm/lumbda.s GC roots — macro_env_head was not marked. Under
   GC_NAIVE (which CL_FULL implies), any collection during a macro-
   heavy workload (like miller-rabin's expanding cl-loops) reclaimed
   the macro table nodes. Next use failed with "unbound variable:
   cl-when" or similar. Fix: mark macro_env_head alongside the
   global env (same 24-byte (sym, val, next) shape as env nodes, so
   gc_mark_env handles it). Guarded .ifdef CL_FULL.

4. asm/lumbda.s prelude — added `cadar` (used by
   cl-loop-finalizer-expr). The previous omission triggered an
   "unbound variable: cadar" in any cl-loop with a `finally (return
   X)` finalizer.

5. cl-compat.lsp — two new helpers routed around asm's reduced
   list-processing builtins:

     * `cl-append` for n-list concatenation. asm's builtin `append`
       is 2-arg only; cl-loop-emit appends five spec groups
       (range + then + simple + across + counter). Reducing with
       2-arg append works on every tier.

     * `cl-zip` for parallel 2-list zip (already in earlier commit,
       mentioned here for completeness — asm's `map` is single-list
       only).

Verification on asm/lumbda-full:

  * /tmp/ursa-load-test.lsp — 18/19 pass (the one remaining fail
    is a random-state expectation, not an asm bug).
  * (primep 97)  → 97
  * (primep 100) → #f
  * (lucas-lehmer-primep 13) → #t  (M₁₃ = 8191, prime)
  * (lucas-lehmer-primep 11) → #f  (M₁₁ = 2047 = 23·89)
  * (of-n-bits 8) → random integer in [128, 256) with top bit set
  * (prime-of-n-bits 8) → random 8-bit prime

make test-all stays green. All three asm variants still 158/158 on
their local test suites. asm's minimal footprint preserved — every
new line above is under .ifdef CL_FULL except the expt/apply fixes,
which are general correctness improvements independent of CL.
This commit is contained in:
russell@unturf.com 2026-04-24 12:17:58 -04:00
parent 4ff87920cf
commit c6658e03a4
8 changed files with 40 additions and 12 deletions

View file

@ -166,6 +166,15 @@
(else (cons (list (car as) (car bs))
(cl-zip (cdr as) (cdr bs))))))
;; N-list append: (cl-append '(a) '(b) '(c)) → (a b c). asm's builtin
;; `append` is strictly 2-arg; cl-loop-emit concatenates five groups
;; of specs (range + then + simple + across + counter). Reducing with
;; the two-arg `append` works on every impl.
(define (cl-append . lsts)
(cond ((null? lsts) '())
((null? (cdr lsts)) (car lsts))
(else (append (car lsts) (apply cl-append (cdr lsts))))))
;; ── declare — ignored no-op ──────────────────────────────────────
;; CL code sprinkles `(declare (optimize (speed 3)) (type integer x))`
;; inside function bodies. These are compile-time directives in SBCL.
@ -395,7 +404,7 @@
;; kept in parallel lists by position below.
;; For parity with downstream expectations, flatten to (var init step).
(all-specs-source
(append
(cl-append
(map (lambda (r) (list (car r) (cadr r) (caddr r))) range-specs)
(map (lambda (t) t) then-specs)
(map (lambda (s) s) simple-specs)
@ -436,7 +445,7 @@
;; stepped values. If any fires, the finalizer runs in the outer
;; scope where state vars still hold their current-iter values.
(lookahead-tests
(append
(cl-append
;; range: term based on new value
(map (lambda (r)
(let* ((var (car r))