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.
8.2 KiB
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:
- A scope type with a bindings dict and parent pointer.
- A lookup that walks parents until a hit (or nil/raise on miss).
- A way to extend — push a fresh child frame.
- 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.
;; 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 withrefl-*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-lookupbecomes(refl-env-lookup-with tcl-frame-cfg frame name).frame-set-topstays where it is — Tcl needs functional updates for the assoc-back-to-interp chain. The kit accommodates both, just likematch.sxaccommodates miniKanren's wire shape and Haskell's term shape.
Roadmap
Phase 1 — Skeleton + Kernel migration
- Create
lib/guest/reflective/env.sxwith the canonical wire shape and mutable defaults. - Migrate
lib/kernel/eval.sxto userefl-make-env/refl-extend-env/refl-env-*. Rename:knl-tag→:refl-tagin 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-cfginlib/tcl/runtime.sx. Wire it throughframe-lookupandtcl-frame-nthcallers viarefl-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.mdto mark Phase 7's env.sx extraction as DONE (one of six). Keep the other five blocked. - Add
lib/guest/reflective/env.sxheader 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 (Schemecall-with-values, CLcompiler-let) needs frame-level indexing.
Non-goals
- Do not extract
combiner.sx,evaluator.sx,hygiene.sx,quoting.sx, orshort-circuit.sxin this branch. Tcl doesn't have operatives/applicatives; the two-consumer rule isn't satisfied for those files. They stay documented-only inplans/kernel-on-sx.mduntil a Scheme/Maru/CL-fexpr consumer arrives. - Do not change Tcl's update model to mutable. The functional
frame-set-topis structural — it's how Tcl threads the interp throughtcl-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-lookupandrefl-env-lookup-or-nil) and consumers pick.
Validation criteria
The extraction is real iff:
- Both consumers compile and pass their full test suites unchanged.
- The shared
env.sxfile 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). - 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.sxlines 5–22 (make-frame,frame-lookup,frame-set-top) — the Tcl consumer's current implementation.lib/kernel/eval.sxlines 39–82 (env block) — the Kernel consumer's current implementation.