#266 Function cells are NAME-keyed, not package-scoped — same-named fns in different packages clobber (blocks multi-library asdf loads)
openROOT CAUSE of asdf:load-system failing on alexandria (and any library that shadows a uiop/asdf internal function name).
MINIMAL REPRO (build/funcl REPL): (defpackage :p1 (:use :cl)) (defpackage :p2 (:use :cl)) (in-package :p1) (defun shared-fn () 111) (in-package :p2) (defun shared-fn () 222) (in-package :cl-user) (list (p1::shared-fn) (p2::shared-fn)) => (222 222) ; WRONG, should be (111 222)
The two symbols are genuinely DISTINCT: (eq (find-symbol "SHARED-FN" :p1) (find-symbol "SHARED-FN" :p2)) => NIL, and symbol-package returns P1 / P2 correctly. But defining a function on one symbol CLOBBERS the other — function storage is keyed by the symbol NAME string, not by symbol identity / package.
- Not fixed by (declaim (notinline shared-fn)): still (222 222).
- (symbol-function (find-symbol "SHARED-FN" :p1)) => 'attempt to call an object that is not a function'. So BOTH the compile-time early-bind table (ag-fn-table) AND the runtime fn-cell install (set-symbol-function / ag-repl-install-fn-cell) are name-keyed. This is the ADR-0026 symbol-unification class, in the function-storage dimension.
IMPACT — this is THE blocker for loading multiple libraries via asdf:
- (asdf:load-system "alexandria") after asdf.lisp is loaded: alexandria's compat/alexandria/functions.lisp defines (defun ensure-function (function-designator) …) — a 1-arg function-or-fdefinition coercer. uiop ALSO defines (defun ensure-function (fun &key (package :cl)) …) — a rich coercer (handles CONS/lambda hooks, strings, keywords, hash-tables) that asdf's call-function / hook machinery depends on. alexandria's def CLOBBERS uiop's (same name ENSURE-FUNCTION, different packages). The moment functions.lisp finishes loading, asdf's next (call-function ) → (ensure-function hook) hits alexandria's 1-arg version → (fdefinition ) → 'attempt to call an object that is not a function | in: LOAD-SYSTEM', at the functions.lisp→lists.lisp boundary. alexandria never finishes loading via asdf.
- Plain (load compat/alexandria/load-all.lisp) works because uiop is NOT present to be clobbered.
VERIFY that this is the asdf blocker: with the cave#265 types.lisp fix, asdf:load-system reaches the perform of functions.lisp cleanly (0 name-corruption errors), then dies exactly here with 'not a function'.
FIX: make function storage package-scoped (key ag-fn-table and the fn-cell by symbol identity / package+name, not the bare name). Large — ADR-0026 territory. Until then, no two loaded libraries may define same-named functions in different packages. NOTE: simple non-colliding systems DO load+run via asdf:load-system (foo/bar verified earlier, c639698); this collision is alexandria-specific-but-general.
Comments (9)
IMPLEMENTATION ATTEMPT 2026-07-09 — REVERTED (reproduced the 2026-07-08 failure mode); narrowed the work.
Implemented a coherent 4-layer runtime-gated (name,pkg) change: (1) ag-fn-key/ag-sym-pkg-scoped-p — mangle a non-CL/non-KEYWORD symbol's fn-table COMPARISON key to PKGNAME:NAME (verified: distinct P1:SHARED-FN / P2:SHARED-FN keys); (2) ag-runtime-fn-addr-lookup package-aware; (3) ag-build-fn-install targets (FIND-SYMBOL name pkg) for non-CL warm symbols; (4) funcl-funcall-fallback passes the symbol. Builds clean.
RESULT: p1/p2 STILL (222 222); fn-table-only REGRESSED asdf (many 'not a function' during asdf.lisp load incl. REGISTER-IMAGE-RESTORE-HOOK — the SAME functions the reverted 2026-07-08 attempt broke). DECISIVE PROBE after the fix: (symbol-function (find-symbol "SHARED-FN" :p1)) => 222 (p1's RECORD holds p2's fn) and :p2's record is EMPTY. So the fn-cell installs land on the WRONG records — the reader-record vs quote-pool vs canonical split. Warm call resolution goes through NONE of {ag-fn-lookup (both -1 cross-form), runtime registry (cold-only), reader's find-symbol record}.
CONCLUSION: the real blocker is SYMBOL-RECORD unification — one canonical record per (pkg,name) shared by reader/quote/intern/fn-cell — the ADR-0026 dragon the 2026-07-08 revert failed on (reexport set-symbol-package churn on a NAME-keyed canonical record; (pkg,name) keying is what fixes the churn). Multi-session. NEXT: make ag-dynamic-symbol-record-allocate + ag-runtime-pooled-symbol-ptr + ag-symbol-pool-offsets (pkg,name)-keyed; verify (symbol-function (find-symbol X P)) is per-package BEFORE touching call resolution. Reverted all 4 files to known-good (types.lisp mvb fix kept).
Perf: symbol-lookup fence-indexes land (mirrors cave#188/#238b). Two self-healing first-byte indexes, both gated on ag-runtime-symbol-unification-p (cold bake keeps linear walks -> self-host byte-identical):
- ag-pkgsym-idx (stage1/compiler) over arena store ag-pkgsym-offsets -> speeds ag-pkgsym-find (ag-intern-in-pkg dedup per new intern). Bucketed by symbol-name first byte, not the composite PKG:NAME key.
- ag-pkg-own-idx (stdlib) -> per-package first-byte index over each package own intern table; find-symbol-in-pkg was O(n) linear -> interning N distinct symbols into one package during a whole-file load was O(n^2). Indexes the ACTUAL own table so covers import/shadowing-import.
Result: load scaling O(n^2) -> ~O(n^1.3). n=1000 defuns 3.1s -> 1.4s (2.2x). alexandria load-all errors 36 -> 24. Symbol identity preserved. Residual ~2.4x/doubling is NOT symbol lookup (diverse and same-prefix names scale identically) -> likely GC pressure.
PRECISE RELOAD-SPLIT MECHANISM (2026-07-10), found while chasing ANSI gensym failures. The ANSI harness runs every test against a save-image, so this split poisons any test that rebinds a special (let ((x N)) …) or compares reader-symbol identity to compiled code.
Minimal repro: (defvar *myspecial* 42)(defun read-it () *myspecial*)(save-image "x.img"), then reload and probe:
(read-it) => 42 ; COMPILED code reads the restored record S's value cell (preserved)
myspecial => NIL ; READER returns a DIFFERENT symbol R (value nil)
(boundp (quote myspecial)) => NIL
(eq (quote myspecial) (quote myspecial)) => T ; reader is self-consistent (always returns R)
ag-runtime-symbol-unification-p => 1 (unification ON)
(null ag-symbol-table) => NIL ; table NON-empty after reload (boot-repopulated)
So: reader-symbol R ≠ compiled-record S' (R.value=nil, S'.value=42). ag-intern-symbol (reader.lisp:1024) path with unification on = static-ptr (nil, no baked record for a runtime defvar) -> ag-runtime-pooled-symbol-ptr -> falls through to ag-intern-symbol-cached (ag-symbol-table). The pooled path (cave#132 arena pool / gc-root-ranges, restored in ag-image-restore-bootstrap-globals) does NOT resolve the runtime special to S'; the reader instead returns a boot-created ag-symbol-table entry R. ag-symbol-table itself is NOT in ag-image-runtime-roots (image.lisp:~1090), so the saved reader identity is not carried across; the post-reload table is whatever boot re-interned.
This is the single-canonical-symbol problem, not a reconciliation point-fix (prior point-fixes #44/#120/#130/#132/#135/#149 are why). The clean fix is ADR-0026: one canonical symbol object per (name[,pkg]) that reader, compiled quote, find-symbol, and the restored heap all share — so there is no R-vs-S' to reconcile. Until then, ANSI tests that thread a special through a save-image (gensym.3/.4/.9, and any (let ((print-/read- …)) …)) fail against the image though they pass on build/funcl-stage2 without an image.
NEGATIVE RESULT (2026-07-10) — tried and REVERTED a point-fix; documenting so it is not retried. Approach: wire the reload-authoritative arena map into the reader's intern fallback — added ag-warm-restored-symbol-ptr (ag-warm-pool-record-vaddr against (gc-root-ranges), record+3 lowtag) and inserted it in ag-intern-symbol BETWEEN ag-runtime-pooled-symbol-ptr and the ag-symbol-table cache (unification-gated, so cold self-host stays byte-identical — build passed). Intent: make reader == compiled-quote after reload (mirror compiler.lisp:442 quote-emit path).
Result: for a CL-HOMED restored special it DID flip (boundp (quote gensym-counter)) NIL→T (reader now finds the restored record). BUT (a) CL-USER-homed symbols (myspecial) stayed split — ag-warm-pool-scan-range filters home-pkg==primordial-CL only; and (b) even for gensym-counter, BEHAVIOR still split: (let ((gensym-counter 1)) (symbol-name (gensym))) => "G0" not "G1", and gensym did NOT increment the counter — so gensym's OWN compiled gensym-counter reference is YET ANOTHER record than the one the reader now returns. Patching one edge (reader→restored record) just exposed the next (compiled-gensym→different cell). And it REGRESSED the suite: symbols/ 593→538 (-55) — returning the restored record instead of the cache entry broke ~55 tests that depended on the prior behavior.
CONFIRMS: this is genuinely multi-cell; no single reader/compiler edge-patch converges. The fix must be ADR-0026 single-canonical-symbol (ONE object per (name[,pkg]) shared by reader, compiled quote, value-cell, and the restored heap) so there is no R-vs-S'-vs-C'' to reconcile. Reverted to the committed baseline (byte-identical).
MAJOR STATE UPDATE 2026-07-11: alexandria + uiop now COEXIST — asdf:load-system "alexandria" completes end-to-end even though BOTH define ensure-function. The clobber didn't manifest because (a) per-package fn-cells + late-bound designator resolution landed (526e540 + the cave#244/#266 arms in funcall/apply/(function …)), and (b) alexandria's ensure-function is call-compatible for the uses uiop makes post-load. The UNDERLYING name-keyed table remains for early-bound direct calls and shadow support (the ansi-aux 'shadow' blocker) — that's the ADR-0026 tail. Severity downgraded from 'blocks multi-library loads' to 'latent collision class + shadow'; next concrete target: package-scoped shadow for ansi-aux (see also #task-7 plan).
New concrete repro + measurement from the fasl work (ADR-0030 slice 4, commit above): in ONE load session on an image-restored binary, (find-symbol "F" "SPK2") and the reader's 'spk2::f return TWO different records (measured bases 1112539216 vs 1080060864) although both nominally route through ag-intern-in-pkg -> ag-pkgsym-find. The three intern stores (pkgsym fence-index ag-pkgsym-idx, package alist, name-keyed pool) lose coherence during fasl replay — the concrete lead is why ag-pkgsym-find misses a just-registered entry during replay (fence/idx sync under image-restore). This is now the LAST blocker for general (alexandria-scale) fasls: package-qualified calls and (function NAME) already work across fasls (KIND-6); only quote/reader eq-identity for pkgsym atoms fails.
CORRECTION to my previous comment: codex review overturned the ADR-0026 attribution — the fasl eq split is NOT the canonical-symbol dragon but a narrow two-store bug: ag-intern-in-pkg's pkgsym-map HIT arm returned without threading the package's own alist (the only store find-symbol reads, and the only one image restore does NOT rebuild). Fixed by ag-pkg-own-publish (commit above). A residual second duplicate-allocation door remains (two (SPK2,F) records still minted during fasl replay) — tracked separately; not this issue's class. Lesson recorded: name the store, not the dragon.
Further correction from the cave#297 resolution: the 'coherence split' I reported earlier was entirely the piped-stdin read-ahead artifact (forms read before the defining load evaluated). The stores cohere fine during fasl replay — one mint, all doors hit it. No ADR-0026 work is implicated by the fasl line at all.
SCOPING + north-star confirmation (2026-07-09).
NORTH-STAR VERIFIED for non-colliding systems: a fresh 3-file system (pkg.lisp defpackage+export, util.lisp defuns, main.lisp cross-file callers) loads and RUNS end-to-end via (asdf:load-system "demoapp") — (demoapp:app-compute 20)=>41 (main.lisp calling util.lisp's util-double/util-inc across files), (app-greeting "asdf")=>"hi asdf", ZERO errors. So asdf load+run WORKS; cave#266 is the collision-specific blocker.
SCOPE of the fix: the runtime funcall-designator path DOES read a per-symbol fn-cell at [sym+29] (cave#244), so it is package-capable. The bug is upstream: