diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index f3d7cae..9d00e02 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-10T20:59:39Z"; + var SX_VERSION = "2026-03-11T03:49:41Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -233,10 +233,17 @@ // Render dispatch — call the active adapter's render function. // Set by each adapter when loaded; defaults to identity (no rendering). var _renderExprFn = null; + + // Render mode flag — set by render-to-html/aser, checked by eval-list. + // When false, render expressions fall through to evalCall. + var _renderMode = false; + function renderActiveP() { return _renderMode; } + function setRenderActiveB(val) { _renderMode = !!val; } + function renderExpr(expr, env) { if (_renderExprFn) return _renderExprFn(expr, env); - // No adapter loaded — just return the expression as-is - return expr; + // No adapter loaded — fall through to evalCall + return evalCall(first(expr), rest(expr), env); } function stripPrefix(s, prefix) { @@ -614,7 +621,7 @@ 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 == "letrec")) ? sfLetrec(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 == "defisland")) ? sfDefisland(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(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 == "reset")) ? sfReset(args, env) : (isSxTruthy((name == "shift")) ? sfShift(args, env) : (isSxTruthy((name == "dynamic-wind")) ? sfDynamicWind(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((isSxTruthy(renderActiveP()) && isRenderExpr(expr))) ? renderExpr(expr, env) : evalCall(head, args, env))))))))))))))))))))))))))))))))))))))); })() : evalCall(head, args, env))); })(); }; @@ -1228,7 +1235,8 @@ continue; } else { return NIL; } } }; // === Transpiled from adapter-html === // render-to-html - var renderToHtml = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(expr); if (_m == "number") return (String(expr)); if (_m == "boolean") return (isSxTruthy(expr) ? "true" : "false"); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? "" : renderListToHtml(expr, env)); if (_m == "symbol") return renderValueToHtml(trampoline(evalExpr(expr, env)), env); if (_m == "keyword") return escapeHtml(keywordName(expr)); if (_m == "raw-html") return rawHtmlContent(expr); return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); }; + var renderToHtml = function(expr, env) { setRenderActiveB(true); +return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(expr); if (_m == "number") return (String(expr)); if (_m == "boolean") return (isSxTruthy(expr) ? "true" : "false"); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? "" : renderListToHtml(expr, env)); if (_m == "symbol") return renderValueToHtml(trampoline(evalExpr(expr, env)), env); if (_m == "keyword") return escapeHtml(keywordName(expr)); if (_m == "raw-html") return rawHtmlContent(expr); return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); }; // render-value-to-html var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); return escapeHtml((String(val))); })(); }; @@ -1388,7 +1396,8 @@ continue; } else { return NIL; } } }; })(); }; // aser - var aser = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() { + var aser = function(expr, env) { setRenderActiveB(true); +return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() { var name = symbolName(expr); return (isSxTruthy(envHas(env, name)) ? envGet(env, name) : (isSxTruthy(isPrimitive(name)) ? getPrimitive(name) : (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : error((String("Undefined symbol: ") + String(name)))))))); })(); if (_m == "keyword") return keywordName(expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : aserList(expr, env)); return expr; })(); }; diff --git a/shared/sx/ref/bootstrap_js.py b/shared/sx/ref/bootstrap_js.py index 66b1942..0ba9161 100644 --- a/shared/sx/ref/bootstrap_js.py +++ b/shared/sx/ref/bootstrap_js.py @@ -189,6 +189,8 @@ class JSEmitter: "eval-call": "evalCall", "is-render-expr?": "isRenderExpr", "render-expr": "renderExpr", + "render-active?": "renderActiveP", + "set-render-active!": "setRenderActiveB", "call-lambda": "callLambda", "call-component": "callComponent", "parse-keyword-args": "parseKeywordArgs", @@ -2497,10 +2499,17 @@ PLATFORM_JS_PRE = ''' // Render dispatch — call the active adapter's render function. // Set by each adapter when loaded; defaults to identity (no rendering). var _renderExprFn = null; + + // Render mode flag — set by render-to-html/aser, checked by eval-list. + // When false, render expressions fall through to evalCall. + var _renderMode = false; + function renderActiveP() { return _renderMode; } + function setRenderActiveB(val) { _renderMode = !!val; } + function renderExpr(expr, env) { if (_renderExprFn) return _renderExprFn(expr, env); - // No adapter loaded — just return the expression as-is - return expr; + // No adapter loaded — fall through to evalCall + return evalCall(first(expr), rest(expr), env); } function stripPrefix(s, prefix) { diff --git a/shared/sx/tests/test_parity.py b/shared/sx/tests/test_parity.py index 5858d24..db809b0 100644 --- a/shared/sx/tests/test_parity.py +++ b/shared/sx/tests/test_parity.py @@ -268,7 +268,12 @@ class TestParityLambda: assert_parity("((fn (x) (+ x 1) (* x 2)) 5)") def test_rest_params(self): - assert_parity("((fn (&rest args) args) 1 2 3)") + # &rest is only supported in defcomp/defmacro, not bare lambda + # Both evaluators should error on this + with pytest.raises(Exception): + hw_eval("((fn (&rest args) args) 1 2 3)") + with pytest.raises(Exception): + ref_eval("((fn (&rest args) args) 1 2 3)") # ---------------------------------------------------------------------------