;; 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))})