Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 43s
lib/guest/reflective/env.sx — added refl-env-find-frame-with (returns
the scope where NAME is bound, or nil). Needed by consumers like
Smalltalk that mutate variables at the source frame rather than
shadowing at the current one. Also added refl-env-find-frame for
the canonical shape.
lib/smalltalk/eval.sx — new st-frame-cfg adapter for the kit.
st-lookup-local now delegates parent-walk to refl-env-find-frame-with
while preserving its Smalltalk-flavoured {:found :value :frame}
return shape (which is used to mutate at the binding's source
frame, not the current one).
lib/smalltalk/test.sh + compare.sh — load lib/guest/reflective/env.sx
before lib/smalltalk/eval.sx.
Three genuinely different wire shapes now share the parent-walk:
- Kernel: {:refl-tag :env :bindings :parent} mutable bindings
- Tcl: {:level :locals :parent} functional update
- Smalltalk: {:self :method-class :locals :parent mutable bindings,
:return-k :active-cell} rich metadata
All three consumers' full test suites unchanged: Smalltalk 847/847,
Kernel 322/322, Tcl 427/427. The cfg adapter pattern (modelled after
lib/guest/match.sx) cleanly handles all three.
160 lines
5.7 KiB
Plaintext
160 lines
5.7 KiB
Plaintext
;; lib/guest/reflective/env.sx — first-class environment kit.
|
|
;;
|
|
;; Extracted from Kernel-on-SX (lib/kernel/eval.sx) when Tcl's
|
|
;; uplevel/upvar machinery (lib/tcl/runtime.sx) materialised as a
|
|
;; second consumer needing the same scope-chain semantics.
|
|
;;
|
|
;; Canonical wire shape
|
|
;; --------------------
|
|
;; {:refl-tag :env :bindings DICT :parent ENV-OR-NIL}
|
|
;;
|
|
;; - :bindings is a mutable SX dict keyed by symbol name.
|
|
;; - :parent is either another env or nil (root).
|
|
;; - Lookup walks the parent chain until a hit or nil.
|
|
;; - Default cfg uses dict-set! to mutate bindings in place.
|
|
;;
|
|
;; Consumers with their own shape (e.g., Tcl's {:level :locals :parent})
|
|
;; pass an adapter cfg dict — same trick as lib/guest/match.sx's cfg
|
|
;; for unification over guest-specific term shapes.
|
|
;;
|
|
;; Adapter cfg keys
|
|
;; ----------------
|
|
;; :bindings-of — fn (scope) → DICT
|
|
;; :parent-of — fn (scope) → SCOPE-OR-NIL
|
|
;; :extend — fn (scope) → SCOPE (push a fresh child)
|
|
;; :bind! — fn (scope name val) → scope (functional or mutable)
|
|
;; :env? — fn (v) → bool (predicate; cheap shape check)
|
|
;;
|
|
;; Public API — canonical shape, mutable, raises on miss
|
|
;;
|
|
;; (refl-make-env)
|
|
;; (refl-extend-env PARENT)
|
|
;; (refl-env? V)
|
|
;; (refl-env-bind! ENV NAME VAL)
|
|
;; (refl-env-has? ENV NAME)
|
|
;; (refl-env-lookup ENV NAME)
|
|
;; (refl-env-lookup-or-nil ENV NAME)
|
|
;;
|
|
;; Public API — adapter-cfg, any shape
|
|
;;
|
|
;; (refl-env-extend-with CFG SCOPE)
|
|
;; (refl-env-bind!-with CFG SCOPE NAME VAL)
|
|
;; (refl-env-has?-with CFG SCOPE NAME)
|
|
;; (refl-env-lookup-with CFG SCOPE NAME)
|
|
;; (refl-env-lookup-or-nil-with CFG SCOPE NAME)
|
|
;; (refl-env-find-frame-with CFG SCOPE NAME)
|
|
;; — returns the scope in the chain that contains NAME (or nil).
|
|
;; Consumers needing source-frame mutation use this.
|
|
;;
|
|
;; (refl-canonical-cfg) — the default cfg, exposed so consumers
|
|
;; can compare or extend it.
|
|
|
|
;; ── Canonical-shape predicates and constructors ─────────────────
|
|
|
|
(define refl-env? (fn (v) (and (dict? v) (= (get v :refl-tag) :env))))
|
|
|
|
(define refl-make-env (fn () {:parent nil :refl-tag :env :bindings {}}))
|
|
|
|
(define refl-extend-env (fn (parent) {:parent parent :refl-tag :env :bindings {}}))
|
|
|
|
(define
|
|
refl-env-bind!
|
|
(fn (env name val) (dict-set! (get env :bindings) name val) env))
|
|
|
|
(define
|
|
refl-env-has?
|
|
(fn
|
|
(env name)
|
|
(cond
|
|
((nil? env) false)
|
|
((not (refl-env? env)) false)
|
|
((dict-has? (get env :bindings) name) true)
|
|
(:else (refl-env-has? (get env :parent) name)))))
|
|
|
|
(define
|
|
refl-env-lookup
|
|
(fn
|
|
(env name)
|
|
(cond
|
|
((nil? env) (error (str "refl-env-lookup: unbound symbol: " name)))
|
|
((not (refl-env? env))
|
|
(error (str "refl-env-lookup: corrupt env: " env)))
|
|
((dict-has? (get env :bindings) name) (get (get env :bindings) name))
|
|
(:else (refl-env-lookup (get env :parent) name)))))
|
|
|
|
(define
|
|
refl-env-lookup-or-nil
|
|
(fn
|
|
(env name)
|
|
(cond
|
|
((nil? env) nil)
|
|
((not (refl-env? env)) nil)
|
|
((dict-has? (get env :bindings) name) (get (get env :bindings) name))
|
|
(:else (refl-env-lookup-or-nil (get env :parent) name)))))
|
|
|
|
;; ── Adapter-cfg variants — any wire shape ───────────────────────
|
|
|
|
(define refl-env-extend-with (fn (cfg scope) ((get cfg :extend) scope)))
|
|
|
|
(define
|
|
refl-env-bind!-with
|
|
(fn (cfg scope name val) ((get cfg :bind!) scope name val)))
|
|
|
|
(define
|
|
refl-env-has?-with
|
|
(fn
|
|
(cfg scope name)
|
|
(cond
|
|
((nil? scope) false)
|
|
((not ((get cfg :env?) scope)) false)
|
|
((dict-has? ((get cfg :bindings-of) scope) name) true)
|
|
(:else (refl-env-has?-with cfg ((get cfg :parent-of) scope) name)))))
|
|
|
|
(define
|
|
refl-env-lookup-with
|
|
(fn
|
|
(cfg scope name)
|
|
(cond
|
|
((nil? scope) (error (str "refl-env-lookup: unbound symbol: " name)))
|
|
((not ((get cfg :env?) scope))
|
|
(error (str "refl-env-lookup: corrupt scope: " scope)))
|
|
((dict-has? ((get cfg :bindings-of) scope) name)
|
|
(get ((get cfg :bindings-of) scope) name))
|
|
(:else (refl-env-lookup-with cfg ((get cfg :parent-of) scope) name)))))
|
|
|
|
(define
|
|
refl-env-lookup-or-nil-with
|
|
(fn
|
|
(cfg scope name)
|
|
(cond
|
|
((nil? scope) nil)
|
|
((not ((get cfg :env?) scope)) nil)
|
|
((dict-has? ((get cfg :bindings-of) scope) name)
|
|
(get ((get cfg :bindings-of) scope) name))
|
|
(:else
|
|
(refl-env-lookup-or-nil-with cfg ((get cfg :parent-of) scope) name)))))
|
|
|
|
;; Returns the SCOPE in the chain that contains NAME, or nil if no
|
|
;; scope binds it. Consumers (e.g. Smalltalk) use this to mutate the
|
|
;; binding at its source frame rather than introducing a new shadow
|
|
;; binding at the current frame. Pairs with `refl-env-lookup-with`
|
|
;; for callers that need both the value and the defining scope.
|
|
|
|
(define refl-env-find-frame-with
|
|
(fn (cfg scope name)
|
|
(cond
|
|
((nil? scope) nil)
|
|
((not ((get cfg :env?) scope)) nil)
|
|
((dict-has? ((get cfg :bindings-of) scope) name) scope)
|
|
(:else
|
|
(refl-env-find-frame-with cfg ((get cfg :parent-of) scope) name)))))
|
|
|
|
(define refl-env-find-frame
|
|
(fn (env name) (refl-env-find-frame-with refl-canonical-cfg env name)))
|
|
|
|
;; ── Default canonical cfg ───────────────────────────────────────
|
|
;; Exposed so consumers can use it explicitly, compose with it, or
|
|
;; check adapter-correctness against the canonical implementation.
|
|
|
|
(define refl-canonical-cfg {:bind! (fn (e n v) (refl-env-bind! e n v)) :parent-of (fn (e) (get e :parent)) :env? (fn (v) (refl-env? v)) :bindings-of (fn (e) (get e :bindings)) :extend (fn (e) (refl-extend-env e))})
|