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:
2026-03-15 11:38:35 +00:00
parent c20369b766
commit 5a03943b39
17 changed files with 188 additions and 139 deletions

View File

@@ -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)