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

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