Add TCO for parser loops in JS bootstrapper, enable SX_USE_REF

The JS parser transpiled from parser.sx used tail-recursive functions
(readStrLoop, skipWs, readListLoop, etc.) which overflow the stack on
large inputs — the bootstrapper page highlights 100KB of Python and
143KB of JavaScript, producing 7620 spans in a 907KB response.

The bootstrapper now detects zero-arg self-tail-recursive functions and
emits them as while(true) loops with continue instead of recursive
calls. Tested with 150K char strings and 8000 sibling elements.

Also enables SX_USE_REF=1 in dev via x-dev-env anchor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 23:09:02 +00:00
parent 38f1f82988
commit 54adc9c216
4 changed files with 455 additions and 2238 deletions

View File

@@ -10,6 +10,7 @@
x-dev-env: &dev-env
RELOAD: "true"
WORKERS: "1"
SX_USE_REF: "1"
x-sibling-models: &sibling-models
# Every app needs all sibling __init__.py + models/ for cross-domain SQLAlchemy imports

View File

@@ -508,6 +508,25 @@
return NIL;
}
// =========================================================================
// Platform interface — Parser
// =========================================================================
// Character classification derived from the grammar:
// ident-start → [a-zA-Z_~*+\-><=/!?&]
// ident-char → ident-start + [0-9.:\/\[\]#,]
var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/;
var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]/;
function isIdentStart(ch) { return _identStartRe.test(ch); }
function isIdentChar(ch) { return _identCharRe.test(ch); }
function parseNumber(s) { return Number(s); }
function escapeString(s) {
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
}
function sxExprSource(e) { return typeof e === "string" ? e : String(e); }
// === Transpiled from eval ===
// trampoline
@@ -528,10 +547,10 @@
var args = rest(expr);
return (isSxTruthy(!sxOr((typeOf(head) == "symbol"), (typeOf(head) == "lambda"), (typeOf(head) == "list"))) ? map(function(x) { return trampoline(evalExpr(x, env)); }, expr) : (isSxTruthy((typeOf(head) == "symbol")) ? (function() {
var name = symbolName(head);
return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defkeyframes")) ? sfDefkeyframes(args, env) : (isSxTruthy((name == "defhandler")) ? sfDefine(args, env) : (isSxTruthy((name == "begin")) ? sfBegin(args, env) : (isSxTruthy((name == "do")) ? sfBegin(args, env) : (isSxTruthy((name == "quote")) ? sfQuote(args, env) : (isSxTruthy((name == "quasiquote")) ? sfQuasiquote(args, env) : (isSxTruthy((name == "->")) ? sfThreadFirst(args, env) : (isSxTruthy((name == "set!")) ? sfSetBang(args, env) : (isSxTruthy((name == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defkeyframes")) ? sfDefkeyframes(args, env) : (isSxTruthy((name == "defhandler")) ? sfDefhandler(args, env) : (isSxTruthy((name == "defpage")) ? sfDefpage(args, env) : (isSxTruthy((name == "defquery")) ? sfDefquery(args, env) : (isSxTruthy((name == "defaction")) ? sfDefaction(args, env) : (isSxTruthy((name == "begin")) ? sfBegin(args, env) : (isSxTruthy((name == "do")) ? sfBegin(args, env) : (isSxTruthy((name == "quote")) ? sfQuote(args, env) : (isSxTruthy((name == "quasiquote")) ? sfQuasiquote(args, env) : (isSxTruthy((name == "->")) ? sfThreadFirst(args, env) : (isSxTruthy((name == "set!")) ? sfSetBang(args, env) : (isSxTruthy((name == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
var mac = envGet(env, name);
return makeThunk(expandMacro(mac, args, env), env);
})() : (isSxTruthy(isRenderExpr(expr)) ? renderExpr(expr, env) : evalCall(head, args, env))))))))))))))))))))))))))))))));
})() : (isSxTruthy(isRenderExpr(expr)) ? renderExpr(expr, env) : evalCall(head, args, env)))))))))))))))))))))))))))))))))));
})() : evalCall(head, args, env)));
})(); };
@@ -788,25 +807,28 @@
return trampoline(evalExpr(macroBody(mac), local));
})(); };
// call-fn
var callFn = function(f, args, env) { return (isSxTruthy(isLambda(f)) ? trampoline(callLambda(f, args, env)) : (isSxTruthy(isCallable(f)) ? apply(f, args) : error((String("Not callable in HO form: ") + String(inspect(f)))))); };
// ho-map
var hoMap = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return map(function(item) { return trampoline(callLambda(f, [item], env)); }, coll);
return map(function(item) { return callFn(f, [item], env); }, coll);
})(); };
// ho-map-indexed
var hoMapIndexed = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return mapIndexed(function(i, item) { return trampoline(callLambda(f, [i, item], env)); }, coll);
return mapIndexed(function(i, item) { return callFn(f, [i, item], env); }, coll);
})(); };
// ho-filter
var hoFilter = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return filter(function(item) { return trampoline(callLambda(f, [item], env)); }, coll);
return filter(function(item) { return callFn(f, [item], env); }, coll);
})(); };
// ho-reduce
@@ -814,28 +836,28 @@
var f = trampoline(evalExpr(first(args), env));
var init = trampoline(evalExpr(nth(args, 1), env));
var coll = trampoline(evalExpr(nth(args, 2), env));
return reduce(function(acc, item) { return trampoline(callLambda(f, [acc, item], env)); }, init, coll);
return reduce(function(acc, item) { return callFn(f, [acc, item], env); }, init, coll);
})(); };
// ho-some
var hoSome = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return some(function(item) { return trampoline(callLambda(f, [item], env)); }, coll);
return some(function(item) { return callFn(f, [item], env); }, coll);
})(); };
// ho-every
var hoEvery = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return isEvery(function(item) { return trampoline(callLambda(f, [item], env)); }, coll);
return isEvery(function(item) { return callFn(f, [item], env); }, coll);
})(); };
// ho-for-each
var hoForEach = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return forEach(function(item) { return trampoline(callLambda(f, [item], env)); }, coll);
return forEach(function(item) { return callFn(f, [item], env); }, coll);
})(); };
@@ -875,6 +897,117 @@
})(); }, keys(attrs))); };
// === Transpiled from parser ===
// sx-parse
var sxParse = function(source) { return (function() {
var pos = 0;
var lenSrc = len(source);
var skipComment = function() { while(true) { if (isSxTruthy((isSxTruthy((pos < lenSrc)) && !(nth(source, pos) == "\n")))) { pos = (pos + 1);
continue; } else { return NIL; } } };
var skipWs = function() { while(true) { if (isSxTruthy((pos < lenSrc))) { { var ch = nth(source, pos);
if (isSxTruthy(sxOr((ch == " "), (ch == "\t"), (ch == "\n"), (ch == "\\r")))) { pos = (pos + 1);
continue; } else if (isSxTruthy((ch == ";"))) { pos = (pos + 1);
skipComment();
continue; } else { return NIL; } } } else { return NIL; } } };
var readString = function() { pos = (pos + 1);
return (function() {
var buf = "";
var readStrLoop = function() { while(true) { if (isSxTruthy((pos >= lenSrc))) { return error("Unterminated string"); } else { { var ch = nth(source, pos);
if (isSxTruthy((ch == "\""))) { pos = (pos + 1);
return NIL; } else if (isSxTruthy((ch == "\\"))) { pos = (pos + 1);
{ var esc = nth(source, pos);
buf = (String(buf) + String((isSxTruthy((esc == "n")) ? "\n" : (isSxTruthy((esc == "t")) ? "\t" : (isSxTruthy((esc == "r")) ? "\\r" : esc)))));
pos = (pos + 1);
continue; } } else { buf = (String(buf) + String(ch));
pos = (pos + 1);
continue; } } } } };
readStrLoop();
return buf;
})(); };
var readIdent = function() { return (function() {
var start = pos;
var readIdentLoop = function() { while(true) { if (isSxTruthy((isSxTruthy((pos < lenSrc)) && isIdentChar(nth(source, pos))))) { pos = (pos + 1);
continue; } else { return NIL; } } };
readIdentLoop();
return slice(source, start, pos);
})(); };
var readKeyword = function() { pos = (pos + 1);
return makeKeyword(readIdent()); };
var readNumber = function() { return (function() {
var start = pos;
if (isSxTruthy((isSxTruthy((pos < lenSrc)) && (nth(source, pos) == "-")))) {
pos = (pos + 1);
}
var readDigits = function() { while(true) { if (isSxTruthy((isSxTruthy((pos < lenSrc)) && (function() {
var c = nth(source, pos);
return (isSxTruthy((c >= "0")) && (c <= "9"));
})()))) { pos = (pos + 1);
continue; } else { return NIL; } } };
readDigits();
if (isSxTruthy((isSxTruthy((pos < lenSrc)) && (nth(source, pos) == ".")))) {
pos = (pos + 1);
readDigits();
}
if (isSxTruthy((isSxTruthy((pos < lenSrc)) && sxOr((nth(source, pos) == "e"), (nth(source, pos) == "E"))))) {
pos = (pos + 1);
if (isSxTruthy((isSxTruthy((pos < lenSrc)) && sxOr((nth(source, pos) == "+"), (nth(source, pos) == "-"))))) {
pos = (pos + 1);
}
readDigits();
}
return parseNumber(slice(source, start, pos));
})(); };
var readSymbol = function() { return (function() {
var name = readIdent();
return (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : makeSymbol(name))));
})(); };
var readList = function(closeCh) { return (function() {
var items = [];
var readListLoop = function() { while(true) { skipWs();
if (isSxTruthy((pos >= lenSrc))) { return error("Unterminated list"); } else { if (isSxTruthy((nth(source, pos) == closeCh))) { pos = (pos + 1);
return NIL; } else { items.push(readExpr());
continue; } } } };
readListLoop();
return items;
})(); };
var readMap = function() { return (function() {
var result = {};
var readMapLoop = function() { while(true) { skipWs();
if (isSxTruthy((pos >= lenSrc))) { return error("Unterminated map"); } else { if (isSxTruthy((nth(source, pos) == "}"))) { pos = (pos + 1);
return NIL; } else { { var keyExpr = readExpr();
var keyStr = (isSxTruthy((typeOf(keyExpr) == "keyword")) ? keywordName(keyExpr) : (String(keyExpr)));
var valExpr = readExpr();
result[keyStr] = valExpr;
continue; } } } } };
readMapLoop();
return result;
})(); };
var readExpr = function() { skipWs();
return (isSxTruthy((pos >= lenSrc)) ? error("Unexpected end of input") : (function() {
var ch = nth(source, pos);
return (isSxTruthy((ch == "(")) ? ((pos = (pos + 1)), readList(")")) : (isSxTruthy((ch == "[")) ? ((pos = (pos + 1)), readList("]")) : (isSxTruthy((ch == "{")) ? ((pos = (pos + 1)), readMap()) : (isSxTruthy((ch == "\"")) ? readString() : (isSxTruthy((ch == ":")) ? readKeyword() : (isSxTruthy((ch == "`")) ? ((pos = (pos + 1)), [makeSymbol("quasiquote"), readExpr()]) : (isSxTruthy((ch == ",")) ? ((pos = (pos + 1)), (isSxTruthy((isSxTruthy((pos < lenSrc)) && (nth(source, pos) == "@"))) ? ((pos = (pos + 1)), [makeSymbol("splice-unquote"), readExpr()]) : [makeSymbol("unquote"), readExpr()])) : (isSxTruthy(sxOr((isSxTruthy((ch >= "0")) && (ch <= "9")), (isSxTruthy((ch == "-")) && isSxTruthy(((pos + 1) < lenSrc)) && (function() {
var nextCh = nth(source, (pos + 1));
return (isSxTruthy((nextCh >= "0")) && (nextCh <= "9"));
})()))) ? readNumber() : (isSxTruthy(isIdentStart(ch)) ? readSymbol() : error((String("Unexpected character: ") + String(ch))))))))))));
})()); };
return (function() {
var exprs = [];
var parseLoop = function() { while(true) { skipWs();
if (isSxTruthy((pos < lenSrc))) { exprs.push(readExpr());
continue; } else { return NIL; } } };
parseLoop();
return exprs;
})();
})(); };
// sx-serialize
var sxSerialize = function(val) { return (function() { var _m = typeOf(val); if (_m == "nil") return "nil"; if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "number") return (String(val)); if (_m == "string") return (String("\"") + String(escapeString(val)) + String("\"")); if (_m == "symbol") return symbolName(val); if (_m == "keyword") return (String(":") + String(keywordName(val))); if (_m == "list") return (String("(") + String(join(" ", map(sxSerialize, val))) + String(")")); if (_m == "dict") return sxSerializeDict(val); if (_m == "sx-expr") return sxExprSource(val); return (String(val)); })(); };
// sx-serialize-dict
var sxSerializeDict = function(d) { return (String("{") + String(join(" ", reduce(function(acc, key) { return concat(acc, [(String(":") + String(key)), sxSerialize(dictGet(d, key))]); }, [], keys(d)))) + String("}")); };
// === Transpiled from adapter-dom ===
// SVG_NS
@@ -1284,7 +1417,7 @@ return forEach(function(attr) { return (isSxTruthy(!domHasAttr(newEl, first(attr
var _preloadCache = {};
// _css-hash
var _cssHash = "";
var _cssHash = NIL;
// dispatch-trigger-events
var dispatchTriggerEvents = function(el, headerVal) { return (isSxTruthy(headerVal) ? (function() {
@@ -1747,7 +1880,7 @@ return (_styleCache = {}); };
return (isSxTruthy(decls) ? ((isSxTruthy(startsWith(base, "animate-")) ? (function() {
var kfName = slice(base, 8);
return (isSxTruthy(dictHas(_styleKeyframes, kfName)) ? append_b(kfNeeded, [kfName, dictGet(_styleKeyframes, kfName)]) : NIL);
})() : NIL), (isSxTruthy(isNil(variant)) ? append_b(baseDecls, decls) : (isSxTruthy(dictHas(_responsiveBreakpoints, variant)) ? append_b(mediaRules, [dictGet(_responsiveBreakpoints, variant), decls]) : (isSxTruthy(dictHas(_pseudoVariants, variant)) ? append_b(pseudoRules, [dictGet(_pseudoVariants, variant), decls]) : (function() {
})() : NIL), (isSxTruthy(isNil(variant)) ? (isSxTruthy(isChildSelectorAtom(base)) ? append_b(pseudoRules, [">:not(:first-child)", decls]) : append_b(baseDecls, decls)) : (isSxTruthy(dictHas(_responsiveBreakpoints, variant)) ? append_b(mediaRules, [dictGet(_responsiveBreakpoints, variant), decls]) : (isSxTruthy(dictHas(_pseudoVariants, variant)) ? append_b(pseudoRules, [dictGet(_pseudoVariants, variant), decls]) : (function() {
var vparts = split(variant, ":");
var mediaPart = NIL;
var pseudoPart = NIL;
@@ -1766,12 +1899,12 @@ return (_styleCache = {}); };
} } }
return (function() {
var hashInput = join(";", baseDecls);
{ var _c = chunkEvery(mediaRules, 2); for (var _i = 0; _i < _c.length; _i++) { var mr = _c[_i]; hashInput = (String(hashInput) + String("@") + String(first(mr)) + String("{") + String(nth(mr, 1)) + String("}")); } }
{ var _c = chunkEvery(pseudoRules, 2); for (var _i = 0; _i < _c.length; _i++) { var pr = _c[_i]; hashInput = (String(hashInput) + String(first(pr)) + String("{") + String(nth(pr, 1)) + String("}")); } }
{ var _c = chunkEvery(kfNeeded, 2); for (var _i = 0; _i < _c.length; _i++) { var kf = _c[_i]; hashInput = (String(hashInput) + String(nth(kf, 1))); } }
{ var _c = mediaRules; for (var _i = 0; _i < _c.length; _i++) { var mr = _c[_i]; hashInput = (String(hashInput) + String("@") + String(first(mr)) + String("{") + String(nth(mr, 1)) + String("}")); } }
{ var _c = pseudoRules; for (var _i = 0; _i < _c.length; _i++) { var pr = _c[_i]; hashInput = (String(hashInput) + String(first(pr)) + String("{") + String(nth(pr, 1)) + String("}")); } }
{ var _c = kfNeeded; for (var _i = 0; _i < _c.length; _i++) { var kf = _c[_i]; hashInput = (String(hashInput) + String(nth(kf, 1))); } }
return (function() {
var cn = (String("sx-") + String(hashStyle(hashInput)));
var sv = makeStyleValue_(cn, join(";", baseDecls), chunkEvery(mediaRules, 2), chunkEvery(pseudoRules, 2), chunkEvery(kfNeeded, 2));
var sv = makeStyleValue_(cn, join(";", baseDecls), mediaRules, pseudoRules, kfNeeded);
_styleCache[key] = sv;
injectStyleValue(sv, atoms);
return sv;
@@ -2740,18 +2873,11 @@ callExpr.push(dictGet(kwargs, k)); } }
if (!cssTarget) return;
var rules = [];
// Child-selector atoms are now routed to pseudoRules by the resolver
// with selector ">:not(:first-child)", so base declarations are always
// applied directly to the class.
if (sv.declarations) {
var hasChild = false;
if (atoms) {
for (var ai = 0; ai < atoms.length; ai++) {
if (isChildSelectorAtom(atoms[ai])) { hasChild = true; break; }
}
}
if (hasChild) {
rules.push("." + sv.className + ">:not(:first-child){" + sv.declarations + "}");
} else {
rules.push("." + sv.className + "{" + sv.declarations + "}");
}
rules.push("." + sv.className + "{" + sv.declarations + "}");
}
for (var pi = 0; pi < sv.pseudoRules.length; pi++) {
var sel = sv.pseudoRules[pi][0], decls = sv.pseudoRules[pi][1];
@@ -2952,107 +3078,8 @@ callExpr.push(dictGet(kwargs, k)); } }
// Expose render functions as primitives so SX code can call them
if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;
// =========================================================================
// Parser
// =========================================================================
function parse(text) {
var pos = 0;
function skipWs() {
while (pos < text.length) {
var ch = text[pos];
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") { pos++; continue; }
if (ch === ";") { while (pos < text.length && text[pos] !== "\n") pos++; continue; }
break;
}
}
function readExpr() {
skipWs();
if (pos >= text.length) return undefined;
var ch = text[pos];
if (ch === "(") { pos++; return readList(")"); }
if (ch === "[") { pos++; return readList("]"); }
if (ch === "{") { pos++; return readMap(); }
if (ch === '"') return readString();
if (ch === ":") return readKeyword();
if (ch === "`") { pos++; return [new Symbol("quasiquote"), readExpr()]; }
if (ch === ",") {
pos++;
if (pos < text.length && text[pos] === "@") { pos++; return [new Symbol("splice-unquote"), readExpr()]; }
return [new Symbol("unquote"), readExpr()];
}
if (ch === "-" && pos + 1 < text.length && text[pos + 1] >= "0" && text[pos + 1] <= "9") return readNumber();
if (ch >= "0" && ch <= "9") return readNumber();
return readSymbol();
}
function readList(close) {
var items = [];
while (true) {
skipWs();
if (pos >= text.length) throw new Error("Unterminated list");
if (text[pos] === close) { pos++; return items; }
items.push(readExpr());
}
}
function readMap() {
var result = {};
while (true) {
skipWs();
if (pos >= text.length) throw new Error("Unterminated map");
if (text[pos] === "}") { pos++; return result; }
var key = readExpr();
var keyStr = (key && key._kw) ? key.name : String(key);
result[keyStr] = readExpr();
}
}
function readString() {
pos++; // skip "
var s = "";
while (pos < text.length) {
var ch = text[pos];
if (ch === '"') { pos++; return s; }
if (ch === "\\") { pos++; var esc = text[pos]; s += esc === "n" ? "\n" : esc === "t" ? "\t" : esc === "r" ? "\r" : esc; pos++; continue; }
s += ch; pos++;
}
throw new Error("Unterminated string");
}
function readKeyword() {
pos++; // skip :
var name = readIdent();
return new Keyword(name);
}
function readNumber() {
var start = pos;
if (text[pos] === "-") pos++;
while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++;
if (pos < text.length && text[pos] === ".") { pos++; while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++; }
if (pos < text.length && (text[pos] === "e" || text[pos] === "E")) {
pos++;
if (pos < text.length && (text[pos] === "+" || text[pos] === "-")) pos++;
while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++;
}
return Number(text.slice(start, pos));
}
function readIdent() {
var start = pos;
while (pos < text.length && /[a-zA-Z0-9_~*+\-><=/!?.:&]/.test(text[pos])) pos++;
return text.slice(start, pos);
}
function readSymbol() {
var name = readIdent();
if (name === "true") return true;
if (name === "false") return false;
if (name === "nil") return NIL;
return new Symbol(name);
}
var exprs = [];
while (true) {
skipWs();
if (pos >= text.length) break;
exprs.push(readExpr());
}
return exprs;
}
// Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes)
var parse = sxParse;
// =========================================================================
// Public API
@@ -3104,7 +3131,7 @@ callExpr.push(dictGet(kwargs, k)); } }
renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,
getEnv: function() { return componentEnv; },
init: typeof bootInit === "function" ? bootInit : null,
_version: "ref-2.0 (boot+cssx+dom+engine+orchestration, bootstrap-compiled)"
_version: "ref-2.0 (boot+cssx+dom+engine+orchestration+parser, bootstrap-compiled)"
};

File diff suppressed because it is too large Load Diff

View File

@@ -717,9 +717,211 @@ class JSEmitter:
def _emit_define(self, expr) -> str:
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
val = self.emit(expr[2])
# Detect zero-arg self-tail-recursive functions and emit as while loops
fn_expr = expr[2] if len(expr) > 2 else None
if (fn_expr and isinstance(fn_expr, list) and fn_expr
and isinstance(fn_expr[0], Symbol) and fn_expr[0].name in ("fn", "lambda")
and isinstance(fn_expr[1], list) and len(fn_expr[1]) == 0
and self._is_self_tail_recursive(name, fn_expr[2:])):
body = fn_expr[2:]
loop_body = self._emit_loop_body(name, body)
return f"var {self._mangle(name)} = function() {{ while(true) {{ {loop_body} }} }};"
val = self.emit(fn_expr) if fn_expr else "NIL"
return f"var {self._mangle(name)} = {val};"
def _is_self_tail_recursive(self, name: str, body: list) -> bool:
"""Check if a function body contains tail calls to itself."""
if not body:
return False
last = body[-1]
return self._has_tail_call(name, last)
def _has_tail_call(self, name: str, expr) -> bool:
"""Check if expr has a tail call to name in any branch."""
if not isinstance(expr, list) or not expr:
return False
head = expr[0]
if not isinstance(head, Symbol):
return False
h = head.name
# Direct tail call
if h == name:
return True
# Branching forms — check if any branch tail-calls
if h == "if":
return (self._has_tail_call(name, expr[2])
or (len(expr) > 3 and self._has_tail_call(name, expr[3])))
if h == "when":
return any(self._has_tail_call(name, e) for e in expr[2:])
if h == "cond":
for clause in expr[1:]:
if isinstance(clause, list) and len(clause) == 2:
if self._has_tail_call(name, clause[1]):
return True
elif isinstance(clause, Keyword):
continue
elif isinstance(clause, list):
if self._has_tail_call(name, clause):
return True
else:
if self._has_tail_call(name, clause):
return True
return False
if h in ("do", "begin"):
return self._has_tail_call(name, expr[-1]) if len(expr) > 1 else False
if h == "let" or h == "let*":
return self._has_tail_call(name, expr[-1]) if len(expr) > 2 else False
return False
def _emit_loop_body(self, name: str, body: list) -> str:
"""Emit a function body as while-loop statements.
Replaces tail-self-calls with `continue` and non-recursive exits with
`return`.
"""
if not body:
return "return NIL;"
# Emit side-effect statements first, then the tail expression as loop logic
parts = []
for b in body[:-1]:
parts.append(self.emit_statement(b))
parts.append(self._emit_tail_as_stmt(name, body[-1]))
return "\n".join(parts)
def _emit_tail_as_stmt(self, name: str, expr) -> str:
"""Emit an expression in tail position as loop statements.
Tail-self-calls → continue; other exits → return expr;
"""
if not isinstance(expr, list) or not expr:
return f"return {self.emit(expr)};"
head = expr[0]
if not isinstance(head, Symbol):
return f"return {self.emit(expr)};"
h = head.name
# Direct tail call to self → continue
if h == name:
return "continue;"
# (do stmt1 stmt2 ... tail) → emit stmts then recurse on tail
if h in ("do", "begin"):
stmts = []
for e in expr[1:-1]:
stmts.append(self.emit_statement(e))
stmts.append(self._emit_tail_as_stmt(name, expr[-1]))
return "\n".join(stmts)
# (if cond then else) → if/else with tail handling in each branch
if h == "if":
cond = self.emit(expr[1])
then_branch = self._emit_tail_as_stmt(name, expr[2])
else_branch = self._emit_tail_as_stmt(name, expr[3]) if len(expr) > 3 else "return NIL;"
return f"if (isSxTruthy({cond})) {{ {then_branch} }} else {{ {else_branch} }}"
# (when cond body...) → if (cond) { body... } else { return NIL; }
if h == "when":
cond = self.emit(expr[1])
body_parts = expr[2:]
if not body_parts:
return f"if (isSxTruthy({cond})) {{}} else {{ return NIL; }}"
stmts = []
for e in body_parts[:-1]:
stmts.append(self.emit_statement(e))
stmts.append(self._emit_tail_as_stmt(name, body_parts[-1]))
inner = "\n".join(stmts)
return f"if (isSxTruthy({cond})) {{ {inner} }} else {{ return NIL; }}"
# (cond clause1 clause2 ...) → if/else if/else chain
if h == "cond":
return self._emit_cond_as_loop_stmt(name, expr[1:])
# (let ((bindings)) body...) → { var ...; tail }
if h in ("let", "let*"):
bindings = expr[1]
body = expr[2:]
parts = []
if isinstance(bindings, list):
if bindings and isinstance(bindings[0], list):
for b in bindings:
vname = b[0].name if isinstance(b[0], Symbol) else str(b[0])
parts.append(f"var {self._mangle(vname)} = {self.emit(b[1])};")
else:
for i in range(0, len(bindings), 2):
vname = bindings[i].name if isinstance(bindings[i], Symbol) else str(bindings[i])
parts.append(f"var {self._mangle(vname)} = {self.emit(bindings[i + 1])};")
for b_expr in body[:-1]:
parts.append(self.emit_statement(b_expr))
parts.append(self._emit_tail_as_stmt(name, body[-1]))
inner = "\n".join(parts)
return f"{{ {inner} }}"
# Not a tail call to self — regular return
return f"return {self.emit(expr)};"
def _emit_cond_as_loop_stmt(self, name: str, clauses) -> str:
"""Emit cond clauses as if/else if/else for loop body."""
if not clauses:
return "return NIL;"
# Detect style: Scheme vs Clojure (same as _emit_cond)
is_scheme = (
all(isinstance(c, list) and len(c) == 2 for c in clauses)
and not any(isinstance(c, Keyword) for c in clauses)
)
if is_scheme:
return self._cond_scheme_loop(name, clauses)
return self._cond_clojure_loop(name, clauses)
def _cond_scheme_loop(self, name: str, clauses) -> str:
parts = []
for i, clause in enumerate(clauses):
cond_expr = clause[0]
body_expr = clause[1]
# Check for :else / else
is_else = (isinstance(cond_expr, Keyword) and cond_expr.name == "else") or \
(isinstance(cond_expr, Symbol) and cond_expr.name == "else") or \
(isinstance(cond_expr, bool) and cond_expr is True)
if is_else:
parts.append(f"{{ {self._emit_tail_as_stmt(name, body_expr)} }}")
break
prefix = "if" if i == 0 else "else if"
cond = self.emit(cond_expr)
body = self._emit_tail_as_stmt(name, body_expr)
parts.append(f"{prefix} (isSxTruthy({cond})) {{ {body} }}")
else:
parts.append("else { return NIL; }")
return " ".join(parts)
def _cond_clojure_loop(self, name: str, clauses) -> str:
parts = []
i = 0
clause_idx = 0
has_else = False
while i < len(clauses):
c = clauses[i]
if isinstance(c, Keyword) and c.name == "else":
if i + 1 < len(clauses):
parts.append(f"else {{ {self._emit_tail_as_stmt(name, clauses[i + 1])} }}")
has_else = True
break
if i + 1 < len(clauses):
prefix = "if" if clause_idx == 0 else "else if"
cond = self.emit(c)
body = self._emit_tail_as_stmt(name, clauses[i + 1])
parts.append(f"{prefix} (isSxTruthy({cond})) {{ {body} }}")
i += 2
else:
parts.append(f"else {{ {self._emit_tail_as_stmt(name, c)} }}")
has_else = True
i += 1
clause_idx += 1
if not has_else:
parts.append("else { return NIL; }")
return " ".join(parts)
def _emit_for_each_stmt(self, expr) -> str:
fn_expr = expr[1]
coll_expr = expr[2]
@@ -774,6 +976,7 @@ def extract_defines(source: str) -> list[tuple[str, list]]:
ADAPTER_FILES = {
"parser": ("parser.sx", "parser"),
"html": ("adapter-html.sx", "adapter-html"),
"sx": ("adapter-sx.sx", "adapter-sx"),
"dom": ("adapter-dom.sx", "adapter-dom"),
@@ -788,7 +991,8 @@ ADAPTER_DEPS = {
"engine": ["dom"],
"orchestration": ["engine", "dom"],
"cssx": [],
"boot": ["dom", "engine", "orchestration", "cssx"],
"boot": ["dom", "engine", "orchestration", "cssx", "parser"],
"parser": [],
}
@@ -805,6 +1009,7 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
# Platform JS blocks keyed by adapter name
adapter_platform = {
"parser": PLATFORM_PARSER_JS,
"dom": PLATFORM_DOM_JS,
"engine": PLATFORM_ENGINE_PURE_JS,
"orchestration": PLATFORM_ORCHESTRATION_JS,
@@ -830,7 +1035,7 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
("eval.sx", "eval"),
("render.sx", "render (core)"),
]
for name in ("html", "sx", "dom", "engine", "orchestration", "cssx", "boot"):
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "cssx", "boot"):
if name in adapter_set:
sx_files.append(ADAPTER_FILES[name])
@@ -852,11 +1057,17 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
has_orch = "orchestration" in adapter_set
has_cssx = "cssx" in adapter_set
has_boot = "boot" in adapter_set
has_parser = "parser" in adapter_set
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
parts = []
parts.append(PREAMBLE)
parts.append(PLATFORM_JS)
# Parser platform must come before compiled parser.sx
if has_parser:
parts.append(adapter_platform["parser"])
for label, defines in all_sections:
parts.append(f"\n // === Transpiled from {label} ===\n")
for name, expr in defines:
@@ -872,7 +1083,7 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
parts.append(adapter_platform[name])
parts.append(fixups_js(has_html, has_sx, has_dom))
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, adapter_label))
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label))
parts.append(EPILOGUE)
return "\n".join(parts)
@@ -1393,6 +1604,26 @@ PLATFORM_JS = '''
return NIL;
}'''
PLATFORM_PARSER_JS = r"""
// =========================================================================
// Platform interface — Parser
// =========================================================================
// Character classification derived from the grammar:
// ident-start → [a-zA-Z_~*+\-><=/!?&]
// ident-char → ident-start + [0-9.:\/\[\]#,]
var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/;
var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]/;
function isIdentStart(ch) { return _identStartRe.test(ch); }
function isIdentChar(ch) { return _identCharRe.test(ch); }
function parseNumber(s) { return Number(s); }
function escapeString(s) {
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
}
function sxExprSource(e) { return typeof e === "string" ? e : String(e); }
"""
PLATFORM_DOM_JS = """
// =========================================================================
// Platform interface — DOM adapter (browser-only)
@@ -2203,18 +2434,11 @@ PLATFORM_CSSX_JS = """
if (!cssTarget) return;
var rules = [];
// Child-selector atoms are now routed to pseudoRules by the resolver
// with selector ">:not(:first-child)", so base declarations are always
// applied directly to the class.
if (sv.declarations) {
var hasChild = false;
if (atoms) {
for (var ai = 0; ai < atoms.length; ai++) {
if (isChildSelectorAtom(atoms[ai])) { hasChild = true; break; }
}
}
if (hasChild) {
rules.push("." + sv.className + ">:not(:first-child){" + sv.declarations + "}");
} else {
rules.push("." + sv.className + "{" + sv.declarations + "}");
}
rules.push("." + sv.className + "{" + sv.declarations + "}");
}
for (var pi = 0; pi < sv.pseudoRules.length; pi++) {
var sel = sv.pseudoRules[pi][0], decls = sv.pseudoRules[pi][1];
@@ -2426,109 +2650,17 @@ def fixups_js(has_html, has_sx, has_dom):
return "\n".join(lines)
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, adapter_label):
# Parser is always included
parser = r'''
// =========================================================================
// Parser
// =========================================================================
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label):
# Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
if has_parser:
parser = '''
// Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes)
var parse = sxParse;'''
else:
parser = r'''
// Minimal fallback parser (no parser adapter)
function parse(text) {
var pos = 0;
function skipWs() {
while (pos < text.length) {
var ch = text[pos];
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") { pos++; continue; }
if (ch === ";") { while (pos < text.length && text[pos] !== "\n") pos++; continue; }
break;
}
}
function readExpr() {
skipWs();
if (pos >= text.length) return undefined;
var ch = text[pos];
if (ch === "(") { pos++; return readList(")"); }
if (ch === "[") { pos++; return readList("]"); }
if (ch === "{") { pos++; return readMap(); }
if (ch === '"') return readString();
if (ch === ":") return readKeyword();
if (ch === "`") { pos++; return [new Symbol("quasiquote"), readExpr()]; }
if (ch === ",") {
pos++;
if (pos < text.length && text[pos] === "@") { pos++; return [new Symbol("splice-unquote"), readExpr()]; }
return [new Symbol("unquote"), readExpr()];
}
if (ch === "-" && pos + 1 < text.length && text[pos + 1] >= "0" && text[pos + 1] <= "9") return readNumber();
if (ch >= "0" && ch <= "9") return readNumber();
return readSymbol();
}
function readList(close) {
var items = [];
while (true) {
skipWs();
if (pos >= text.length) throw new Error("Unterminated list");
if (text[pos] === close) { pos++; return items; }
items.push(readExpr());
}
}
function readMap() {
var result = {};
while (true) {
skipWs();
if (pos >= text.length) throw new Error("Unterminated map");
if (text[pos] === "}") { pos++; return result; }
var key = readExpr();
var keyStr = (key && key._kw) ? key.name : String(key);
result[keyStr] = readExpr();
}
}
function readString() {
pos++; // skip "
var s = "";
while (pos < text.length) {
var ch = text[pos];
if (ch === '"') { pos++; return s; }
if (ch === "\\") { pos++; var esc = text[pos]; s += esc === "n" ? "\n" : esc === "t" ? "\t" : esc === "r" ? "\r" : esc; pos++; continue; }
s += ch; pos++;
}
throw new Error("Unterminated string");
}
function readKeyword() {
pos++; // skip :
var name = readIdent();
return new Keyword(name);
}
function readNumber() {
var start = pos;
if (text[pos] === "-") pos++;
while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++;
if (pos < text.length && text[pos] === ".") { pos++; while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++; }
if (pos < text.length && (text[pos] === "e" || text[pos] === "E")) {
pos++;
if (pos < text.length && (text[pos] === "+" || text[pos] === "-")) pos++;
while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++;
}
return Number(text.slice(start, pos));
}
function readIdent() {
var start = pos;
while (pos < text.length && /[a-zA-Z0-9_~*+\-><=/!?.:&]/.test(text[pos])) pos++;
return text.slice(start, pos);
}
function readSymbol() {
var name = readIdent();
if (name === "true") return true;
if (name === "false") return false;
if (name === "nil") return NIL;
return new Symbol(name);
}
var exprs = [];
while (true) {
skipWs();
if (pos >= text.length) break;
exprs.push(readExpr());
}
return exprs;
throw new Error("Parser adapter not included — cannot parse SX source at runtime");
}'''
# Public API — conditional on adapters