Merge branch 'main' into worktree-typed-sx
# Conflicts: # shared/sx/ref/platform_py.py # shared/sx/ref/sx_ref.py
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-11T04:41:27Z";
|
||||
var SX_VERSION = "2026-03-11T16:56:11Z";
|
||||
|
||||
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);
|
||||
})(); };
|
||||
@@ -764,8 +787,11 @@
|
||||
return (isSxTruthy((isSxTruthy(condition) && !isSxTruthy(isNil(condition)))) ? (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 1, (len(args) - 1))), makeThunk(last(args), env)) : NIL);
|
||||
})(); };
|
||||
|
||||
// cond-scheme?
|
||||
var condScheme_p = function(clauses) { return isEvery(function(c) { return (isSxTruthy((typeOf(c) == "list")) && (len(c) == 2)); }, clauses); };
|
||||
|
||||
// sf-cond
|
||||
var sfCond = function(args, env) { return (isSxTruthy((isSxTruthy((typeOf(first(args)) == "list")) && (len(first(args)) == 2))) ? sfCondScheme(args, env) : sfCondClojure(args, env)); };
|
||||
var sfCond = function(args, env) { return (isSxTruthy(condScheme_p(args)) ? sfCondScheme(args, env) : sfCondClojure(args, env)); };
|
||||
|
||||
// sf-cond-scheme
|
||||
var sfCondScheme = function(clauses, env) { return (isSxTruthy(isEmpty(clauses)) ? NIL : (function() {
|
||||
@@ -841,7 +867,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 +891,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 +907,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 +950,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 +965,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 +982,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;
|
||||
})(); };
|
||||
|
||||
@@ -974,8 +1000,8 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
var head = first(template);
|
||||
return (isSxTruthy((isSxTruthy((typeOf(head) == "symbol")) && (symbolName(head) == "unquote"))) ? trampoline(evalExpr(nth(template, 1), env)) : reduce(function(result, item) { return (isSxTruthy((isSxTruthy((typeOf(item) == "list")) && isSxTruthy((len(item) == 2)) && isSxTruthy((typeOf(first(item)) == "symbol")) && (symbolName(first(item)) == "splice-unquote"))) ? (function() {
|
||||
var spliced = trampoline(evalExpr(nth(item, 1), env));
|
||||
return (isSxTruthy((typeOf(spliced) == "list")) ? concat(result, spliced) : (isSxTruthy(isNil(spliced)) ? result : append(result, spliced)));
|
||||
})() : append(result, qqExpand(item, env))); }, [], template));
|
||||
return (isSxTruthy((typeOf(spliced) == "list")) ? concat(result, spliced) : (isSxTruthy(isNil(spliced)) ? result : concat(result, [spliced])));
|
||||
})() : concat(result, [qqExpand(item, env)])); }, [], template));
|
||||
})())); };
|
||||
|
||||
// sf-thread-first
|
||||
@@ -996,7 +1022,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 +1047,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 +1072,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));
|
||||
})(); };
|
||||
@@ -1143,7 +1169,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
|
||||
})(); }, keys(attrs))); };
|
||||
|
||||
// eval-cond
|
||||
var evalCond = function(clauses, env) { return (isSxTruthy((isSxTruthy(!isSxTruthy(isEmpty(clauses))) && isSxTruthy((typeOf(first(clauses)) == "list")) && (len(first(clauses)) == 2))) ? evalCondScheme(clauses, env) : evalCondClojure(clauses, env)); };
|
||||
var evalCond = function(clauses, env) { return (isSxTruthy(condScheme_p(clauses)) ? evalCondScheme(clauses, env) : evalCondClojure(clauses, env)); };
|
||||
|
||||
// eval-cond-scheme
|
||||
var evalCondScheme = function(clauses, env) { return (isSxTruthy(isEmpty(clauses)) ? NIL : (function() {
|
||||
@@ -1162,7 +1188,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 +1418,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 +1484,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 ===
|
||||
@@ -1505,30 +1531,34 @@ return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if
|
||||
|
||||
// aser-fragment
|
||||
var aserFragment = function(children, env) { return (function() {
|
||||
var parts = filter(function(x) { return !isSxTruthy(isNil(x)); }, map(function(c) { return aser(c, env); }, children));
|
||||
return (isSxTruthy(isEmpty(parts)) ? "" : (String("(<> ") + String(join(" ", map(serialize, parts))) + String(")")));
|
||||
var parts = [];
|
||||
{ var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() {
|
||||
var result = aser(c, env);
|
||||
return (isSxTruthy((typeOf(result) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, result) : (isSxTruthy(!isSxTruthy(isNil(result))) ? append_b(parts, serialize(result)) : NIL));
|
||||
})(); } }
|
||||
return (isSxTruthy(isEmpty(parts)) ? "" : (String("(<> ") + String(join(" ", parts)) + String(")")));
|
||||
})(); };
|
||||
|
||||
// aser-call
|
||||
var aserCall = function(name, args, env) { return (function() {
|
||||
var parts = [name];
|
||||
reduce(function(state, arg) { return (function() {
|
||||
var skip = get(state, "skip");
|
||||
return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
|
||||
var val = aser(nth(args, (get(state, "i") + 1)), env);
|
||||
var skip = false;
|
||||
var i = 0;
|
||||
{ var _c = args; for (var _i = 0; _i < _c.length; _i++) { var arg = _c[_i]; (isSxTruthy(skip) ? ((skip = false), (i = (i + 1))) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((i + 1) < len(args)))) ? (function() {
|
||||
var val = aser(nth(args, (i + 1)), env);
|
||||
if (isSxTruthy(!isSxTruthy(isNil(val)))) {
|
||||
parts.push((String(":") + String(keywordName(arg))));
|
||||
parts.push(serialize(val));
|
||||
}
|
||||
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
|
||||
skip = true;
|
||||
return (i = (i + 1));
|
||||
})() : (function() {
|
||||
var val = aser(arg, env);
|
||||
if (isSxTruthy(!isSxTruthy(isNil(val)))) {
|
||||
parts.push(serialize(val));
|
||||
(isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, val) : append_b(parts, serialize(val)));
|
||||
}
|
||||
return assoc(state, "i", (get(state, "i") + 1));
|
||||
})()));
|
||||
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||
return (i = (i + 1));
|
||||
})())); } }
|
||||
return (String("(") + String(join(" ", parts)) + String(")"));
|
||||
})(); };
|
||||
|
||||
@@ -1582,7 +1612,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() {
|
||||
@@ -1590,8 +1620,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() {
|
||||
@@ -1600,7 +1630,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);
|
||||
@@ -1692,7 +1722,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();
|
||||
@@ -1883,7 +1913,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();
|
||||
@@ -2998,9 +3028,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);
|
||||
@@ -3229,17 +3259,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, "");
|
||||
@@ -3433,6 +3463,125 @@ callExpr.push(dictGet(kwargs, k)); } }
|
||||
})(); }, keys(env)); };
|
||||
|
||||
|
||||
// === Transpiled from page-helpers (pure data transformation helpers) ===
|
||||
|
||||
// special-form-category-map
|
||||
var specialFormCategoryMap = {"if": "Control Flow", "when": "Control Flow", "cond": "Control Flow", "case": "Control Flow", "and": "Control Flow", "or": "Control Flow", "let": "Binding", "let*": "Binding", "letrec": "Binding", "define": "Binding", "set!": "Binding", "lambda": "Functions & Components", "fn": "Functions & Components", "defcomp": "Functions & Components", "defmacro": "Functions & Components", "begin": "Sequencing & Threading", "do": "Sequencing & Threading", "->": "Sequencing & Threading", "quote": "Quoting", "quasiquote": "Quoting", "reset": "Continuations", "shift": "Continuations", "dynamic-wind": "Guards", "map": "Higher-Order Forms", "map-indexed": "Higher-Order Forms", "filter": "Higher-Order Forms", "reduce": "Higher-Order Forms", "some": "Higher-Order Forms", "every?": "Higher-Order Forms", "for-each": "Higher-Order Forms", "defstyle": "Domain Definitions", "defhandler": "Domain Definitions", "defpage": "Domain Definitions", "defquery": "Domain Definitions", "defaction": "Domain Definitions"};
|
||||
|
||||
// extract-define-kwargs
|
||||
var extractDefineKwargs = function(expr) { return (function() {
|
||||
var result = {};
|
||||
var items = slice(expr, 2);
|
||||
var n = len(items);
|
||||
{ var _c = range(0, n); for (var _i = 0; _i < _c.length; _i++) { var idx = _c[_i]; if (isSxTruthy((isSxTruthy(((idx + 1) < n)) && (typeOf(nth(items, idx)) == "keyword")))) {
|
||||
(function() {
|
||||
var key = keywordName(nth(items, idx));
|
||||
var val = nth(items, (idx + 1));
|
||||
return dictSet(result, key, (isSxTruthy((typeOf(val) == "list")) ? (String("(") + String(join(" ", map(serialize, val))) + String(")")) : (String(val))));
|
||||
})();
|
||||
} } }
|
||||
return result;
|
||||
})(); };
|
||||
|
||||
// categorize-special-forms
|
||||
var categorizeSpecialForms = function(parsedExprs) { return (function() {
|
||||
var categories = {};
|
||||
{ var _c = parsedExprs; for (var _i = 0; _i < _c.length; _i++) { var expr = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(expr) == "list")) && isSxTruthy((len(expr) >= 2)) && isSxTruthy((typeOf(first(expr)) == "symbol")) && (symbolName(first(expr)) == "define-special-form")))) {
|
||||
(function() {
|
||||
var name = nth(expr, 1);
|
||||
var kwargs = extractDefineKwargs(expr);
|
||||
var category = sxOr(get(specialFormCategoryMap, name), "Other");
|
||||
if (isSxTruthy(!isSxTruthy(dictHas(categories, category)))) {
|
||||
categories[category] = [];
|
||||
}
|
||||
return append_b(get(categories, category), {"name": name, "syntax": sxOr(get(kwargs, "syntax"), ""), "doc": sxOr(get(kwargs, "doc"), ""), "tail-position": sxOr(get(kwargs, "tail-position"), ""), "example": sxOr(get(kwargs, "example"), "")});
|
||||
})();
|
||||
} } }
|
||||
return categories;
|
||||
})(); };
|
||||
|
||||
// build-ref-items-with-href
|
||||
var buildRefItemsWithHref = function(items, basePath, detailKeys, nFields) { return map(function(item) { return (isSxTruthy((nFields == 3)) ? (function() {
|
||||
var name = nth(item, 0);
|
||||
var field2 = nth(item, 1);
|
||||
var field3 = nth(item, 2);
|
||||
return {"name": name, "desc": field2, "exists": field3, "href": (isSxTruthy((isSxTruthy(field3) && some(function(k) { return (k == name); }, detailKeys))) ? (String(basePath) + String(name)) : NIL)};
|
||||
})() : (function() {
|
||||
var name = nth(item, 0);
|
||||
var desc = nth(item, 1);
|
||||
return {"name": name, "desc": desc, "href": (isSxTruthy(some(function(k) { return (k == name); }, detailKeys)) ? (String(basePath) + String(name)) : NIL)};
|
||||
})()); }, items); };
|
||||
|
||||
// build-reference-data
|
||||
var buildReferenceData = function(slug, rawData, detailKeys) { return (function() { var _m = slug; if (_m == "attributes") return {"req-attrs": buildRefItemsWithHref(get(rawData, "req-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "beh-attrs": buildRefItemsWithHref(get(rawData, "beh-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "uniq-attrs": buildRefItemsWithHref(get(rawData, "uniq-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3)}; if (_m == "headers") return {"req-headers": buildRefItemsWithHref(get(rawData, "req-headers"), "/hypermedia/reference/headers/", detailKeys, 3), "resp-headers": buildRefItemsWithHref(get(rawData, "resp-headers"), "/hypermedia/reference/headers/", detailKeys, 3)}; if (_m == "events") return {"events-list": buildRefItemsWithHref(get(rawData, "events-list"), "/hypermedia/reference/events/", detailKeys, 2)}; if (_m == "js-api") return {"js-api-list": map(function(item) { return {"name": nth(item, 0), "desc": nth(item, 1)}; }, get(rawData, "js-api-list"))}; return {"req-attrs": buildRefItemsWithHref(get(rawData, "req-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "beh-attrs": buildRefItemsWithHref(get(rawData, "beh-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "uniq-attrs": buildRefItemsWithHref(get(rawData, "uniq-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3)}; })(); };
|
||||
|
||||
// build-attr-detail
|
||||
var buildAttrDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"attr-not-found": true} : {"attr-not-found": NIL, "attr-title": slug, "attr-description": get(detail, "description"), "attr-example": get(detail, "example"), "attr-handler": get(detail, "handler"), "attr-demo": get(detail, "demo"), "attr-wire-id": (isSxTruthy(dictHas(detail, "handler")) ? (String("ref-wire-") + String(replace_(replace_(slug, ":", "-"), "*", "star"))) : NIL)}); };
|
||||
|
||||
// build-header-detail
|
||||
var buildHeaderDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"header-not-found": true} : {"header-not-found": NIL, "header-title": slug, "header-direction": get(detail, "direction"), "header-description": get(detail, "description"), "header-example": get(detail, "example"), "header-demo": get(detail, "demo")}); };
|
||||
|
||||
// build-event-detail
|
||||
var buildEventDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"event-not-found": true} : {"event-not-found": NIL, "event-title": slug, "event-description": get(detail, "description"), "event-example": get(detail, "example"), "event-demo": get(detail, "demo")}); };
|
||||
|
||||
// build-component-source
|
||||
var buildComponentSource = function(compData) { return (function() {
|
||||
var compType = get(compData, "type");
|
||||
var name = get(compData, "name");
|
||||
var params = get(compData, "params");
|
||||
var hasChildren = get(compData, "has-children");
|
||||
var bodySx = get(compData, "body-sx");
|
||||
var affinity = get(compData, "affinity");
|
||||
return (isSxTruthy((compType == "not-found")) ? (String(";; component ") + String(name) + String(" not found")) : (function() {
|
||||
var paramStrs = (isSxTruthy(isEmpty(params)) ? (isSxTruthy(hasChildren) ? ["&rest", "children"] : []) : (isSxTruthy(hasChildren) ? append(cons("&key", params), ["&rest", "children"]) : cons("&key", params)));
|
||||
var paramsSx = (String("(") + String(join(" ", paramStrs)) + String(")"));
|
||||
var formName = (isSxTruthy((compType == "island")) ? "defisland" : "defcomp");
|
||||
var affinityStr = (isSxTruthy((isSxTruthy((compType == "component")) && isSxTruthy(!isSxTruthy(isNil(affinity))) && !isSxTruthy((affinity == "auto")))) ? (String(" :affinity ") + String(affinity)) : "");
|
||||
return (String("(") + String(formName) + String(" ") + String(name) + String(" ") + String(paramsSx) + String(affinityStr) + String("\n ") + String(bodySx) + String(")"));
|
||||
})());
|
||||
})(); };
|
||||
|
||||
// build-bundle-analysis
|
||||
var buildBundleAnalysis = function(pagesRaw, componentsRaw, totalComponents, totalMacros, pureCount, ioCount) { return (function() {
|
||||
var pagesData = [];
|
||||
{ var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var page = _c[_i]; (function() {
|
||||
var neededNames = get(page, "needed-names");
|
||||
var n = len(neededNames);
|
||||
var pct = (isSxTruthy((totalComponents > 0)) ? round(((n / totalComponents) * 100)) : 0);
|
||||
var savings = (100 - pct);
|
||||
var pureInPage = 0;
|
||||
var ioInPage = 0;
|
||||
var pageIoRefs = [];
|
||||
var compDetails = [];
|
||||
{ var _c = neededNames; for (var _i = 0; _i < _c.length; _i++) { var compName = _c[_i]; (function() {
|
||||
var info = get(componentsRaw, compName);
|
||||
return (isSxTruthy(!isSxTruthy(isNil(info))) ? ((isSxTruthy(get(info, "is-pure")) ? (pureInPage = (pureInPage + 1)) : ((ioInPage = (ioInPage + 1)), forEach(function(ref) { return (isSxTruthy(!isSxTruthy(some(function(r) { return (r == ref); }, pageIoRefs))) ? append_b(pageIoRefs, ref) : NIL); }, sxOr(get(info, "io-refs"), [])))), append_b(compDetails, {"name": compName, "is-pure": get(info, "is-pure"), "affinity": get(info, "affinity"), "render-target": get(info, "render-target"), "io-refs": sxOr(get(info, "io-refs"), []), "deps": sxOr(get(info, "deps"), []), "source": get(info, "source")})) : NIL);
|
||||
})(); } }
|
||||
return append_b(pagesData, {"name": get(page, "name"), "path": get(page, "path"), "direct": get(page, "direct"), "needed": n, "pct": pct, "savings": savings, "io-refs": len(pageIoRefs), "pure-in-page": pureInPage, "io-in-page": ioInPage, "components": compDetails});
|
||||
})(); } }
|
||||
return {"pages": pagesData, "total-components": totalComponents, "total-macros": totalMacros, "pure-count": pureCount, "io-count": ioCount};
|
||||
})(); };
|
||||
|
||||
// build-routing-analysis
|
||||
var buildRoutingAnalysis = function(pagesRaw) { return (function() {
|
||||
var pagesData = [];
|
||||
var clientCount = 0;
|
||||
var serverCount = 0;
|
||||
{ var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var page = _c[_i]; (function() {
|
||||
var hasData = get(page, "has-data");
|
||||
var contentSrc = sxOr(get(page, "content-src"), "");
|
||||
var mode = NIL;
|
||||
var reason = "";
|
||||
(isSxTruthy(hasData) ? ((mode = "server"), (reason = "Has :data expression — needs server IO"), (serverCount = (serverCount + 1))) : (isSxTruthy(isEmpty(contentSrc)) ? ((mode = "server"), (reason = "No content expression"), (serverCount = (serverCount + 1))) : ((mode = "client"), (clientCount = (clientCount + 1)))));
|
||||
return append_b(pagesData, {"name": get(page, "name"), "path": get(page, "path"), "mode": mode, "has-data": hasData, "content-expr": (isSxTruthy((len(contentSrc) > 80)) ? (String(slice(contentSrc, 0, 80)) + String("...")) : contentSrc), "reason": reason});
|
||||
})(); } }
|
||||
return {"pages": pagesData, "total-pages": (clientCount + serverCount), "client-count": clientCount, "server-count": serverCount};
|
||||
})(); };
|
||||
|
||||
// build-affinity-analysis
|
||||
var buildAffinityAnalysis = function(demoComponents, pagePlans) { return {"components": demoComponents, "page-plans": pagePlans}; };
|
||||
|
||||
|
||||
// === Transpiled from router (client-side route matching) ===
|
||||
|
||||
// split-path-segments
|
||||
@@ -3853,8 +4002,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);
|
||||
@@ -3947,7 +4099,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function nowMs() { return Date.now(); }
|
||||
function nowMs() { return (typeof performance !== "undefined") ? performance.now() : Date.now(); }
|
||||
|
||||
function parseHeaderValue(s) {
|
||||
if (!s) return null;
|
||||
@@ -5060,6 +5212,10 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
if (typeof elementValue === "function") PRIMITIVES["element-value"] = elementValue;
|
||||
if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml;
|
||||
if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml;
|
||||
if (typeof domTextContent === "function") PRIMITIVES["dom-text-content"] = domTextContent;
|
||||
if (typeof jsonParse === "function") PRIMITIVES["json-parse"] = jsonParse;
|
||||
if (typeof nowMs === "function") PRIMITIVES["now-ms"] = nowMs;
|
||||
PRIMITIVES["sx-parse"] = sxParse;
|
||||
|
||||
// Expose deps module functions as primitives so runtime-evaluated SX code
|
||||
// (e.g. test-deps.sx in browser) can call them
|
||||
@@ -5090,6 +5246,19 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
PRIMITIVES["render-target"] = renderTarget;
|
||||
PRIMITIVES["page-render-plan"] = pageRenderPlan;
|
||||
|
||||
// Expose page-helper functions as primitives
|
||||
PRIMITIVES["categorize-special-forms"] = categorizeSpecialForms;
|
||||
PRIMITIVES["extract-define-kwargs"] = extractDefineKwargs;
|
||||
PRIMITIVES["build-reference-data"] = buildReferenceData;
|
||||
PRIMITIVES["build-ref-items-with-href"] = buildRefItemsWithHref;
|
||||
PRIMITIVES["build-attr-detail"] = buildAttrDetail;
|
||||
PRIMITIVES["build-header-detail"] = buildHeaderDetail;
|
||||
PRIMITIVES["build-event-detail"] = buildEventDetail;
|
||||
PRIMITIVES["build-component-source"] = buildComponentSource;
|
||||
PRIMITIVES["build-bundle-analysis"] = buildBundleAnalysis;
|
||||
PRIMITIVES["build-routing-analysis"] = buildRoutingAnalysis;
|
||||
PRIMITIVES["build-affinity-analysis"] = buildAffinityAnalysis;
|
||||
|
||||
// =========================================================================
|
||||
// Async IO: Promise-aware rendering for client-side IO primitives
|
||||
// =========================================================================
|
||||
@@ -5823,6 +5992,15 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
||||
transitiveIoRefs: transitiveIoRefs,
|
||||
computeAllIoRefs: computeAllIoRefs,
|
||||
componentPure_p: componentPure_p,
|
||||
categorizeSpecialForms: categorizeSpecialForms,
|
||||
buildReferenceData: buildReferenceData,
|
||||
buildAttrDetail: buildAttrDetail,
|
||||
buildHeaderDetail: buildHeaderDetail,
|
||||
buildEventDetail: buildEventDetail,
|
||||
buildComponentSource: buildComponentSource,
|
||||
buildBundleAnalysis: buildBundleAnalysis,
|
||||
buildRoutingAnalysis: buildRoutingAnalysis,
|
||||
buildAffinityAnalysis: buildAffinityAnalysis,
|
||||
splitPathSegments: splitPathSegments,
|
||||
parseRoutePattern: parseRoutePattern,
|
||||
matchRoute: matchRoute,
|
||||
|
||||
@@ -31,11 +31,8 @@ from .parser import (
|
||||
parse_all,
|
||||
serialize,
|
||||
)
|
||||
from .evaluator import (
|
||||
EvalError,
|
||||
evaluate,
|
||||
make_env,
|
||||
)
|
||||
from .types import EvalError
|
||||
from .ref.sx_ref import evaluate, make_env
|
||||
|
||||
from .primitives import (
|
||||
all_primitives,
|
||||
|
||||
@@ -53,7 +53,8 @@ from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
|
||||
_expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
||||
"_expand_components", default=False
|
||||
)
|
||||
from .evaluator import _expand_macro, EvalError
|
||||
from .ref.sx_ref import expand_macro as _expand_macro
|
||||
from .types import EvalError
|
||||
from .primitives import _PRIMITIVES
|
||||
from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io
|
||||
from .parser import SxExpr, serialize
|
||||
@@ -420,23 +421,23 @@ async def _asf_define(expr, env, ctx):
|
||||
|
||||
|
||||
async def _asf_defcomp(expr, env, ctx):
|
||||
from .evaluator import _sf_defcomp
|
||||
return _sf_defcomp(expr, env)
|
||||
from .ref.sx_ref import sf_defcomp
|
||||
return sf_defcomp(expr[1:], env)
|
||||
|
||||
|
||||
async def _asf_defstyle(expr, env, ctx):
|
||||
from .evaluator import _sf_defstyle
|
||||
return _sf_defstyle(expr, env)
|
||||
from .ref.sx_ref import sf_defstyle
|
||||
return sf_defstyle(expr[1:], env)
|
||||
|
||||
|
||||
async def _asf_defmacro(expr, env, ctx):
|
||||
from .evaluator import _sf_defmacro
|
||||
return _sf_defmacro(expr, env)
|
||||
from .ref.sx_ref import sf_defmacro
|
||||
return sf_defmacro(expr[1:], env)
|
||||
|
||||
|
||||
async def _asf_defhandler(expr, env, ctx):
|
||||
from .evaluator import _sf_defhandler
|
||||
return _sf_defhandler(expr, env)
|
||||
from .ref.sx_ref import sf_defhandler
|
||||
return sf_defhandler(expr[1:], env)
|
||||
|
||||
|
||||
async def _asf_begin(expr, env, ctx):
|
||||
@@ -599,7 +600,7 @@ async def _asf_reset(expr, env, ctx):
|
||||
_ASYNC_RESET_RESUME.append(value if value is not None else NIL)
|
||||
try:
|
||||
# Sync re-evaluation; the async caller will trampoline
|
||||
from .evaluator import _eval as sync_eval, _trampoline
|
||||
from .ref.sx_ref import eval_expr as sync_eval, trampoline as _trampoline
|
||||
return _trampoline(sync_eval(body, env))
|
||||
finally:
|
||||
_ASYNC_RESET_RESUME.pop()
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
"""
|
||||
S-expression evaluator — thin shim over bootstrapped sx_ref.py.
|
||||
|
||||
All evaluation logic lives in the spec (shared/sx/ref/eval.sx) and is
|
||||
bootstrapped to Python (shared/sx/ref/sx_ref.py). This module re-exports
|
||||
the public API and internal helpers under their historical names so that
|
||||
existing callers don't need updating.
|
||||
|
||||
Imports are lazy (inside functions/properties) to avoid circular imports
|
||||
during bootstrapping: bootstrap_py.py → parser → __init__ → evaluator → sx_ref.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def _ref():
|
||||
"""Lazy import of the bootstrapped evaluator."""
|
||||
from .ref import sx_ref
|
||||
return sx_ref
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API — these are the most used, so we make them importable directly
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class EvalError(Exception):
|
||||
"""Error during expression evaluation.
|
||||
|
||||
Delegates to the bootstrapped EvalError at runtime but is defined here
|
||||
so imports don't fail during bootstrapping.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def evaluate(expr, env=None):
|
||||
return _ref().evaluate(expr, env)
|
||||
|
||||
|
||||
def make_env(**kwargs):
|
||||
return _ref().make_env(**kwargs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers — used by html.py, async_eval.py, handlers.py, etc.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _eval(expr, env):
|
||||
return _ref().eval_expr(expr, env)
|
||||
|
||||
|
||||
def _trampoline(val):
|
||||
return _ref().trampoline(val)
|
||||
|
||||
|
||||
def _call_lambda(fn, args, caller_env):
|
||||
return _ref().call_lambda(fn, args, caller_env)
|
||||
|
||||
|
||||
def _call_component(comp, raw_args, env):
|
||||
return _ref().call_component(comp, raw_args, env)
|
||||
|
||||
|
||||
def _expand_macro(macro, raw_args, env):
|
||||
return _ref().expand_macro(macro, raw_args, env)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Special-form wrappers: callers pass (expr, env) with expr[0] = head symbol.
|
||||
# sx_ref.py special forms take (args, env) where args = expr[1:].
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _sf_defcomp(expr, env):
|
||||
return _ref().sf_defcomp(expr[1:], env)
|
||||
|
||||
def _sf_defisland(expr, env):
|
||||
return _ref().sf_defisland(expr[1:], env)
|
||||
|
||||
def _sf_defstyle(expr, env):
|
||||
return _ref().sf_defstyle(expr[1:], env)
|
||||
|
||||
def _sf_defmacro(expr, env):
|
||||
return _ref().sf_defmacro(expr[1:], env)
|
||||
|
||||
def _sf_defhandler(expr, env):
|
||||
return _ref().sf_defhandler(expr[1:], env)
|
||||
|
||||
def _sf_defpage(expr, env):
|
||||
return _ref().sf_defpage(expr[1:], env)
|
||||
|
||||
def _sf_defquery(expr, env):
|
||||
return _ref().sf_defquery(expr[1:], env)
|
||||
|
||||
def _sf_defaction(expr, env):
|
||||
return _ref().sf_defaction(expr[1:], env)
|
||||
@@ -70,10 +70,7 @@ def load_handler_file(filepath: str, service_name: str) -> list[HandlerDef]:
|
||||
"""Parse an .sx file, evaluate it, and register any HandlerDef values."""
|
||||
from .parser import parse_all
|
||||
import os
|
||||
if os.environ.get("SX_USE_REF") == "1":
|
||||
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
|
||||
else:
|
||||
from .evaluator import _eval as _raw_eval, _trampoline
|
||||
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
|
||||
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
|
||||
from .jinja_bridge import get_component_env
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import contextvars
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
|
||||
from .evaluator import _eval as _raw_eval, _call_component as _raw_call_component, _expand_macro, _trampoline
|
||||
from .ref.sx_ref import eval_expr as _raw_eval, call_component as _raw_call_component, expand_macro as _expand_macro, trampoline as _trampoline
|
||||
|
||||
def _eval(expr, env):
|
||||
"""Evaluate and unwrap thunks — all html.py _eval calls are non-tail."""
|
||||
@@ -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:
|
||||
"""Render an island as static HTML with hydration attributes.
|
||||
|
||||
Produces: <span data-sx-island="name" data-sx-state='{"k":"v",...}'>body HTML</span>
|
||||
The client hydrates this into a reactive island.
|
||||
Produces: <span data-sx-island="name" data-sx-state="{:k "v"}">body HTML</span>
|
||||
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] = {}
|
||||
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)
|
||||
|
||||
# Serialize state for hydration — only keyword args
|
||||
state = {}
|
||||
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 ""
|
||||
# Serialize state for hydration — SX format (not JSON)
|
||||
state_sx = _escape_attr(_sx_serialize(kwargs)) if kwargs else ""
|
||||
island_name = _escape_attr(island.name)
|
||||
|
||||
parts = [f'<span data-sx-island="{island_name}"']
|
||||
if state_json:
|
||||
parts.append(f' data-sx-state="{state_json}"')
|
||||
if state_sx:
|
||||
parts.append(f' data-sx-state="{state_sx}"')
|
||||
parts.append(">")
|
||||
parts.append(body_html)
|
||||
parts.append("</span>")
|
||||
|
||||
@@ -229,10 +229,7 @@ def register_components(sx_source: str) -> None:
|
||||
(div :class "..." (div :class "..." title)))))
|
||||
''')
|
||||
"""
|
||||
if _os.environ.get("SX_USE_REF") == "1":
|
||||
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
|
||||
else:
|
||||
from .evaluator import _eval as _raw_eval, _trampoline
|
||||
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
|
||||
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
|
||||
from .parser import parse_all
|
||||
from .css_registry import scan_classes_from_sx
|
||||
|
||||
@@ -127,7 +127,7 @@ def get_page_helpers(service: str) -> dict[str, Any]:
|
||||
def load_page_file(filepath: str, service_name: str) -> list[PageDef]:
|
||||
"""Parse an .sx file, evaluate it, and register any PageDef values."""
|
||||
from .parser import parse_all
|
||||
from .evaluator import _eval as _raw_eval, _trampoline
|
||||
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
|
||||
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
|
||||
from .jinja_bridge import get_component_env
|
||||
|
||||
@@ -170,7 +170,11 @@ async def _eval_slot(expr: Any, env: dict, ctx: Any) -> str:
|
||||
Expands component calls (so IO in the body executes) but serializes
|
||||
the result as SX wire format, not HTML.
|
||||
"""
|
||||
from .async_eval import async_eval_slot_to_sx
|
||||
import os
|
||||
if os.environ.get("SX_USE_REF") == "1":
|
||||
from .ref.async_eval_ref import async_eval_slot_to_sx
|
||||
else:
|
||||
from .async_eval import async_eval_slot_to_sx
|
||||
return await async_eval_slot_to_sx(expr, env, ctx)
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ def _resolve_sx_reader_macro(name: str):
|
||||
"""
|
||||
try:
|
||||
from .jinja_bridge import get_component_env
|
||||
from .evaluator import _trampoline, _call_lambda
|
||||
from .ref.sx_ref import trampoline as _trampoline, call_lambda as _call_lambda
|
||||
from .types import Lambda
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
@@ -78,7 +78,7 @@ def clear(service: str | None = None) -> None:
|
||||
def load_query_file(filepath: str, service_name: str) -> list[QueryDef]:
|
||||
"""Parse an .sx file and register any defquery definitions."""
|
||||
from .parser import parse_all
|
||||
from .evaluator import _eval as _raw_eval, _trampoline
|
||||
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
|
||||
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
|
||||
from .jinja_bridge import get_component_env
|
||||
|
||||
@@ -103,7 +103,7 @@ def load_query_file(filepath: str, service_name: str) -> list[QueryDef]:
|
||||
def load_action_file(filepath: str, service_name: str) -> list[ActionDef]:
|
||||
"""Parse an .sx file and register any defaction definitions."""
|
||||
from .parser import parse_all
|
||||
from .evaluator import _eval as _raw_eval, _trampoline
|
||||
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
|
||||
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
|
||||
from .jinja_bridge import get_component_env
|
||||
|
||||
|
||||
1262
shared/sx/ref/adapter-async.sx
Normal file
1262
shared/sx/ref/adapter-async.sx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -433,11 +433,11 @@
|
||||
|
||||
;; Render the island body as HTML
|
||||
(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
|
||||
(str "<span data-sx-island=\"" (escape-attr island-name) "\""
|
||||
(if state-json
|
||||
(str " data-sx-state=\"" (escape-attr state-json) "\"")
|
||||
(if state-sx
|
||||
(str " data-sx-state=\"" (escape-attr state-sx) "\"")
|
||||
"")
|
||||
">"
|
||||
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).
|
||||
;; Functions, components, and other non-serializable values are skipped.
|
||||
;; Uses the SX serializer (not JSON) so the client can parse with sx-parse.
|
||||
;; Handles all SX types natively: numbers, strings, booleans, nil, lists, dicts.
|
||||
|
||||
(define serialize-island-state
|
||||
(fn (kwargs)
|
||||
(if (empty-dict? kwargs)
|
||||
nil
|
||||
(json-serialize kwargs))))
|
||||
(sx-serialize kwargs))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -476,8 +476,8 @@
|
||||
;; Raw HTML construction:
|
||||
;; (make-raw-html s) → wrap string as raw HTML (not double-escaped)
|
||||
;;
|
||||
;; JSON serialization (for island state):
|
||||
;; (json-serialize dict) → JSON string
|
||||
;; Island state serialization:
|
||||
;; (sx-serialize val) → SX source string (from parser.sx)
|
||||
;; (empty-dict? d) → boolean
|
||||
;; (escape-attr s) → HTML attribute escape
|
||||
;;
|
||||
|
||||
@@ -106,35 +106,56 @@
|
||||
(define aser-fragment
|
||||
(fn (children env)
|
||||
;; Serialize (<> child1 child2 ...) to sx source string
|
||||
(let ((parts (filter
|
||||
(fn (x) (not (nil? x)))
|
||||
(map (fn (c) (aser c env)) children))))
|
||||
;; Must flatten list results (e.g. from map/filter) to avoid nested parens
|
||||
(let ((parts (list)))
|
||||
(for-each
|
||||
(fn (c)
|
||||
(let ((result (aser c env)))
|
||||
(if (= (type-of result) "list")
|
||||
(for-each
|
||||
(fn (item)
|
||||
(when (not (nil? item))
|
||||
(append! parts (serialize item))))
|
||||
result)
|
||||
(when (not (nil? result))
|
||||
(append! parts (serialize result))))))
|
||||
children)
|
||||
(if (empty? parts)
|
||||
""
|
||||
(str "(<> " (join " " (map serialize parts)) ")")))))
|
||||
(str "(<> " (join " " parts) ")")))))
|
||||
|
||||
|
||||
(define aser-call
|
||||
(fn (name args env)
|
||||
;; Serialize (name :key val child ...) — evaluate args but keep as sx
|
||||
(let ((parts (list name)))
|
||||
(reduce
|
||||
(fn (state arg)
|
||||
(let ((skip (get state "skip")))
|
||||
(if skip
|
||||
(assoc state "skip" false "i" (inc (get state "i")))
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc (get state "i")) (len args)))
|
||||
(let ((val (aser (nth args (inc (get state "i"))) env)))
|
||||
(when (not (nil? val))
|
||||
(append! parts (str ":" (keyword-name arg)))
|
||||
(append! parts (serialize val)))
|
||||
(assoc state "skip" true "i" (inc (get state "i"))))
|
||||
(let ((val (aser arg env)))
|
||||
(when (not (nil? val))
|
||||
(append! parts (serialize val)))
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
;; Uses for-each + mutable state (not reduce) so bootstrapper emits for-loops
|
||||
;; that can contain nested for-each for list flattening.
|
||||
(let ((parts (list name))
|
||||
(skip false)
|
||||
(i 0))
|
||||
(for-each
|
||||
(fn (arg)
|
||||
(if skip
|
||||
(do (set! skip false)
|
||||
(set! i (inc i)))
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc i) (len args)))
|
||||
(let ((val (aser (nth args (inc i)) env)))
|
||||
(when (not (nil? val))
|
||||
(append! parts (str ":" (keyword-name arg)))
|
||||
(append! parts (serialize val)))
|
||||
(set! skip true)
|
||||
(set! i (inc i)))
|
||||
(let ((val (aser arg env)))
|
||||
(when (not (nil? val))
|
||||
(if (= (type-of val) "list")
|
||||
(for-each
|
||||
(fn (item)
|
||||
(when (not (nil? item))
|
||||
(append! parts (serialize item))))
|
||||
val)
|
||||
(append! parts (serialize val))))
|
||||
(set! i (inc i))))))
|
||||
args)
|
||||
(str "(" (join " " parts) ")"))))
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -344,15 +344,15 @@
|
||||
(define hydrate-island
|
||||
(fn (el)
|
||||
(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))
|
||||
(env (get-render-env nil)))
|
||||
(let ((comp (env-get env comp-name)))
|
||||
(if (not (or (component? comp) (island? comp)))
|
||||
(log-warn (str "hydrate-island: unknown island " comp-name))
|
||||
|
||||
;; Parse state and build keyword args
|
||||
(let ((kwargs (json-parse state-json))
|
||||
;; Parse state and build keyword args — SX format, not JSON
|
||||
(let ((kwargs (or (first (sx-parse state-sx)) {}))
|
||||
(disposers (list))
|
||||
(local (env-merge (component-closure comp) env)))
|
||||
|
||||
@@ -494,8 +494,8 @@
|
||||
;; (log-info msg) → void (console.log with prefix)
|
||||
;; (log-parse-error label text err) → void (diagnostic parse error)
|
||||
;;
|
||||
;; === JSON ===
|
||||
;; (json-parse str) → dict/list/value (JSON.parse)
|
||||
;; === Parsing (island state) ===
|
||||
;; (sx-parse str) → list of AST expressions (from parser.sx)
|
||||
;;
|
||||
;; === Processing markers ===
|
||||
;; (mark-processed! el key) → void
|
||||
|
||||
@@ -49,6 +49,8 @@ class PyEmitter:
|
||||
|
||||
def __init__(self):
|
||||
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:
|
||||
"""Emit a Python expression from an SX AST node."""
|
||||
@@ -80,6 +82,8 @@ class PyEmitter:
|
||||
name = head.name
|
||||
if name == "define":
|
||||
return self._emit_define(expr, indent)
|
||||
if name == "define-async":
|
||||
return self._emit_define_async(expr, indent)
|
||||
if name == "set!":
|
||||
return f"{pad}{self._mangle(expr[1].name)} = {self.emit(expr[2])}"
|
||||
if name == "when":
|
||||
@@ -275,6 +279,19 @@ class PyEmitter:
|
||||
"sf-defisland": "sf_defisland",
|
||||
# adapter-sx.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",
|
||||
"eval-case-aser": "eval_case_aser",
|
||||
"sx-serialize": "sx_serialize",
|
||||
@@ -417,6 +434,8 @@ class PyEmitter:
|
||||
# Regular function call
|
||||
fn_name = self._mangle(name)
|
||||
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})"
|
||||
|
||||
# --- Special form emitters ---
|
||||
@@ -513,13 +532,16 @@ class PyEmitter:
|
||||
body_parts = expr[2:]
|
||||
lines = [f"{pad}if sx_truthy({cond}):"]
|
||||
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)
|
||||
|
||||
def _emit_cond(self, expr) -> str:
|
||||
clauses = expr[1:]
|
||||
if not clauses:
|
||||
return "NIL"
|
||||
# Check ALL clauses are 2-element lists (scheme-style).
|
||||
# Checking only the first is ambiguous — (nil? x) is a 2-element
|
||||
# function call, not a scheme clause ((test body)).
|
||||
is_scheme = (
|
||||
all(isinstance(c, list) and len(c) == 2 for c in clauses)
|
||||
and not any(isinstance(c, Keyword) for c in clauses)
|
||||
@@ -642,6 +664,16 @@ class PyEmitter:
|
||||
val = self.emit(val_expr)
|
||||
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:
|
||||
"""Check if a fn expression's body (recursively) uses set!."""
|
||||
def _has_set(node):
|
||||
@@ -654,12 +686,16 @@ class PyEmitter:
|
||||
body = fn_expr[2:]
|
||||
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.
|
||||
|
||||
This is used for functions that contain set! — Python closures can't
|
||||
rebind outer lambda params, so we need proper def + local variables.
|
||||
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
|
||||
params = fn_expr[1]
|
||||
@@ -686,14 +722,19 @@ class PyEmitter:
|
||||
py_name = self._mangle(name)
|
||||
# Find set! target variables that are used from nested lambda scopes
|
||||
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:
|
||||
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_async = self._in_async
|
||||
self._current_cell_vars = nested_set_vars
|
||||
if is_async:
|
||||
self._in_async = True
|
||||
self._emit_body_stmts(body, lines, indent + 1)
|
||||
self._current_cell_vars = old_cells
|
||||
self._in_async = old_async
|
||||
return "\n".join(lines)
|
||||
|
||||
def _find_nested_set_vars(self, body) -> set[str]:
|
||||
@@ -750,7 +791,7 @@ class PyEmitter:
|
||||
if is_last:
|
||||
self._emit_return_expr(expr, lines, indent)
|
||||
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:
|
||||
"""Emit an expression in return position, flattening control flow."""
|
||||
@@ -775,6 +816,11 @@ class PyEmitter:
|
||||
if name in ("do", "begin"):
|
||||
self._emit_body_stmts(expr[1:], lines, indent)
|
||||
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)}")
|
||||
|
||||
def _emit_if_return(self, expr, lines: list, indent: int) -> None:
|
||||
@@ -1034,12 +1080,15 @@ class PyEmitter:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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)
|
||||
defines = []
|
||||
for expr in exprs:
|
||||
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])
|
||||
defines.append((name, expr))
|
||||
return defines
|
||||
@@ -1196,6 +1245,8 @@ def compile_ref_to_py(
|
||||
spec_mod_set.add("deps")
|
||||
if "signals" in SPEC_MODULES:
|
||||
spec_mod_set.add("signals")
|
||||
if "page-helpers" in SPEC_MODULES:
|
||||
spec_mod_set.add("page-helpers")
|
||||
has_deps = "deps" in spec_mod_set
|
||||
|
||||
# Core files always included, then selected adapters, then spec modules
|
||||
@@ -1210,6 +1261,28 @@ def compile_ref_to_py(
|
||||
for name in sorted(spec_mod_set):
|
||||
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 = []
|
||||
for filename, label in sx_files:
|
||||
filepath = os.path.join(ref_dir, filename)
|
||||
@@ -1246,6 +1319,9 @@ def compile_ref_to_py(
|
||||
if has_deps:
|
||||
parts.append(PLATFORM_DEPS_PY)
|
||||
|
||||
if has_async:
|
||||
parts.append(PLATFORM_ASYNC_PY)
|
||||
|
||||
for label, defines in all_sections:
|
||||
parts.append(f"\n# === Transpiled from {label} ===\n")
|
||||
for name, expr in defines:
|
||||
@@ -1256,7 +1332,7 @@ def compile_ref_to_py(
|
||||
parts.append(FIXUPS_PY)
|
||||
if has_continuations:
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ def _emit_py(suites: list[dict], preamble: list) -> str:
|
||||
lines.append('')
|
||||
lines.append('import pytest')
|
||||
lines.append('from shared.sx.parser import parse_all')
|
||||
lines.append('from shared.sx.evaluator import _eval, _trampoline')
|
||||
lines.append('from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline')
|
||||
lines.append('')
|
||||
lines.append('')
|
||||
lines.append(f"_PREAMBLE = '''{preamble_escaped}'''")
|
||||
|
||||
@@ -34,11 +34,12 @@
|
||||
(define parse-time
|
||||
(fn (s)
|
||||
;; Parse time string: "2s" → 2000, "500ms" → 500
|
||||
(cond
|
||||
(nil? s) 0
|
||||
(ends-with? s "ms") (parse-int s 0)
|
||||
(ends-with? s "s") (* (parse-int (replace s "s" "") 0) 1000)
|
||||
:else (parse-int s 0))))
|
||||
;; Uses nested if (not cond) because cond misclassifies 2-element
|
||||
;; function calls like (nil? s) as scheme-style ((test body)) clauses.
|
||||
(if (nil? s) 0
|
||||
(if (ends-with? s "ms") (parse-int s 0)
|
||||
(if (ends-with? s "s") (* (parse-int (replace s "s" "") 0) 1000)
|
||||
(parse-int s 0))))))
|
||||
|
||||
|
||||
(define parse-trigger-spec
|
||||
@@ -219,20 +220,19 @@
|
||||
;; Filter form parameters by sx-params spec.
|
||||
;; all-params is a list of (key value) pairs.
|
||||
;; Returns filtered list of (key value) pairs.
|
||||
(cond
|
||||
(nil? params-spec) all-params
|
||||
(= params-spec "none") (list)
|
||||
(= params-spec "*") all-params
|
||||
(starts-with? params-spec "not ")
|
||||
(let ((excluded (map trim (split (slice params-spec 4) ","))))
|
||||
(filter
|
||||
(fn (p) (not (contains? excluded (first p))))
|
||||
all-params))
|
||||
:else
|
||||
(let ((allowed (map trim (split params-spec ","))))
|
||||
(filter
|
||||
(fn (p) (contains? allowed (first p)))
|
||||
all-params)))))
|
||||
;; Uses nested if (not cond) — see parse-time comment.
|
||||
(if (nil? params-spec) all-params
|
||||
(if (= params-spec "none") (list)
|
||||
(if (= params-spec "*") all-params
|
||||
(if (starts-with? params-spec "not ")
|
||||
(let ((excluded (map trim (split (slice params-spec 4) ","))))
|
||||
(filter
|
||||
(fn (p) (not (contains? excluded (first p))))
|
||||
all-params))
|
||||
(let ((allowed (map trim (split params-spec ","))))
|
||||
(filter
|
||||
(fn (p) (contains? allowed (first p)))
|
||||
all-params))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -309,14 +309,18 @@
|
||||
nil))))
|
||||
|
||||
|
||||
;; cond-scheme? — check if ALL clauses are 2-element lists (scheme-style).
|
||||
;; Checking only the first arg is ambiguous — (nil? x) is a 2-element
|
||||
;; function call, not a scheme clause ((test body)).
|
||||
(define cond-scheme?
|
||||
(fn (clauses)
|
||||
(every? (fn (c) (and (= (type-of c) "list") (= (len c) 2)))
|
||||
clauses)))
|
||||
|
||||
(define sf-cond
|
||||
(fn (args env)
|
||||
;; Detect scheme-style: first arg is a 2-element list
|
||||
(if (and (= (type-of (first args)) "list")
|
||||
(= (len (first args)) 2))
|
||||
;; Scheme-style: ((test body) ...)
|
||||
(if (cond-scheme? args)
|
||||
(sf-cond-scheme args env)
|
||||
;; Clojure-style: test body test body ...
|
||||
(sf-cond-clojure args env))))
|
||||
|
||||
(define sf-cond-scheme
|
||||
|
||||
@@ -1290,8 +1290,9 @@
|
||||
(= name "append!")
|
||||
(str (js-expr (nth expr 1)) ".push(" (js-expr (nth expr 2)) ");")
|
||||
(= name "env-set!")
|
||||
(str (js-expr (nth expr 1)) "[" (js-expr (nth expr 2))
|
||||
"] = " (js-expr (nth expr 3)) ";")
|
||||
(str "envSet(" (js-expr (nth expr 1))
|
||||
", " (js-expr (nth expr 2))
|
||||
", " (js-expr (nth expr 3)) ");")
|
||||
(= name "set-lambda-name!")
|
||||
(str (js-expr (nth expr 1)) ".name = " (js-expr (nth expr 2)) ";")
|
||||
:else
|
||||
|
||||
368
shared/sx/ref/page-helpers.sx
Normal file
368
shared/sx/ref/page-helpers.sx
Normal file
@@ -0,0 +1,368 @@
|
||||
;; ==========================================================================
|
||||
;; page-helpers.sx — Pure data-transformation page helpers
|
||||
;;
|
||||
;; These functions take raw data (from Python I/O edge) and return
|
||||
;; structured dicts for page rendering. No I/O — pure transformations
|
||||
;; only. Bootstrapped to every host.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; categorize-special-forms
|
||||
;;
|
||||
;; Parses define-special-form declarations from special-forms.sx AST,
|
||||
;; categorizes each form by name lookup, returns dict of category → forms.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define special-form-category-map
|
||||
{"if" "Control Flow" "when" "Control Flow" "cond" "Control Flow"
|
||||
"case" "Control Flow" "and" "Control Flow" "or" "Control Flow"
|
||||
"let" "Binding" "let*" "Binding" "letrec" "Binding"
|
||||
"define" "Binding" "set!" "Binding"
|
||||
"lambda" "Functions & Components" "fn" "Functions & Components"
|
||||
"defcomp" "Functions & Components" "defmacro" "Functions & Components"
|
||||
"begin" "Sequencing & Threading" "do" "Sequencing & Threading"
|
||||
"->" "Sequencing & Threading"
|
||||
"quote" "Quoting" "quasiquote" "Quoting"
|
||||
"reset" "Continuations" "shift" "Continuations"
|
||||
"dynamic-wind" "Guards"
|
||||
"map" "Higher-Order Forms" "map-indexed" "Higher-Order Forms"
|
||||
"filter" "Higher-Order Forms" "reduce" "Higher-Order Forms"
|
||||
"some" "Higher-Order Forms" "every?" "Higher-Order Forms"
|
||||
"for-each" "Higher-Order Forms"
|
||||
"defstyle" "Domain Definitions"
|
||||
"defhandler" "Domain Definitions" "defpage" "Domain Definitions"
|
||||
"defquery" "Domain Definitions" "defaction" "Domain Definitions"})
|
||||
|
||||
|
||||
(define extract-define-kwargs
|
||||
(fn (expr)
|
||||
;; Extract keyword args from a define-special-form expression.
|
||||
;; Returns dict of keyword-name → string value.
|
||||
;; Walks items pairwise: when item[i] is a keyword, item[i+1] is its value.
|
||||
(let ((result {})
|
||||
(items (slice expr 2))
|
||||
(n (len items)))
|
||||
(for-each
|
||||
(fn (idx)
|
||||
(when (and (< (+ idx 1) n)
|
||||
(= (type-of (nth items idx)) "keyword"))
|
||||
(let ((key (keyword-name (nth items idx)))
|
||||
(val (nth items (+ idx 1))))
|
||||
(dict-set! result key
|
||||
(if (= (type-of val) "list")
|
||||
(str "(" (join " " (map serialize val)) ")")
|
||||
(str val))))))
|
||||
(range 0 n))
|
||||
result)))
|
||||
|
||||
|
||||
(define categorize-special-forms
|
||||
(fn (parsed-exprs)
|
||||
;; parsed-exprs: result of parse-all on special-forms.sx
|
||||
;; Returns dict of category-name → list of form dicts.
|
||||
(let ((categories {}))
|
||||
(for-each
|
||||
(fn (expr)
|
||||
(when (and (= (type-of expr) "list")
|
||||
(>= (len expr) 2)
|
||||
(= (type-of (first expr)) "symbol")
|
||||
(= (symbol-name (first expr)) "define-special-form"))
|
||||
(let ((name (nth expr 1))
|
||||
(kwargs (extract-define-kwargs expr))
|
||||
(category (or (get special-form-category-map name) "Other")))
|
||||
(when (not (has-key? categories category))
|
||||
(dict-set! categories category (list)))
|
||||
(append! (get categories category)
|
||||
{"name" name
|
||||
"syntax" (or (get kwargs "syntax") "")
|
||||
"doc" (or (get kwargs "doc") "")
|
||||
"tail-position" (or (get kwargs "tail-position") "")
|
||||
"example" (or (get kwargs "example") "")}))))
|
||||
parsed-exprs)
|
||||
categories)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-reference-data
|
||||
;;
|
||||
;; Takes a slug and raw reference data, returns structured dict for rendering.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-ref-items-with-href
|
||||
(fn (items base-path detail-keys n-fields)
|
||||
;; items: list of lists (tuples), each with n-fields elements
|
||||
;; base-path: e.g. "/hypermedia/reference/attributes/"
|
||||
;; detail-keys: list of strings (keys that have detail pages)
|
||||
;; n-fields: 2 or 3 (number of fields per tuple)
|
||||
(map
|
||||
(fn (item)
|
||||
(if (= n-fields 3)
|
||||
;; [name, desc/value, exists/desc]
|
||||
(let ((name (nth item 0))
|
||||
(field2 (nth item 1))
|
||||
(field3 (nth item 2)))
|
||||
{"name" name
|
||||
"desc" field2
|
||||
"exists" field3
|
||||
"href" (if (and field3 (some (fn (k) (= k name)) detail-keys))
|
||||
(str base-path name)
|
||||
nil)})
|
||||
;; [name, desc]
|
||||
(let ((name (nth item 0))
|
||||
(desc (nth item 1)))
|
||||
{"name" name
|
||||
"desc" desc
|
||||
"href" (if (some (fn (k) (= k name)) detail-keys)
|
||||
(str base-path name)
|
||||
nil)})))
|
||||
items)))
|
||||
|
||||
|
||||
(define build-reference-data
|
||||
(fn (slug raw-data detail-keys)
|
||||
;; slug: "attributes", "headers", "events", "js-api"
|
||||
;; raw-data: dict with the raw data lists for this slug
|
||||
;; detail-keys: list of names that have detail pages
|
||||
(case slug
|
||||
"attributes"
|
||||
{"req-attrs" (build-ref-items-with-href
|
||||
(get raw-data "req-attrs")
|
||||
"/hypermedia/reference/attributes/" detail-keys 3)
|
||||
"beh-attrs" (build-ref-items-with-href
|
||||
(get raw-data "beh-attrs")
|
||||
"/hypermedia/reference/attributes/" detail-keys 3)
|
||||
"uniq-attrs" (build-ref-items-with-href
|
||||
(get raw-data "uniq-attrs")
|
||||
"/hypermedia/reference/attributes/" detail-keys 3)}
|
||||
|
||||
"headers"
|
||||
{"req-headers" (build-ref-items-with-href
|
||||
(get raw-data "req-headers")
|
||||
"/hypermedia/reference/headers/" detail-keys 3)
|
||||
"resp-headers" (build-ref-items-with-href
|
||||
(get raw-data "resp-headers")
|
||||
"/hypermedia/reference/headers/" detail-keys 3)}
|
||||
|
||||
"events"
|
||||
{"events-list" (build-ref-items-with-href
|
||||
(get raw-data "events-list")
|
||||
"/hypermedia/reference/events/" detail-keys 2)}
|
||||
|
||||
"js-api"
|
||||
{"js-api-list" (map (fn (item) {"name" (nth item 0) "desc" (nth item 1)})
|
||||
(get raw-data "js-api-list"))}
|
||||
|
||||
;; default: attributes
|
||||
:else
|
||||
{"req-attrs" (build-ref-items-with-href
|
||||
(get raw-data "req-attrs")
|
||||
"/hypermedia/reference/attributes/" detail-keys 3)
|
||||
"beh-attrs" (build-ref-items-with-href
|
||||
(get raw-data "beh-attrs")
|
||||
"/hypermedia/reference/attributes/" detail-keys 3)
|
||||
"uniq-attrs" (build-ref-items-with-href
|
||||
(get raw-data "uniq-attrs")
|
||||
"/hypermedia/reference/attributes/" detail-keys 3)})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-attr-detail / build-header-detail / build-event-detail
|
||||
;;
|
||||
;; Lookup a slug in a detail dict, reshape for page rendering.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-attr-detail
|
||||
(fn (slug detail)
|
||||
;; detail: dict with "description", "example", "handler", "demo" keys or nil
|
||||
(if (nil? detail)
|
||||
{"attr-not-found" true}
|
||||
{"attr-not-found" nil
|
||||
"attr-title" slug
|
||||
"attr-description" (get detail "description")
|
||||
"attr-example" (get detail "example")
|
||||
"attr-handler" (get detail "handler")
|
||||
"attr-demo" (get detail "demo")
|
||||
"attr-wire-id" (if (has-key? detail "handler")
|
||||
(str "ref-wire-"
|
||||
(replace (replace slug ":" "-") "*" "star"))
|
||||
nil)})))
|
||||
|
||||
|
||||
(define build-header-detail
|
||||
(fn (slug detail)
|
||||
(if (nil? detail)
|
||||
{"header-not-found" true}
|
||||
{"header-not-found" nil
|
||||
"header-title" slug
|
||||
"header-direction" (get detail "direction")
|
||||
"header-description" (get detail "description")
|
||||
"header-example" (get detail "example")
|
||||
"header-demo" (get detail "demo")})))
|
||||
|
||||
|
||||
(define build-event-detail
|
||||
(fn (slug detail)
|
||||
(if (nil? detail)
|
||||
{"event-not-found" true}
|
||||
{"event-not-found" nil
|
||||
"event-title" slug
|
||||
"event-description" (get detail "description")
|
||||
"event-example" (get detail "example")
|
||||
"event-demo" (get detail "demo")})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-component-source
|
||||
;;
|
||||
;; Reconstruct defcomp/defisland source from component metadata.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-component-source
|
||||
(fn (comp-data)
|
||||
;; comp-data: dict with "type", "name", "params", "has-children", "body-sx", "affinity"
|
||||
(let ((comp-type (get comp-data "type"))
|
||||
(name (get comp-data "name"))
|
||||
(params (get comp-data "params"))
|
||||
(has-children (get comp-data "has-children"))
|
||||
(body-sx (get comp-data "body-sx"))
|
||||
(affinity (get comp-data "affinity")))
|
||||
(if (= comp-type "not-found")
|
||||
(str ";; component " name " not found")
|
||||
(let ((param-strs (if (empty? params)
|
||||
(if has-children
|
||||
(list "&rest" "children")
|
||||
(list))
|
||||
(if has-children
|
||||
(append (cons "&key" params) (list "&rest" "children"))
|
||||
(cons "&key" params))))
|
||||
(params-sx (str "(" (join " " param-strs) ")"))
|
||||
(form-name (if (= comp-type "island") "defisland" "defcomp"))
|
||||
(affinity-str (if (and (= comp-type "component")
|
||||
(not (nil? affinity))
|
||||
(not (= affinity "auto")))
|
||||
(str " :affinity " affinity)
|
||||
"")))
|
||||
(str "(" form-name " " name " " params-sx affinity-str "\n " body-sx ")"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-bundle-analysis
|
||||
;;
|
||||
;; Compute per-page bundle stats from pre-extracted component data.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-bundle-analysis
|
||||
(fn (pages-raw components-raw total-components total-macros pure-count io-count)
|
||||
;; pages-raw: list of {:name :path :direct :needed-names}
|
||||
;; components-raw: dict of name → {:is-pure :affinity :render-target :io-refs :deps :source}
|
||||
(let ((pages-data (list)))
|
||||
(for-each
|
||||
(fn (page)
|
||||
(let ((needed-names (get page "needed-names"))
|
||||
(n (len needed-names))
|
||||
(pct (if (> total-components 0)
|
||||
(round (* (/ n total-components) 100))
|
||||
0))
|
||||
(savings (- 100 pct))
|
||||
(pure-in-page 0)
|
||||
(io-in-page 0)
|
||||
(page-io-refs (list))
|
||||
(comp-details (list)))
|
||||
;; Walk needed components
|
||||
(for-each
|
||||
(fn (comp-name)
|
||||
(let ((info (get components-raw comp-name)))
|
||||
(when (not (nil? info))
|
||||
(if (get info "is-pure")
|
||||
(set! pure-in-page (+ pure-in-page 1))
|
||||
(do
|
||||
(set! io-in-page (+ io-in-page 1))
|
||||
(for-each
|
||||
(fn (ref) (when (not (some (fn (r) (= r ref)) page-io-refs))
|
||||
(append! page-io-refs ref)))
|
||||
(or (get info "io-refs") (list)))))
|
||||
(append! comp-details
|
||||
{"name" comp-name
|
||||
"is-pure" (get info "is-pure")
|
||||
"affinity" (get info "affinity")
|
||||
"render-target" (get info "render-target")
|
||||
"io-refs" (or (get info "io-refs") (list))
|
||||
"deps" (or (get info "deps") (list))
|
||||
"source" (get info "source")}))))
|
||||
needed-names)
|
||||
(append! pages-data
|
||||
{"name" (get page "name")
|
||||
"path" (get page "path")
|
||||
"direct" (get page "direct")
|
||||
"needed" n
|
||||
"pct" pct
|
||||
"savings" savings
|
||||
"io-refs" (len page-io-refs)
|
||||
"pure-in-page" pure-in-page
|
||||
"io-in-page" io-in-page
|
||||
"components" comp-details})))
|
||||
pages-raw)
|
||||
{"pages" pages-data
|
||||
"total-components" total-components
|
||||
"total-macros" total-macros
|
||||
"pure-count" pure-count
|
||||
"io-count" io-count})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-routing-analysis
|
||||
;;
|
||||
;; Classify pages by routing mode (client vs server).
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-routing-analysis
|
||||
(fn (pages-raw)
|
||||
;; pages-raw: list of {:name :path :has-data :content-src}
|
||||
(let ((pages-data (list))
|
||||
(client-count 0)
|
||||
(server-count 0))
|
||||
(for-each
|
||||
(fn (page)
|
||||
(let ((has-data (get page "has-data"))
|
||||
(content-src (or (get page "content-src") ""))
|
||||
(mode nil)
|
||||
(reason ""))
|
||||
(cond
|
||||
has-data
|
||||
(do (set! mode "server")
|
||||
(set! reason "Has :data expression — needs server IO")
|
||||
(set! server-count (+ server-count 1)))
|
||||
(empty? content-src)
|
||||
(do (set! mode "server")
|
||||
(set! reason "No content expression")
|
||||
(set! server-count (+ server-count 1)))
|
||||
:else
|
||||
(do (set! mode "client")
|
||||
(set! client-count (+ client-count 1))))
|
||||
(append! pages-data
|
||||
{"name" (get page "name")
|
||||
"path" (get page "path")
|
||||
"mode" mode
|
||||
"has-data" has-data
|
||||
"content-expr" (if (> (len content-src) 80)
|
||||
(str (slice content-src 0 80) "...")
|
||||
content-src)
|
||||
"reason" reason})))
|
||||
pages-raw)
|
||||
{"pages" pages-data
|
||||
"total-pages" (+ client-count server-count)
|
||||
"client-count" client-count
|
||||
"server-count" server-count})))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; build-affinity-analysis
|
||||
;;
|
||||
;; Package component affinity info + page render plans for display.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define build-affinity-analysis
|
||||
(fn (demo-components page-plans)
|
||||
{"components" demo-components
|
||||
"page-plans" page-plans}))
|
||||
3189
shared/sx/ref/platform_js.py
Normal file
3189
shared/sx/ref/platform_js.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -470,10 +470,7 @@ def invoke(f, *args):
|
||||
|
||||
def json_serialize(obj):
|
||||
import json
|
||||
try:
|
||||
return json.dumps(obj)
|
||||
except (TypeError, ValueError):
|
||||
return "{}"
|
||||
return json.dumps(obj)
|
||||
|
||||
|
||||
def is_empty_dict(d):
|
||||
@@ -608,7 +605,7 @@ def sx_expr_source(x):
|
||||
|
||||
|
||||
try:
|
||||
from shared.sx.evaluator import EvalError
|
||||
from shared.sx.types import EvalError
|
||||
except ImportError:
|
||||
class EvalError(Exception):
|
||||
pass
|
||||
@@ -1075,10 +1072,19 @@ import inspect
|
||||
|
||||
from shared.sx.primitives_io import (
|
||||
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
|
||||
_expand_components_cv: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
||||
"_expand_components_ref", default=False
|
||||
@@ -1102,18 +1108,22 @@ def expand_components_p():
|
||||
|
||||
|
||||
def svg_context_p():
|
||||
_ensure_html_imports()
|
||||
return _svg_context_cv.get(False)
|
||||
|
||||
|
||||
def svg_context_set(val):
|
||||
_ensure_html_imports()
|
||||
return _svg_context_cv.set(val)
|
||||
|
||||
|
||||
def svg_context_reset(token):
|
||||
_ensure_html_imports()
|
||||
_svg_context_cv.reset(token)
|
||||
|
||||
|
||||
def css_class_collect(val):
|
||||
_ensure_html_imports()
|
||||
collector = _css_class_collector_cv.get(None)
|
||||
if collector is not None:
|
||||
collector.update(str(val).split())
|
||||
@@ -1131,6 +1141,25 @@ def is_sx_expr(x):
|
||||
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):
|
||||
return inspect.iscoroutine(x)
|
||||
|
||||
@@ -1207,48 +1236,16 @@ async def async_eval_slot_to_sx(expr, env, ctx=None):
|
||||
ctx = RequestContext()
|
||||
token = _expand_components_cv.set(True)
|
||||
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:
|
||||
_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
|
||||
'''
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1374,7 +1371,8 @@ aser_special = _aser_special_with_continuations
|
||||
# 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 = [
|
||||
'',
|
||||
'# =========================================================================',
|
||||
@@ -1427,8 +1425,9 @@ def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ADAPTER_FILES = {
|
||||
"html": ("adapter-html.sx", "adapter-html"),
|
||||
"sx": ("adapter-sx.sx", "adapter-sx"),
|
||||
"html": ("adapter-html.sx", "adapter-html"),
|
||||
"sx": ("adapter-sx.sx", "adapter-sx"),
|
||||
"async": ("adapter-async.sx", "adapter-async"),
|
||||
}
|
||||
|
||||
SPEC_MODULES = {
|
||||
@@ -1436,6 +1435,7 @@ SPEC_MODULES = {
|
||||
"router": ("router.sx", "router (client-side route matching)"),
|
||||
"engine": ("engine.sx", "engine (fetch/swap/trigger pure logic)"),
|
||||
"signals": ("signals.sx", "signals (reactive signal runtime)"),
|
||||
"page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"),
|
||||
"types": ("types.sx", "types (gradual type system)"),
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ def _get_z3_env() -> dict[str, Any]:
|
||||
return _z3_env
|
||||
|
||||
from shared.sx.parser import parse_all
|
||||
from shared.sx.evaluator import make_env, _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import make_env, eval_expr as _eval, trampoline as _trampoline
|
||||
|
||||
env = make_env()
|
||||
z3_path = os.path.join(os.path.dirname(__file__), "z3.sx")
|
||||
@@ -60,7 +60,7 @@ def z3_translate(expr: Any) -> str:
|
||||
|
||||
Delegates to z3-translate defined in z3.sx.
|
||||
"""
|
||||
from shared.sx.evaluator import _trampoline, _call_lambda
|
||||
from shared.sx.ref.sx_ref import trampoline as _trampoline, call_lambda as _call_lambda
|
||||
|
||||
env = _get_z3_env()
|
||||
return _trampoline(_call_lambda(env["z3-translate"], [expr], env))
|
||||
@@ -72,7 +72,7 @@ def z3_translate_file(source: str) -> str:
|
||||
Delegates to z3-translate-file defined in z3.sx.
|
||||
"""
|
||||
from shared.sx.parser import parse_all
|
||||
from shared.sx.evaluator import _trampoline, _call_lambda
|
||||
from shared.sx.ref.sx_ref import trampoline as _trampoline, call_lambda as _call_lambda
|
||||
|
||||
env = _get_z3_env()
|
||||
exprs = parse_all(source)
|
||||
|
||||
@@ -134,12 +134,8 @@
|
||||
;; (test body test body ...).
|
||||
(define eval-cond
|
||||
(fn (clauses env)
|
||||
(if (and (not (empty? clauses))
|
||||
(= (type-of (first clauses)) "list")
|
||||
(= (len (first clauses)) 2))
|
||||
;; Scheme-style
|
||||
(if (cond-scheme? clauses)
|
||||
(eval-cond-scheme clauses env)
|
||||
;; Clojure-style
|
||||
(eval-cond-clojure clauses env))))
|
||||
|
||||
(define eval-cond-scheme
|
||||
@@ -178,7 +174,9 @@
|
||||
;; bindings = ((name1 expr1) (name2 expr2) ...)
|
||||
(define process-bindings
|
||||
(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
|
||||
(fn (pair)
|
||||
(when (and (= (type-of pair) "list") (>= (len pair) 2))
|
||||
|
||||
@@ -49,7 +49,7 @@ def load_js_sx() -> dict:
|
||||
|
||||
exprs = parse_all(source)
|
||||
|
||||
from shared.sx.evaluator import evaluate, make_env
|
||||
from shared.sx.ref.sx_ref import evaluate, make_env
|
||||
|
||||
env = make_env()
|
||||
for expr in exprs:
|
||||
@@ -74,7 +74,7 @@ def compile_ref_to_js(
|
||||
spec_modules: List of spec modules (deps, router, signals). None = auto.
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
from shared.sx.evaluator import evaluate
|
||||
from shared.sx.ref.sx_ref import evaluate
|
||||
|
||||
ref_dir = _HERE
|
||||
env = load_js_sx()
|
||||
@@ -103,8 +103,11 @@ def compile_ref_to_js(
|
||||
if "boot" in adapter_set:
|
||||
spec_mod_set.add("router")
|
||||
spec_mod_set.add("deps")
|
||||
if "page-helpers" in SPEC_MODULES:
|
||||
spec_mod_set.add("page-helpers")
|
||||
has_deps = "deps" in spec_mod_set
|
||||
has_router = "router" in spec_mod_set
|
||||
has_page_helpers = "page-helpers" in spec_mod_set
|
||||
|
||||
# Resolve extensions
|
||||
ext_set = set()
|
||||
@@ -198,12 +201,12 @@ def compile_ref_to_js(
|
||||
if name in adapter_set and name in adapter_platform:
|
||||
parts.append(adapter_platform[name])
|
||||
|
||||
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps))
|
||||
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps, has_page_helpers))
|
||||
if has_continuations:
|
||||
parts.append(CONTINUATIONS_JS)
|
||||
if has_dom:
|
||||
parts.append(ASYNC_IO_JS)
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals))
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals, has_page_helpers))
|
||||
parts.append(EPILOGUE)
|
||||
|
||||
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
@@ -38,7 +38,7 @@ def load_py_sx(evaluator_env: dict) -> dict:
|
||||
exprs = parse_all(source)
|
||||
|
||||
# Import the evaluator
|
||||
from shared.sx.evaluator import evaluate, make_env
|
||||
from shared.sx.ref.sx_ref import evaluate, make_env
|
||||
|
||||
env = make_env()
|
||||
for expr in exprs:
|
||||
@@ -60,7 +60,7 @@ def extract_defines(source: str) -> list[tuple[str, list]]:
|
||||
|
||||
|
||||
def main():
|
||||
from shared.sx.evaluator import evaluate
|
||||
from shared.sx.ref.sx_ref import evaluate
|
||||
|
||||
# Load py.sx into evaluator
|
||||
env = load_py_sx({})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
272
shared/sx/ref/test-aser.sx
Normal file
272
shared/sx/ref/test-aser.sx
Normal file
@@ -0,0 +1,272 @@
|
||||
;; ==========================================================================
|
||||
;; test-aser.sx — Tests for the SX wire format (aser) adapter
|
||||
;;
|
||||
;; Requires: test-framework.sx loaded first.
|
||||
;; Modules tested: adapter-sx.sx (aser, aser-call, aser-fragment, aser-special)
|
||||
;;
|
||||
;; Platform functions required (beyond test framework):
|
||||
;; render-sx (sx-source) -> SX wire format string
|
||||
;; Parses the sx-source string, evaluates via aser in a
|
||||
;; fresh env, and returns the resulting SX wire format string.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Basic serialization
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "aser-basics"
|
||||
(deftest "number literal passes through"
|
||||
(assert-equal "42"
|
||||
(render-sx "42")))
|
||||
|
||||
(deftest "string literal passes through"
|
||||
;; aser returns the raw string value; render-sx concatenates it directly
|
||||
(assert-equal "hello"
|
||||
(render-sx "\"hello\"")))
|
||||
|
||||
(deftest "boolean true passes through"
|
||||
(assert-equal "true"
|
||||
(render-sx "true")))
|
||||
|
||||
(deftest "boolean false passes through"
|
||||
(assert-equal "false"
|
||||
(render-sx "false")))
|
||||
|
||||
(deftest "nil produces empty"
|
||||
(assert-equal ""
|
||||
(render-sx "nil"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; HTML tag serialization
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "aser-tags"
|
||||
(deftest "simple div"
|
||||
(assert-equal "(div \"hello\")"
|
||||
(render-sx "(div \"hello\")")))
|
||||
|
||||
(deftest "nested tags"
|
||||
(assert-equal "(div (span \"hi\"))"
|
||||
(render-sx "(div (span \"hi\"))")))
|
||||
|
||||
(deftest "multiple children"
|
||||
(assert-equal "(div (p \"a\") (p \"b\"))"
|
||||
(render-sx "(div (p \"a\") (p \"b\"))")))
|
||||
|
||||
(deftest "attributes serialize"
|
||||
(assert-equal "(div :class \"foo\" \"bar\")"
|
||||
(render-sx "(div :class \"foo\" \"bar\")")))
|
||||
|
||||
(deftest "multiple attributes"
|
||||
(assert-equal "(a :href \"/home\" :class \"link\" \"Home\")"
|
||||
(render-sx "(a :href \"/home\" :class \"link\" \"Home\")")))
|
||||
|
||||
(deftest "void elements"
|
||||
(assert-equal "(br)"
|
||||
(render-sx "(br)")))
|
||||
|
||||
(deftest "void element with attrs"
|
||||
(assert-equal "(img :src \"pic.jpg\")"
|
||||
(render-sx "(img :src \"pic.jpg\")"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Fragment serialization
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "aser-fragments"
|
||||
(deftest "simple fragment"
|
||||
(assert-equal "(<> (p \"a\") (p \"b\"))"
|
||||
(render-sx "(<> (p \"a\") (p \"b\"))")))
|
||||
|
||||
(deftest "empty fragment"
|
||||
(assert-equal ""
|
||||
(render-sx "(<>)")))
|
||||
|
||||
(deftest "single-child fragment"
|
||||
(assert-equal "(<> (div \"x\"))"
|
||||
(render-sx "(<> (div \"x\"))"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Control flow in aser mode
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "aser-control-flow"
|
||||
(deftest "if true branch"
|
||||
(assert-equal "(p \"yes\")"
|
||||
(render-sx "(if true (p \"yes\") (p \"no\"))")))
|
||||
|
||||
(deftest "if false branch"
|
||||
(assert-equal "(p \"no\")"
|
||||
(render-sx "(if false (p \"yes\") (p \"no\"))")))
|
||||
|
||||
(deftest "when true"
|
||||
(assert-equal "(p \"ok\")"
|
||||
(render-sx "(when true (p \"ok\"))")))
|
||||
|
||||
(deftest "when false"
|
||||
(assert-equal ""
|
||||
(render-sx "(when false (p \"ok\"))")))
|
||||
|
||||
(deftest "cond serializes matching branch"
|
||||
(assert-equal "(p \"two\")"
|
||||
(render-sx "(cond false (p \"one\") true (p \"two\") :else (p \"three\"))")))
|
||||
|
||||
(deftest "cond with 2-element predicate test"
|
||||
;; Regression: cond misclassifies (nil? x) as scheme-style clause.
|
||||
(assert-equal "(p \"yes\")"
|
||||
(render-sx "(cond (nil? nil) (p \"yes\") :else (p \"no\"))"))
|
||||
(assert-equal "(p \"no\")"
|
||||
(render-sx "(cond (nil? \"x\") (p \"yes\") :else (p \"no\"))")))
|
||||
|
||||
(deftest "let binds then serializes"
|
||||
(assert-equal "(p \"hello\")"
|
||||
(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"
|
||||
(assert-equal "(p \"last\")"
|
||||
(render-sx "(begin (p \"first\") (p \"last\"))"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; THE BUG — map/filter list flattening in children (critical regression)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "aser-list-flattening"
|
||||
(deftest "map inside tag flattens children"
|
||||
(assert-equal "(div (span \"a\") (span \"b\") (span \"c\"))"
|
||||
(render-sx "(do (define items (list \"a\" \"b\" \"c\"))
|
||||
(div (map (fn (x) (span x)) items)))")))
|
||||
|
||||
(deftest "map inside tag with other children"
|
||||
(assert-equal "(ul (li \"first\") (li \"a\") (li \"b\"))"
|
||||
(render-sx "(do (define items (list \"a\" \"b\"))
|
||||
(ul (li \"first\") (map (fn (x) (li x)) items)))")))
|
||||
|
||||
(deftest "filter result via let binding as children"
|
||||
;; Note: (filter ...) is treated as an SVG tag in aser dispatch (SVG has <filter>),
|
||||
;; so we evaluate filter via let binding + map to serialize children
|
||||
(assert-equal "(ul (li \"a\") (li \"b\"))"
|
||||
(render-sx "(do (define items (list \"a\" nil \"b\"))
|
||||
(define kept (filter (fn (x) (not (nil? x))) items))
|
||||
(ul (map (fn (x) (li x)) kept)))")))
|
||||
|
||||
(deftest "map inside fragment flattens"
|
||||
(assert-equal "(<> (p \"a\") (p \"b\"))"
|
||||
(render-sx "(do (define items (list \"a\" \"b\"))
|
||||
(<> (map (fn (x) (p x)) items)))")))
|
||||
|
||||
(deftest "nested map does not double-wrap"
|
||||
(assert-equal "(div (span \"1\") (span \"2\"))"
|
||||
(render-sx "(do (define nums (list 1 2))
|
||||
(div (map (fn (n) (span (str n))) nums)))")))
|
||||
|
||||
(deftest "map with component-like output flattens"
|
||||
(assert-equal "(div (li \"x\") (li \"y\"))"
|
||||
(render-sx "(do (define items (list \"x\" \"y\"))
|
||||
(div (map (fn (x) (li x)) items)))"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Component serialization (NOT expanded in basic aser mode)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "aser-components"
|
||||
(deftest "unknown component serializes as-is"
|
||||
(assert-equal "(~foo :title \"bar\")"
|
||||
(render-sx "(~foo :title \"bar\")")))
|
||||
|
||||
(deftest "defcomp then unexpanded component call"
|
||||
(assert-equal "(~card :title \"Hi\")"
|
||||
(render-sx "(do (defcomp ~card (&key title) (h1 title)) (~card :title \"Hi\"))")))
|
||||
|
||||
(deftest "component with children serializes unexpanded"
|
||||
(assert-equal "(~box (p \"inside\"))"
|
||||
(render-sx "(do (defcomp ~box (&key &rest children) (div children))
|
||||
(~box (p \"inside\")))"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Definition forms in aser mode
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "aser-definitions"
|
||||
(deftest "define evaluates for side effects, returns nil"
|
||||
(assert-equal "(p 42)"
|
||||
(render-sx "(do (define x 42) (p x))")))
|
||||
|
||||
(deftest "defcomp evaluates and returns nil"
|
||||
(assert-equal "(~tag :x 1)"
|
||||
(render-sx "(do (defcomp ~tag (&key x) (span x)) (~tag :x 1))")))
|
||||
|
||||
(deftest "defisland evaluates AND serializes"
|
||||
(let ((result (render-sx "(defisland ~counter (&key count) (span count))")))
|
||||
(assert-true (string-contains? result "defisland")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Function calls in aser mode
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "aser-function-calls"
|
||||
(deftest "named function call evaluates fully"
|
||||
(assert-equal "3"
|
||||
(render-sx "(do (define inc1 (fn (x) (+ x 1))) (inc1 2))")))
|
||||
|
||||
(deftest "define + call"
|
||||
(assert-equal "10"
|
||||
(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"
|
||||
(let ((result (render-sx "(map (fn (x) (+ x 1)) (list 1 2 3))")))
|
||||
;; map at top level returns a list, not serialized tags
|
||||
(assert-true (not (nil? result))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; and/or short-circuit in aser mode
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "aser-logic"
|
||||
(deftest "and short-circuits on false"
|
||||
(assert-equal "false"
|
||||
(render-sx "(and true false true)")))
|
||||
|
||||
(deftest "and returns last truthy"
|
||||
(assert-equal "3"
|
||||
(render-sx "(and 1 2 3)")))
|
||||
|
||||
(deftest "or short-circuits on true"
|
||||
(assert-equal "1"
|
||||
(render-sx "(or 1 2 3)")))
|
||||
|
||||
(deftest "or returns last falsy"
|
||||
(assert-equal "false"
|
||||
(render-sx "(or false false)"))))
|
||||
@@ -277,6 +277,29 @@
|
||||
false "b"
|
||||
:else "c")))
|
||||
|
||||
(deftest "cond with 2-element predicate as first test"
|
||||
;; Regression: cond misclassifies Clojure-style as scheme-style when
|
||||
;; the first test is a 2-element list like (nil? x) or (empty? x).
|
||||
;; The evaluator checks: is first arg a 2-element list? If yes, treats
|
||||
;; as scheme-style ((test body) ...) — returning the arg instead of
|
||||
;; evaluating the predicate call.
|
||||
(assert-equal 0 (cond (nil? nil) 0 :else 1))
|
||||
(assert-equal 1 (cond (nil? "x") 0 :else 1))
|
||||
(assert-equal "empty" (cond (empty? (list)) "empty" :else "not-empty"))
|
||||
(assert-equal "not-empty" (cond (empty? (list 1)) "empty" :else "not-empty"))
|
||||
(assert-equal "yes" (cond (not false) "yes" :else "no"))
|
||||
(assert-equal "no" (cond (not true) "yes" :else "no")))
|
||||
|
||||
(deftest "cond with 2-element predicate and no :else"
|
||||
;; Same bug, but without :else — this is the worst case because the
|
||||
;; bootstrapper heuristic also breaks (all clauses are 2-element lists).
|
||||
(assert-equal "found"
|
||||
(cond (nil? nil) "found"
|
||||
(nil? "x") "other"))
|
||||
(assert-equal "b"
|
||||
(cond (nil? "x") "a"
|
||||
(not false) "b")))
|
||||
|
||||
(deftest "and"
|
||||
(assert-true (and true true))
|
||||
(assert-false (and true false))
|
||||
|
||||
@@ -149,7 +149,27 @@
|
||||
|
||||
(deftest "let in render context"
|
||||
(assert-equal "<p>hello</p>"
|
||||
(render-html "(let ((x \"hello\")) (p x))"))))
|
||||
(render-html "(let ((x \"hello\")) (p x))")))
|
||||
|
||||
(deftest "cond with 2-element predicate test"
|
||||
;; Regression: cond misclassifies (nil? x) as scheme-style clause.
|
||||
(assert-equal "<p>yes</p>"
|
||||
(render-html "(cond (nil? nil) (p \"yes\") :else (p \"no\"))"))
|
||||
(assert-equal "<p>no</p>"
|
||||
(render-html "(cond (nil? \"x\") (p \"yes\") :else (p \"no\"))")))
|
||||
|
||||
(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))))"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -165,3 +185,46 @@
|
||||
(let ((html (render-html "(do (defcomp ~box (&key &rest children) (div :class \"box\" children)) (~box (p \"inside\")))")))
|
||||
(assert-true (string-contains? html "class=\"box\""))
|
||||
(assert-true (string-contains? html "<p>inside</p>")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Map/filter producing multiple children (aser-adjacent regression tests)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "render-map-children"
|
||||
(deftest "map producing multiple children inside tag"
|
||||
(assert-equal "<ul><li>a</li><li>b</li><li>c</li></ul>"
|
||||
(render-html "(do (define items (list \"a\" \"b\" \"c\"))
|
||||
(ul (map (fn (x) (li x)) items)))")))
|
||||
|
||||
(deftest "map with other siblings"
|
||||
(assert-equal "<ul><li>first</li><li>a</li><li>b</li></ul>"
|
||||
(render-html "(do (define items (list \"a\" \"b\"))
|
||||
(ul (li \"first\") (map (fn (x) (li x)) items)))")))
|
||||
|
||||
(deftest "filter with nil results inside tag"
|
||||
(assert-equal "<ul><li>a</li><li>c</li></ul>"
|
||||
(render-html "(do (define items (list \"a\" nil \"c\"))
|
||||
(ul (map (fn (x) (li x))
|
||||
(filter (fn (x) (not (nil? x))) items))))")))
|
||||
|
||||
(deftest "nested map inside let"
|
||||
(assert-equal "<div><span>1</span><span>2</span></div>"
|
||||
(render-html "(let ((nums (list 1 2)))
|
||||
(div (map (fn (n) (span n)) nums)))")))
|
||||
|
||||
(deftest "component with &rest receiving mapped results"
|
||||
(let ((html (render-html "(do (defcomp ~list-box (&key &rest children) (div :class \"lb\" children))
|
||||
(define items (list \"x\" \"y\"))
|
||||
(~list-box (map (fn (x) (p x)) items)))")))
|
||||
(assert-true (string-contains? html "class=\"lb\""))
|
||||
(assert-true (string-contains? html "<p>x</p>"))
|
||||
(assert-true (string-contains? html "<p>y</p>"))))
|
||||
|
||||
(deftest "map-indexed renders with index"
|
||||
(assert-equal "<li>0: a</li><li>1: b</li>"
|
||||
(render-html "(map-indexed (fn (i x) (li (str i \": \" x))) (list \"a\" \"b\"))")))
|
||||
|
||||
(deftest "for-each renders each item"
|
||||
(assert-equal "<p>1</p><p>2</p>"
|
||||
(render-html "(for-each (fn (x) (p x)) (list 1 2))"))))
|
||||
|
||||
@@ -31,7 +31,7 @@ import asyncio
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Keyword, Lambda, NIL, Symbol
|
||||
from .evaluator import _eval as _raw_eval, _trampoline
|
||||
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
|
||||
|
||||
def _eval(expr, env):
|
||||
"""Evaluate and unwrap thunks — all resolver.py _eval calls are non-tail."""
|
||||
|
||||
@@ -20,7 +20,7 @@ _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
||||
sys.path.insert(0, _PROJECT)
|
||||
|
||||
from shared.sx.parser import parse_all
|
||||
from shared.sx.evaluator import _eval, _trampoline, _call_lambda
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline, call_lambda as _call_lambda
|
||||
from shared.sx.types import Symbol, Keyword, Lambda, NIL, Component, Island
|
||||
|
||||
# --- Test state ---
|
||||
@@ -127,13 +127,38 @@ def render_html(sx_source):
|
||||
except ImportError:
|
||||
raise RuntimeError("render-to-html not available — sx_ref.py not built")
|
||||
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 = ""
|
||||
for expr in exprs:
|
||||
result += _render_to_html(expr, render_env)
|
||||
return result
|
||||
|
||||
|
||||
# --- Render SX (aser) platform function ---
|
||||
|
||||
def render_sx(sx_source):
|
||||
"""Parse SX source and serialize to SX wire format via the bootstrapped evaluator."""
|
||||
try:
|
||||
from shared.sx.ref.sx_ref import aser as _aser, serialize as _serialize
|
||||
except ImportError:
|
||||
raise RuntimeError("aser not available — sx_ref.py not built")
|
||||
exprs = parse_all(sx_source)
|
||||
# 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 = ""
|
||||
for expr in exprs:
|
||||
val = _aser(expr, render_env)
|
||||
if isinstance(val, str):
|
||||
result += val
|
||||
elif val is None or val is NIL:
|
||||
pass
|
||||
else:
|
||||
result += _serialize(val)
|
||||
return result
|
||||
|
||||
|
||||
# --- Signal platform primitives ---
|
||||
# Implements the signal runtime platform interface for testing signals.sx
|
||||
|
||||
@@ -258,6 +283,7 @@ SPECS = {
|
||||
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
|
||||
"router": {"file": "test-router.sx", "needs": []},
|
||||
"render": {"file": "test-render.sx", "needs": ["render-html"]},
|
||||
"aser": {"file": "test-aser.sx", "needs": ["render-sx"]},
|
||||
"deps": {"file": "test-deps.sx", "needs": []},
|
||||
"engine": {"file": "test-engine.sx", "needs": []},
|
||||
"orchestration": {"file": "test-orchestration.sx", "needs": []},
|
||||
@@ -297,8 +323,9 @@ env = _Env({
|
||||
"make-keyword": make_keyword,
|
||||
"symbol-name": symbol_name,
|
||||
"keyword-name": keyword_name,
|
||||
# Render platform function
|
||||
# Render platform functions
|
||||
"render-html": render_html,
|
||||
"render-sx": render_sx,
|
||||
# Extra primitives needed by spec modules (router.sx, deps.sx)
|
||||
"for-each-indexed": "_deferred", # replaced below
|
||||
"dict-set!": "_deferred",
|
||||
@@ -848,9 +875,9 @@ def main():
|
||||
print(f"# --- {spec_name} ---")
|
||||
eval_file(spec["file"], env)
|
||||
|
||||
# Reset render state after render tests to avoid leaking
|
||||
# Reset render state after render/aser tests to avoid leaking
|
||||
# into subsequent specs (bootstrapped evaluator checks render_active)
|
||||
if spec_name == "render":
|
||||
if spec_name in ("render", "aser"):
|
||||
try:
|
||||
from shared.sx.ref.sx_ref import set_render_active_b
|
||||
set_render_active_b(False)
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestJsSxTranslation:
|
||||
|
||||
def _translate(self, sx_source: str) -> str:
|
||||
"""Translate a single SX expression to JS using js.sx."""
|
||||
from shared.sx.evaluator import evaluate
|
||||
from shared.sx.ref.sx_ref import evaluate
|
||||
env = load_js_sx()
|
||||
expr = parse(sx_source)
|
||||
env["_def_expr"] = expr
|
||||
|
||||
@@ -18,7 +18,7 @@ from shared.sx.deps import (
|
||||
|
||||
def make_env(*sx_sources: str) -> dict:
|
||||
"""Parse and evaluate component definitions into an env dict."""
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
|
||||
env: dict = {}
|
||||
for source in sx_sources:
|
||||
exprs = parse_all(source)
|
||||
|
||||
@@ -23,7 +23,7 @@ from shared.sx.deps import (
|
||||
|
||||
def make_env(*sx_sources: str) -> dict:
|
||||
"""Parse and evaluate component definitions into an env dict."""
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
|
||||
env: dict = {}
|
||||
for source in sx_sources:
|
||||
exprs = parse_all(source)
|
||||
|
||||
@@ -20,7 +20,7 @@ from shared.sx.deps import (
|
||||
|
||||
def make_env(*sx_sources: str) -> dict:
|
||||
"""Parse and evaluate component definitions into an env dict."""
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
|
||||
env: dict = {}
|
||||
for source in sx_sources:
|
||||
exprs = parse_all(source)
|
||||
@@ -282,7 +282,7 @@ class TestIoRoutingLogic:
|
||||
"""
|
||||
|
||||
def _eval(self, src, env):
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
|
||||
result = None
|
||||
for expr in parse_all(src):
|
||||
result = _trampoline(_eval(expr, env))
|
||||
|
||||
@@ -156,7 +156,7 @@ class TestDataPageDeps:
|
||||
def test_deps_computed_for_data_page(self):
|
||||
from shared.sx.deps import components_needed
|
||||
from shared.sx.parser import parse_all as pa
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
|
||||
|
||||
# Define a component
|
||||
env = {}
|
||||
@@ -172,7 +172,7 @@ class TestDataPageDeps:
|
||||
def test_deps_transitive_for_data_page(self):
|
||||
from shared.sx.deps import components_needed
|
||||
from shared.sx.parser import parse_all as pa
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
|
||||
|
||||
env = {}
|
||||
source = """
|
||||
@@ -205,7 +205,7 @@ class TestDataPipelineSimulation:
|
||||
|
||||
def test_full_pipeline(self):
|
||||
from shared.sx.parser import parse_all as pa
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
|
||||
|
||||
# 1. Define a component that uses only pure primitives
|
||||
env = {}
|
||||
@@ -236,7 +236,7 @@ class TestDataPipelineSimulation:
|
||||
|
||||
def test_pipeline_with_list_data(self):
|
||||
from shared.sx.parser import parse_all as pa
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
|
||||
|
||||
env = {}
|
||||
for expr in pa('''
|
||||
@@ -262,7 +262,7 @@ class TestDataPipelineSimulation:
|
||||
def test_pipeline_data_isolation(self):
|
||||
"""Different data for the same content produces different results."""
|
||||
from shared.sx.parser import parse_all as pa
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
|
||||
|
||||
env = {}
|
||||
for expr in pa('(defcomp ~page (&key title count) (str title ": " count))'):
|
||||
@@ -298,7 +298,7 @@ class TestDataCache:
|
||||
def _make_env(self, current_time_ms=1000):
|
||||
"""Create an env with cache functions and a controllable now-ms."""
|
||||
from shared.sx.parser import parse_all as pa
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
|
||||
|
||||
env = {}
|
||||
# Mock now-ms as a callable that returns current_time_ms
|
||||
@@ -344,7 +344,7 @@ class TestDataCache:
|
||||
|
||||
def _eval(self, src, env):
|
||||
from shared.sx.parser import parse_all as pa
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
|
||||
result = None
|
||||
for expr in pa(src):
|
||||
result = _trampoline(_eval(expr, env))
|
||||
|
||||
@@ -18,7 +18,7 @@ from shared.sx.types import Symbol, Keyword, Lambda, Component, Macro, NIL
|
||||
|
||||
def hw_eval(text, env=None):
|
||||
"""Evaluate via hand-written evaluator.py."""
|
||||
from shared.sx.evaluator import evaluate as _evaluate, EvalError
|
||||
from shared.sx.ref.sx_ref import evaluate as _evaluate
|
||||
if env is None:
|
||||
env = {}
|
||||
return _evaluate(parse(text), env)
|
||||
@@ -50,7 +50,7 @@ def ref_render(text, env=None):
|
||||
|
||||
def hw_eval_multi(text, env=None):
|
||||
"""Evaluate multiple expressions (e.g. defines then call)."""
|
||||
from shared.sx.evaluator import evaluate as _evaluate
|
||||
from shared.sx.ref.sx_ref import evaluate as _evaluate
|
||||
if env is None:
|
||||
env = {}
|
||||
result = None
|
||||
@@ -736,7 +736,7 @@ class TestParityDeps:
|
||||
|
||||
class TestParityErrors:
|
||||
def test_undefined_symbol(self):
|
||||
from shared.sx.evaluator import EvalError as HwError
|
||||
from shared.sx.types import EvalError as HwError
|
||||
from shared.sx.ref.sx_ref import EvalError as RefError
|
||||
with pytest.raises(HwError):
|
||||
hw_eval("undefined_var")
|
||||
|
||||
@@ -12,7 +12,7 @@ import pytest
|
||||
|
||||
from shared.sx.parser import parse, parse_all
|
||||
from shared.sx.html import render as py_render
|
||||
from shared.sx.evaluator import evaluate
|
||||
from shared.sx.ref.sx_ref import evaluate
|
||||
|
||||
SX_JS = Path(__file__).resolve().parents[2] / "static" / "scripts" / "sx.js"
|
||||
SX_TEST_JS = Path(__file__).resolve().parents[2] / "static" / "scripts" / "sx-test.js"
|
||||
|
||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from shared.sx.parser import parse_all
|
||||
from shared.sx.evaluator import _eval, _trampoline
|
||||
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
|
||||
|
||||
|
||||
_PREAMBLE = '''(define assert-equal (fn (expected actual) (assert (equal? expected actual) (str "Expected " (str expected) " but got " (str actual)))))
|
||||
|
||||
@@ -376,6 +376,15 @@ class _ShiftSignal(BaseException):
|
||||
self.env = env
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EvalError
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class EvalError(Exception):
|
||||
"""Error during expression evaluation."""
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Type alias
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user