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

@@ -1162,12 +1162,12 @@ PLATFORM_JS_PRE = '''
function makeSymbol(n) { return new Symbol(n); }
function makeKeyword(n) { return new Keyword(n); }
function makeLambda(params, body, env) { return new Lambda(params, body, merge(env)); }
function makeLambda(params, body, env) { return new Lambda(params, body, env); }
function makeComponent(name, params, hasChildren, body, env, affinity) {
return new Component(name, params, hasChildren, body, merge(env), affinity);
return new Component(name, params, hasChildren, body, env, affinity);
}
function makeMacro(params, restParam, body, env, name) {
return new Macro(params, restParam, body, merge(env), name);
return new Macro(params, restParam, body, env, name);
}
function makeThunk(expr, env) { return new Thunk(expr, env); }
@@ -1257,7 +1257,7 @@ PLATFORM_JS_PRE = '''
// Island platform
function makeIsland(name, params, hasChildren, body, env) {
return new Island(name, params, hasChildren, body, merge(env));
return new Island(name, params, hasChildren, body, env);
}
// JSON / dict helpers for island state serialization
@@ -1272,6 +1272,11 @@ PLATFORM_JS_PRE = '''
function envHas(env, name) { return name in env; }
function envGet(env, name) { return env[name]; }
function envBind(env, name, val) {
// Direct property set — creates or overwrites on THIS env only.
// Used by let, define, defcomp, lambda param binding.
env[name] = val;
}
function envSet(env, name, val) {
// Walk prototype chain to find where the variable is defined (for set!)
var obj = env;

View File

@@ -72,8 +72,9 @@ env["cek-eval"] = function(s) {
env["eval-expr-cek"] = function(expr, e) { return Sx.eval(expr, e || env); };
env["env-get"] = function(e, k) { return e && e[k] !== undefined ? e[k] : null; };
env["env-has?"] = function(e, k) { return e && k in e; };
env["env-bind!"] = function(e, k, v) { if (e) e[k] = v; return v; };
env["env-set!"] = function(e, k, v) { if (e) e[k] = v; return v; };
env["env-extend"] = function(e) { return Object.assign({}, e); };
env["env-extend"] = function(e) { return Object.create(e); };
env["env-merge"] = function(a, b) { return Object.assign({}, a, b); };
// Missing primitives referenced by tests

View File

@@ -107,6 +107,7 @@
"get-primitive" "getPrimitive"
"env-has?" "envHas"
"env-get" "envGet"
"env-bind!" "envBind"
"env-set!" "envSet"
"env-extend" "envExtend"
"env-merge" "envMerge"
@@ -989,6 +990,11 @@
", " (js-expr (nth args 1))
", " (js-expr (nth args 2)) ")")
(= op "env-bind!")
(str "envBind(" (js-expr (nth args 0))
", " (js-expr (nth args 1))
", " (js-expr (nth args 2)) ")")
(= op "env-set!")
(str "envSet(" (js-expr (nth args 0))
", " (js-expr (nth args 1))
@@ -1396,6 +1402,10 @@
"] = " (js-expr (nth expr 3)) ";")
(= name "append!")
(str (js-expr (nth expr 1)) ".push(" (js-expr (nth expr 2)) ");")
(= name "env-bind!")
(str "envBind(" (js-expr (nth expr 1))
", " (js-expr (nth expr 2))
", " (js-expr (nth expr 3)) ");")
(= name "env-set!")
(str "envSet(" (js-expr (nth expr 1))
", " (js-expr (nth expr 2))