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 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 isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
|
|
||||||
// JSON / dict helpers for island state serialization
|
// JSON / dict helpers for island state serialization
|
||||||
function jsonSerialize(obj) {
|
function jsonSerialize(obj) {
|
||||||
try { return JSON.stringify(obj); } catch(e) { return "{}"; }
|
return JSON.stringify(obj);
|
||||||
}
|
}
|
||||||
function isEmptyDict(d) {
|
function isEmptyDict(d) {
|
||||||
if (!d || typeof d !== "object") return true;
|
if (!d || typeof d !== "object") return true;
|
||||||
@@ -214,11 +214,34 @@
|
|||||||
|
|
||||||
function envHas(env, name) { return name in env; }
|
function envHas(env, name) { return name in env; }
|
||||||
function envGet(env, name) { return env[name]; }
|
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 envExtend(env) { return Object.create(env); }
|
||||||
function envMerge(base, overlay) {
|
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);
|
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;
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,9 +755,9 @@
|
|||||||
var kwargs = first(parsed);
|
var kwargs = first(parsed);
|
||||||
var children = nth(parsed, 1);
|
var children = nth(parsed, 1);
|
||||||
var local = envMerge(componentClosure(comp), env);
|
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))) {
|
if (isSxTruthy(componentHasChildren(comp))) {
|
||||||
local["children"] = children;
|
envSet(local, "children", children);
|
||||||
}
|
}
|
||||||
return makeThunk(componentBody(comp), local);
|
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 loopBody = (isSxTruthy((len(body) == 1)) ? first(body) : cons(makeSymbol("begin"), body));
|
||||||
var loopFn = makeLambda(params, loopBody, env);
|
var loopFn = makeLambda(params, loopBody, env);
|
||||||
loopFn.name = loopName;
|
loopFn.name = loopName;
|
||||||
lambdaClosure(loopFn)[loopName] = loopFn;
|
envSet(lambdaClosure(loopFn), loopName, loopFn);
|
||||||
return (function() {
|
return (function() {
|
||||||
var initVals = map(function(e) { return trampoline(evalExpr(e, env)); }, inits);
|
var initVals = map(function(e) { return trampoline(evalExpr(e, env)); }, inits);
|
||||||
return callLambda(loopFn, initVals, env);
|
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))))) {
|
if (isSxTruthy((isSxTruthy(isLambda(value)) && isNil(lambdaName(value))))) {
|
||||||
value.name = symbolName(nameSym);
|
value.name = symbolName(nameSym);
|
||||||
}
|
}
|
||||||
env[symbolName(nameSym)] = value;
|
envSet(env, symbolName(nameSym), value);
|
||||||
return 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");
|
var affinity = defcompKwarg(args, "affinity", "auto");
|
||||||
return (function() {
|
return (function() {
|
||||||
var comp = makeComponent(compName, params, hasChildren, body, env, affinity);
|
var comp = makeComponent(compName, params, hasChildren, body, env, affinity);
|
||||||
env[symbolName(nameSym)] = comp;
|
envSet(env, symbolName(nameSym), comp);
|
||||||
return comp;
|
return comp;
|
||||||
})();
|
})();
|
||||||
})(); };
|
})(); };
|
||||||
@@ -924,7 +947,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
|||||||
var hasChildren = nth(parsed, 1);
|
var hasChildren = nth(parsed, 1);
|
||||||
return (function() {
|
return (function() {
|
||||||
var island = makeIsland(compName, params, hasChildren, body, env);
|
var island = makeIsland(compName, params, hasChildren, body, env);
|
||||||
env[symbolName(nameSym)] = island;
|
envSet(env, symbolName(nameSym), island);
|
||||||
return island;
|
return island;
|
||||||
})();
|
})();
|
||||||
})(); };
|
})(); };
|
||||||
@@ -939,7 +962,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
|||||||
var restParam = nth(parsed, 1);
|
var restParam = nth(parsed, 1);
|
||||||
return (function() {
|
return (function() {
|
||||||
var mac = makeMacro(params, restParam, body, env, symbolName(nameSym));
|
var mac = makeMacro(params, restParam, body, env, symbolName(nameSym));
|
||||||
env[symbolName(nameSym)] = mac;
|
envSet(env, symbolName(nameSym), mac);
|
||||||
return 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 sfDefstyle = function(args, env) { return (function() {
|
||||||
var nameSym = first(args);
|
var nameSym = first(args);
|
||||||
var value = trampoline(evalExpr(nth(args, 1), env));
|
var value = trampoline(evalExpr(nth(args, 1), env));
|
||||||
env[symbolName(nameSym)] = value;
|
envSet(env, symbolName(nameSym), value);
|
||||||
return 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 sfSetBang = function(args, env) { return (function() {
|
||||||
var name = symbolName(first(args));
|
var name = symbolName(first(args));
|
||||||
var value = trampoline(evalExpr(nth(args, 1), env));
|
var value = trampoline(evalExpr(nth(args, 1), env));
|
||||||
env[name] = value;
|
envSet(env, name, value);
|
||||||
return 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))));
|
})(); }, NIL, range(0, (len(bindings) / 2))));
|
||||||
(function() {
|
(function() {
|
||||||
var values = map(function(e) { return trampoline(evalExpr(e, local)); }, valExprs);
|
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);
|
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)); } }
|
{ 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
|
// expand-macro
|
||||||
var expandMacro = function(mac, rawArgs, env) { return (function() {
|
var expandMacro = function(mac, rawArgs, env) { return (function() {
|
||||||
var local = envMerge(macroClosure(mac), env);
|
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))) {
|
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));
|
return trampoline(evalExpr(macroBody(mac), local));
|
||||||
})(); };
|
})(); };
|
||||||
@@ -1162,7 +1185,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
|||||||
|
|
||||||
// process-bindings
|
// process-bindings
|
||||||
var processBindings = function(bindings, env) { return (function() {
|
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)))) {
|
{ 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() {
|
(function() {
|
||||||
var name = (isSxTruthy((typeOf(first(pair)) == "symbol")) ? symbolName(first(pair)) : (String(first(pair))));
|
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);
|
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||||
return (function() {
|
return (function() {
|
||||||
var local = envMerge(componentClosure(comp), env);
|
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))) {
|
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);
|
return renderToHtml(componentBody(comp), local);
|
||||||
})();
|
})();
|
||||||
@@ -1458,20 +1481,20 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
|
|||||||
return (function() {
|
return (function() {
|
||||||
var local = envMerge(componentClosure(island), env);
|
var local = envMerge(componentClosure(island), env);
|
||||||
var islandName = componentName(island);
|
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))) {
|
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() {
|
return (function() {
|
||||||
var bodyHtml = renderToHtml(componentBody(island), local);
|
var bodyHtml = renderToHtml(componentBody(island), local);
|
||||||
var stateJson = serializeIslandState(kwargs);
|
var stateSx = 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>"));
|
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
|
// 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 ===
|
// === Transpiled from adapter-sx ===
|
||||||
@@ -1586,7 +1609,7 @@ return result; }, args);
|
|||||||
var coll = trampoline(evalExpr(nth(args, 1), env));
|
var coll = trampoline(evalExpr(nth(args, 1), env));
|
||||||
return map(function(item) { return (isSxTruthy(isLambda(f)) ? (function() {
|
return map(function(item) { return (isSxTruthy(isLambda(f)) ? (function() {
|
||||||
var local = envMerge(lambdaClosure(f), env);
|
var local = envMerge(lambdaClosure(f), env);
|
||||||
local[first(lambdaParams(f))] = item;
|
envSet(local, first(lambdaParams(f)), item);
|
||||||
return aser(lambdaBody(f), local);
|
return aser(lambdaBody(f), local);
|
||||||
})() : invoke(f, item)); }, coll);
|
})() : invoke(f, item)); }, coll);
|
||||||
})() : (isSxTruthy((name == "map-indexed")) ? (function() {
|
})() : (isSxTruthy((name == "map-indexed")) ? (function() {
|
||||||
@@ -1594,8 +1617,8 @@ return result; }, args);
|
|||||||
var coll = trampoline(evalExpr(nth(args, 1), env));
|
var coll = trampoline(evalExpr(nth(args, 1), env));
|
||||||
return mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? (function() {
|
return mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? (function() {
|
||||||
var local = envMerge(lambdaClosure(f), env);
|
var local = envMerge(lambdaClosure(f), env);
|
||||||
local[first(lambdaParams(f))] = i;
|
envSet(local, first(lambdaParams(f)), i);
|
||||||
local[nth(lambdaParams(f), 1)] = item;
|
envSet(local, nth(lambdaParams(f), 1), item);
|
||||||
return aser(lambdaBody(f), local);
|
return aser(lambdaBody(f), local);
|
||||||
})() : invoke(f, i, item)); }, coll);
|
})() : invoke(f, i, item)); }, coll);
|
||||||
})() : (isSxTruthy((name == "for-each")) ? (function() {
|
})() : (isSxTruthy((name == "for-each")) ? (function() {
|
||||||
@@ -1604,7 +1627,7 @@ return result; }, args);
|
|||||||
var results = [];
|
var results = [];
|
||||||
{ var _c = coll; for (var _i = 0; _i < _c.length; _i++) { var item = _c[_i]; (isSxTruthy(isLambda(f)) ? (function() {
|
{ 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);
|
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));
|
return append_b(results, aser(lambdaBody(f), local));
|
||||||
})() : invoke(f, item)); } }
|
})() : invoke(f, item)); } }
|
||||||
return (isSxTruthy(isEmpty(results)) ? NIL : results);
|
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));
|
var attrExpr = nth(args, (get(state, "i") + 1));
|
||||||
(isSxTruthy(startsWith(attrName, "on-")) ? (function() {
|
(isSxTruthy(startsWith(attrName, "on-")) ? (function() {
|
||||||
var attrVal = trampoline(evalExpr(attrExpr, env));
|
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() {
|
})() : (isSxTruthy((attrName == "bind")) ? (function() {
|
||||||
var attrVal = trampoline(evalExpr(attrExpr, env));
|
var attrVal = trampoline(evalExpr(attrExpr, env));
|
||||||
return (isSxTruthy(isSignal(attrVal)) ? bindInput(el, attrVal) : NIL);
|
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);
|
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||||
return (function() {
|
return (function() {
|
||||||
var local = envMerge(componentClosure(comp), env);
|
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))) {
|
if (isSxTruthy(componentHasChildren(comp))) {
|
||||||
(function() {
|
(function() {
|
||||||
var childFrag = createFragment();
|
var childFrag = createFragment();
|
||||||
@@ -1887,7 +1910,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
|
|||||||
return (function() {
|
return (function() {
|
||||||
var local = envMerge(componentClosure(island), env);
|
var local = envMerge(componentClosure(island), env);
|
||||||
var islandName = componentName(island);
|
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))) {
|
if (isSxTruthy(componentHasChildren(island))) {
|
||||||
(function() {
|
(function() {
|
||||||
var childFrag = createFragment();
|
var childFrag = createFragment();
|
||||||
@@ -3002,9 +3025,9 @@ return postSwap(target); }))) : NIL);
|
|||||||
var exprs = sxParse(body);
|
var exprs = sxParse(body);
|
||||||
return domListen(el, eventName, function(e) { return (function() {
|
return domListen(el, eventName, function(e) { return (function() {
|
||||||
var handlerEnv = envExtend({});
|
var handlerEnv = envExtend({});
|
||||||
handlerEnv["event"] = e;
|
envSet(handlerEnv, "event", e);
|
||||||
handlerEnv["this"] = el;
|
envSet(handlerEnv, "this", el);
|
||||||
handlerEnv["detail"] = eventDetail(e);
|
envSet(handlerEnv, "detail", eventDetail(e));
|
||||||
return forEach(function(expr) { return evalExpr(expr, handlerEnv); }, exprs);
|
return forEach(function(expr) { return evalExpr(expr, handlerEnv); }, exprs);
|
||||||
})(); });
|
})(); });
|
||||||
})()) : NIL);
|
})()) : NIL);
|
||||||
@@ -3233,17 +3256,17 @@ callExpr.push(dictGet(kwargs, k)); } }
|
|||||||
// hydrate-island
|
// hydrate-island
|
||||||
var hydrateIsland = function(el) { return (function() {
|
var hydrateIsland = function(el) { return (function() {
|
||||||
var name = domGetAttr(el, "data-sx-island");
|
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() {
|
return (function() {
|
||||||
var compName = (String("~") + String(name));
|
var compName = (String("~") + String(name));
|
||||||
var env = getRenderEnv(NIL);
|
var env = getRenderEnv(NIL);
|
||||||
return (function() {
|
return (function() {
|
||||||
var comp = envGet(env, compName);
|
var comp = envGet(env, compName);
|
||||||
return (isSxTruthy(!isSxTruthy(sxOr(isComponent(comp), isIsland(comp)))) ? logWarn((String("hydrate-island: unknown island ") + String(compName))) : (function() {
|
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 disposers = [];
|
||||||
var local = envMerge(componentClosure(comp), env);
|
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() {
|
return (function() {
|
||||||
var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); });
|
var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); });
|
||||||
domSetTextContent(el, "");
|
domSetTextContent(el, "");
|
||||||
@@ -3976,8 +3999,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
|||||||
function domListen(el, name, handler) {
|
function domListen(el, name, handler) {
|
||||||
if (!_hasDom || !el) return function() {};
|
if (!_hasDom || !el) return function() {};
|
||||||
// Wrap SX lambdas from runtime-evaluated island code into native fns
|
// 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)
|
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;
|
: handler;
|
||||||
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
||||||
el.addEventListener(name, wrapped);
|
el.addEventListener(name, wrapped);
|
||||||
|
|||||||
@@ -414,10 +414,10 @@ def _render_component(comp: Component, args: list, env: dict[str, Any]) -> str:
|
|||||||
def _render_island(island: Island, args: list, env: dict[str, Any]) -> str:
|
def _render_island(island: Island, args: list, env: dict[str, Any]) -> str:
|
||||||
"""Render an island as static HTML with hydration attributes.
|
"""Render an island as static HTML with hydration attributes.
|
||||||
|
|
||||||
Produces: <span data-sx-island="name" data-sx-state='{"k":"v",...}'>body HTML</span>
|
Produces: <span data-sx-island="name" data-sx-state="{:k "v"}">body HTML</span>
|
||||||
The client hydrates this into a reactive island.
|
The client hydrates this into a reactive island via sx-parse (not JSON).
|
||||||
"""
|
"""
|
||||||
import json as _json
|
from .parser import serialize as _sx_serialize
|
||||||
|
|
||||||
kwargs: dict[str, Any] = {}
|
kwargs: dict[str, Any] = {}
|
||||||
children: list[Any] = []
|
children: list[Any] = []
|
||||||
@@ -443,26 +443,13 @@ def _render_island(island: Island, args: list, env: dict[str, Any]) -> str:
|
|||||||
|
|
||||||
body_html = _render(island.body, local)
|
body_html = _render(island.body, local)
|
||||||
|
|
||||||
# Serialize state for hydration — only keyword args
|
# Serialize state for hydration — SX format (not JSON)
|
||||||
state = {}
|
state_sx = _escape_attr(_sx_serialize(kwargs)) if kwargs else ""
|
||||||
for k, v in kwargs.items():
|
|
||||||
if isinstance(v, (str, int, float, bool)):
|
|
||||||
state[k] = v
|
|
||||||
elif v is NIL or v is None:
|
|
||||||
state[k] = None
|
|
||||||
elif isinstance(v, list):
|
|
||||||
state[k] = v
|
|
||||||
elif isinstance(v, dict):
|
|
||||||
state[k] = v
|
|
||||||
else:
|
|
||||||
state[k] = str(v)
|
|
||||||
|
|
||||||
state_json = _escape_attr(_json.dumps(state, separators=(",", ":"))) if state else ""
|
|
||||||
island_name = _escape_attr(island.name)
|
island_name = _escape_attr(island.name)
|
||||||
|
|
||||||
parts = [f'<span data-sx-island="{island_name}"']
|
parts = [f'<span data-sx-island="{island_name}"']
|
||||||
if state_json:
|
if state_sx:
|
||||||
parts.append(f' data-sx-state="{state_json}"')
|
parts.append(f' data-sx-state="{state_sx}"')
|
||||||
parts.append(">")
|
parts.append(">")
|
||||||
parts.append(body_html)
|
parts.append(body_html)
|
||||||
parts.append("</span>")
|
parts.append("</span>")
|
||||||
|
|||||||
@@ -450,7 +450,9 @@
|
|||||||
|
|
||||||
(define-async async-process-bindings
|
(define-async async-process-bindings
|
||||||
(fn (bindings env ctx)
|
(fn (bindings env ctx)
|
||||||
(let ((local (merge env)))
|
;; env-extend (not merge) — Env is not a dict subclass, so merge()
|
||||||
|
;; returns an empty dict, losing all parent scope bindings.
|
||||||
|
(let ((local (env-extend env)))
|
||||||
(if (and (= (type-of bindings) "list") (not (empty? bindings)))
|
(if (and (= (type-of bindings) "list") (not (empty? bindings)))
|
||||||
(if (= (type-of (first bindings)) "list")
|
(if (= (type-of (first bindings)) "list")
|
||||||
;; Scheme-style: ((name val) ...)
|
;; Scheme-style: ((name val) ...)
|
||||||
@@ -669,7 +671,10 @@
|
|||||||
(evaled-args (async-eval-args args env ctx)))
|
(evaled-args (async-eval-args args env ctx)))
|
||||||
(cond
|
(cond
|
||||||
(and (callable? f) (not (lambda? f)) (not (component? f)))
|
(and (callable? f) (not (lambda? f)) (not (component? f)))
|
||||||
(async-invoke f evaled-args)
|
;; apply directly — async-invoke takes &rest so passing a list
|
||||||
|
;; would wrap it in another list
|
||||||
|
(let ((r (apply f evaled-args)))
|
||||||
|
(if (async-coroutine? r) (async-await! r) r))
|
||||||
(lambda? f)
|
(lambda? f)
|
||||||
(let ((local (env-merge (lambda-closure f) env)))
|
(let ((local (env-merge (lambda-closure f) env)))
|
||||||
(for-each-indexed
|
(for-each-indexed
|
||||||
@@ -1166,19 +1171,20 @@
|
|||||||
|
|
||||||
(define-async async-eval-slot-inner
|
(define-async async-eval-slot-inner
|
||||||
(fn (expr env ctx)
|
(fn (expr env ctx)
|
||||||
(let ((result
|
;; NOTE: Uses statement-form let + set! to avoid expression-context
|
||||||
(if (and (list? expr) (not (empty? expr)))
|
;; let (IIFE lambdas) which can't contain await in Python.
|
||||||
(let ((head (first expr)))
|
(let ((result nil))
|
||||||
(if (and (= (type-of head) "symbol")
|
(if (and (list? expr) (not (empty? expr)))
|
||||||
(starts-with? (symbol-name head) "~"))
|
(let ((head (first expr)))
|
||||||
(let ((name (symbol-name head))
|
(if (and (= (type-of head) "symbol")
|
||||||
(val (if (env-has? env name) (env-get env name) nil)))
|
(starts-with? (symbol-name head) "~"))
|
||||||
(if (component? val)
|
(let ((name (symbol-name head))
|
||||||
(async-aser-component val (rest expr) env ctx)
|
(val (if (env-has? env name) (env-get env name) nil)))
|
||||||
;; Islands and unknown components — fall through to aser
|
(if (component? val)
|
||||||
(async-maybe-expand-result (async-aser expr env ctx) env ctx)))
|
(set! result (async-aser-component val (rest expr) env ctx))
|
||||||
(async-maybe-expand-result (async-aser expr env ctx) env ctx)))
|
(set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx))))
|
||||||
(async-maybe-expand-result (async-aser expr env ctx) env ctx))))
|
(set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx))))
|
||||||
|
(set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx)))
|
||||||
;; Normalize result to SxExpr
|
;; Normalize result to SxExpr
|
||||||
(if (sx-expr? result)
|
(if (sx-expr? result)
|
||||||
result
|
result
|
||||||
|
|||||||
@@ -186,15 +186,10 @@
|
|||||||
(attr-expr (nth args (inc (get state "i")))))
|
(attr-expr (nth args (inc (get state "i")))))
|
||||||
(cond
|
(cond
|
||||||
;; Event handler: evaluate eagerly, bind listener
|
;; Event handler: evaluate eagerly, bind listener
|
||||||
;; If handler is a 0-arity lambda, wrap to ignore the event arg
|
|
||||||
(starts-with? attr-name "on-")
|
(starts-with? attr-name "on-")
|
||||||
(let ((attr-val (trampoline (eval-expr attr-expr env))))
|
(let ((attr-val (trampoline (eval-expr attr-expr env))))
|
||||||
(when (callable? attr-val)
|
(when (callable? attr-val)
|
||||||
(dom-listen el (slice attr-name 3)
|
(dom-listen el (slice attr-name 3) attr-val)))
|
||||||
(if (and (lambda? attr-val)
|
|
||||||
(= (len (lambda-params attr-val)) 0))
|
|
||||||
(fn (e) (call-lambda attr-val (list) (lambda-closure attr-val)))
|
|
||||||
attr-val))))
|
|
||||||
;; Two-way input binding: :bind signal
|
;; Two-way input binding: :bind signal
|
||||||
(= attr-name "bind")
|
(= attr-name "bind")
|
||||||
(let ((attr-val (trampoline (eval-expr attr-expr env))))
|
(let ((attr-val (trampoline (eval-expr attr-expr env))))
|
||||||
|
|||||||
@@ -433,11 +433,11 @@
|
|||||||
|
|
||||||
;; Render the island body as HTML
|
;; Render the island body as HTML
|
||||||
(let ((body-html (render-to-html (component-body island) local))
|
(let ((body-html (render-to-html (component-body island) local))
|
||||||
(state-json (serialize-island-state kwargs)))
|
(state-sx (serialize-island-state kwargs)))
|
||||||
;; Wrap in container with hydration attributes
|
;; Wrap in container with hydration attributes
|
||||||
(str "<span data-sx-island=\"" (escape-attr island-name) "\""
|
(str "<span data-sx-island=\"" (escape-attr island-name) "\""
|
||||||
(if state-json
|
(if state-sx
|
||||||
(str " data-sx-state=\"" (escape-attr state-json) "\"")
|
(str " data-sx-state=\"" (escape-attr state-sx) "\"")
|
||||||
"")
|
"")
|
||||||
">"
|
">"
|
||||||
body-html
|
body-html
|
||||||
@@ -445,17 +445,17 @@
|
|||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; serialize-island-state — serialize kwargs to JSON for hydration
|
;; serialize-island-state — serialize kwargs to SX for hydration
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;;
|
;;
|
||||||
;; Only serializes simple values (numbers, strings, booleans, nil, lists, dicts).
|
;; Uses the SX serializer (not JSON) so the client can parse with sx-parse.
|
||||||
;; Functions, components, and other non-serializable values are skipped.
|
;; Handles all SX types natively: numbers, strings, booleans, nil, lists, dicts.
|
||||||
|
|
||||||
(define serialize-island-state
|
(define serialize-island-state
|
||||||
(fn (kwargs)
|
(fn (kwargs)
|
||||||
(if (empty-dict? kwargs)
|
(if (empty-dict? kwargs)
|
||||||
nil
|
nil
|
||||||
(json-serialize kwargs))))
|
(sx-serialize kwargs))))
|
||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
@@ -476,8 +476,8 @@
|
|||||||
;; Raw HTML construction:
|
;; Raw HTML construction:
|
||||||
;; (make-raw-html s) → wrap string as raw HTML (not double-escaped)
|
;; (make-raw-html s) → wrap string as raw HTML (not double-escaped)
|
||||||
;;
|
;;
|
||||||
;; JSON serialization (for island state):
|
;; Island state serialization:
|
||||||
;; (json-serialize dict) → JSON string
|
;; (sx-serialize val) → SX source string (from parser.sx)
|
||||||
;; (empty-dict? d) → boolean
|
;; (empty-dict? d) → boolean
|
||||||
;; (escape-attr s) → HTML attribute escape
|
;; (escape-attr s) → HTML attribute escape
|
||||||
;;
|
;;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -344,15 +344,15 @@
|
|||||||
(define hydrate-island
|
(define hydrate-island
|
||||||
(fn (el)
|
(fn (el)
|
||||||
(let ((name (dom-get-attr el "data-sx-island"))
|
(let ((name (dom-get-attr el "data-sx-island"))
|
||||||
(state-json (or (dom-get-attr el "data-sx-state") "{}")))
|
(state-sx (or (dom-get-attr el "data-sx-state") "{}")))
|
||||||
(let ((comp-name (str "~" name))
|
(let ((comp-name (str "~" name))
|
||||||
(env (get-render-env nil)))
|
(env (get-render-env nil)))
|
||||||
(let ((comp (env-get env comp-name)))
|
(let ((comp (env-get env comp-name)))
|
||||||
(if (not (or (component? comp) (island? comp)))
|
(if (not (or (component? comp) (island? comp)))
|
||||||
(log-warn (str "hydrate-island: unknown island " comp-name))
|
(log-warn (str "hydrate-island: unknown island " comp-name))
|
||||||
|
|
||||||
;; Parse state and build keyword args
|
;; Parse state and build keyword args — SX format, not JSON
|
||||||
(let ((kwargs (json-parse state-json))
|
(let ((kwargs (or (first (sx-parse state-sx)) {}))
|
||||||
(disposers (list))
|
(disposers (list))
|
||||||
(local (env-merge (component-closure comp) env)))
|
(local (env-merge (component-closure comp) env)))
|
||||||
|
|
||||||
@@ -494,8 +494,8 @@
|
|||||||
;; (log-info msg) → void (console.log with prefix)
|
;; (log-info msg) → void (console.log with prefix)
|
||||||
;; (log-parse-error label text err) → void (diagnostic parse error)
|
;; (log-parse-error label text err) → void (diagnostic parse error)
|
||||||
;;
|
;;
|
||||||
;; === JSON ===
|
;; === Parsing (island state) ===
|
||||||
;; (json-parse str) → dict/list/value (JSON.parse)
|
;; (sx-parse str) → list of AST expressions (from parser.sx)
|
||||||
;;
|
;;
|
||||||
;; === Processing markers ===
|
;; === Processing markers ===
|
||||||
;; (mark-processed! el key) → void
|
;; (mark-processed! el key) → void
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ class PyEmitter:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.indent = 0
|
self.indent = 0
|
||||||
|
self._async_names: set[str] = set() # SX names of define-async functions
|
||||||
|
self._in_async: bool = False # Currently emitting async def body?
|
||||||
|
|
||||||
def emit(self, expr) -> str:
|
def emit(self, expr) -> str:
|
||||||
"""Emit a Python expression from an SX AST node."""
|
"""Emit a Python expression from an SX AST node."""
|
||||||
@@ -80,6 +82,8 @@ class PyEmitter:
|
|||||||
name = head.name
|
name = head.name
|
||||||
if name == "define":
|
if name == "define":
|
||||||
return self._emit_define(expr, indent)
|
return self._emit_define(expr, indent)
|
||||||
|
if name == "define-async":
|
||||||
|
return self._emit_define_async(expr, indent)
|
||||||
if name == "set!":
|
if name == "set!":
|
||||||
return f"{pad}{self._mangle(expr[1].name)} = {self.emit(expr[2])}"
|
return f"{pad}{self._mangle(expr[1].name)} = {self.emit(expr[2])}"
|
||||||
if name == "when":
|
if name == "when":
|
||||||
@@ -275,6 +279,19 @@ class PyEmitter:
|
|||||||
"sf-defisland": "sf_defisland",
|
"sf-defisland": "sf_defisland",
|
||||||
# adapter-sx.sx
|
# adapter-sx.sx
|
||||||
"render-to-sx": "render_to_sx",
|
"render-to-sx": "render_to_sx",
|
||||||
|
# adapter-async.sx platform primitives
|
||||||
|
"svg-context-set!": "svg_context_set",
|
||||||
|
"svg-context-reset!": "svg_context_reset",
|
||||||
|
"css-class-collect!": "css_class_collect",
|
||||||
|
"is-raw-html?": "is_raw_html",
|
||||||
|
"async-coroutine?": "is_async_coroutine",
|
||||||
|
"async-await!": "async_await",
|
||||||
|
"is-sx-expr?": "is_sx_expr",
|
||||||
|
"sx-expr?": "is_sx_expr",
|
||||||
|
"io-primitive?": "io_primitive_p",
|
||||||
|
"expand-components?": "expand_components_p",
|
||||||
|
"svg-context?": "svg_context_p",
|
||||||
|
"make-sx-expr": "make_sx_expr",
|
||||||
"aser": "aser",
|
"aser": "aser",
|
||||||
"eval-case-aser": "eval_case_aser",
|
"eval-case-aser": "eval_case_aser",
|
||||||
"sx-serialize": "sx_serialize",
|
"sx-serialize": "sx_serialize",
|
||||||
@@ -417,6 +434,8 @@ class PyEmitter:
|
|||||||
# Regular function call
|
# Regular function call
|
||||||
fn_name = self._mangle(name)
|
fn_name = self._mangle(name)
|
||||||
args = ", ".join(self.emit(x) for x in expr[1:])
|
args = ", ".join(self.emit(x) for x in expr[1:])
|
||||||
|
if self._in_async and name in self._async_names:
|
||||||
|
return f"(await {fn_name}({args}))"
|
||||||
return f"{fn_name}({args})"
|
return f"{fn_name}({args})"
|
||||||
|
|
||||||
# --- Special form emitters ---
|
# --- Special form emitters ---
|
||||||
@@ -513,7 +532,7 @@ class PyEmitter:
|
|||||||
body_parts = expr[2:]
|
body_parts = expr[2:]
|
||||||
lines = [f"{pad}if sx_truthy({cond}):"]
|
lines = [f"{pad}if sx_truthy({cond}):"]
|
||||||
for b in body_parts:
|
for b in body_parts:
|
||||||
lines.append(self.emit_statement(b, indent + 1))
|
self._emit_stmt_recursive(b, lines, indent + 1)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _emit_cond(self, expr) -> str:
|
def _emit_cond(self, expr) -> str:
|
||||||
@@ -642,6 +661,16 @@ class PyEmitter:
|
|||||||
val = self.emit(val_expr)
|
val = self.emit(val_expr)
|
||||||
return f"{pad}{self._mangle(name)} = {val}"
|
return f"{pad}{self._mangle(name)} = {val}"
|
||||||
|
|
||||||
|
def _emit_define_async(self, expr, indent: int = 0) -> str:
|
||||||
|
"""Emit a define-async form as an async def statement."""
|
||||||
|
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
|
||||||
|
val_expr = expr[2]
|
||||||
|
if (isinstance(val_expr, list) and val_expr and
|
||||||
|
isinstance(val_expr[0], Symbol) and val_expr[0].name in ("fn", "lambda")):
|
||||||
|
return self._emit_define_as_def(name, val_expr, indent, is_async=True)
|
||||||
|
# Shouldn't happen — define-async should always wrap fn/lambda
|
||||||
|
return self._emit_define(expr, indent)
|
||||||
|
|
||||||
def _body_uses_set(self, fn_expr) -> bool:
|
def _body_uses_set(self, fn_expr) -> bool:
|
||||||
"""Check if a fn expression's body (recursively) uses set!."""
|
"""Check if a fn expression's body (recursively) uses set!."""
|
||||||
def _has_set(node):
|
def _has_set(node):
|
||||||
@@ -654,12 +683,16 @@ class PyEmitter:
|
|||||||
body = fn_expr[2:]
|
body = fn_expr[2:]
|
||||||
return any(_has_set(b) for b in body)
|
return any(_has_set(b) for b in body)
|
||||||
|
|
||||||
def _emit_define_as_def(self, name: str, fn_expr, indent: int = 0) -> str:
|
def _emit_define_as_def(self, name: str, fn_expr, indent: int = 0,
|
||||||
|
is_async: bool = False) -> str:
|
||||||
"""Emit a define with fn value as a proper def statement.
|
"""Emit a define with fn value as a proper def statement.
|
||||||
|
|
||||||
This is used for functions that contain set! — Python closures can't
|
This is used for functions that contain set! — Python closures can't
|
||||||
rebind outer lambda params, so we need proper def + local variables.
|
rebind outer lambda params, so we need proper def + local variables.
|
||||||
Variables mutated by set! from nested lambdas use a _cells dict.
|
Variables mutated by set! from nested lambdas use a _cells dict.
|
||||||
|
|
||||||
|
When is_async=True, emits 'async def' and sets _in_async so that
|
||||||
|
calls to other async functions receive 'await'.
|
||||||
"""
|
"""
|
||||||
pad = " " * indent
|
pad = " " * indent
|
||||||
params = fn_expr[1]
|
params = fn_expr[1]
|
||||||
@@ -686,14 +719,19 @@ class PyEmitter:
|
|||||||
py_name = self._mangle(name)
|
py_name = self._mangle(name)
|
||||||
# Find set! target variables that are used from nested lambda scopes
|
# Find set! target variables that are used from nested lambda scopes
|
||||||
nested_set_vars = self._find_nested_set_vars(body)
|
nested_set_vars = self._find_nested_set_vars(body)
|
||||||
lines = [f"{pad}def {py_name}({params_str}):"]
|
def_kw = "async def" if is_async else "def"
|
||||||
|
lines = [f"{pad}{def_kw} {py_name}({params_str}):"]
|
||||||
if nested_set_vars:
|
if nested_set_vars:
|
||||||
lines.append(f"{pad} _cells = {{}}")
|
lines.append(f"{pad} _cells = {{}}")
|
||||||
# Emit body with cell var tracking
|
# Emit body with cell var tracking (and async context if needed)
|
||||||
old_cells = getattr(self, '_current_cell_vars', set())
|
old_cells = getattr(self, '_current_cell_vars', set())
|
||||||
|
old_async = self._in_async
|
||||||
self._current_cell_vars = nested_set_vars
|
self._current_cell_vars = nested_set_vars
|
||||||
|
if is_async:
|
||||||
|
self._in_async = True
|
||||||
self._emit_body_stmts(body, lines, indent + 1)
|
self._emit_body_stmts(body, lines, indent + 1)
|
||||||
self._current_cell_vars = old_cells
|
self._current_cell_vars = old_cells
|
||||||
|
self._in_async = old_async
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _find_nested_set_vars(self, body) -> set[str]:
|
def _find_nested_set_vars(self, body) -> set[str]:
|
||||||
@@ -750,7 +788,7 @@ class PyEmitter:
|
|||||||
if is_last:
|
if is_last:
|
||||||
self._emit_return_expr(expr, lines, indent)
|
self._emit_return_expr(expr, lines, indent)
|
||||||
else:
|
else:
|
||||||
lines.append(self.emit_statement(expr, indent))
|
self._emit_stmt_recursive(expr, lines, indent)
|
||||||
|
|
||||||
def _emit_return_expr(self, expr, lines: list, indent: int) -> None:
|
def _emit_return_expr(self, expr, lines: list, indent: int) -> None:
|
||||||
"""Emit an expression in return position, flattening control flow."""
|
"""Emit an expression in return position, flattening control flow."""
|
||||||
@@ -775,6 +813,11 @@ class PyEmitter:
|
|||||||
if name in ("do", "begin"):
|
if name in ("do", "begin"):
|
||||||
self._emit_body_stmts(expr[1:], lines, indent)
|
self._emit_body_stmts(expr[1:], lines, indent)
|
||||||
return
|
return
|
||||||
|
if name == "for-each":
|
||||||
|
# for-each in return position: emit as statement, return NIL
|
||||||
|
lines.append(self._emit_for_each_stmt(expr, indent))
|
||||||
|
lines.append(f"{pad}return NIL")
|
||||||
|
return
|
||||||
lines.append(f"{pad}return {self.emit(expr)}")
|
lines.append(f"{pad}return {self.emit(expr)}")
|
||||||
|
|
||||||
def _emit_if_return(self, expr, lines: list, indent: int) -> None:
|
def _emit_if_return(self, expr, lines: list, indent: int) -> None:
|
||||||
@@ -1034,12 +1077,15 @@ class PyEmitter:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def extract_defines(source: str) -> list[tuple[str, list]]:
|
def extract_defines(source: str) -> list[tuple[str, list]]:
|
||||||
"""Parse .sx source, return list of (name, define-expr) for top-level defines."""
|
"""Parse .sx source, return list of (name, define-expr) for top-level defines.
|
||||||
|
|
||||||
|
Extracts both (define ...) and (define-async ...) forms.
|
||||||
|
"""
|
||||||
exprs = parse_all(source)
|
exprs = parse_all(source)
|
||||||
defines = []
|
defines = []
|
||||||
for expr in exprs:
|
for expr in exprs:
|
||||||
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
|
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
|
||||||
if expr[0].name == "define":
|
if expr[0].name in ("define", "define-async"):
|
||||||
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
|
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
|
||||||
defines.append((name, expr))
|
defines.append((name, expr))
|
||||||
return defines
|
return defines
|
||||||
@@ -1212,6 +1258,28 @@ def compile_ref_to_py(
|
|||||||
for name in sorted(spec_mod_set):
|
for name in sorted(spec_mod_set):
|
||||||
sx_files.append(SPEC_MODULES[name])
|
sx_files.append(SPEC_MODULES[name])
|
||||||
|
|
||||||
|
# Pre-scan define-async names (needed before transpilation so emitter
|
||||||
|
# knows which calls require 'await')
|
||||||
|
has_async = "async" in adapter_set
|
||||||
|
if has_async:
|
||||||
|
async_filename = ADAPTER_FILES["async"][0]
|
||||||
|
async_filepath = os.path.join(ref_dir, async_filename)
|
||||||
|
if os.path.exists(async_filepath):
|
||||||
|
with open(async_filepath) as f:
|
||||||
|
async_src = f.read()
|
||||||
|
for aexpr in parse_all(async_src):
|
||||||
|
if (isinstance(aexpr, list) and aexpr
|
||||||
|
and isinstance(aexpr[0], Symbol)
|
||||||
|
and aexpr[0].name == "define-async"):
|
||||||
|
aname = aexpr[1].name if isinstance(aexpr[1], Symbol) else str(aexpr[1])
|
||||||
|
emitter._async_names.add(aname)
|
||||||
|
# Platform async primitives (provided by host, also need await)
|
||||||
|
emitter._async_names.update({
|
||||||
|
"async-eval", "execute-io", "async-await!",
|
||||||
|
})
|
||||||
|
# Async adapter is transpiled last (after sync adapters)
|
||||||
|
sx_files.append(ADAPTER_FILES["async"])
|
||||||
|
|
||||||
all_sections = []
|
all_sections = []
|
||||||
for filename, label in sx_files:
|
for filename, label in sx_files:
|
||||||
filepath = os.path.join(ref_dir, filename)
|
filepath = os.path.join(ref_dir, filename)
|
||||||
@@ -1248,6 +1316,9 @@ def compile_ref_to_py(
|
|||||||
if has_deps:
|
if has_deps:
|
||||||
parts.append(PLATFORM_DEPS_PY)
|
parts.append(PLATFORM_DEPS_PY)
|
||||||
|
|
||||||
|
if has_async:
|
||||||
|
parts.append(PLATFORM_ASYNC_PY)
|
||||||
|
|
||||||
for label, defines in all_sections:
|
for label, defines in all_sections:
|
||||||
parts.append(f"\n# === Transpiled from {label} ===\n")
|
parts.append(f"\n# === Transpiled from {label} ===\n")
|
||||||
for name, expr in defines:
|
for name, expr in defines:
|
||||||
@@ -1258,7 +1329,7 @@ def compile_ref_to_py(
|
|||||||
parts.append(FIXUPS_PY)
|
parts.append(FIXUPS_PY)
|
||||||
if has_continuations:
|
if has_continuations:
|
||||||
parts.append(CONTINUATIONS_PY)
|
parts.append(CONTINUATIONS_PY)
|
||||||
parts.append(public_api_py(has_html, has_sx, has_deps))
|
parts.append(public_api_py(has_html, has_sx, has_deps, has_async))
|
||||||
return "\n".join(parts)
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1290,8 +1290,9 @@
|
|||||||
(= name "append!")
|
(= name "append!")
|
||||||
(str (js-expr (nth expr 1)) ".push(" (js-expr (nth expr 2)) ");")
|
(str (js-expr (nth expr 1)) ".push(" (js-expr (nth expr 2)) ");")
|
||||||
(= name "env-set!")
|
(= name "env-set!")
|
||||||
(str (js-expr (nth expr 1)) "[" (js-expr (nth expr 2))
|
(str "envSet(" (js-expr (nth expr 1))
|
||||||
"] = " (js-expr (nth expr 3)) ";")
|
", " (js-expr (nth expr 2))
|
||||||
|
", " (js-expr (nth expr 3)) ");")
|
||||||
(= name "set-lambda-name!")
|
(= name "set-lambda-name!")
|
||||||
(str (js-expr (nth expr 1)) ".name = " (js-expr (nth expr 2)) ";")
|
(str (js-expr (nth expr 1)) ".name = " (js-expr (nth expr 2)) ";")
|
||||||
:else
|
:else
|
||||||
|
|||||||
@@ -1194,7 +1194,7 @@ PLATFORM_JS_PRE = '''
|
|||||||
|
|
||||||
// JSON / dict helpers for island state serialization
|
// JSON / dict helpers for island state serialization
|
||||||
function jsonSerialize(obj) {
|
function jsonSerialize(obj) {
|
||||||
try { return JSON.stringify(obj); } catch(e) { return "{}"; }
|
return JSON.stringify(obj);
|
||||||
}
|
}
|
||||||
function isEmptyDict(d) {
|
function isEmptyDict(d) {
|
||||||
if (!d || typeof d !== "object") return true;
|
if (!d || typeof d !== "object") return true;
|
||||||
@@ -1204,11 +1204,34 @@ PLATFORM_JS_PRE = '''
|
|||||||
|
|
||||||
function envHas(env, name) { return name in env; }
|
function envHas(env, name) { return name in env; }
|
||||||
function envGet(env, name) { return env[name]; }
|
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 envExtend(env) { return Object.create(env); }
|
||||||
function envMerge(base, overlay) {
|
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);
|
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;
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1649,8 +1672,11 @@ PLATFORM_DOM_JS = """
|
|||||||
function domListen(el, name, handler) {
|
function domListen(el, name, handler) {
|
||||||
if (!_hasDom || !el) return function() {};
|
if (!_hasDom || !el) return function() {};
|
||||||
// Wrap SX lambdas from runtime-evaluated island code into native fns
|
// 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)
|
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;
|
: handler;
|
||||||
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
||||||
el.addEventListener(name, wrapped);
|
el.addEventListener(name, wrapped);
|
||||||
|
|||||||
@@ -462,10 +462,7 @@ def invoke(f, *args):
|
|||||||
|
|
||||||
def json_serialize(obj):
|
def json_serialize(obj):
|
||||||
import json
|
import json
|
||||||
try:
|
return json.dumps(obj)
|
||||||
return json.dumps(obj)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return "{}"
|
|
||||||
|
|
||||||
|
|
||||||
def is_empty_dict(d):
|
def is_empty_dict(d):
|
||||||
@@ -1067,10 +1064,19 @@ import inspect
|
|||||||
|
|
||||||
from shared.sx.primitives_io import (
|
from shared.sx.primitives_io import (
|
||||||
IO_PRIMITIVES, RequestContext, execute_io,
|
IO_PRIMITIVES, RequestContext, execute_io,
|
||||||
css_class_collector as _css_class_collector_cv,
|
|
||||||
_svg_context as _svg_context_cv,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Lazy imports to avoid circular dependency (html.py imports sx_ref.py)
|
||||||
|
_css_class_collector_cv = None
|
||||||
|
_svg_context_cv = None
|
||||||
|
|
||||||
|
def _ensure_html_imports():
|
||||||
|
global _css_class_collector_cv, _svg_context_cv
|
||||||
|
if _css_class_collector_cv is None:
|
||||||
|
from shared.sx.html import css_class_collector, _svg_context
|
||||||
|
_css_class_collector_cv = css_class_collector
|
||||||
|
_svg_context_cv = _svg_context
|
||||||
|
|
||||||
# When True, async_aser expands known components server-side
|
# When True, async_aser expands known components server-side
|
||||||
_expand_components_cv: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
_expand_components_cv: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
||||||
"_expand_components_ref", default=False
|
"_expand_components_ref", default=False
|
||||||
@@ -1094,18 +1100,22 @@ def expand_components_p():
|
|||||||
|
|
||||||
|
|
||||||
def svg_context_p():
|
def svg_context_p():
|
||||||
|
_ensure_html_imports()
|
||||||
return _svg_context_cv.get(False)
|
return _svg_context_cv.get(False)
|
||||||
|
|
||||||
|
|
||||||
def svg_context_set(val):
|
def svg_context_set(val):
|
||||||
|
_ensure_html_imports()
|
||||||
return _svg_context_cv.set(val)
|
return _svg_context_cv.set(val)
|
||||||
|
|
||||||
|
|
||||||
def svg_context_reset(token):
|
def svg_context_reset(token):
|
||||||
|
_ensure_html_imports()
|
||||||
_svg_context_cv.reset(token)
|
_svg_context_cv.reset(token)
|
||||||
|
|
||||||
|
|
||||||
def css_class_collect(val):
|
def css_class_collect(val):
|
||||||
|
_ensure_html_imports()
|
||||||
collector = _css_class_collector_cv.get(None)
|
collector = _css_class_collector_cv.get(None)
|
||||||
if collector is not None:
|
if collector is not None:
|
||||||
collector.update(str(val).split())
|
collector.update(str(val).split())
|
||||||
@@ -1123,6 +1133,25 @@ def is_sx_expr(x):
|
|||||||
return isinstance(x, SxExpr)
|
return isinstance(x, SxExpr)
|
||||||
|
|
||||||
|
|
||||||
|
# Predicate helpers used by adapter-async (these are in PRIMITIVES but
|
||||||
|
# the bootstrapped code calls them as plain functions)
|
||||||
|
def string_p(x):
|
||||||
|
return isinstance(x, str)
|
||||||
|
|
||||||
|
|
||||||
|
def list_p(x):
|
||||||
|
return isinstance(x, _b_list)
|
||||||
|
|
||||||
|
|
||||||
|
def number_p(x):
|
||||||
|
return isinstance(x, (int, float)) and not isinstance(x, bool)
|
||||||
|
|
||||||
|
|
||||||
|
def sx_parse(src):
|
||||||
|
from shared.sx.parser import parse_all
|
||||||
|
return parse_all(src)
|
||||||
|
|
||||||
|
|
||||||
def is_async_coroutine(x):
|
def is_async_coroutine(x):
|
||||||
return inspect.iscoroutine(x)
|
return inspect.iscoroutine(x)
|
||||||
|
|
||||||
@@ -1199,48 +1228,16 @@ async def async_eval_slot_to_sx(expr, env, ctx=None):
|
|||||||
ctx = RequestContext()
|
ctx = RequestContext()
|
||||||
token = _expand_components_cv.set(True)
|
token = _expand_components_cv.set(True)
|
||||||
try:
|
try:
|
||||||
return await _eval_slot_inner(expr, env, ctx)
|
result = await async_eval_slot_inner(expr, env, ctx)
|
||||||
|
if isinstance(result, SxExpr):
|
||||||
|
return result
|
||||||
|
if result is None or result is NIL:
|
||||||
|
return SxExpr("")
|
||||||
|
if isinstance(result, str):
|
||||||
|
return SxExpr(result)
|
||||||
|
return SxExpr(sx_serialize(result))
|
||||||
finally:
|
finally:
|
||||||
_expand_components_cv.reset(token)
|
_expand_components_cv.reset(token)
|
||||||
|
|
||||||
|
|
||||||
async def _eval_slot_inner(expr, env, ctx):
|
|
||||||
if isinstance(expr, list) and expr:
|
|
||||||
head = expr[0]
|
|
||||||
if isinstance(head, Symbol) and head.name.startswith("~"):
|
|
||||||
comp = env.get(head.name)
|
|
||||||
if isinstance(comp, Component):
|
|
||||||
result = await async_aser_component(comp, expr[1:], env, ctx)
|
|
||||||
if isinstance(result, SxExpr):
|
|
||||||
return result
|
|
||||||
if result is None or result is NIL:
|
|
||||||
return SxExpr("")
|
|
||||||
if isinstance(result, str):
|
|
||||||
return SxExpr(result)
|
|
||||||
return SxExpr(sx_serialize(result))
|
|
||||||
result = await async_aser(expr, env, ctx)
|
|
||||||
result = await _maybe_expand_component_result(result, env, ctx)
|
|
||||||
if isinstance(result, SxExpr):
|
|
||||||
return result
|
|
||||||
if result is None or result is NIL:
|
|
||||||
return SxExpr("")
|
|
||||||
if isinstance(result, str):
|
|
||||||
return SxExpr(result)
|
|
||||||
return SxExpr(sx_serialize(result))
|
|
||||||
|
|
||||||
|
|
||||||
async def _maybe_expand_component_result(result, env, ctx):
|
|
||||||
raw = None
|
|
||||||
if isinstance(result, SxExpr):
|
|
||||||
raw = str(result).strip()
|
|
||||||
elif isinstance(result, str):
|
|
||||||
raw = result.strip()
|
|
||||||
if raw and raw.startswith("(~"):
|
|
||||||
from shared.sx.parser import parse_all as _pa
|
|
||||||
parsed = _pa(raw)
|
|
||||||
if parsed:
|
|
||||||
return await async_eval_slot_to_sx(parsed[0], env, ctx)
|
|
||||||
return result
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1366,7 +1363,8 @@ aser_special = _aser_special_with_continuations
|
|||||||
# Public API generator
|
# Public API generator
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str:
|
def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False,
|
||||||
|
has_async: bool = False) -> str:
|
||||||
lines = [
|
lines = [
|
||||||
'',
|
'',
|
||||||
'# =========================================================================',
|
'# =========================================================================',
|
||||||
@@ -1419,8 +1417,9 @@ def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
ADAPTER_FILES = {
|
ADAPTER_FILES = {
|
||||||
"html": ("adapter-html.sx", "adapter-html"),
|
"html": ("adapter-html.sx", "adapter-html"),
|
||||||
"sx": ("adapter-sx.sx", "adapter-sx"),
|
"sx": ("adapter-sx.sx", "adapter-sx"),
|
||||||
|
"async": ("adapter-async.sx", "adapter-async"),
|
||||||
}
|
}
|
||||||
|
|
||||||
SPEC_MODULES = {
|
SPEC_MODULES = {
|
||||||
|
|||||||
@@ -178,7 +178,9 @@
|
|||||||
;; bindings = ((name1 expr1) (name2 expr2) ...)
|
;; bindings = ((name1 expr1) (name2 expr2) ...)
|
||||||
(define process-bindings
|
(define process-bindings
|
||||||
(fn (bindings env)
|
(fn (bindings env)
|
||||||
(let ((local (merge env)))
|
;; env-extend (not merge) — Env is not a dict subclass, so merge()
|
||||||
|
;; returns an empty dict, losing all parent scope bindings.
|
||||||
|
(let ((local (env-extend env)))
|
||||||
(for-each
|
(for-each
|
||||||
(fn (pair)
|
(fn (pair)
|
||||||
(when (and (= (type-of pair) "list") (>= (len pair) 2))
|
(when (and (= (type-of pair) "list") (>= (len pair) 2))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -119,6 +119,19 @@
|
|||||||
(assert-equal "(p \"hello\")"
|
(assert-equal "(p \"hello\")"
|
||||||
(render-sx "(let ((x \"hello\")) (p x))")))
|
(render-sx "(let ((x \"hello\")) (p x))")))
|
||||||
|
|
||||||
|
(deftest "let preserves outer scope bindings"
|
||||||
|
;; Regression: process-bindings must preserve parent env scope chain.
|
||||||
|
;; Using merge() instead of env-extend loses parent scope items.
|
||||||
|
(assert-equal "(p \"outer\")"
|
||||||
|
(render-sx "(do (define theme \"outer\") (let ((x 1)) (p theme)))")))
|
||||||
|
|
||||||
|
(deftest "nested let preserves outer scope"
|
||||||
|
(assert-equal "(div (span \"hello\") (span \"world\"))"
|
||||||
|
(render-sx "(do (define a \"hello\")
|
||||||
|
(define b \"world\")
|
||||||
|
(div (let ((x 1)) (span a))
|
||||||
|
(let ((y 2)) (span b))))")))
|
||||||
|
|
||||||
(deftest "begin serializes last"
|
(deftest "begin serializes last"
|
||||||
(assert-equal "(p \"last\")"
|
(assert-equal "(p \"last\")"
|
||||||
(render-sx "(begin (p \"first\") (p \"last\"))"))))
|
(render-sx "(begin (p \"first\") (p \"last\"))"))))
|
||||||
@@ -213,6 +226,17 @@
|
|||||||
(assert-equal "10"
|
(assert-equal "10"
|
||||||
(render-sx "(do (define double (fn (x) (* x 2))) (double 5))")))
|
(render-sx "(do (define double (fn (x) (* x 2))) (double 5))")))
|
||||||
|
|
||||||
|
(deftest "native callable with multiple args"
|
||||||
|
;; Regression: async-aser-eval-call passed evaled-args list to
|
||||||
|
;; async-invoke (&rest), wrapping it in another list. apply(f, [list])
|
||||||
|
;; calls f(list) instead of f(*list).
|
||||||
|
(assert-equal "3"
|
||||||
|
(render-sx "(do (define my-add +) (my-add 1 2))")))
|
||||||
|
|
||||||
|
(deftest "native callable with two args via alias"
|
||||||
|
(assert-equal "hello world"
|
||||||
|
(render-sx "(do (define my-join str) (my-join \"hello\" \" world\"))")))
|
||||||
|
|
||||||
(deftest "higher-order: map returns list"
|
(deftest "higher-order: map returns list"
|
||||||
(let ((result (render-sx "(map (fn (x) (+ x 1)) (list 1 2 3))")))
|
(let ((result (render-sx "(map (fn (x) (+ x 1)) (list 1 2 3))")))
|
||||||
;; map at top level returns a list, not serialized tags
|
;; map at top level returns a list, not serialized tags
|
||||||
|
|||||||
@@ -149,7 +149,20 @@
|
|||||||
|
|
||||||
(deftest "let in render context"
|
(deftest "let in render context"
|
||||||
(assert-equal "<p>hello</p>"
|
(assert-equal "<p>hello</p>"
|
||||||
(render-html "(let ((x \"hello\")) (p x))"))))
|
(render-html "(let ((x \"hello\")) (p x))")))
|
||||||
|
|
||||||
|
(deftest "let preserves outer scope bindings"
|
||||||
|
;; Regression: process-bindings must preserve parent env scope chain.
|
||||||
|
;; Using merge() on Env objects returns empty dict (Env is not dict subclass).
|
||||||
|
(assert-equal "<p>outer</p>"
|
||||||
|
(render-html "(do (define theme \"outer\") (let ((x 1)) (p theme)))")))
|
||||||
|
|
||||||
|
(deftest "nested let preserves outer scope"
|
||||||
|
(assert-equal "<div><span>hello</span><span>world</span></div>"
|
||||||
|
(render-html "(do (define a \"hello\")
|
||||||
|
(define b \"world\")
|
||||||
|
(div (let ((x 1)) (span a))
|
||||||
|
(let ((y 2)) (span b))))"))))
|
||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -127,7 +127,8 @@ def render_html(sx_source):
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
raise RuntimeError("render-to-html not available — sx_ref.py not built")
|
raise RuntimeError("render-to-html not available — sx_ref.py not built")
|
||||||
exprs = parse_all(sx_source)
|
exprs = parse_all(sx_source)
|
||||||
render_env = dict(env)
|
# Use Env (not flat dict) so tests exercise the real scope chain path.
|
||||||
|
render_env = _Env(dict(env))
|
||||||
result = ""
|
result = ""
|
||||||
for expr in exprs:
|
for expr in exprs:
|
||||||
result += _render_to_html(expr, render_env)
|
result += _render_to_html(expr, render_env)
|
||||||
@@ -143,7 +144,9 @@ def render_sx(sx_source):
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
raise RuntimeError("aser not available — sx_ref.py not built")
|
raise RuntimeError("aser not available — sx_ref.py not built")
|
||||||
exprs = parse_all(sx_source)
|
exprs = parse_all(sx_source)
|
||||||
render_env = dict(env)
|
# Use Env (not flat dict) so tests exercise the real scope chain path.
|
||||||
|
# Using dict(env) hides bugs where merge() drops Env parent scopes.
|
||||||
|
render_env = _Env(dict(env))
|
||||||
result = ""
|
result = ""
|
||||||
for expr in exprs:
|
for expr in exprs:
|
||||||
val = _aser(expr, render_env)
|
val = _aser(expr, render_env)
|
||||||
|
|||||||
@@ -42,7 +42,6 @@
|
|||||||
(sf-ms (- (now-ms) t1))
|
(sf-ms (- (now-ms) t1))
|
||||||
(sf-cats {})
|
(sf-cats {})
|
||||||
(sf-total 0)
|
(sf-total 0)
|
||||||
|
|
||||||
;; 2. build-reference-data
|
;; 2. build-reference-data
|
||||||
(t2 (now-ms))
|
(t2 (now-ms))
|
||||||
(ref-result (build-reference-data "attributes"
|
(ref-result (build-reference-data "attributes"
|
||||||
|
|||||||
Reference in New Issue
Block a user