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:
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user