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:
@@ -229,7 +229,7 @@
|
||||
;; Build env: closure + caller env + params
|
||||
(let ((local (env-merge (component-closure comp) env)))
|
||||
(for-each
|
||||
(fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(fn (p) (env-bind! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params comp))
|
||||
;; Pre-render children to raw HTML
|
||||
(when (component-has-children? comp)
|
||||
@@ -237,7 +237,7 @@
|
||||
(for-each
|
||||
(fn (c) (append! parts (async-render c env ctx)))
|
||||
children)
|
||||
(env-set! local "children"
|
||||
(env-bind! local "children"
|
||||
(make-raw-html (join "" parts)))))
|
||||
(async-render (component-body comp) local ctx)))))
|
||||
|
||||
@@ -254,7 +254,7 @@
|
||||
(let ((local (env-merge (component-closure island) env))
|
||||
(island-name (component-name island)))
|
||||
(for-each
|
||||
(fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(fn (p) (env-bind! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params island))
|
||||
;; Pre-render children
|
||||
(when (component-has-children? island)
|
||||
@@ -262,7 +262,7 @@
|
||||
(for-each
|
||||
(fn (c) (append! parts (async-render c env ctx)))
|
||||
children)
|
||||
(env-set! local "children"
|
||||
(env-bind! local "children"
|
||||
(make-raw-html (join "" parts)))))
|
||||
(let ((body-html (async-render (component-body island) local ctx))
|
||||
(state-json (serialize-island-state kwargs)))
|
||||
@@ -283,7 +283,7 @@
|
||||
(fn ((f :as lambda) (args :as list) (env :as dict) ctx)
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(for-each-indexed
|
||||
(fn (i p) (env-set! local p (nth args i)))
|
||||
(fn (i p) (env-bind! local p (nth args i)))
|
||||
(lambda-params f))
|
||||
(async-render (lambda-body f) local ctx))))
|
||||
|
||||
@@ -517,7 +517,7 @@
|
||||
(let ((name (if (= (type-of (first pair)) "symbol")
|
||||
(symbol-name (first pair))
|
||||
(str (first pair)))))
|
||||
(env-set! local name (async-eval (nth pair 1) local ctx)))))
|
||||
(env-bind! local name (async-eval (nth pair 1) local ctx)))))
|
||||
bindings)
|
||||
;; Clojure-style: (name val name val ...)
|
||||
(async-process-bindings-flat bindings local ctx)))
|
||||
@@ -538,7 +538,7 @@
|
||||
(symbol-name item)
|
||||
(str item))))
|
||||
(when (< (inc i) (len bindings))
|
||||
(env-set! local name
|
||||
(env-bind! local name
|
||||
(async-eval (nth bindings (inc i)) local ctx))))
|
||||
(set! skip true)
|
||||
(set! i (inc i)))))
|
||||
@@ -735,7 +735,7 @@
|
||||
(lambda? f)
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(for-each-indexed
|
||||
(fn (i p) (env-set! local p (nth evaled-args i)))
|
||||
(fn (i p) (env-bind! local p (nth evaled-args i)))
|
||||
(lambda-params f))
|
||||
(async-aser (lambda-body f) local ctx))
|
||||
(component? f)
|
||||
@@ -807,7 +807,7 @@
|
||||
(async-parse-aser-kw-args args kwargs children env ctx)
|
||||
(let ((local (env-merge (component-closure comp) env)))
|
||||
(for-each
|
||||
(fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(fn (p) (env-bind! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params comp))
|
||||
(when (component-has-children? comp)
|
||||
(let ((child-parts (list)))
|
||||
@@ -823,7 +823,7 @@
|
||||
(when (not (nil? result))
|
||||
(append! child-parts (serialize result))))))
|
||||
children)
|
||||
(env-set! local "children"
|
||||
(env-bind! local "children"
|
||||
(make-sx-expr (str "(<> " (join " " child-parts) ")")))))
|
||||
(async-aser (component-body comp) local ctx)))))
|
||||
|
||||
@@ -1033,7 +1033,7 @@
|
||||
;; set!
|
||||
(= name "set!")
|
||||
(let ((value (async-eval (nth args 1) env ctx)))
|
||||
(env-set! env (symbol-name (first args)) value)
|
||||
(env-bind! env (symbol-name (first args)) value)
|
||||
value)
|
||||
|
||||
;; map
|
||||
@@ -1197,7 +1197,7 @@
|
||||
(lambda? f)
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(for-each-indexed
|
||||
(fn (i p) (env-set! local p (nth args i)))
|
||||
(fn (i p) (env-bind! local p (nth args i)))
|
||||
(lambda-params f))
|
||||
(async-eval (lambda-body f) local ctx))
|
||||
:else
|
||||
@@ -1217,7 +1217,7 @@
|
||||
(fn (item)
|
||||
(if (lambda? f)
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(env-set! local (first (lambda-params f)) item)
|
||||
(env-bind! local (first (lambda-params f)) item)
|
||||
(append! results (async-aser (lambda-body f) local ctx)))
|
||||
(append! results (async-invoke f item))))
|
||||
coll)
|
||||
@@ -1234,8 +1234,8 @@
|
||||
(fn (item)
|
||||
(if (lambda? f)
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(env-set! local (first (lambda-params f)) i)
|
||||
(env-set! local (nth (lambda-params f) 1) item)
|
||||
(env-bind! local (first (lambda-params f)) i)
|
||||
(env-bind! local (nth (lambda-params f) 1) item)
|
||||
(append! results (async-aser (lambda-body f) local ctx)))
|
||||
(append! results (async-invoke f i item)))
|
||||
(set! i (inc i)))
|
||||
@@ -1252,7 +1252,7 @@
|
||||
(fn (item)
|
||||
(if (lambda? f)
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(env-set! local (first (lambda-params f)) item)
|
||||
(env-bind! local (first (lambda-params f)) item)
|
||||
(append! results (async-aser (lambda-body f) local ctx)))
|
||||
(append! results (async-invoke f item))))
|
||||
coll)
|
||||
|
||||
@@ -307,7 +307,7 @@
|
||||
;; Bind params from kwargs
|
||||
(for-each
|
||||
(fn (p)
|
||||
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(env-bind! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params comp))
|
||||
|
||||
;; If component accepts children, pre-render them to a fragment
|
||||
@@ -320,7 +320,7 @@
|
||||
(when (not (spread? result))
|
||||
(dom-append child-frag result))))
|
||||
children)
|
||||
(env-set! local "children" child-frag)))
|
||||
(env-bind! local "children" child-frag)))
|
||||
|
||||
(render-to-dom (component-body comp) local ns)))))
|
||||
|
||||
@@ -687,7 +687,7 @@
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(for-each-indexed
|
||||
(fn (i p)
|
||||
(env-set! local p (nth args i)))
|
||||
(env-bind! local p (nth args i)))
|
||||
(lambda-params f))
|
||||
(render-to-dom (lambda-body f) local ns))))
|
||||
|
||||
@@ -734,7 +734,7 @@
|
||||
;; Bind params from kwargs
|
||||
(for-each
|
||||
(fn (p)
|
||||
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(env-bind! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params island))
|
||||
|
||||
;; If island accepts children, pre-render them to a fragment
|
||||
@@ -743,7 +743,7 @@
|
||||
(for-each
|
||||
(fn (c) (dom-append child-frag (render-to-dom c env ns)))
|
||||
children)
|
||||
(env-set! local "children" child-frag)))
|
||||
(env-bind! local "children" child-frag)))
|
||||
|
||||
;; Create the island container element
|
||||
(let ((container (dom-create-element "span" nil))
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(for-each-indexed
|
||||
(fn (i p)
|
||||
(env-set! local p (nth args i)))
|
||||
(env-bind! local p (nth args i)))
|
||||
(lambda-params f))
|
||||
(render-to-html (lambda-body f) local))))
|
||||
|
||||
@@ -315,11 +315,11 @@
|
||||
;; Bind params from kwargs
|
||||
(for-each
|
||||
(fn (p)
|
||||
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(env-bind! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params comp))
|
||||
;; If component accepts children, pre-render them to raw HTML
|
||||
(when (component-has-children? comp)
|
||||
(env-set! local "children"
|
||||
(env-bind! local "children"
|
||||
(make-raw-html (join "" (map (fn (c) (render-to-html c env)) children)))))
|
||||
(render-to-html (component-body comp) local)))))
|
||||
|
||||
@@ -481,12 +481,12 @@
|
||||
;; Bind params from kwargs
|
||||
(for-each
|
||||
(fn (p)
|
||||
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(env-bind! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params island))
|
||||
|
||||
;; If island accepts children, pre-render them to raw HTML
|
||||
(when (component-has-children? island)
|
||||
(env-set! local "children"
|
||||
(env-bind! local "children"
|
||||
(make-raw-html (join "" (map (fn (c) (render-to-html c env)) children)))))
|
||||
|
||||
;; Render the island body as HTML
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
(map (fn (item)
|
||||
(if (lambda? f)
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(env-set! local (first (lambda-params f)) item)
|
||||
(env-bind! local (first (lambda-params f)) item)
|
||||
(aser (lambda-body f) local))
|
||||
(cek-call f (list item))))
|
||||
coll))
|
||||
@@ -301,8 +301,8 @@
|
||||
(map-indexed (fn (i item)
|
||||
(if (lambda? f)
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(env-set! local (first (lambda-params f)) i)
|
||||
(env-set! local (nth (lambda-params f) 1) item)
|
||||
(env-bind! local (first (lambda-params f)) i)
|
||||
(env-bind! local (nth (lambda-params f) 1) item)
|
||||
(aser (lambda-body f) local))
|
||||
(cek-call f (list i item))))
|
||||
coll))
|
||||
@@ -315,7 +315,7 @@
|
||||
(for-each (fn (item)
|
||||
(if (lambda? f)
|
||||
(let ((local (env-merge (lambda-closure f) env)))
|
||||
(env-set! local (first (lambda-params f)) item)
|
||||
(env-bind! local (first (lambda-params f)) item)
|
||||
(append! results (aser (lambda-body f) local)))
|
||||
(cek-call f (list item))))
|
||||
coll)
|
||||
|
||||
@@ -361,7 +361,7 @@
|
||||
;; Bind params from kwargs
|
||||
(for-each
|
||||
(fn ((p :as string))
|
||||
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(env-bind! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params comp))
|
||||
|
||||
;; Render the island body in a reactive scope
|
||||
|
||||
10
web/forms.sx
10
web/forms.sx
@@ -98,7 +98,7 @@
|
||||
(params (get parsed "params"))
|
||||
(body (get parsed "body")))
|
||||
(let ((hdef (make-handler-def name params body env opts)))
|
||||
(env-set! env (str "handler:" name) hdef)
|
||||
(env-bind! env (str "handler:" name) hdef)
|
||||
hdef))))
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
(doc (if has-doc (nth args 2) ""))
|
||||
(body (if has-doc (nth args 3) (nth args 2))))
|
||||
(let ((qdef (make-query-def name params doc body env)))
|
||||
(env-set! env (str "query:" name) qdef)
|
||||
(env-bind! env (str "query:" name) qdef)
|
||||
qdef))))
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
(doc (if has-doc (nth args 2) ""))
|
||||
(body (if has-doc (nth args 3) (nth args 2))))
|
||||
(let ((adef (make-action-def name params doc body env)))
|
||||
(env-set! env (str "action:" name) adef)
|
||||
(env-bind! env (str "action:" name) adef)
|
||||
adef))))
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
(nth args (+ idx 1))))))
|
||||
(range 1 max-i 2)))
|
||||
(let ((pdef (make-page-def name slots env)))
|
||||
(env-set! env (str "page:" name) pdef)
|
||||
(env-bind! env (str "page:" name) pdef)
|
||||
pdef))))
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
(bindings (stream-chunk-bindings chunk)))
|
||||
(for-each
|
||||
(fn ((key :as string))
|
||||
(env-set! env (normalize-binding-key key)
|
||||
(env-bind! env (normalize-binding-key key)
|
||||
(get bindings key)))
|
||||
(keys bindings))
|
||||
env)))
|
||||
|
||||
@@ -1103,9 +1103,9 @@
|
||||
(dom-listen el event-name
|
||||
(fn (e)
|
||||
(let ((handler-env (env-extend (dict))))
|
||||
(env-set! handler-env "event" e)
|
||||
(env-set! handler-env "this" el)
|
||||
(env-set! handler-env "detail" (event-detail e))
|
||||
(env-bind! handler-env "event" e)
|
||||
(env-bind! handler-env "this" el)
|
||||
(env-bind! handler-env "detail" (event-detail e))
|
||||
(for-each
|
||||
(fn (expr) (eval-expr expr handler-env))
|
||||
exprs))))))))))
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
(defsuite "deref signal without reactive-reset"
|
||||
(deftest "deref signal returns current value"
|
||||
(let ((s (signal 99)))
|
||||
(env-set! (test-env) "test-sig" s)
|
||||
(env-bind! (test-env) "test-sig" s)
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(deref test-sig)")
|
||||
(test-env))))
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
(deftest "deref signal in expression returns computed value"
|
||||
(let ((s (signal 10)))
|
||||
(env-set! (test-env) "test-sig" s)
|
||||
(env-bind! (test-env) "test-sig" s)
|
||||
(let ((result (eval-expr-cek
|
||||
(sx-parse-one "(+ 5 (deref test-sig))")
|
||||
(test-env))))
|
||||
@@ -67,7 +67,7 @@
|
||||
(make-cek-state
|
||||
(sx-parse-one "(deref test-sig)")
|
||||
(let ((e (env-extend (test-env))))
|
||||
(env-set! e "test-sig" s)
|
||||
(env-bind! e "test-sig" s)
|
||||
e)
|
||||
(list (make-reactive-reset-frame
|
||||
(test-env)
|
||||
@@ -83,7 +83,7 @@
|
||||
;; Set up reactive-reset with tracking update-fn
|
||||
(scope-push! "sx-island-scope" nil)
|
||||
(let ((e (env-extend (test-env))))
|
||||
(env-set! e "test-sig" s)
|
||||
(env-bind! e "test-sig" s)
|
||||
(cek-run
|
||||
(make-cek-state
|
||||
(sx-parse-one "(deref test-sig)")
|
||||
@@ -107,7 +107,7 @@
|
||||
(update-calls (list)))
|
||||
(scope-push! "sx-island-scope" nil)
|
||||
(let ((e (env-extend (test-env))))
|
||||
(env-set! e "test-sig" s)
|
||||
(env-bind! e "test-sig" s)
|
||||
;; (str "val=" (deref test-sig)) — continuation captures (str "val=" [HOLE])
|
||||
(let ((result (cek-run
|
||||
(make-cek-state
|
||||
@@ -137,7 +137,7 @@
|
||||
;; Create island scope with collector that accumulates disposers
|
||||
(scope-push! "sx-island-scope" (fn (d) (append! disposers d)))
|
||||
(let ((e (env-extend (test-env))))
|
||||
(env-set! e "test-sig" s)
|
||||
(env-bind! e "test-sig" s)
|
||||
(cek-run
|
||||
(make-cek-state
|
||||
(sx-parse-one "(deref test-sig)")
|
||||
@@ -266,7 +266,7 @@
|
||||
|
||||
(deftest "for-each through CEK"
|
||||
(let ((log (list)))
|
||||
(env-set! (test-env) "test-log" log)
|
||||
(env-bind! (test-env) "test-log" log)
|
||||
(eval-expr-cek
|
||||
(sx-parse-one "(for-each (fn (x) (append! test-log x)) (list 1 2 3))")
|
||||
(test-env))
|
||||
|
||||
Reference in New Issue
Block a user