asm/lumbda-full now runs Zoë's CL source end-to-end per commit
c6658e0. Rewrite the Known Issues section into a Resolved section
explaining what each of the four underlying asm bugs was (bi_apply
clobber, bi_expt infinite loop on negative exponent, macro_env_head
missing from GC roots, cadar missing from the prelude) and why the
158-test asm suite did not catch them before.
9.7 KiB
0005 — asm/lumbda-full: CL compat on the asm tier
Status: resolved (commit c6658e0)
Resolved: 2026-04-24
Reporter: fox (via blackops)
Implementer: blackops
Opened: 2026-04-24
Problem
Ticket 0004 landed Common Lisp compatibility on the Python and C
tiers. Zoë Trout's favorites at examples/ursa.lisp.txt load via
cl-compat.lsp and run unchanged on both. The asm tier — 22 KB
stripped, 14 syscalls, no libc — was intentionally scoped out of
0004 because the minimal runtime lacks three features that cl- compat.lsp and the cl-loop macro depend on:
- Quasiquote (
` ,foo ,@bar) — every macro expansion incl-compat.lspuses quasiquote templates. Without it the file does not even parse on asm. define-macro— cl-compat is ~90% macros. Without a macro system the file cannot register them.case—cl-loop-emitusescasefor range-term dispatch (to/below/downto). A macro definition via define-macro suffices once (1) and (2) exist.
Phase 1 (commit 8cf6f44) added rest-args, cadr, sort, and let*
to the default asm — enough to run examples/ursa-scheme.lsp, the
idiomatic Scheme port of Zoë's favorites. Phase 2 (this ticket) adds
the macro machinery so her CL source runs on asm unchanged.
Goal
Build a third asm variant, asm/lumbda-full, produced with
--defsym CL_FULL=1 --defsym GC_NAIVE=1. It ships with:
- everything the existing
asm/lumbda-gchas, - quasiquote / unquote / unquote-splicing in the reader + evaluator,
define-macroas a special form with a macro table and detection pass that runs before procedure dispatch,- an embedded Scheme prelude loaded at init that defines
caddr,cadddr,cddr,cdddr,cddddr,1+,1-,add1,sub1,square,list-ref,assq, andcase(as a macro), - optional: a way to opt out of the prelude at startup for strict R5RS work.
Acceptance: ./asm/lumbda-full examples/ursa.lisp.txt loads Zoë's
CL source unchanged and produces identical answers to the Python and
C paths across the same test suite (tests/ursa.lsp).
Scope — three asm variants
| Binary | Flags | Size target | Features |
|---|---|---|---|
asm/lumbda |
(none) | ~22 KB | minimal Scheme, bump allocator |
asm/lumbda-gc |
GC_NAIVE=1 |
~55 KB | + mark-sweep GC + arena |
asm/lumbda-full |
CL_FULL=1 GC_NAIVE=1 |
~70 KB | + quasiquote + macros + case + prelude |
The CL_FULL guard ensures the default and -gc tiers stay at their
current footprint. All new asm lives inside .ifdef CL_FULL blocks.
Non-goals (stay out of scope)
defgeneric/defmethod— requires CLOS dispatch tables. The Scheme port'sdigitsfunction already hand-ports this as type-predicate cond, and the CL file omitsdigitsaccordingly.make-array :adjustable :fill-pointer,vector-push-extend,vector-pop— requires a dynamic-array type and portal-format extension. The Scheme port'srhouses a list-backed work stack, and the CL file omitsrho.- Generalized
setf(oncar,vector-ref, etc.) — simple-variable setf stays the supported shape.
Migration path
Staged commits:
- Makefile target + empty CL_FULL blocks (compiles, no behavior).
- Prelude load mechanism — at init (CL_FULL only), evaluate an embedded Scheme string that defines the small accessors.
- Quasiquote — reader recognizes
`/,/,@, produces(quasiquote …)/(unquote …)/(unquote-splicing …)forms. Evaluator special-form forquasiquotethat walks the template and emitscons/list/appendcalls. define-macro— new special form, macro table keyed by symbol, detection pass in the eval dispatch before procedure apply, re-eval of expansion result.caseas a macro (simpler than a special form once 4 exists).- Port
tests/zoe-favorites-test.shto run againstlumbda-full.
Commit after every stage compiles and passes asm-test. Roll back any stage that regresses the minimal or -gc tiers.
Test strategy
asm/test.sh --fullruns the existing 158 assertions againstlumbda-full(should all pass — CL_FULL is purely additive).tests/zoe-favorites-test.shlearns a third case that feedstests/ursa.lsptoasm/lumbda-fulland compares against the Python and C answers.- A new
asm/test-full.shor similar covers quasiquote, define- macro, and case in isolation.
What landed in this drop
asm/lumbda-fulltarget built withCL_FULL=1 GC_NAIVE=1. Default and-gctiers stay at their current footprint (all new code is guarded by.ifdef CL_FULL— 158/158 asm tests pass against every variant).- Rest args on
lambda/define— landed in default asm (earlier commit8cf6f44). Both.ac_bindand.apr_bindhandle(lambda (a . b) ...)and(define (f x . rest) ...). - Reader backtrack on digit-prefixed symbols —
1+,1-,abc123now parse as symbols. After accumulating digits, the reader peeks at the next char; if it is not a delimiter, input_pos is rewound and control falls through to the symbol reader. gensymbuiltin — formats"g%d"via an in-BSS counter, interns viaintern_static. Available in every variant.- Quasiquote / unquote / unquote-splicing — reader recognizes
`/,/,@(CL_FULL). Evaluator.ev_quasiquotewalks the template: unquote evaluates; unquote-splicing evaluates thenlist_append_absplices; other pairs recurse andmake_pair. No nested quasiquote support (deliberate — see Non-goals). define-macrospecial form — stores macros in a dedicatedmacro_env_headlinked list of 24-byte(sym, closure, next)nodes, separate from the value env. Eval dispatch checksmacro_lookupfor any symbol operator that is not a special form; on hit, the closure is applied to the unevaluated argument list and the expansion re-enters.eval_topunder TCO.- Prelude auto-loaded at startup (
load_cl_full_prelude) — evaluates an embedded Scheme string before the REPL starts. Definescaar,cdar,caddr,cadddr,cddr,cdddr,cddddr,1+,1-,add1,sub1,square,eq?(alias foreqv?),memq,list-ref,assq, andcaseas a macro. Input state is saved and restored around the load so user scripts see a pristine reader.
Verified on asm/lumbda-full
defun,setf,flet,multiple-value-bind,declareall expand and evaluate correctly.&optionalargs with defaults work.casemacro dispatches by value on flat-list keys withelse.- Simple
cl-loopforms work:while + do + finally return,for VAR from A to Bwithdo/thenbodies. - Quasiquote templates including
,@splicing produce correct shape. examples/ursa.lisp.txtloads to completion — everydefunregisters and its body parses under the shim.
Issues resolved in the follow-up drop (commit c6658e0)
All four were asm-side bugs surfaced by cl-loop expansions in Zoë's
programs; none were visible in the prior 158-test asm suite because
that suite never exercised apply on a variadic closure, a negative-
exponent expt, a macro-heavy workload long enough to trigger GC, or
the cadar accessor.
-
bi_applyclobbered its second argument.(apply f LIST)on asm was silently discarding LIST and calling f with no args. Fix: load the args-list straight into%rsi, stop copying the proc over itself. -
bi_exptlooped forever on negative exponents. cl-loop's look-ahead termination stages step values in alet*before checking the terminate predicate, so afor i from N downto 0clause ends up evaluating(expt 2 -1)on the last step. asm is integer-only; guard the negative case and return 0. The step's result is unused (look-ahead aborts the iteration), so returning 0 is correct for cl-loop's purposes. -
macro_env_headwas not a GC root. Under GC_NAIVE (which CL_FULL implies), a macro-heavy workload like miller-rabin's nested cl-loops triggered a collection partway through, which reclaimed every macro-table node. Next macro use failed with "unbound variable: cl-when" (or similar). Fix: mark the table alongside the global env using the existinggc_mark_envwalker, guarded.ifdef CL_FULL. -
Prelude missing
cadar.cl-loop-finalizer-expruses it to extract the return value from(finally (return X)). Added to the embedded prelude.
Zoë's full CL file now runs 18/19 on asm/lumbda-full. The one remaining failure is a test-fixture expectation about a specific random value, not an asm bug. All three asm variants pass 158/158 on their local suites.
Lingering follow-up (not blocking 0005 resolution)
examples/ursa-scheme.lsp—factorcrashes on some inputs under certain random seeds on asm (e.g. seed=2,(factor 91)). Default asm has no macro overhead but does hit this under long rhoff retry chains. Believed to be asm's bump allocator growth under deep recursion; independent of CL_FULL and out of ticket 0005's scope.
Risk
- Quasiquote in asm is non-trivial, especially nested
`inside another`. Start with depth-1 only; raise error on nested. define-macrochanges the eval dispatch path; regressions in minimal tier are catastrophic. Guard strictly with.ifdef CL_FULL; do not share dispatch tables between flavors.- Prelude load at init must survive portal resume — a portal file
dumped from
-fullloads state relative to the already-initialized prelude, which is an env snapshot. Verify portal round-trip stays green. - Stone-lisp image-based development (ticket 0004 framing) implies
some users keep
-fullprocesses alive indefinitely. The 1 MB GC chunks in-gcapply — memory discipline per CLAUDE.md stays in effect.