# Path to Full Common Lisp This document describes the path from the current FunCL bootstrap to a complete Common Lisp. It is intentionally an engineering roadmap, not a marketing contract. The governing rule is that every phase must preserve the self-host feedback loop: the compiler remains able to compile itself, and each large semantic change gets a small witness gate that catches drift between the host and self-host paths. ## Current Position FunCL is well past the toy stage. The current tree has: - a self-hosted stage1 compiler built from `src/*.lisp` (stage2 == stage3, byte-identical fixed point); - an AST-IR based byte emitter for x86-64; - direct ELF emission and an in-tree PE/COFF path selected by `ag-build-image`; - target OS shims for Linux and Windows boundary operations; - a REPL that JITs forms into an executable code buffer, with gdb JIT/DWARF integration; - code objects for REPL and file-compiled functions; - `.fco` save/load for user code objects and user macros; - `save-image` + image restart, with a kernel-build-id identity gate before CODE-MMAP install; - **real symbols and packages**: widetagged symbol records with name / home- package / value-cell / function-cell slots; deterministic interning; a real `symbol-package`; the static symbol pool unified with runtime package tables; `*package*` unified across the baked and runtime preludes; the symbol value- cell is the canonical storage for `defvar`/`setq`/`symbol-value`/`set`; - a largely first-class environment: `eval`, `load`, `compile`-into-the-image, `boundp`/`fboundp`/`symbol-function`/`makunbound`, and the bootstrap REPL split from the user-facing REPL (ADR 0013); - a working Shenandoah-style generational GC with frame records and root enumeration. That is a real Lisp core — Phases 1–2 are essentially complete and Phase 3 is mostly there (dynamic binding via `progv` and escaping closures landed). What it is **not yet** is a full image with the whole ANSI substrate: general arrays, bignums, and single-floats have landed, but **ratios** are still missing (the last leg of the number tower); the condition *system* (typed dispatch, `handler-bind`, restarts, `define-condition` classes, a debugger) is stubbed even though error *reporting* is solid, `catch`/`throw` are absent, and CLOS compiles (PCL bootstrap) but can't yet fully run. Full CL needs all of packages, symbols, globals, function cells, heap objects, code objects, dynamic bindings, conditions, streams, and compiler state represented as Lisp-visible objects with stable semantics; the remaining phases below fill that in. ## Runtime Model The long-term model should be: ``` executable = loader/runtime + serialized image image = heap graph + code objects + global environment + metadata ``` A code object is only one image component. It should contain executable bytes, constant/literal references, relocation records, calling convention metadata, debug/GC metadata, and a binding to a function object. An image additionally contains the root set: packages, intern tables, global value cells, function cells, macro definitions, class objects, condition/restart state that is valid to persist, and startup policy. The current `.fco` and `save-image` work should therefore be treated as Phase 0 image work: useful persistence of compiled definitions, not `save-lisp-and-die` yet. The next step is to make the saved unit an image object graph rather than a copied executable plus appended code-object delta. This roadmap is the authoritative implementation sequence. The PRD and ADRs describe required release capabilities and the intended end-state architecture; they do not require profile instrumentation, inline caches, SSA optimization, or deoptimization work before the semantic and runtime prerequisites below are in place. ## Non-Negotiable Invariants - Stage2 and stage3 must remain byte-identical, except during an explicitly documented transition that has its own witness gate. - The Emacs host may remain permanent, but it must not become a second language implementation with different semantics. - Every representation flip needs a rollback flag or a mechanically checkable witness. - The compiler should move toward compiling normal Lisp loaded into an image, not toward adding more special REPL/compiler-only global tables. - Cross-target support must stay structural: one compiler and runtime model, with platform-specific OS shims and binary wrappers. ## Phase 1: Stabilize Code Objects and Images Goal: make the current `.fco`/`save-image` path a correct code-object cache. Status: Largely done. `.fco` round-trips, `save-image`/restart work, the image build-id identity gate is in place (cave#12), and ADR 0023 landed relocatable serialization. Open: complete relocations + function-redefinition indirection (cave#46); fixed-size image/format buffers with no bounds-check (cave#18). Required work: - Record every code reference as relocation metadata, including references that were resolvable at compile time. - Replace specialized patch lists with typed relocation records carrying at least kind, offset, target, and any target-specific addend. - Give each code object its own constant/literal vector or explicit literal ownership. - Define a versioned, extensible code-object metadata contract. It must identify the target architecture/ABI and compiler pipeline (`direct` initially), and reserve optional sections for source maps, safepoints, stack maps, profile data, dependencies, deoptimization state, code version, and invalidation state. Reserved sections need not be populated yet. - Keep stable source-site identities through AST-IR lowering so later call-site, branch, allocation, and safepoint metadata can refer to them without changing the serialized code-object shape. - Keep exploratory REPL thunks out of persistent state unless explicitly saved. - Version `.fco` by target ABI and runtime ABI. - Replace fixed-size image buffers with length-driven reads/writes or explicit overflow errors. - Define the load order: runtime prelude, packages, symbols, macros, functions, globals, then entrypoint. Exit gate: - `save-cos` and `load-cos` round-trip mutually recursive functions, macros, prelude calls, function values, quoted symbols, and redefinitions. - `save-image` produces a binary that starts with the same user-visible function/macro environment as the saving REPL. ## Phase 2: Real Symbols and Packages Goal: stop treating symbols as byte-vectors and land ANSI package identity. Status: Done. Symbols are widetagged records; interning is deterministic and witness-gate-checked across host/stage1/self-host; the static pool is unified with runtime package tables (cave#71); `*package*` is unified across baked and runtime preludes (cave#72); the value-cell is canonical storage (cave#45); `find-package`/`intern`/`find-symbol` (with package designators, cave#75), `symbol-name`/`symbol-package`/`make-symbol`/`gensym`/`defpackage`/`in-package` and package-aware printing all work; keywords self-evaluate. The accepted direction is already correct: dispatch shim first, symbol objects second, user-level package machinery third. Required work: - Flip symbols to widetagged heap records with name, home package, and binding slots. - Make interning deterministic across host and self-host paths. - Make the literal pool key derivable from package plus symbol name. - Implement keyword symbols as self-evaluating objects. - Add user-facing `find-package`, `intern`, `find-symbol`, `symbol-name`, `symbol-package`, `make-symbol`, `gensym`, `defpackage`, and `in-package`. - Teach the printer package-aware symbol printing. Exit gate: - The witness dump for packages and symbols byte-matches between host, stage1, and self-host. - Package-heavy source can be read without flattening names into fake symbols. ## Phase 3: Environments, Bindings, and Evaluation Goal: make the Lisp environment first-class enough for `eval`, `compile`, and `load` to be runtime operations. Status: Essentially done; the ADR 0013 exit gate is met. Value and function cells are separate; `eval`/`load`/`compile`-into-image, `boundp`/`fboundp`/ `symbol-function`/`set`/`makunbound`, and bare `defvar`/`setq`/`symbol-value`/ `boundp` agreement (cave#45) all work. Dynamic binding works: `let` of a registered special saves/restores, and `progv` is implemented for registered specials (cave#51). Closures lift correctly when they escape — returned from a function or bound to a closed let-init lambda (cave#27). Remaining edges: function-entry redefinition indirection (cave#46); a let-init lambda capturing a *sibling* let binding still needs true runtime lexical closures (funcl is lift-only); `progv`/`let` on an *unregistered* free var (free-vars-as-special) is deferred — it would break cave#35's PCL trap; `return-from`/`go` bypass `unwind-protect` cleanup. Required work: - Separate value cells and function cells. - Define function-entry indirection so a function cell can later switch between direct and optimizing versions without changing the Lisp calling convention. Only the direct version needs to exist in this phase. - Implement global special bindings and dynamic binding stacks. - Add lexical closures and function objects. - Define `fboundp`, `boundp`, `symbol-value`, `symbol-function`, `fdefinition`, `set`, `makunbound`, and `fmakunbound`. - Implement `eval` over the real environment, not as a REPL-only path. - Make `compile` produce code objects installable into the running image. - Make `load` update packages, globals, macros, and functions consistently. Exit gate (revised per ADR 0013): - A file loaded in the REPL can define macros, globals, and functions; bare `defvar`, `setq`, `symbol-value`, and `boundp` agree on what's bound; `eval` is callable from user code; the bootstrap REPL splits cleanly from the user-facing REPL application. - Image save / restart / cross-restart compilation moves to ADR 0014 (the image format) — it depends on Phase 3's environment work but is itself Phase 3.5 or a new phase, not part of the Phase 3 exit gate. ## Phase 4: Core ANSI Data Types Goal: provide the object substrate required by real CL libraries. Status: Partial — the largest remaining gap, but advancing. Have: strings distinct from byte-vectors (ADR 0021), vectors (cave#11), general arrays — `make-array`/`aref`/`array-dimensions`/rank/total-size, 1-D and multi-dimensional, on a runtime-sized vector allocator `%alloc-vector`; hash tables, `defstruct`, setf expanders / generalized places, sequence functions over lists, multiple values (`multiple-value-bind`/`-list`/`nth-value`, cave#50), characters (`characterp`/`char-code`/`code-char`/`char-upcase`/ `digit-char-p`/…); **bignums** (sign-magnitude base-2²⁸, transparent fixnum promotion through `%generic-*`, cave#52) and **single-floats** (immediate `(ieee32<<8)|0x2F`, SSE arithmetic, exact decimal→binary32 reader, contagion, printer — cave#52). Missing: **ratios** (the remaining number type — `1/3` reads as a symbol); double-floats (single only); char comparison ops (`char=`/`char<`/`char-equal`); array fill-pointers / adjustable / nested multi-dim `:initial-contents`; hash-table robustness — no auto-rehash and a structural `hash-table-p` false-positive (cave#19). Known gap: stage1 (the elisp-host build) can't yet *read* a float literal token (cave#78); funcl / funcl-cold read floats correctly. Required work: - Characters and strings distinct from byte-vectors. - General vectors and arrays. - Hash tables with CL equality modes. - Multiple values. - ~~Bignums~~ and ~~floats~~ (single) landed (cave#52); **ratios** remain. - Pathnames enough for ASDF/Quicklisp-style loading. - Structures via `defstruct`. - Setf expanders and generalized places. - Sequence functions over lists and vectors. Exit gate: - Alexandria-style utility code can run without compatibility patches. - The reader/printer can round-trip ordinary symbols, strings, lists, vectors, characters, and numbers. ## Phase 5: Control Flow and Conditions Goal: support the control abstractions that real Common Lisp code assumes. Status: Partial. Have: `block`/`return-from`, `tagbody`/`go`, `unwind-protect`, a working single-level `handler-case`, and best-in-class error *reporting* — typed condition objects, report-and-continue at the REPL, and signalled arity / undefined-function / type / division-by-zero errors (ERRORS.md). Missing: `catch`/`throw`; and the real condition *system* — `handler-bind`/`restart-case`/`define-condition`/`signal` are no-op stubs, there is no typed handler dispatch, no restarts, no standard condition hierarchy, and no debugger loop (cave#16/#57/#58). Required work: - `block` / `return-from`, `catch` / `throw`, `tagbody` / `go`. - `unwind-protect`. - Dynamic extent cleanup. - Condition objects, `handler-bind`, `handler-case`, `restart-case`, `invoke-restart`, and a basic debugger loop. - Standard condition hierarchy enough for ANSI and library code. Exit gate: - Non-local exits correctly unwind dynamic bindings and cleanup forms. - Conditions can cross compiled function boundaries and present restarts at the top level. ### Phase 5 — design (the unified dynamic control stack) Decided 2026-06-14: conditions are **struct-free** for now — a condition is a `(cons TYPE-NAME slot-plist)` (matching today's `make-condition` / `make-division-by-zero`), with a runtime **type-hierarchy registry** (NAME → parent names). CLOS isn't running in funcl yet (`defclass` doesn't compile; mini-clos is unbuilt with active macro/cave#34 blockers), so coupling conditions to CLOS would mean doing Phase 6 first. Struct-free conditions keep Phase 5 self-contained and are re-homed onto real CLOS classes in Phase 6 — exactly how SBCL bootstraps. The architectural core is **one runtime control stack** (a dynamic/special var) that unifies every dynamic non-local construct as a tagged frame: - `:catch` — (tag, saved-rsp, saved-rip); established by `catch`, target of `throw`. - `:handler` — list of (type-test . handler-fn); pushed by `handler-bind`, walked by `signal`. - `:restart` — list of restart records; pushed by `restart-{case,bind}`, walked by `invoke-restart`/`find-restart`/`compute-restarts`. - `:cleanup` — a thunk; pushed by `unwind-protect`, run by ANY unwind crossing it. A non-local transfer is one primitive: **unwind-to(frame F, value V)** — pop frames from the top down to F, running every `:cleanup` thunk passed, then `longjmp` to F's saved rsp/rip with V. This generalizes today's *single-level* `handler-case` longjmp to N frames; `throw`, handler exit, and `invoke-restart` all reduce to it. `handler-case` becomes `(block B (handler-bind ((type (lambda (c) (return-from B …)))) body))`; `signal` walks `:handler` frames top-down (a handler that returns "declines"); `error` = `signal` then debugger/abort. Stages (each green + committed): - **C1 — DONE (simpler than planned).** funcl's condition-raise chain is already the unified dynamic-unwind channel: `handler-case` catches any raise and `unwind-protect` rides on it (catch → cleanup → re-raise). So `catch`/ `throw` drop in as control-transfer *tokens* on that channel — pure prelude macros (`throw` raises a distinguished token; `catch` is a `handler-case` that returns on tag-match, else re-raises) with ZERO new runtime/codegen and no rewrite of the single-level machinery. `unwind-protect` cleanups run on `throw` automatically (it already catches+cleans+re-raises), and since special-var bindings ride on `unwind-protect`, they unwind too. Consequence: C3/C4 build `handler-case`/`restart-case` on `catch`/`throw` (gensym tags), NOT on escaping `block`/`go` — funcl's lexical `block`/`return-from` stay as-is, sidestepping the originally-feared control-stack rewrite entirely. - **C2** — condition objects + hierarchy registry + `define-condition` (direct expansion, no expander-time defun calls → avoids cave#34); standard tree (`condition` ⊃ `serious-condition` ⊃ `error`/`warning`, `type-error`, `arithmetic-error` ⊃ `division-by-zero`, …); built-in raised errors become instances; fix `typep`/add `condition-typep` over the hierarchy. - **C3** — DONE. **C3a** typed, multi-clause `handler-case` (condition-typep dispatch + re-raise on no match). **C3b** `*handler-clusters*` + `handler-bind` + `signal`: handlers run in the signal's dynamic context (no unwind), escape via `throw`; unhandled `signal` returns NIL; nested handler-binds run newest-first; `*handler-clusters*` restored via unwind-protect (cave#83 work-around). Closures live in the cluster and are funcall'd from there (needed cave#84). Nested handler-binds expand to nested `unwind-protect`s; the inner handler/funcall lambdas used to be trapped unlifted because `unwind-protect` bound its protected form in a LET *initializer* (only the conservative closed-lambda lifter runs there) — fixed by evaluating the protected form in a SETQ in the let *body* (full lifting), not in the initializer; the lambda-lifter itself is untouched (warm-safe). `error`/`warn` integration with handler-bind deferred to C4/C5. - **C4** — DONE. Restarts on the catch/throw channel, parallel to handler-bind. `*restart-clusters*` dynamic stack; `restart-case` pushes a cluster of `(NAME TAG . INDEX)` records (TAG a fresh per-invocation eq-unique cons) and catches TAG; `invoke-restart` finds a record by NAME (newest-first) and THROWs a restart-result carrying INDEX + args, unwinding to the restart-case, which dispatches to the matching clause body bound to the args. A handler-bind handler can invoke a restart established further out (signal runs the handler in-context → invoke-restart → unwind to restart-case). `find-restart` / `compute-restarts` walk the clusters; standard restarts (`continue`, `abort`, `use-value`, `store-value`, `muffle-warning`) invoke the like-named restart if active. FORM is evaluated in a SETQ body (full lifting), not a LET init (cave#84-class trap). `restart-bind` / `with-condition-restarts` remain stubs (lower-level / restart↔condition association — not needed yet). - **C5** — IN PROGRESS. `error` is now a prelude defun = `signal` (run handler-bind handlers, which may transfer control via `throw`/ `invoke-restart`) then `%raise` (the renamed raw-raise special form) so the nearest `handler-case` catches or the top-level trampoline reports + exits. This closes the gap where `error` raised straight to `handler-case`, bypassing `handler-bind` — the full asdf/pcl recovery pattern (error → handler-bind → invoke-restart → restart-case) now works. The control macros (throw/catch/unwind-protect/%hc-dispatch) re-raise via `%raise` directly so a throw-token or a re-raise never re-runs handler-bind. `warn` (signal + a muffle-warning restart), `cerror` (continue restart), and a batch `invoke-debugger` (report condition + active restarts, then `%raise`) added. Divergence from CL: funcl runs ALL handler-bind handlers (via signal) then handler-case (via %raise), rather than interleaving them by dynamic nesting — acceptable for now. **asdf/pcl retest (2026-06-15):** the condition system is no longer the blocker — `pcl-bootstrap` still fails at `std-class.lisp` (CLOS metaobjects: the `slot-value` setf-expander chain), exactly as before the condition work, and `asdf.lisp` on its own remaining gaps + the reader consing-dot issue (cave#43). Both stay xfail; unblocking them is CLOS- metaobject + reader work, not conditions. The condition+restart system (C1–C5) is functionally complete. Independent pre-work: `typep` corrected for the number tower (number/real/rational/integer/float/ratio); the `fixnump`/`integerp` split is DONE (cave#39 / R5) — `fixnump` is the fast fixnum-only special form (the old `integerp` lowtag check, renamed), and `integerp` is now a broad defun `(or fixnump bignump)`, so `(integerp BIGNUM)` is true. All fixnum-only internal guards (compiler offsets, byte positions, hash codes, printers where a `bignump` clause already precedes) were audited to `fixnump`; integer-semantic sites (`equal`, `typep` integer/range/byte-specs, mini-clos `class-of`/`typep`) use the broad `integerp`. A subtlety: the cold tree-walker macroexpander does not evaluate `symbolp`/`not` reliably in a macro body, so predicates there use only `null`/`consp`/`bv-eq` (same trap as cave#43). **Validated against ECL, Mezzano, SBCL/CMUCL (2026-06-14).** Three findings sharpen the plan: 1. **The control stack is one tagged frame stack with a per-frame saved binding-stack depth, and one unwind primitive.** ECL's `ecl_unwind` (`~/CL/ecl/src/c/stacks.d:768`) and Mezzano's `%%unwind-to` (`~/CL/mezzano/runtime/runtime.lisp:15`) are each ~15 lines: walk top→target, dispatch per frame kind (pop a special binding / run an unwind-protect cleanup / invalidate a block-or-tagbody exit), restore the binding-stack depth, then `longjmp`. Multiple values must live OUTSIDE the jmpbuf (a thread `values[]`); unwind-protect saves/restores them around its cleanup. ECL stops at the first unwind-protect barrier and re-invokes the unwinder (one hop per barrier); Mezzano runs all teardowns in one interleaved pass. 2. **Handlers and restarts are NOT control-stack frames — they are two dynamic variables.** `*handler-clusters*` / `*restart-clusters*`, each a list of clusters, pushed by a plain `let` rebinding in `handler-bind` / `restart-bind`. `signal` walks newest-first, rebinding `*handler-clusters*` to the *older* clusters while running each type-matching handler (the "handler not active within itself" rule); a handler that returns "declines" and the walk continues; if all decline, `signal` returns NIL. `error` = `signal` then `invoke-debugger`; `warn` = `restart-case`+`signal`+ `muffle-warning`. The whole system needs only `(typep condition type)` — **no CLOS**. 3. **`handler-case`/`restart-case` are macros = `block` + `tagbody` + `handler-bind`/`restart-bind`, where each handler/restart closure does `(go TAG)`.** Restart transfer is `go`/`return-from`, NOT `catch`/`throw`. So the ONLY runtime control primitive the entire condition+restart system needs (beyond dynamic binding, which funcl has) is **an escaping non-local exit**: a closure captures a `block`/`tagbody` exit point and invokes it from a deeper dynamic context (`signal`, called deep in the stack, funcalls the handler closure whose `go` must unwind back out to the `handler-case`). **This is the crux of C1.** funcl's current `block`/`return-from` and `tagbody`/`go` are LEXICAL — compile-time same-function jumps that cannot escape a call boundary or run intervening `unwind-protect` cleanups. C1 must make them ESCAPING on the unified control stack (push an exit-point frame; `return-from`/`go`/`throw` = `unwind-to` that frame). Once escaping exits + unwind-protect-on-unwind exist, C2–C4 are ~200 lines of portable Lisp (the `signal`/handler-walk + the macros). Reference reading: ECL `src/c/stacks.d` + `src/h/stacks.h` + `src/clos/conditions.lsp`; Mezzano `runtime/runtime.lisp` + `system/{condition,restarts,error}.lisp`; SBCL `src/code/{cold-error,error,restart,target-error}.lisp`; CMUCL `src/code/error.lisp`. ## Phase 6: CLOS and Generic Dispatch Goal: implement the object system in a way that can later support optimization without changing semantics. Status: In progress. A mini-CLOS exists and the PCL bootstrap *compiles* (green test), but CLOS can't fully run until the Phase 4/5 substrate (arrays, conditions, dynamic binding) fills in. Required work: - Class objects, instances, slots, allocation, and initialization. - Generic functions and methods. - Method combination. - Slot access protocol. - A minimal MOP matching the project PRD's supported surface. - Redefinition rules for classes, methods, and generic functions. Exit gate: - CLOS-heavy libraries can define classes, generic functions, methods, and metaclass hooks within the supported MOP surface. ## Phase 7: Files, Streams, ASDF, and the Library World Goal: run normal Common Lisp projects, not only self-contained compiler tests. Status: Partial. File I/O, `format`, and an asdf-lite that compiles exist; real stream objects, `compile-file`/FASL, and full ASDF loading do not. Required work: - Real stream objects and standard streams. - File streams, string streams, broadcast/concatenated streams as needed. - `read`, `print`, `format`, and pathname operations at library quality. - `compile-file` and a FASL/image-code-object format. - ASDF loading support. - Compatibility packages for implementation extensions where the PRD allows them. Exit gate: - A pinned ASDF/Quicklisp corpus loads and runs without source patches except for explicitly documented unsupported implementation-specific features. ## Phase 8: Garbage Collection and Safepoints Goal: make long-running CL programs practical. Status: Runs, not yet robust. A first collector with frame records and root enumeration works and the REPL/compiler run under it; precision and robustness issues are open — rest/optional-prologue allocation sites are unwalkable (cave#64), precise-walk desync under churn (cave#67), commit/decommit handling (cave#62), and the GC-neutral allocation boundary + authoritative object descriptor table (cave#47/#48). Required work: - Precise object maps for stack frames and code objects. - Finalize the safepoint polling ABI and populate the stack-map/safepoint metadata sections reserved during Phase 1. - Allocation paths with safepoints. - Root enumeration for packages, globals, dynamic bindings, threads/fibers, code objects, and image metadata. - A first collector that validates the interface before the target concurrent collector lands. - Weak references and weak hash tables later, once object movement and final root policy are stable. Exit gate: - The compiler and REPL can run under GC stress with image save/load still preserving reachable persistent state. ## Phase 9: Full Image Save Goal: make `save-lisp-and-die` real. Status: Partial. `save-image` + restart work (heap graph from explicit roots, build-id-gated CODE-MMAP, package/symbol/binding restore — cave#45/#73/#74/#12), but the exit gate is not met: we cannot yet build a compiler image by loading the sources into a running FunCL and saving, then use that image as the normal compiler. Today's save is still closer to "executable + serialized state" than "runtime + image." Required work: - Serialize the heap graph from an explicit root set. - Serialize code objects with relocations and GC/debug metadata. - Serialize package and symbol tables. - Serialize value/function/macro bindings. - Record runtime ABI, target ABI, feature set, and entrypoint. - Define what is not saved: live stacks, raw file descriptors, sockets, OS handles, blocked syscalls, and active foreign state. - Reinitialize transient runtime state at image startup. Exit gate: - A compiler image can be built by loading compiler sources into a running FunCL, saving the image, then using the saved image as the normal compiler. - Application images can specify a toplevel and start without entering the development REPL. ## Phase 10: Performance and Concurrency Goal: make the complete implementation competitive enough for the PRD. Status: Not started (correctly deferred — see the note at the end of this file). The direct compiler and bootstrap shortcuts are still in place. Required work: - Direct compiler cleanup after semantics stabilize. - The optimizing compiler and SSA/lower IR described in the IR ADRs. - Inline caches for generic dispatch, slot access, and function calls. - Runtime profiling, dependency tracking, invalidation, and tier promotion. - Representation-aware unboxing. - Fibers, scheduler, nonblocking I/O, and OS integration. - Concurrent/low-pause GC once the precise root interface is proven. Exit gate: - The compiler, library corpus, CLOS workloads, and fiber benchmarks run under the intended runtime rather than under bootstrap-only shortcuts. ## Recommended Near-Term Order The original near-term sequence is largely behind us: 1. ~~Finish `.fco` correctness and embedded `save-image` tests.~~ Done. 2. ~~Land the symbol-object flip behind the ADR 0012 witness gates.~~ Done. 3. ~~Implement user-level package operations.~~ Done. 4. Move globals/function cells/macros into image-owned environment objects. *Partial* — the symbol value-cell is now the canonical binding (cave#45), but macros still live in a compiler-side table and function cells aren't yet full image-owned environment objects. 5. Turn `save-image` from "executable plus CO delta" into "runtime plus image". *Partial* — restart works; the load+save-the-compiler exit gate (Phase 9) isn't met yet. With Phases 1–2 done and the "don't start CLOS/ASDF/GC until packages, symbols, bindings, and code objects are real" gate cleared, the live frontier is three-way: - ~~**Finish Phase 3**: dynamic special bindings / `progv` and closures that survive escape.~~ Done — `progv` (cave#51) and escaping/closed-let-init closures (cave#27) landed; the remaining Phase-3 edges (cave#46 redefinition indirection, sibling-capturing let-init lambdas, free-vars-as-special) are narrow. The frontier is now two-way: - **Phase 4 data types**: general arrays landed (1-D + multi-dim on `%alloc-vector`); ~~bignums~~ and ~~single-floats~~ landed (cave#52, the number-tower design pass). **Ratios** are the last leg of the number tower — smaller than bignums/floats since they reuse the bignum arithmetic + gcd substrate (see "Ratios — scope" below). The other Phase-4 remainders (char comparison ops, array fill-pointers/adjustable, hash-table auto-rehash) are small and additive. - **Phase 5 real conditions**: `catch`/`throw` plus a typed condition system (`handler-bind`, restarts, `define-condition` classes, typed `handler-case` dispatch, a debugger) — builds directly on the error-reporting work already in place and unblocks CLOS/PCL. ### Ratios — scope (Phase 4 number tower, step 3) The number tower is two-thirds done: fixnums, bignums (sign-magnitude base-2²⁸), and single-floats, all dispatched through the both-fixnum-guarded `%generic-*` hot path with transparent promotion. Ratios are the last leg and are **smaller than bignums or floats** because the hard substrate now exists: general bignum division — `bignum-divmod` (schoolbook long division, base-2²⁸, binary quotient-digit search) behind the both-fixnum-guarded `%generic-truncate`/`-rem` slow path — landed with cave#79, so `truncate`/`rem`/`mod`/`/` and `gcd` are all correct over bignums. The `%generic-*` lattice + exact-rational machinery (built for the float reader) are reusable. - **R0 — fix `gcd` over bignums (cave#79). DONE.** The real cause was not gcd but `truncate`/`rem` compiling to an *unguarded* `idiv` (no fixnum/bignum check), so bignum operands produced garbage that gcd then dereferenced. Fixed by adding the both-fixnum guard + general `bignum-divmod` slow path. Then the ratio work proper: - **R1 — representation.** A boxed leaf, new widetag (bignum is 0x04, double-float reserved 0x08 → take 0x0C), two slots `numerator` + `denominator` (each a fixnum or bignum). `%make-ratio`, `%ratio-num`, `%ratio-den`, `ratiop`. NOTE: this is the first *number* heap object with interior boxed pointers (bignum is a pointer-free leaf), so `gc-mark`/heal must scan both slots — confirm and add a test. - **R2 — normalize.** `make-rational(n,d)`: force `d > 0` (sign onto `n`), divide both by `gcd(|n|,|d|)`, and collapse `d == 1` back to an integer (fixnum or bignum). This is where R0 (cave#79) bites. - **R3 — reader.** A `[+-]?digits/digits` token → `make-rational` (reuse the integer token parse for each half; reject `1/0`). Today `1/3` falls through to a symbol. - **R4 — arithmetic + contagion** (non-additive — touches `%generic-*`, so full-suite + PCL gate, like bignum Stage C). Extend the lattice with rational ⊃ integer: `a/b ± c/d = (ad±bc)/(bd)`, `* = ac/bd`, all normalized; ratio+integer → ratio; ratio+float → float; ordered comparisons. **Open decision:** funcl's `/` currently truncates int/int (`(/ 7 2)` → 3). ANSI CL `/` is *exact* (`(/ 7 2)` → `7/2`). Going CL-correct means `/` returns a ratio (with `truncate`/`floor`/`rem`/`mod` keeping the integer ops) — a deliberate semantic change to settle before R4. - **R5 — printer.** `num"/"den`; dispatch `ratiop` before `integerp` in `ag-print-thing` / `fmt-print-thing` / `fmt-princ-thing` (the integer-part printer already exists). - **R6 — predicates/types.** `rationalp` = `(or integerp ratiop)`, `numerator`, `denominator`; `typep` for `ratio`/`rational`/`real`/`number`. Fold in the deferred Stage-C/F3 cleanup here: split `fixnump` out of `integerp`, make `integerp = (or fixnump bignump)`, and audit fixnum-only guards (hash-of etc.) → `fixnump`. - **R7 — tests + manifest.** Drive the matrix through funcl-cold (stage1 can't read float/ratio literal tokens yet — cave#78); resync `cold-bootstrap-retained.lisp`. This closes the number tower; after it, the live Phase-4 remainders are small (char comparison ops, array fill-pointers/adjustable, hash-table auto-rehash), and the next big lever is **Phase 5 conditions** (the CLOS/PCL unblocker). Avoid starting CLOS, ASDF, or GC before packages, symbols, bindings, and code objects are real. Those systems depend on stable identity and root semantics; building them on byte-vector symbols or compiler globals will create work that has to be unwound later. Likewise, do not start profile counters, inline caches, SSA optimization, escape analysis, deoptimization, OSR, or background compilation in the near-term sequence. Preserve their metadata and ABI requirements now; implement them when the corresponding phases make their semantics measurable and stable.