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))))))
|
||||
|
||||
Reference in New Issue
Block a user