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

8.2 KiB
Raw Blame History

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.

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