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

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