plans: lib-guest-reflective extraction kicked off — Tcl uplevel as second consumer
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 51s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 51s
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.
This commit is contained in:
133
plans/lib-guest-reflective.md
Normal file
133
plans/lib-guest-reflective.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# 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 5–22 (`make-frame`, `frame-lookup`, `frame-set-top`) — the Tcl consumer's current implementation.
|
||||
- `lib/kernel/eval.sx` lines 39–82 (env block) — the Kernel consumer's current implementation.
|
||||
Reference in New Issue
Block a user