Split env-bind! from env-set!: fix lexical scoping and closures
Two fundamental environment bugs fixed: 1. env-set! was used for both binding creation (let, define, params) and mutation (set!). Binding creation must NOT walk the scope chain — it should set on the immediate env. Only set! should walk. Fix: introduce env-bind! for all binding creation. env-set! now exclusively means "mutate existing binding, walk scope chain". Changed across spec (eval.sx, cek.sx, render.sx) and all web adapters (dom, html, sx, async, boot, orchestration, forms). 2. makeLambda/makeComponent/makeMacro/makeIsland used merge(env) to flatten the closure into a plain object, destroying the prototype chain. This meant set! inside closures couldn't reach the original binding — it modified a snapshot copy instead. Fix: store env directly as closure (no merge). The prototype chain is preserved, so set! walks up to the original scope. Tests: 499/516 passing (96.7%), up from 485/516. Fixed: define self-reference, let scope isolation, set! through closures, counter-via-closure pattern, recursive functions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
16
spec/cek.sx
16
spec/cek.sx
@@ -396,7 +396,7 @@
|
||||
(let ((k (make-cek-continuation captured rest-kont)))
|
||||
;; Evaluate shift body with k bound, continuation goes to rest-kont
|
||||
(let ((shift-env (env-extend env)))
|
||||
(env-set! shift-env k-name k)
|
||||
(env-bind! shift-env k-name k)
|
||||
(make-cek-state body shift-env rest-kont))))))
|
||||
|
||||
|
||||
@@ -604,7 +604,7 @@
|
||||
(body (get frame "body"))
|
||||
(local (get frame "env")))
|
||||
;; Bind the value
|
||||
(env-set! local name value)
|
||||
(env-bind! local name value)
|
||||
;; More bindings?
|
||||
(if (empty? remaining)
|
||||
;; All bindings done — evaluate body
|
||||
@@ -628,7 +628,7 @@
|
||||
(effect-list (get frame "effect-list")))
|
||||
(when (and (lambda? value) (nil? (lambda-name value)))
|
||||
(set-lambda-name! value name))
|
||||
(env-set! fenv name value)
|
||||
(env-bind! fenv name value)
|
||||
;; Effect annotation
|
||||
(when has-effects
|
||||
(let ((effect-names (if (= (type-of effect-list) "list")
|
||||
@@ -640,7 +640,7 @@
|
||||
(env-get fenv "*effect-annotations*")
|
||||
(dict))))
|
||||
(dict-set! effect-anns name effect-names)
|
||||
(env-set! fenv "*effect-annotations*" effect-anns)))
|
||||
(env-bind! fenv "*effect-annotations*" effect-anns)))
|
||||
(make-cek-value value fenv rest-k))
|
||||
|
||||
;; --- SetFrame: value evaluated ---
|
||||
@@ -969,10 +969,10 @@
|
||||
" expects " (len params) " args, got " (len args)))
|
||||
(do
|
||||
(for-each
|
||||
(fn (pair) (env-set! local (first pair) (nth pair 1)))
|
||||
(fn (pair) (env-bind! local (first pair) (nth pair 1)))
|
||||
(zip params args))
|
||||
(for-each
|
||||
(fn (p) (env-set! local p nil))
|
||||
(fn (p) (env-bind! local p nil))
|
||||
(slice params (len args)))
|
||||
(make-cek-state (lambda-body f) local kont))))
|
||||
|
||||
@@ -983,10 +983,10 @@
|
||||
(children (nth parsed 1))
|
||||
(local (env-merge (component-closure f) env)))
|
||||
(for-each
|
||||
(fn (p) (env-set! local p (or (dict-get kwargs p) nil)))
|
||||
(fn (p) (env-bind! local p (or (dict-get kwargs p) nil)))
|
||||
(component-params f))
|
||||
(when (component-has-children? f)
|
||||
(env-set! local "children" children))
|
||||
(env-bind! local "children" children))
|
||||
(make-cek-state (component-body f) local kont))
|
||||
|
||||
:else (error (str "Not callable: " (inspect f))))))
|
||||
|
||||
47
spec/eval.sx
47
spec/eval.sx
@@ -229,10 +229,10 @@
|
||||
(do
|
||||
;; Bind params — provided args first, then nil for missing
|
||||
(for-each
|
||||
(fn (pair) (env-set! local (first pair) (nth pair 1)))
|
||||
(fn (pair) (env-bind! local (first pair) (nth pair 1)))
|
||||
(zip params args))
|
||||
(for-each
|
||||
(fn (p) (env-set! local p nil))
|
||||
(fn (p) (env-bind! local p nil))
|
||||
(slice params (len args)))
|
||||
;; Return thunk for TCO
|
||||
(make-thunk (lambda-body f) local))))))
|
||||
@@ -247,11 +247,11 @@
|
||||
(local (env-merge (component-closure comp) env)))
|
||||
;; Bind keyword params
|
||||
(for-each
|
||||
(fn (p) (env-set! local p (or (dict-get kwargs p) nil)))
|
||||
(fn (p) (env-bind! local p (or (dict-get kwargs p) nil)))
|
||||
(component-params comp))
|
||||
;; Bind children if component accepts them
|
||||
(when (component-has-children? comp)
|
||||
(env-set! local "children" children))
|
||||
(env-bind! local "children" children))
|
||||
;; Return thunk — body evaluated in local env
|
||||
(make-thunk (component-body comp) local))))
|
||||
|
||||
@@ -423,7 +423,7 @@
|
||||
(let ((vname (if (= (type-of (first binding)) "symbol")
|
||||
(symbol-name (first binding))
|
||||
(first binding))))
|
||||
(env-set! local vname (trampoline (eval-expr (nth binding 1) local)))))
|
||||
(env-bind! local vname (trampoline (eval-expr (nth binding 1) local)))))
|
||||
bindings)
|
||||
;; Clojure-style
|
||||
(let ((i 0))
|
||||
@@ -433,7 +433,7 @@
|
||||
(symbol-name (nth bindings (* pair-idx 2)))
|
||||
(nth bindings (* pair-idx 2))))
|
||||
(val-expr (nth bindings (inc (* pair-idx 2)))))
|
||||
(env-set! local vname (trampoline (eval-expr val-expr local)))))
|
||||
(env-bind! local vname (trampoline (eval-expr val-expr local)))))
|
||||
nil
|
||||
(range 0 (/ (len bindings) 2)))))
|
||||
;; Evaluate body — last expression in tail position
|
||||
@@ -480,7 +480,7 @@
|
||||
(loop-fn (make-lambda params loop-body env)))
|
||||
;; Self-reference: loop can call itself by name
|
||||
(set-lambda-name! loop-fn loop-name)
|
||||
(env-set! (lambda-closure loop-fn) loop-name loop-fn)
|
||||
(env-bind! (lambda-closure loop-fn) loop-name loop-fn)
|
||||
;; Evaluate initial values in enclosing env, then call
|
||||
(let ((init-vals (map (fn (e) (trampoline (eval-expr e env))) inits)))
|
||||
(call-lambda loop-fn init-vals env))))))
|
||||
@@ -522,7 +522,7 @@
|
||||
(value (trampoline (eval-expr (nth args val-idx) env))))
|
||||
(when (and (lambda? value) (nil? (lambda-name value)))
|
||||
(set-lambda-name! value (symbol-name name-sym)))
|
||||
(env-set! env (symbol-name name-sym) value)
|
||||
(env-bind! env (symbol-name name-sym) value)
|
||||
;; Store effect annotation if declared
|
||||
(when has-effects
|
||||
(let ((effects-raw (nth args 2))
|
||||
@@ -535,7 +535,7 @@
|
||||
(env-get env "*effect-annotations*")
|
||||
(dict))))
|
||||
(dict-set! effect-anns (symbol-name name-sym) effect-list)
|
||||
(env-set! env "*effect-annotations*" effect-anns)))
|
||||
(env-bind! env "*effect-annotations*" effect-anns)))
|
||||
value)))
|
||||
|
||||
|
||||
@@ -570,8 +570,8 @@
|
||||
(env-get env "*effect-annotations*")
|
||||
(dict))))
|
||||
(dict-set! effect-anns (symbol-name name-sym) effect-list)
|
||||
(env-set! env "*effect-annotations*" effect-anns)))
|
||||
(env-set! env (symbol-name name-sym) comp)
|
||||
(env-bind! env "*effect-annotations*" effect-anns)))
|
||||
(env-bind! env (symbol-name name-sym) comp)
|
||||
comp))))
|
||||
|
||||
(define defcomp-kwarg
|
||||
@@ -646,7 +646,7 @@
|
||||
(params (first parsed))
|
||||
(has-children (nth parsed 1)))
|
||||
(let ((island (make-island comp-name params has-children body env)))
|
||||
(env-set! env (symbol-name name-sym) island)
|
||||
(env-bind! env (symbol-name name-sym) island)
|
||||
island))))
|
||||
|
||||
|
||||
@@ -659,7 +659,7 @@
|
||||
(params (first parsed))
|
||||
(rest-param (nth parsed 1)))
|
||||
(let ((mac (make-macro params rest-param body env (symbol-name name-sym))))
|
||||
(env-set! env (symbol-name name-sym) mac)
|
||||
(env-bind! env (symbol-name name-sym) mac)
|
||||
mac))))
|
||||
|
||||
(define parse-macro-params
|
||||
@@ -688,7 +688,7 @@
|
||||
;; (defstyle name expr) — bind name to evaluated expr (string, function, etc.)
|
||||
(let ((name-sym (first args))
|
||||
(value (trampoline (eval-expr (nth args 1) env))))
|
||||
(env-set! env (symbol-name name-sym) value)
|
||||
(env-bind! env (symbol-name name-sym) value)
|
||||
value)))
|
||||
|
||||
|
||||
@@ -749,7 +749,7 @@
|
||||
(dict))))
|
||||
(dict-set! registry type-name
|
||||
(make-type-def type-name type-params body))
|
||||
(env-set! env "*type-registry*" registry)
|
||||
(env-bind! env "*type-registry*" registry)
|
||||
nil))))
|
||||
|
||||
|
||||
@@ -764,7 +764,7 @@
|
||||
(list))))
|
||||
(when (not (contains? registry effect-name))
|
||||
(append! registry effect-name))
|
||||
(env-set! env "*effect-registry*" registry)
|
||||
(env-bind! env "*effect-registry*" registry)
|
||||
nil)))
|
||||
|
||||
|
||||
@@ -879,7 +879,7 @@
|
||||
(first binding))))
|
||||
(append! names vname)
|
||||
(append! val-exprs (nth binding 1))
|
||||
(env-set! local vname nil)))
|
||||
(env-bind! local vname nil)))
|
||||
bindings)
|
||||
;; Clojure-style
|
||||
(reduce
|
||||
@@ -890,21 +890,21 @@
|
||||
(val-expr (nth bindings (inc (* pair-idx 2)))))
|
||||
(append! names vname)
|
||||
(append! val-exprs val-expr)
|
||||
(env-set! local vname nil)))
|
||||
(env-bind! local vname nil)))
|
||||
nil
|
||||
(range 0 (/ (len bindings) 2))))
|
||||
;; Second pass: evaluate values (they can see each other's names)
|
||||
(let ((values (map (fn (e) (trampoline (eval-expr e local))) val-exprs)))
|
||||
;; Bind final values
|
||||
(for-each
|
||||
(fn (pair) (env-set! local (first pair) (nth pair 1)))
|
||||
(fn (pair) (env-bind! local (first pair) (nth pair 1)))
|
||||
(zip names values))
|
||||
;; Patch lambda closures so they see the final bindings
|
||||
(for-each
|
||||
(fn (val)
|
||||
(when (lambda? val)
|
||||
(for-each
|
||||
(fn (n) (env-set! (lambda-closure val) n (env-get local n)))
|
||||
(fn (n) (env-bind! (lambda-closure val) n (env-get local n)))
|
||||
names)))
|
||||
values))
|
||||
;; Evaluate body
|
||||
@@ -998,14 +998,14 @@
|
||||
;; Bind positional params (unevaluated)
|
||||
(for-each
|
||||
(fn (pair)
|
||||
(env-set! local (first pair)
|
||||
(env-bind! local (first pair)
|
||||
(if (< (nth pair 1) (len raw-args))
|
||||
(nth raw-args (nth pair 1))
|
||||
nil)))
|
||||
(map-indexed (fn (i p) (list p i)) (macro-params mac)))
|
||||
;; Bind &rest param
|
||||
(when (macro-rest-param mac)
|
||||
(env-set! local (macro-rest-param mac)
|
||||
(env-bind! local (macro-rest-param mac)
|
||||
(slice raw-args (len (macro-params mac)))))
|
||||
;; Evaluate body → new AST
|
||||
(trampoline (eval-expr (macro-body mac) local)))))
|
||||
@@ -1153,7 +1153,8 @@
|
||||
;; Environment:
|
||||
;; (env-has? env name) → boolean
|
||||
;; (env-get env name) → value
|
||||
;; (env-set! env name val) → void (mutating)
|
||||
;; (env-bind! env name val) → void (create binding on THIS env, no chain walk)
|
||||
;; (env-set! env name val) → void (mutate existing binding, walks scope chain)
|
||||
;; (env-extend env) → new env inheriting from env
|
||||
;; (env-merge base overlay) → new env with overlay on top
|
||||
;;
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
(let ((name (if (= (type-of (first pair)) "symbol")
|
||||
(symbol-name (first pair))
|
||||
(str (first pair)))))
|
||||
(env-set! local name (trampoline (eval-expr (nth pair 1) local))))))
|
||||
(env-bind! local name (trampoline (eval-expr (nth pair 1) local))))))
|
||||
bindings)
|
||||
local)))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user