Fix process-bindings scope loss and async-invoke arity, bootstrap async adapter
Two bugs fixed:
1. process-bindings used merge(env) which returns {} for Env objects
(Env is not a dict subclass). Changed to env-extend in render.sx
and adapter-async.sx. This caused "Undefined symbol: theme" etc.
2. async-aser-eval-call passed evaled-args list to async-invoke(&rest),
double-wrapping it. Changed to inline apply + coroutine check.
Also: bootstrap define-async into sx_ref.py (Phase 6), replace ~1000 LOC
hand-written async_eval_ref.py with 24-line thin re-export shim.
Test runner now uses Env (not flat dict) for render envs to catch scope bugs.
8 new regression tests (4 scope chain, 2 native callable arity, 2 render).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-11T14:54:55Z";
|
||||
var SX_VERSION = "2026-03-11T16:35:21Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -204,7 +204,7 @@
|
||||
|
||||
// JSON / dict helpers for island state serialization
|
||||
function jsonSerialize(obj) {
|
||||
try { return JSON.stringify(obj); } catch(e) { return "{}"; }
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
function isEmptyDict(d) {
|
||||
if (!d || typeof d !== "object") return true;
|
||||
@@ -214,11 +214,34 @@
|
||||
|
||||
function envHas(env, name) { return name in env; }
|
||||
function envGet(env, name) { return env[name]; }
|
||||
function envSet(env, name, val) { env[name] = val; }
|
||||
function envSet(env, name, val) {
|
||||
// Walk prototype chain to find where the variable is defined (for set!)
|
||||
var obj = env;
|
||||
while (obj !== null && obj !== Object.prototype) {
|
||||
if (obj.hasOwnProperty(name)) { obj[name] = val; return; }
|
||||
obj = Object.getPrototypeOf(obj);
|
||||
}
|
||||
// Not found in any parent scope — set on the immediate env
|
||||
env[name] = val;
|
||||
}
|
||||
function envExtend(env) { return Object.create(env); }
|
||||
function envMerge(base, overlay) {
|
||||
// Same env or overlay is descendant of base — just extend, no copy.
|
||||
// This prevents set! inside lambdas from modifying shadow copies.
|
||||
if (base === overlay) return Object.create(base);
|
||||
var p = overlay;
|
||||
for (var d = 0; p && p !== Object.prototype && d < 100; d++) {
|
||||
if (p === base) return Object.create(base);
|
||||
p = Object.getPrototypeOf(p);
|
||||
}
|
||||
// General case: extend base, copy ONLY overlay properties that don't
|
||||
// exist in the base chain (avoids shadowing closure bindings).
|
||||
var child = Object.create(base);
|
||||
if (overlay) for (var k in overlay) if (overlay.hasOwnProperty(k)) child[k] = overlay[k];
|
||||
if (overlay) {
|
||||
for (var k in overlay) {
|
||||
if (overlay.hasOwnProperty(k) && !(k in base)) child[k] = overlay[k];
|
||||
}
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
@@ -732,9 +755,9 @@
|
||||
var kwargs = first(parsed);
|
||||
var children = nth(parsed, 1);
|
||||
var local = envMerge(componentClosure(comp), env);
|
||||
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = sxOr(dictGet(kwargs, p), NIL); } }
|
||||
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, sxOr(dictGet(kwargs, p), NIL)); } }
|
||||
if (isSxTruthy(componentHasChildren(comp))) {
|
||||
local["children"] = children;
|
||||
envSet(local, "children", children);
|
||||
}
|
||||
return makeThunk(componentBody(comp), local);
|
||||
})(); };
|
||||
@@ -841,7 +864,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var loopBody = (isSxTruthy((len(body) == 1)) ? first(body) : cons(makeSymbol("begin"), body));
|
||||
var loopFn = makeLambda(params, loopBody, env);
|
||||
loopFn.name = loopName;
|
||||
lambdaClosure(loopFn)[loopName] = loopFn;
|
||||
envSet(lambdaClosure(loopFn), loopName, loopFn);
|
||||
return (function() {
|
||||
var initVals = map(function(e) { return trampoline(evalExpr(e, env)); }, inits);
|
||||
return callLambda(loopFn, initVals, env);
|
||||
@@ -865,7 +888,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
if (isSxTruthy((isSxTruthy(isLambda(value)) && isNil(lambdaName(value))))) {
|
||||
value.name = symbolName(nameSym);
|
||||
}
|
||||
env[symbolName(nameSym)] = value;
|
||||
envSet(env, symbolName(nameSym), value);
|
||||
return value;
|
||||
})(); };
|
||||
|
||||
@@ -881,7 +904,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var affinity = defcompKwarg(args, "affinity", "auto");
|
||||
return (function() {
|
||||
var comp = makeComponent(compName, params, hasChildren, body, env, affinity);
|
||||
env[symbolName(nameSym)] = comp;
|
||||
envSet(env, symbolName(nameSym), comp);
|
||||
return comp;
|
||||
})();
|
||||
})(); };
|
||||
@@ -924,7 +947,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var hasChildren = nth(parsed, 1);
|
||||
return (function() {
|
||||
var island = makeIsland(compName, params, hasChildren, body, env);
|
||||
env[symbolName(nameSym)] = island;
|
||||
envSet(env, symbolName(nameSym), island);
|
||||
return island;
|
||||
})();
|
||||
})(); };
|
||||
@@ -939,7 +962,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var restParam = nth(parsed, 1);
|
||||
return (function() {
|
||||
var mac = makeMacro(params, restParam, body, env, symbolName(nameSym));
|
||||
env[symbolName(nameSym)] = mac;
|
||||
envSet(env, symbolName(nameSym), mac);
|
||||
return mac;
|
||||
})();
|
||||
})(); };
|
||||
@@ -956,7 +979,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var sfDefstyle = function(args, env) { return (function() {
|
||||
var nameSym = first(args);
|
||||
var value = trampoline(evalExpr(nth(args, 1), env));
|
||||
env[symbolName(nameSym)] = value;
|
||||
envSet(env, symbolName(nameSym), value);
|
||||
return value;
|
||||
})(); };
|
||||
|
||||
@@ -996,7 +1019,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var sfSetBang = function(args, env) { return (function() {
|
||||
var name = symbolName(first(args));
|
||||
var value = trampoline(evalExpr(nth(args, 1), env));
|
||||
env[name] = value;
|
||||
envSet(env, name, value);
|
||||
return value;
|
||||
})(); };
|
||||
|
||||
@@ -1021,7 +1044,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
})(); }, NIL, range(0, (len(bindings) / 2))));
|
||||
(function() {
|
||||
var values = map(function(e) { return trampoline(evalExpr(e, local)); }, valExprs);
|
||||
{ var _c = zip(names, values); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; local[first(pair)] = nth(pair, 1); } }
|
||||
{ var _c = zip(names, values); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; envSet(local, first(pair), nth(pair, 1)); } }
|
||||
return forEach(function(val) { return (isSxTruthy(isLambda(val)) ? forEach(function(n) { return envSet(lambdaClosure(val), n, envGet(local, n)); }, names) : NIL); }, values);
|
||||
})();
|
||||
{ var _c = slice(body, 0, (len(body) - 1)); for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; trampoline(evalExpr(e, local)); } }
|
||||
@@ -1046,9 +1069,9 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
// expand-macro
|
||||
var expandMacro = function(mac, rawArgs, env) { return (function() {
|
||||
var local = envMerge(macroClosure(mac), env);
|
||||
{ var _c = mapIndexed(function(i, p) { return [p, i]; }, macroParams(mac)); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; local[first(pair)] = (isSxTruthy((nth(pair, 1) < len(rawArgs))) ? nth(rawArgs, nth(pair, 1)) : NIL); } }
|
||||
{ var _c = mapIndexed(function(i, p) { return [p, i]; }, macroParams(mac)); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; envSet(local, first(pair), (isSxTruthy((nth(pair, 1) < len(rawArgs))) ? nth(rawArgs, nth(pair, 1)) : NIL)); } }
|
||||
if (isSxTruthy(macroRestParam(mac))) {
|
||||
local[macroRestParam(mac)] = slice(rawArgs, len(macroParams(mac)));
|
||||
envSet(local, macroRestParam(mac), slice(rawArgs, len(macroParams(mac))));
|
||||
}
|
||||
return trampoline(evalExpr(macroBody(mac), local));
|
||||
})(); };
|
||||
@@ -1162,7 +1185,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
|
||||
// process-bindings
|
||||
var processBindings = function(bindings, env) { return (function() {
|
||||
var local = merge(env);
|
||||
var local = envExtend(env);
|
||||
{ var _c = bindings; for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(pair) == "list")) && (len(pair) >= 2)))) {
|
||||
(function() {
|
||||
var name = (isSxTruthy((typeOf(first(pair)) == "symbol")) ? symbolName(first(pair)) : (String(first(pair))));
|
||||
@@ -1392,9 +1415,9 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
|
||||
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||
return (function() {
|
||||
var local = envMerge(componentClosure(comp), env);
|
||||
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
|
||||
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
|
||||
if (isSxTruthy(componentHasChildren(comp))) {
|
||||
local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)));
|
||||
envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))));
|
||||
}
|
||||
return renderToHtml(componentBody(comp), local);
|
||||
})();
|
||||
@@ -1458,20 +1481,20 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
|
||||
return (function() {
|
||||
var local = envMerge(componentClosure(island), env);
|
||||
var islandName = componentName(island);
|
||||
{ var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
|
||||
{ var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
|
||||
if (isSxTruthy(componentHasChildren(island))) {
|
||||
local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)));
|
||||
envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))));
|
||||
}
|
||||
return (function() {
|
||||
var bodyHtml = renderToHtml(componentBody(island), local);
|
||||
var stateJson = serializeIslandState(kwargs);
|
||||
return (String("<span data-sx-island=\"") + String(escapeAttr(islandName)) + String("\"") + String((isSxTruthy(stateJson) ? (String(" data-sx-state=\"") + String(escapeAttr(stateJson)) + String("\"")) : "")) + String(">") + String(bodyHtml) + String("</span>"));
|
||||
var stateSx = serializeIslandState(kwargs);
|
||||
return (String("<span data-sx-island=\"") + String(escapeAttr(islandName)) + String("\"") + String((isSxTruthy(stateSx) ? (String(" data-sx-state=\"") + String(escapeAttr(stateSx)) + String("\"")) : "")) + String(">") + String(bodyHtml) + String("</span>"));
|
||||
})();
|
||||
})();
|
||||
})(); };
|
||||
|
||||
// serialize-island-state
|
||||
var serializeIslandState = function(kwargs) { return (isSxTruthy(isEmptyDict(kwargs)) ? NIL : jsonSerialize(kwargs)); };
|
||||
var serializeIslandState = function(kwargs) { return (isSxTruthy(isEmptyDict(kwargs)) ? NIL : sxSerialize(kwargs)); };
|
||||
|
||||
|
||||
// === Transpiled from adapter-sx ===
|
||||
@@ -1586,7 +1609,7 @@ return result; }, args);
|
||||
var coll = trampoline(evalExpr(nth(args, 1), env));
|
||||
return map(function(item) { return (isSxTruthy(isLambda(f)) ? (function() {
|
||||
var local = envMerge(lambdaClosure(f), env);
|
||||
local[first(lambdaParams(f))] = item;
|
||||
envSet(local, first(lambdaParams(f)), item);
|
||||
return aser(lambdaBody(f), local);
|
||||
})() : invoke(f, item)); }, coll);
|
||||
})() : (isSxTruthy((name == "map-indexed")) ? (function() {
|
||||
@@ -1594,8 +1617,8 @@ return result; }, args);
|
||||
var coll = trampoline(evalExpr(nth(args, 1), env));
|
||||
return mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? (function() {
|
||||
var local = envMerge(lambdaClosure(f), env);
|
||||
local[first(lambdaParams(f))] = i;
|
||||
local[nth(lambdaParams(f), 1)] = item;
|
||||
envSet(local, first(lambdaParams(f)), i);
|
||||
envSet(local, nth(lambdaParams(f), 1), item);
|
||||
return aser(lambdaBody(f), local);
|
||||
})() : invoke(f, i, item)); }, coll);
|
||||
})() : (isSxTruthy((name == "for-each")) ? (function() {
|
||||
@@ -1604,7 +1627,7 @@ return result; }, args);
|
||||
var results = [];
|
||||
{ var _c = coll; for (var _i = 0; _i < _c.length; _i++) { var item = _c[_i]; (isSxTruthy(isLambda(f)) ? (function() {
|
||||
var local = envMerge(lambdaClosure(f), env);
|
||||
local[first(lambdaParams(f))] = item;
|
||||
envSet(local, first(lambdaParams(f)), item);
|
||||
return append_b(results, aser(lambdaBody(f), local));
|
||||
})() : invoke(f, item)); } }
|
||||
return (isSxTruthy(isEmpty(results)) ? NIL : results);
|
||||
@@ -1662,7 +1685,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
|
||||
var attrExpr = nth(args, (get(state, "i") + 1));
|
||||
(isSxTruthy(startsWith(attrName, "on-")) ? (function() {
|
||||
var attrVal = trampoline(evalExpr(attrExpr, env));
|
||||
return (isSxTruthy(isCallable(attrVal)) ? domListen(el, slice(attrName, 3), (isSxTruthy((isSxTruthy(isLambda(attrVal)) && (len(lambdaParams(attrVal)) == 0))) ? function(e) { return callLambda(attrVal, [], lambdaClosure(attrVal)); } : attrVal)) : NIL);
|
||||
return (isSxTruthy(isCallable(attrVal)) ? domListen(el, slice(attrName, 3), attrVal) : NIL);
|
||||
})() : (isSxTruthy((attrName == "bind")) ? (function() {
|
||||
var attrVal = trampoline(evalExpr(attrExpr, env));
|
||||
return (isSxTruthy(isSignal(attrVal)) ? bindInput(el, attrVal) : NIL);
|
||||
@@ -1696,7 +1719,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
|
||||
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||
return (function() {
|
||||
var local = envMerge(componentClosure(comp), env);
|
||||
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
|
||||
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
|
||||
if (isSxTruthy(componentHasChildren(comp))) {
|
||||
(function() {
|
||||
var childFrag = createFragment();
|
||||
@@ -1887,7 +1910,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
|
||||
return (function() {
|
||||
var local = envMerge(componentClosure(island), env);
|
||||
var islandName = componentName(island);
|
||||
{ var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
|
||||
{ var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
|
||||
if (isSxTruthy(componentHasChildren(island))) {
|
||||
(function() {
|
||||
var childFrag = createFragment();
|
||||
@@ -3002,9 +3025,9 @@ return postSwap(target); }))) : NIL);
|
||||
var exprs = sxParse(body);
|
||||
return domListen(el, eventName, function(e) { return (function() {
|
||||
var handlerEnv = envExtend({});
|
||||
handlerEnv["event"] = e;
|
||||
handlerEnv["this"] = el;
|
||||
handlerEnv["detail"] = eventDetail(e);
|
||||
envSet(handlerEnv, "event", e);
|
||||
envSet(handlerEnv, "this", el);
|
||||
envSet(handlerEnv, "detail", eventDetail(e));
|
||||
return forEach(function(expr) { return evalExpr(expr, handlerEnv); }, exprs);
|
||||
})(); });
|
||||
})()) : NIL);
|
||||
@@ -3233,17 +3256,17 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
// hydrate-island
|
||||
var hydrateIsland = function(el) { return (function() {
|
||||
var name = domGetAttr(el, "data-sx-island");
|
||||
var stateJson = sxOr(domGetAttr(el, "data-sx-state"), "{}");
|
||||
var stateSx = sxOr(domGetAttr(el, "data-sx-state"), "{}");
|
||||
return (function() {
|
||||
var compName = (String("~") + String(name));
|
||||
var env = getRenderEnv(NIL);
|
||||
return (function() {
|
||||
var comp = envGet(env, compName);
|
||||
return (isSxTruthy(!isSxTruthy(sxOr(isComponent(comp), isIsland(comp)))) ? logWarn((String("hydrate-island: unknown island ") + String(compName))) : (function() {
|
||||
var kwargs = jsonParse(stateJson);
|
||||
var kwargs = sxOr(first(sxParse(stateSx)), {});
|
||||
var disposers = [];
|
||||
var local = envMerge(componentClosure(comp), env);
|
||||
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
|
||||
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
|
||||
return (function() {
|
||||
var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); });
|
||||
domSetTextContent(el, "");
|
||||
@@ -3976,8 +3999,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
function domListen(el, name, handler) {
|
||||
if (!_hasDom || !el) return function() {};
|
||||
// Wrap SX lambdas from runtime-evaluated island code into native fns
|
||||
// If lambda takes 0 params, call without event arg (convenience for on-click handlers)
|
||||
var wrapped = isLambda(handler)
|
||||
? function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
|
||||
? (lambdaParams(handler).length === 0
|
||||
? function(e) { try { invoke(handler); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
|
||||
: function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
|
||||
: handler;
|
||||
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
||||
el.addEventListener(name, wrapped);
|
||||
|
||||
Reference in New Issue
Block a user