Files
rose-ash/plans/lib-guest-reflective.md
giles 24d8e362d5
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 51s
plans: lib-guest-reflective extraction kicked off — Tcl uplevel as second consumer
The kernel-on-sx loop documented six candidate reflective API files
gated on the two-consumer rule. This plan opens that block by
selecting Tcl's existing uplevel/upvar machinery as the second
consumer for env.sx specifically (the highest-fit candidate).

Discovery: Kernel and Tcl have identical scope-chain semantics but
diverge on mutable-vs-functional update. Solution: adapter-cfg
pattern, same as lib/guest/match.sx. Canonical wire shape with
mutable defaults for Kernel; Tcl provides its own cfg keeping
the functional model.

Roadmap: env.sx extracted, both consumers migrated, all tests green.
The other five candidate files (combiner, evaluator, hygiene,
quoting, short-circuit) stay deferred — Tcl has no operatives.
2026-05-11 22:12:26 +00:00

134 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 522 (`make-frame`, `frame-lookup`, `frame-set-top`) — the Tcl consumer's current implementation.
- `lib/kernel/eval.sx` lines 3982 (env block) — the Kernel consumer's current implementation.