Third asm variant — built with CL_FULL=1 GC_NAIVE=1 via new Makefile
target. Adds the macro machinery needed for cl-compat.lsp on the asm
tier, keeping every addition behind .ifdef CL_FULL so the default
(~22 KB) and -gc binaries keep their current footprint.
Landed in this drop:
* Reader: backtrack on digit-prefixed symbols. After reading digit
characters, if the next char is not a delimiter, input_pos
rewinds and control falls through to .sr_symbol. Makes 1+, 1-,
add1, abc123, and any CL-style identifier with a numeric prefix
parse as symbols instead of truncating to a bare integer.
* Reader: `` ` `` / `,` / `,@` produce (quasiquote X) / (unquote X)
/ (unquote-splicing X) forms. Same build shape as the existing
`'` quote branch.
* Evaluator: .ev_quasiquote + quasiquote_expand walk the template.
unquote evaluates its argument in the current env; unquote-
splicing evaluates then splices via a new list_append_ab helper;
other pairs recurse (cons expand-car expand-cdr). Atoms pass
through. No nested quasiquote depth (deliberate; ticket 0005
scope).
* Evaluator: .ev_define_macro + macro_env_head linked list. Each
(define-macro (name p...) body) prepends a 24-byte
(sym, closure, next) node. Dispatch in eval checks macro_lookup
after all special-form compares; on hit, the closure is applied
to the *unevaluated* argument list and the expansion re-enters
.eval_top under TCO.
* Binding: rest-arg support extended to .apr_bind inside
apply_proc_raw. Previously only .ac_bind (direct .app_closure
path) handled `(lambda (a . b) ...)` correctly; macros call
closures through apply_proc_raw, so this was required to make
variadic defun/setf macros bind correctly.
* Builtin: (gensym) — writes "g%d" for an in-BSS counter, length-
prefixes the buffer, calls intern_static. Available in every
variant (not CL_FULL-gated — useful outside macros too).
* Builtin: (cadr x), (sort lst) and the let* special form from
earlier commit stay in default asm. These are Scheme staples.
* Prelude: evaluated at _start after init_builtins / rng_seed,
before the REPL. Embedded string, input state saved + restored
around the load. Defines caar, cdar, caddr, cadddr, cddr,
cdddr, cddddr, 1+, 1-, add1, sub1, square, eq? (= eqv? for
interned symbols), memq, list-ref, assq, and `case` as a macro.
cl-compat.lsp: two small changes to work under asm's single-list
`map`:
* Added cl-zip helper. Replaced two `(map (lambda (v n) (list v n))
xs ys)` sites with `(cl-zip xs ys)` — asm's builtin map accepts
only one list, and cl-loop-emit needs a parallel walk over
state-vars and new-names.
* Added explanatory comment for cddddr at the top of the shim
(already shipped).
Tests:
* make asm-test (lumbda) — 158/158 pass.
* make asm-test-gc — 158/158 pass.
* make asm-test-full — 158/158 pass on synchronous run.
* Zoë's `examples/ursa.lisp.txt` LOADS on asm/lumbda-full.
`(expt-mod 3 7 100)` = 87.
Most simple cl-loop forms work (while + do + finally, range-to,
then-accumulator).
Known open issues documented in docs/tickets/0005-asm-cl-full.md:
* cl-loop-emit produces wrong output for inputs with `simple` iters
(`(simple a 5)` → state binding dropped). Python/C return the
correct form; asm version is missing the binding. Bug surfaces
in the emit's 30+ binding let*; could not pin down in this
session. Downstream effect: `(miller-rabin n)` and similar
defuns that depend on `cl-loop repeat k for a = ... unless ...
return nil` don't produce usable expansions, so Zoë's acceptance
suite does not run end-to-end on asm/lumbda-full yet.
* examples/ursa-scheme.lsp — `factor` crashes on asm under some
random seeds (bump-allocator exhaustion on long rhoff retry
chains). Out of CL_FULL scope; tracked in same ticket.
Next steps live in ticket 0005. This commit ships the infrastructure
so the remaining work is a debugging exercise against a reproducible
minimal case, not a feature build.
8.7 KiB
0005 — asm/lumbda-full: CL compat on the asm tier
Status: partially resolved — infrastructure landed, cl-loop-emit edge case open 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.
Known open issues (follow-up work)
-
cl-loop-emitcrashes on some parsed inputs. Specifically,(cl-loop-emit '(() ((simple a 5)) () () () #f () ()))returns(let* () (let g0 () (g0)))— thesimpleiter's state binding is dropped. Same input on Python and C returns the correct(let* ((g3 5)) (let g0 ((a g3)) ...)). The parse output on asm is correct; the bug is inside the emit's giantlet*(30+ bindings). Reproduced in isolation; could not pin down after a few hours — likely an asm-side env or stack interaction that surfaces only inside this specific call shape.Consequence:
(miller-rabin n)and other defuns whose bodies usecl-loop repeat k for a = ...expand incorrectly and crash at run time. Zoë's full acceptance suite does not run end-to-end yet onasm/lumbda-full. -
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 / stack growth under deep recursion; out of CL_FULL'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.