#280 fboundp returns NIL for primitives, macros, and special operators (only recognizes stdlib/user defuns)
closed(fboundp 'car) -> NIL, (fboundp 'mapcar) -> NIL, (fboundp 'when) -> NIL, (fboundp 'setf) -> NIL, (fboundp 'if) -> NIL. Only stdlib/user DEFUNS work: (fboundp 'replace) -> T, (fboundp 'my-user-defun) -> T. Per CLHS, fboundp is true for any fbound symbol: functions (incl. built-ins), macros, and special operators.
ROOT: fboundp (src/stdlib.lisp:6718) only checks (symbol-function sym) vs the unbound marker. funcl's compiler PRIMITIVES (car/cons/+/mapcar...) are emitted INLINE and never populate a symbol-function cell (though #'car still yields a callable via emit-ir-function's compile-time resolution). PRELUDE MACROS (when/setf/push/loop) live in the tree-walker's closed dispatch, not a retrievable table — (macro-function 'when) also returns NIL. So neither symbol-function nor macro-function can see them at runtime.
FIX (architecture-deep, not a quick stdlib patch): needs a runtime-queryable registry of (a) primitive names and (b) prelude-macro names + special operators, that fboundp (and macro-function, special-operator-p) can consult. Touches the compiler's primitive/macro tables. Reserve for a dedicated pass.
Affects ANSI tests that probe fboundp of standard symbols, and any feature-detection code using fboundp. Confirmed 2026-07-11 on build/funcl. Related: macro-function also incomplete (returns NIL for prelude macros/special forms — same root).
Comments (5)
FOLLOW-UP (2026-07-11): tried to close the function-case via the fn-table but hit a hard wall — the cold helpers ag-runtime-fn-addr-lookup and ag-sym-eq-name are NOT cleanly callable from stdlib/REPL: (ag-sym-eq-name 'car 'car) errors from the REPL, and ag-runtime-fn-addr-lookup returns -1/errors on the fence index. Likely tree-shaken or cold-only / different runtime availability. So reusing the existing fn-table lookup for fboundp does NOT work as-is. The real fix needs a dedicated RUNTIME-SAFE fn-table membership predicate (a stdlib-callable walk over ag-runtime-fn-addrs with a runtime-safe name comparator, or a properly-exported lookup), plus a macro/special-operator registry for those cases. Confirmed non-trivial — reserve for a dedicated pass; do not rush (risks a broken fboundp used by image.lisp).
ROOT CAUSE (2026-07-11): cave#280 is ENTANGLED with ADR-0026, not independently fixable. Traced through 5 approaches; the real blocker is that (symbol-name 'car) returns NIL from the REPL — the symbol read at the REPL is an ADR-0026-split record whose name cell isn't accessible. So ANY name-based fn-table membership check fails: you can't get the symbol's name to compare against the table keys. This is the same symbol-identity/name-access dragon as cave#266/#150/task#7. fboundp-for-functions therefore depends on the ADR-0026 canonical-symbol work landing first (so symbol-name is reliable). Re-file under / block on ADR-0026 rather than treating as a standalone stdlib fix.
PARTIAL PROGRESS (probe 2026-07-11): (fboundp 'car) => T now (primitives resolve; cave#216 closed accordingly). Remaining: macros ((fboundp 'when) => NIL) and special operators ((fboundp 'if) => NIL) — ANSI requires T for both. Macro names live in the macro table, not fn cells; fboundp needs an ag-macro-lookup arm plus a static special-operator list.
FIXED: fboundp consults the macro table (new ag-macro-known-p) and the 25-name ANSI special-operator list; special-operator-p added as a bonus (was missing entirely). Probes: (fboundp 'when)/(fboundp 'setf)/(fboundp 'if)/(fboundp 'car) => T, unknown => NIL; (special-operator-p 'if) => T, 'when => NIL.
PROMISING FIX LEAD (2026-07-11): the fn-table ag-runtime-fn-addrs (used for the perf-map, 3604 entries) DOES contain primitives — CAR/CONS/MAPCAR are in it. And ag-runtime-fn-addr-lookup(name) (src/cold/cold-bootstrap.lisp:284) returns the fn-ptr or -1. So fboundp could do: T if symbol-function is bound OR ag-runtime-fn-addr-lookup() >= 0. This is NAME-keyed, so it sidesteps ADR-0026.
CAVEAT: calling ag-runtime-fn-addr-lookup from the REPL with (ag-norm-to-name 'car) returned -1/NIL, and with a bare symbol or "CAR" string it appeared to error — the lookup goes through ag-fn-idx-key + the fence index (ag-rt-fn-idx), which expects a specific key format the hosted compiler passes at stage1.lisp:999. Need to determine that exact format (read how is derived just before the call) and confirm ag-rt-fn-idx-sync populates primitives at runtime. Once the key format is right, fboundp-for-functions is a small stdlib change. MACROS/special-operators still need a separate registry (they're not in the fn-table). So this closes the FUNCTION case (the common one) but not macros.