diff --git a/plans/lib-guest-reflective.md b/plans/lib-guest-reflective.md new file mode 100644 index 00000000..e4e1dadb --- /dev/null +++ b/plans/lib-guest-reflective.md @@ -0,0 +1,133 @@ +# 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 + +- [ ] Create `lib/guest/reflective/env.sx` with the canonical wire shape and mutable defaults. +- [ ] 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). +- [ ] All 322 Kernel tests must stay green. + +### Phase 2 — Tcl adapter + +- [ ] Add `tcl-frame-cfg` in `lib/tcl/runtime.sx`. Wire it through `frame-lookup` and `tcl-frame-nth` callers via `refl-env-lookup-with`. Keep Tcl's level/locals/parent shape unchanged. +- [ ] Tcl test suite (must not regress). + +### Phase 3 — Documentation + cross-reference + +- [ ] Update `plans/kernel-on-sx.md` to mark Phase 7's *env.sx* extraction as DONE (one of six). Keep the other five blocked. +- [ ] Add `lib/guest/reflective/env.sx` header docstring listing both consumers and pointing at 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`. + +## 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.