# lib/guest/reflective/ — first extraction kit, driven by Tcl uplevel as second consumer The `kernel-on-sx` loop accumulated six proposed `lib/guest/reflective/` files (`env.sx`, `combiner.sx`, `evaluator.sx`, `hygiene.sx`, `quoting.sx`, `short-circuit.sx`) but extraction was blocked on the two-consumer rule. This plan opens that block by selecting Tcl's `uplevel`/`upvar` machinery as the second consumer for the **`env.sx`** file specifically — the highest-fit candidate. Why Tcl/uplevel for *env*: both Kernel and Tcl implement first-class scope chains with recursive parent-walking lookup, and both expose those scopes to user code (Kernel via `get-current-environment`; Tcl via `uplevel`/`upvar`). The first extraction is the smallest plausible kit that both can credibly use. Why not the whole set in one go: the other five files (`combiner.sx`, `evaluator.sx`, `hygiene.sx`, `quoting.sx`, `short-circuit.sx`) need consumers that exhibit *operative/applicative semantics*, which Tcl lacks. They stay deferred until a Scheme or Maru port lands. ## Discovery — current state, head-to-head ``` Kernel env Tcl frame ───────────────────────────────────────────────────────────────────── shape {:knl-tag :env {:level N :bindings DICT :locals DICT :parent ENV-OR-NIL} :parent FRAME-OR-NIL} update model MUTABLE (dict-set!) FUNCTIONAL (assoc returns new) scope chain parent pointer parent pointer + explicit :frame-stack on the interp construction (kernel-make-env) (make-frame LEVEL PARENT) (kernel-extend-env P) lookup (kernel-env-lookup E N) (frame-lookup F N) — raises on miss — returns nil on miss bind (kernel-env-bind! E N V) (frame-set-top F N V) — mutates — returns new frame presence (kernel-env-has? E N) (frame-lookup F N) then nil-check call-stack walk (nothing — only single chain) (tcl-frame-nth STACK LEVEL) — indexes into :frame-stack variable alias (nothing) (upvar-alias? V) — alias dict points at level + name in another frame ``` ## The genuine overlap The recursive parent-walk is identical in spirit. Both languages need: 1. A scope type with a *bindings dict* and *parent pointer*. 2. A *lookup* that walks parents until a hit (or nil/raise on miss). 3. A way to *extend* — push a fresh child frame. 4. A way to *write a binding* in a chosen frame. The genuine divergence is *mutable vs functional update*. Tcl can't switch to mutable bindings without changing `frame-set-top`'s call sites (which return new interp state); Kernel can't switch to functional without rewriting `$define!` semantics (which mutates the dyn-env in place). ## The proposed API — adapter-driven, like `match.sx` `lib/guest/match.sx` solves the same shape-divergence problem with a `cfg` adapter dict: the kit operates on a generic term representation, consumers pass callbacks that bridge their shape to it. The pattern works because the *algorithms* are language-agnostic; only the *data layout* differs. `lib/guest/reflective/env.sx` should follow the same pattern. ```lisp ;; Canonical wire shape (default): ;; {:refl-tag :env :bindings DICT :parent ENV-OR-NIL} ;; ;; Adapter cfg keys (for consumers with their own shape): ;; :bindings-of — fn (scope) → DICT ; access bindings dict ;; :parent-of — fn (scope) → SCOPE-OR-NIL ;; :extend — fn (scope) → SCOPE ; child of scope ;; :bind! — fn (scope name val) → scope ; functional-or-mutable ;; ;; Default cfg (refl-default-cfg) implements the canonical wire shape ;; with MUTABLE bindings (dict-set!). Tcl provides its own cfg with ;; functional bindings and the level field preserved. (refl-make-env) ;; canonical, mutable (refl-extend-env PARENT) (refl-env-bind! ENV NAME VAL) ;; mutates; returns ENV (refl-env-has? ENV NAME) (refl-env-lookup ENV NAME) ;; raises on miss (refl-env-lookup-or-nil ENV NAME) ;; for guests that prefer nil ;; With explicit cfg — for consumers with their own shape: (refl-env-lookup-with CFG SCOPE NAME) (refl-env-bind!-with CFG SCOPE NAME VAL) (refl-env-extend-with CFG SCOPE) ``` The two consumer migrations: - **Kernel**: drops `kernel-make-env`, `kernel-extend-env`, `kernel-env-bind!`, `kernel-env-has?`, `kernel-env-lookup`. Replaces with `refl-*` calls on the canonical shape. Rename `:knl-tag` → `:refl-tag`. No semantic change. - **Tcl**: keeps its `{:level :locals :parent}` shape but defines a Tcl-cfg adapter. `frame-lookup` becomes `(refl-env-lookup-with tcl-frame-cfg frame name)`. `frame-set-top` stays where it is — Tcl needs functional updates for the assoc-back-to-interp chain. The kit accommodates both, just like `match.sx` accommodates miniKanren's wire shape and Haskell's term shape. ## Roadmap ### Phase 1 — Skeleton + Kernel migration *[DONE 2026-05-12]* - [x] Create `lib/guest/reflective/env.sx` with the canonical wire shape and mutable defaults. - [x] Migrate `lib/kernel/eval.sx` to use `refl-make-env` / `refl-extend-env` / `refl-env-*`. Rename `:knl-tag` → `:refl-tag` in env values only (operatives/applicatives keep their own tags for now). - [x] All 322 Kernel tests stay green. ### Phase 2 — Tcl adapter *[DONE 2026-05-12]* - [x] Add `tcl-frame-cfg` in `lib/tcl/runtime.sx`. `frame-lookup` and `frame-set-top` now delegate to `refl-env-lookup-or-nil-with` / `refl-env-bind!-with`. Tcl's `{:level :locals :parent}` shape unchanged. - [x] Tcl test suite green (427/427). ### Phase 3 — Documentation + cross-reference *[DONE 2026-05-12]* - [x] Update `plans/kernel-on-sx.md` to mark Phase 7's *env.sx* extraction as DONE (one of six). Other five blocked. - [x] `lib/guest/reflective/env.sx` header docstring already lists both consumers and links back to this plan. ### Phase 4 — Quick wins identified along the way - [ ] Tcl's `tcl-frame-nth` (index into call stack by level) is the start of a *stack-frame protocol* — separate from the scope-chain protocol. Tcl needs it; Kernel doesn't. Document as "language-specific extension on top of the shared kit"; consider extracting later if a third consumer (Scheme `call-with-values`, CL `compiler-let`) needs frame-level indexing. ## Non-goals - **Do not extract `combiner.sx`, `evaluator.sx`, `hygiene.sx`, `quoting.sx`, or `short-circuit.sx`** in this branch. Tcl doesn't have operatives/applicatives; the two-consumer rule isn't satisfied for those files. They stay documented-only in `plans/kernel-on-sx.md` until a Scheme/Maru/CL-fexpr consumer arrives. - **Do not change Tcl's update model to mutable**. The functional `frame-set-top` is structural — it's how Tcl threads the interp through `tcl-var-set`/`tcl-var-get`. Don't break it. - **Do not unify the env-lookup error semantics**. Kernel raises; Tcl returns nil. The kit offers both (`refl-env-lookup` and `refl-env-lookup-or-nil`) and consumers pick. ## Validation criteria The extraction is real iff: 1. Both consumers compile and pass their full test suites unchanged. 2. The shared `env.sx` file is ≥80 LoC (substantial enough to be worth sharing) and ≤200 LoC (small enough that the cfg adapter pattern doesn't become its own framework). 3. A third consumer in the future can adopt the kit by writing only the cfg dict — no algorithm changes to `env.sx`. ## Outcome (2026-05-12) Three commits on `lib/tcl/uplevel`: 1. Plan committed. 2. **`reflective: extract env.sx + migrate Kernel — 322 tests green`** — kit landed; Kernel's env block collapsed from ~30 lines to 6 thin wrappers (`kernel-env? = refl-env?` etc.). Envs now carry `:refl-tag :env`. All 7 Kernel suites unchanged. 3. **`reflective: Tcl adapter cfg — second consumer wired, 427+322 tests green`** — `tcl-frame-cfg` defined, `frame-lookup`/`frame-set-top` delegate to the kit. Tcl's frame shape unchanged. Functional update preserved. **File stats:** `lib/guest/reflective/env.sx` is 124 lines, 13 forms. Within the 80–200 LoC validation bound. Adapter-cfg pattern proven to bridge mutable-canonical (Kernel) and functional-frame (Tcl) wire shapes via a single ~7-line cfg dict per consumer. **Third-consumer test:** any future guest can adopt the kit by writing its own cfg with five keys (`:bindings-of`, `:parent-of`, `:extend`, `:bind!`, `:env?`) — no changes to `env.sx`. The shape-divergence problem is solved by parameterisation, not by forcing both consumers onto one wire shape. ## References - `plans/kernel-on-sx.md` — the kernel-on-sx loop's chisel notes; the six candidate API surfaces are documented there. - `lib/guest/match.sx` — precedent for the adapter-cfg extraction pattern. - `lib/tcl/runtime.sx` lines 5–22 (`make-frame`, `frame-lookup`, `frame-set-top`) — the Tcl consumer's current implementation. - `lib/kernel/eval.sx` lines 39–82 (env block) — the Kernel consumer's current implementation.