plans/kernel-on-sx.md — Phase 7 header updated from "partial" to "env.sx EXTRACTED 2026-05-12"; second-consumer-found checkbox ticked for env.sx specifically. Other five files (combiner, evaluator, hygiene, quoting, short-circuit) stay blocked pending their own second consumers. plans/lib-guest-reflective.md — Phases 1-3 ticked off with date stamps; Outcome section added summarising the three commits, file stats (124 LoC, within 80-200 bound), and the third-consumer adoption protocol (cfg with five keys, no changes to env.sx).
9.4 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 [DONE 2026-05-12]
- 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 stay green.
Phase 2 — Tcl adapter [DONE 2026-05-12]
- Add
tcl-frame-cfginlib/tcl/runtime.sx.frame-lookupandframe-set-topnow delegate torefl-env-lookup-or-nil-with/refl-env-bind!-with. Tcl's{:level :locals :parent}shape unchanged. - Tcl test suite green (427/427).
Phase 3 — Documentation + cross-reference [DONE 2026-05-12]
- Update
plans/kernel-on-sx.mdto mark Phase 7's env.sx extraction as DONE (one of six). Other five blocked. lib/guest/reflective/env.sxheader 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 (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.
Outcome (2026-05-12)
Three commits on lib/tcl/uplevel:
- Plan committed.
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.reflective: Tcl adapter cfg — second consumer wired, 427+322 tests green—tcl-frame-cfgdefined,frame-lookup/frame-set-topdelegate 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.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.