Files
rose-ash/plans/lib-guest-reflective.md
giles c27db9b78f
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 19s
reflective: Phase 3 docs — mark env.sx extraction DONE, others still blocked
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).
2026-05-12 07:04:17 +00:00

9.4 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 [DONE 2026-05-12]

  • 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 stay green.

Phase 2 — Tcl adapter [DONE 2026-05-12]

  • Add tcl-frame-cfg in lib/tcl/runtime.sx. frame-lookup and frame-set-top now delegate to refl-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.md to mark Phase 7's env.sx extraction as DONE (one of six). Other five blocked.
  • lib/guest/reflective/env.sx header 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 (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.

Outcome (2026-05-12)

Three commits on lib/tcl/uplevel:

  1. Plan committed.
  2. 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.
  3. reflective: Tcl adapter cfg — second consumer wired, 427+322 tests greentcl-frame-cfg defined, frame-lookup/frame-set-top delegate 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 80200 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.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.