Add TCO trampolining to async evaluator and sx.js client

Both evaluators now use thunk-based trampolining to eliminate stack
overflow on deep tail recursion (verified at 50K+ depth). Mirrors
the sync evaluator TCO added in 5069072.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 10:53:16 +00:00
parent da8d2e342f
commit e72f7485f4
2 changed files with 126 additions and 84 deletions

View File

@@ -54,6 +54,11 @@
}
Macro.prototype._macro = true;
/** Thunk — deferred evaluation for tail-call optimization. */
function _Thunk(expr, env) { this.expr = expr; this.env = env; }
_Thunk.prototype._thunk = true;
function isThunk(x) { return x && x._thunk; }
/** Marker for pre-rendered HTML that bypasses escaping. */
function RawHTML(html) { this.html = html; }
RawHTML.prototype._raw = true;
@@ -338,7 +343,17 @@
// --- Evaluator ---
function sxEval(expr, env) {
/** Unwrap thunks by re-entering the evaluator until we get an actual value. */
function trampoline(val) {
while (isThunk(val)) val = _sxEval(val.expr, val.env);
return val;
}
/** Public evaluator — trampolines thunks from tail positions. */
function sxEval(expr, env) { return trampoline(_sxEval(expr, env)); }
/** Internal evaluator — may return _Thunk for tail positions. */
function _sxEval(expr, env) {
// Literals
if (typeof expr === "number" || typeof expr === "string" || typeof expr === "boolean") return expr;
if (isNil(expr)) return NIL;
@@ -387,7 +402,7 @@
var macroVal = env[head.name];
if (isMacro(macroVal)) {
var expanded = expandMacro(macroVal, expr.slice(1), env);
return sxEval(expanded, env);
return new _Thunk(expanded, env);
}
}
}
@@ -409,7 +424,7 @@
}
var local = merge({}, fn.closure, callerEnv);
for (var i = 0; i < fn.params.length; i++) local[fn.params[i]] = args[i];
return sxEval(fn.body, local);
return new _Thunk(fn.body, local);
}
function callComponent(comp, rawArgs, env) {
@@ -430,7 +445,7 @@
local[p] = (p in kwargs) ? kwargs[p] : NIL;
}
if (comp.hasChildren) local["children"] = children;
return sxEval(comp.body, local);
return new _Thunk(comp.body, local);
}
// --- Shared helpers for special/render forms ---
@@ -501,20 +516,19 @@
SPECIAL_FORMS["if"] = function (expr, env) {
var cond = sxEval(expr[1], env);
if (isSxTruthy(cond)) return sxEval(expr[2], env);
return expr.length > 3 ? sxEval(expr[3], env) : NIL;
if (isSxTruthy(cond)) return new _Thunk(expr[2], env);
return expr.length > 3 ? new _Thunk(expr[3], env) : NIL;
};
SPECIAL_FORMS["when"] = function (expr, env) {
if (!isSxTruthy(sxEval(expr[1], env))) return NIL;
var result = NIL;
for (var i = 2; i < expr.length; i++) result = sxEval(expr[i], env);
return result;
for (var i = 2; i < expr.length - 1; i++) sxEval(expr[i], env);
return new _Thunk(expr[expr.length - 1], env);
};
SPECIAL_FORMS["cond"] = function (expr, env) {
var branch = _evalCond(expr.slice(1), env);
return branch ? sxEval(branch, env) : NIL;
return branch ? new _Thunk(branch, env) : NIL;
};
SPECIAL_FORMS["case"] = function (expr, env) {
@@ -522,8 +536,8 @@
for (var i = 2; i < expr.length - 1; i += 2) {
var t = expr[i];
if ((isKw(t) && t.name === "else") || (isSym(t) && (t.name === ":else" || t.name === "else")))
return sxEval(expr[i + 1], env);
if (val == sxEval(t, env)) return sxEval(expr[i + 1], env);
return new _Thunk(expr[i + 1], env);
if (val == sxEval(t, env)) return new _Thunk(expr[i + 1], env);
}
return NIL;
};
@@ -548,9 +562,8 @@
SPECIAL_FORMS["let"] = SPECIAL_FORMS["let*"] = function (expr, env) {
var local = _processBindings(expr[1], env);
var result = NIL;
for (var k = 2; k < expr.length; k++) result = sxEval(expr[k], local);
return result;
for (var k = 2; k < expr.length - 1; k++) sxEval(expr[k], local);
return expr.length > 2 ? new _Thunk(expr[expr.length - 1], local) : NIL;
};
SPECIAL_FORMS["lambda"] = SPECIAL_FORMS["fn"] = function (expr, env) {
@@ -590,9 +603,8 @@
};
SPECIAL_FORMS["begin"] = SPECIAL_FORMS["do"] = function (expr, env) {
var result = NIL;
for (var i = 1; i < expr.length; i++) result = sxEval(expr[i], env);
return result;
for (var i = 1; i < expr.length - 1; i++) sxEval(expr[i], env);
return expr.length > 1 ? new _Thunk(expr[expr.length - 1], env) : NIL;
};
SPECIAL_FORMS["quote"] = function (expr) { return expr[1]; };
@@ -617,7 +629,7 @@
args = [result];
}
if (typeof fn === "function") result = fn.apply(null, args);
else if (isLambda(fn)) result = callLambda(fn, args, env);
else if (isLambda(fn)) result = trampoline(callLambda(fn, args, env));
else throw new Error("-> form not callable: " + fn);
}
return result;
@@ -687,32 +699,32 @@
HO_FORMS["map"] = function (expr, env) {
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
return coll.map(function (item) { return isLambda(fn) ? callLambda(fn, [item], env) : fn(item); });
return coll.map(function (item) { return isLambda(fn) ? trampoline(callLambda(fn, [item], env)) : fn(item); });
};
HO_FORMS["map-indexed"] = function (expr, env) {
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
return coll.map(function (item, i) { return isLambda(fn) ? callLambda(fn, [i, item], env) : fn(i, item); });
return coll.map(function (item, i) { return isLambda(fn) ? trampoline(callLambda(fn, [i, item], env)) : fn(i, item); });
};
HO_FORMS["filter"] = function (expr, env) {
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
return coll.filter(function (item) {
var r = isLambda(fn) ? callLambda(fn, [item], env) : fn(item);
var r = isLambda(fn) ? trampoline(callLambda(fn, [item], env)) : fn(item);
return isSxTruthy(r);
});
};
HO_FORMS["reduce"] = function (expr, env) {
var fn = sxEval(expr[1], env), acc = sxEval(expr[2], env), coll = sxEval(expr[3], env);
for (var i = 0; i < coll.length; i++) acc = isLambda(fn) ? callLambda(fn, [acc, coll[i]], env) : fn(acc, coll[i]);
for (var i = 0; i < coll.length; i++) acc = isLambda(fn) ? trampoline(callLambda(fn, [acc, coll[i]], env)) : fn(acc, coll[i]);
return acc;
};
HO_FORMS["some"] = function (expr, env) {
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
for (var i = 0; i < coll.length; i++) {
var r = isLambda(fn) ? callLambda(fn, [coll[i]], env) : fn(coll[i]);
var r = isLambda(fn) ? trampoline(callLambda(fn, [coll[i]], env)) : fn(coll[i]);
if (isSxTruthy(r)) return r;
}
return NIL;
@@ -721,14 +733,14 @@
HO_FORMS["every?"] = function (expr, env) {
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
for (var i = 0; i < coll.length; i++) {
if (!isSxTruthy(isLambda(fn) ? callLambda(fn, [coll[i]], env) : fn(coll[i]))) return false;
if (!isSxTruthy(isLambda(fn) ? trampoline(callLambda(fn, [coll[i]], env)) : fn(coll[i]))) return false;
}
return true;
};
HO_FORMS["for-each"] = function (expr, env) {
var fn = sxEval(expr[1], env), coll = sxEval(expr[2], env);
for (var i = 0; i < coll.length; i++) isLambda(fn) ? callLambda(fn, [coll[i]], env) : fn(coll[i]);
for (var i = 0; i < coll.length; i++) isLambda(fn) ? trampoline(callLambda(fn, [coll[i]], env)) : fn(coll[i]);
return NIL;
};
@@ -1352,7 +1364,7 @@
_types: { NIL: NIL, Symbol: Symbol, Keyword: Keyword, Lambda: Lambda, Component: Component, RawHTML: RawHTML },
_eval: sxEval,
_expandMacro: expandMacro,
_callLambda: callLambda,
_callLambda: function (fn, args, env) { return trampoline(callLambda(fn, args, env)); },
_renderDOM: renderDOM,
};