#318 Epic: "be dynamic linking" — address-model migration for fast loading & linkage (ADR 0033)

open
Opened by atgreen

Epic: "Be dynamic linking" — address-model migration for fast loading & linkage

Tracking issue for the campaign defined in ADR 0033 and PRD-loading-and-linkage.md (both on main, committed 4c9731b). This is an epic; individual stages get their own issues that link back here.


1. Goal in one sentence

Move funcl's warm/product loading path from the slow middle of the source → fasl → image spectrum to the fast right end (dynamic linking), so build/funcl-asdf boots to an asdf-ready REPL in a fraction of a second instead of ~2.8 s (image) / ~10 s (fasl replay), by deleting per-object reconstruction and per-site relocation, not by tuning them.

2. Why it is slow today (the problem)

Two root causes, both instances of funcl's absolute-address worldview:

  1. Early binding. The compiler bakes each callee's absolute address into every call site, so loading a fasl is an O(reference-sites) relocation re-walk (the per-CO patch pass in the FUNCLFA replay path).
  2. Overlay image. save-image serializes the heap by a GC walk (collect reachable objects, index-encode inter-object pointers) and the restore path is O(live-objects) allocate-and-decode. Worse, the image is restored onto an already-cold-booted runtime that has already built its own primordial packages / symbol pool / keyword pool, so restore must reconcile identity (keyword canonicalization, pkgsym-offset rebuild, pool-slot relocation). This reconcile path is the source of a whole bug class (cave#74/#44/#132/#135/#136/#137/#314).

This is the inverse of how C dynamic linking is fast (PC-relative code needs no fixup; GOT/PLT indirection makes relocation O(symbols) not O(references); lazy binding defers resolution to first call).

3. Target architecture — two axes

Read PRD-loading-and-linkage.md for the full statement. In brief:

4. Docs to read first (in order)

  1. PRD-loading-and-linkage.md — the target architecture + baked-in constraints + acceptance criteria.
  2. docs/decisions/0033-recover-not-rewrite-migration.md — the governing decision (recover, don't rewrite) + the staged migration ordering.
  3. docs/decisions/0025-security-posture-and-relocation-bitmap-image.mdAxis B is largely this ADR's D2/D3. The relocation-bitmap format, save/load procedure, and the delta-0-hides-missing-bits trap are all specified here.
  4. docs/decisions/0032-warm-call-linkage-through-fn-cells.md — Axis A, Stages A/B already shipped; read the codex-P1 deltas.
  5. docs/decisions/0001-gc-algorithm.md + GC.md — exact pointer-slot metadata requirement; the bitmap MUST be built on the same slot descriptors the (future, moving) GC uses, not a private map.
  6. docs/decisions/0014-image-format.md — the current FUNCLIMG format Axis B supersedes the loading half of.

5. Code map (accurate as of 4c9731b)

Axis A (compiler + linkage):

Axis B (image + loader + regions):

6. Migration plan + acceptance gates

Strangler-fig: every new path is built ALONGSIDE the old, proven, then the old retired. The tree is never broken; suite-green at every stage (make test-fast-sharded AND the ANSI run AND (asdf:load-system "alexandria") — the last one is non-negotiable; cave#311 was suite-green while asdf was broken).

Axis A (continues ADR 0032 tracking):

Axis B (file B1–B6 as sub-issues, cross-ref ADR 0025 D2/D3). Order is deliberately ledger-first so the hard invariant is exercised before anything is deleted:

Recommended first PR (smallest de-risk): B1 + B3 — land the boot seam still dispatching to the old loader, and start emitting the relocation bitmap while delta is zero. Builds the load-bearing ledger with zero behavior change and no deletion, so nothing can regress.

7. Invariants / guardrails (do not violate)

8. funcl-specific gotchas an outside dev WILL hit (each cost us a day)

9. Definition of done (all simultaneously)

10. Scope note

This is an address-model migration — it touches codegen operands, image format, symbol residency, GC slot metadata, and the eventual PIE/ASLR posture. It does NOT touch the language layer (reader, macroexpander, type system, mini-clos, conditions, ELF/PE emitter) — those self-host today and stay as-is. Wide but shallow: how addresses are encoded and relocated, not what the language means.

Comments (2)

atgreen3993109387

Sub-issue breakdown

Axis A (code linkage, continues ADR 0032):

  • #319 — A2: retire per-site text relocation (PC-relative linkage slot)
  • #320 — A3: widen linkage table to funcall + setf/fdefinition (depends on #319)

Axis B (relocation-bitmap authoritative core, ADR 0025 D2/D3) — ledger-first order:

  • #321 — B1: boot seam, dispatch to old loader (first step)
  • #322 — B2: raw-region dump/load at preferred addresses (depends on #321)
  • #323 — B3: emit relocation bitmap from GC slot descriptors, delta-0 (first step)
  • #324 — B4: forced non-zero-delta verifier — THE correctness gate (depends on #322+#323)
  • #325 — B5: authoritative image + flip probe to adopt (depends on #324)
  • #326 — B6: retire reconcile/reconstruct paths (depends on #324 green + #325)

Starting with #321 (B1) + #323 (B3) on branch dynlink-b1-boot-seam.

atgreen3993113711

Ordering finding: B2 part 2 is entangled with B5 (not a clean isolated stage)

While reading the save/restore path for B2 part 2, found that a raw-heap round-trip cannot be cleanly separated from B5 (authoritative adoption), contra the ADR 0033 ordering:

The current restore pipeline is coupled to the OBJECT-walk representation. The reconcile/fixup steps (ag-image-rebuild-symbol-pool-offsets, canonicalize-heap-keywords, restore-bootstrap-globals, relocate-pool-slots, restore-mc-tables/cond-tables) all read from *ag-image-loaded-heap-master*, which is built BY the object-walk load. A raw-heap load bypasses that master entirely, so those steps have nothing to operate on. Making a raw-heap image actually BOOT therefore requires deciding, per fixup, whether it becomes a no-op (pointers already absolute and in place — the B5 win) or must be re-expressed against the raw heap.

Implication: B2 part 2 (bootable round-trip) and B5 (authoritative adoption) should likely MERGE into one stage, or B2 part 2 be scoped to save-side emission

  • a load that still runs the object path (transport only, unbootable-raw). The clean split the ADR assumed does not exist. Worth an ADR 0033 amendment before that stage starts.

Scaffold landed and pushed (branch dynlink-b1-boot-seam): B1 2f9295c, B2p1 d2b3f8f, B3p1 8bd5c24 (bitmap functionally verified). Remaining stages are the entangled/risky half.