# 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.
