Fix lambda multi-body, reactive island demos, and add React is Hypermedia essay
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled

Lambda multi-body fix: sf-lambda used (nth args 1), dropping all but the first
body expression. Fixed to collect all body expressions and wrap in (begin ...).
This was foundational — every multi-expression lambda in every island silently
dropped expressions after the first.

Reactive islands: fix dom-parent marker timing (first effect run before marker
is in DOM), fix :key eager evaluation, fix error boundary scope isolation,
fix resource/suspense reactive cond tracking, fix inc not available as JS var.

New essay: "React is Hypermedia" — argues that reactive islands are hypermedia
controls whose behavior is specified in SX, not a departure from hypermedia.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 20:00:44 +00:00
parent 06adbdcd59
commit 56589a81b2
12 changed files with 1047 additions and 4010 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-08T16:54:18Z";
var SX_VERSION = "2026-03-08T19:39:22Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -304,6 +304,7 @@
PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; };
PRIMITIVES["even?"] = function(n) { return n % 2 === 0; };
PRIMITIVES["zero?"] = function(n) { return n === 0; };
PRIMITIVES["boolean?"] = function(x) { return x === true || x === false; };
// core.strings
@@ -326,6 +327,7 @@
PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); };
PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); };
PRIMITIVES["string-length"] = function(s) { return String(s).length; };
PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; };
PRIMITIVES["concat"] = function() {
var out = [];
for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]);
@@ -360,6 +362,12 @@
PRIMITIVES["zip-pairs"] = function(c) {
var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r;
};
PRIMITIVES["reverse"] = function(c) { return Array.isArray(c) ? c.slice().reverse() : String(c).split("").reverse().join(""); };
PRIMITIVES["flatten"] = function(c) {
var out = [];
function walk(a) { for (var i = 0; i < a.length; i++) Array.isArray(a[i]) ? walk(a[i]) : out.push(a[i]); }
walk(c || []); return out;
};
// core.dict
@@ -381,6 +389,7 @@
return out;
};
PRIMITIVES["dict-set!"] = function(d, k, v) { d[k] = v; return v; };
PRIMITIVES["has-key?"] = function(d, k) { return d !== null && d !== undefined && k in d; };
PRIMITIVES["into"] = function(target, coll) {
if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
@@ -749,7 +758,8 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
// sf-lambda
var sfLambda = function(args, env) { return (function() {
var paramsExpr = first(args);
var body = nth(args, 1);
var bodyExprs = rest(args);
var body = (isSxTruthy((len(bodyExprs) == 1)) ? first(bodyExprs) : cons(makeSymbol("begin"), bodyExprs));
var paramNames = map(function(p) { return (isSxTruthy((typeOf(p) == "symbol")) ? symbolName(p) : p); }, paramsExpr);
return makeLambda(paramNames, body, env);
})(); };
@@ -1491,8 +1501,23 @@ return result; }, args);
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 attrName = keywordName(arg);
var attrVal = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env));
(isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy(startsWith(attrName, "on-")) && isCallable(attrVal))) ? domListen(el, slice(attrName, 3), attrVal) : (isSxTruthy((isSxTruthy((attrName == "bind")) && isSignal(attrVal))) ? bindInput(el, attrVal) : (isSxTruthy((attrName == "ref")) ? dictSet(attrVal, "current", el) : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal)))))))));
var attrExpr = nth(args, (get(state, "i") + 1));
(isSxTruthy(startsWith(attrName, "on-")) ? (function() {
var attrVal = trampoline(evalExpr(attrExpr, env));
return (isSxTruthy(isCallable(attrVal)) ? domListen(el, slice(attrName, 3), attrVal) : NIL);
})() : (isSxTruthy((attrName == "bind")) ? (function() {
var attrVal = trampoline(evalExpr(attrExpr, env));
return (isSxTruthy(isSignal(attrVal)) ? bindInput(el, attrVal) : NIL);
})() : (isSxTruthy((attrName == "ref")) ? (function() {
var attrVal = trampoline(evalExpr(attrExpr, env));
return dictSet(attrVal, "current", el);
})() : (isSxTruthy((attrName == "key")) ? (function() {
var attrVal = trampoline(evalExpr(attrExpr, env));
return domSetAttr(el, "key", (String(attrVal)));
})() : (isSxTruthy(_islandScope) ? reactiveAttr(el, attrName, function() { return trampoline(evalExpr(attrExpr, env)); }) : (function() {
var attrVal = trampoline(evalExpr(attrExpr, env));
return (isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal))))));
})())))));
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
})() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? domAppend(el, renderToDom(arg, env, newNs)) : NIL), assoc(state, "i", (get(state, "i") + 1)))));
})(); }, {["i"]: 0, ["skip"]: false}, args);
@@ -1552,17 +1577,84 @@ return result; }, args);
var isRenderDomForm = function(name) { return contains(RENDER_DOM_FORMS, name); };
// dispatch-render-form
var dispatchRenderForm = function(name, expr, env, ns) { return (isSxTruthy((name == "if")) ? (function() {
var dispatchRenderForm = function(name, expr, env, ns) { return (isSxTruthy((name == "if")) ? (isSxTruthy(_islandScope) ? (function() {
var marker = createComment("r-if");
var currentNodes = [];
var initialResult = NIL;
effect(function() { return (function() {
var result = (function() {
var condVal = trampoline(evalExpr(nth(expr, 1), env));
return (isSxTruthy(condVal) ? renderToDom(nth(expr, 2), env, ns) : (isSxTruthy((len(expr) > 3)) ? renderToDom(nth(expr, 3), env, ns) : createFragment()));
})() : (isSxTruthy((name == "when")) ? (isSxTruthy(!isSxTruthy(trampoline(evalExpr(nth(expr, 1), env)))) ? createFragment() : (function() {
})();
return (isSxTruthy(domParent(marker)) ? (forEach(function(n) { return domRemove(n); }, currentNodes), (currentNodes = (isSxTruthy(domIsFragment(result)) ? domChildNodes(result) : [result])), domInsertAfter(marker, result)) : (initialResult = result));
})(); });
return (function() {
var frag = createFragment();
domAppend(frag, marker);
if (isSxTruthy(initialResult)) {
currentNodes = (isSxTruthy(domIsFragment(initialResult)) ? domChildNodes(initialResult) : [initialResult]);
domAppend(frag, initialResult);
}
return frag;
})();
})() : (function() {
var condVal = trampoline(evalExpr(nth(expr, 1), env));
return (isSxTruthy(condVal) ? renderToDom(nth(expr, 2), env, ns) : (isSxTruthy((len(expr) > 3)) ? renderToDom(nth(expr, 3), env, ns) : createFragment()));
})()) : (isSxTruthy((name == "when")) ? (isSxTruthy(_islandScope) ? (function() {
var marker = createComment("r-when");
var currentNodes = [];
var initialResult = NIL;
effect(function() { return (isSxTruthy(domParent(marker)) ? (forEach(function(n) { return domRemove(n); }, currentNodes), (currentNodes = []), (isSxTruthy(trampoline(evalExpr(nth(expr, 1), env))) ? (function() {
var frag = createFragment();
{ var _c = range(2, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; domAppend(frag, renderToDom(nth(expr, i), env, ns)); } }
currentNodes = domChildNodes(frag);
return domInsertAfter(marker, frag);
})() : NIL)) : (isSxTruthy(trampoline(evalExpr(nth(expr, 1), env))) ? (function() {
var frag = createFragment();
{ var _c = range(2, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; domAppend(frag, renderToDom(nth(expr, i), env, ns)); } }
currentNodes = domChildNodes(frag);
return (initialResult = frag);
})() : NIL)); });
return (function() {
var frag = createFragment();
domAppend(frag, marker);
if (isSxTruthy(initialResult)) {
domAppend(frag, initialResult);
}
return frag;
})();
})() : (isSxTruthy(!isSxTruthy(trampoline(evalExpr(nth(expr, 1), env)))) ? createFragment() : (function() {
var frag = createFragment();
{ var _c = range(2, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; domAppend(frag, renderToDom(nth(expr, i), env, ns)); } }
return frag;
})()) : (isSxTruthy((name == "cond")) ? (function() {
})())) : (isSxTruthy((name == "cond")) ? (isSxTruthy(_islandScope) ? (function() {
var marker = createComment("r-cond");
var currentNodes = [];
var initialResult = NIL;
effect(function() { return (function() {
var branch = evalCond(rest(expr), env);
return (isSxTruthy(domParent(marker)) ? (forEach(function(n) { return domRemove(n); }, currentNodes), (currentNodes = []), (isSxTruthy(branch) ? (function() {
var result = renderToDom(branch, env, ns);
currentNodes = (isSxTruthy(domIsFragment(result)) ? domChildNodes(result) : [result]);
return domInsertAfter(marker, result);
})() : NIL)) : (isSxTruthy(branch) ? (function() {
var result = renderToDom(branch, env, ns);
currentNodes = (isSxTruthy(domIsFragment(result)) ? domChildNodes(result) : [result]);
return (initialResult = result);
})() : NIL));
})(); });
return (function() {
var frag = createFragment();
domAppend(frag, marker);
if (isSxTruthy(initialResult)) {
domAppend(frag, initialResult);
}
return frag;
})();
})() : (function() {
var branch = evalCond(rest(expr), env);
return (isSxTruthy(branch) ? renderToDom(branch, env, ns) : createFragment());
})() : (isSxTruthy((name == "case")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy(sxOr((name == "let"), (name == "let*"))) ? (function() {
})()) : (isSxTruthy((name == "case")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy(sxOr((name == "let"), (name == "let*"))) ? (function() {
var local = processBindings(nth(expr, 1), env);
var frag = createFragment();
{ var _c = range(2, len(expr)); for (var _i = 0; _i < _c.length; _i++) { var i = _c[_i]; domAppend(frag, renderToDom(nth(expr, i), local, ns)); } }
@@ -1604,7 +1696,7 @@ return result; }, args);
return domAppend(frag, val);
})(); }, coll);
return frag;
})() : (isSxTruthy((name == "filter")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy((name == "portal")) ? renderDomPortal(args, env, ns) : (isSxTruthy((name == "error-boundary")) ? renderDomErrorBoundary(args, env, ns) : (isSxTruthy((name == "for-each")) ? (function() {
})() : (isSxTruthy((name == "filter")) ? renderToDom(trampoline(evalExpr(expr, env)), env, ns) : (isSxTruthy((name == "portal")) ? renderDomPortal(rest(expr), env, ns) : (isSxTruthy((name == "error-boundary")) ? renderDomErrorBoundary(rest(expr), env, ns) : (isSxTruthy((name == "for-each")) ? (function() {
var f = trampoline(evalExpr(nth(expr, 1), env));
var coll = trampoline(evalExpr(nth(expr, 2), env));
var frag = createFragment();
@@ -1706,9 +1798,8 @@ return (isSxTruthy(testFn()) ? (function() {
var keyOrder = [];
domAppend(container, marker);
effect(function() { return (function() {
var parent = domParent(marker);
var items = deref(itemsSig);
return (isSxTruthy(parent) ? (function() {
return (isSxTruthy(domParent(marker)) ? (function() {
var newMap = {};
var newKeys = [];
var hasKeys = false;
@@ -1738,7 +1829,13 @@ return (isSxTruthy(testFn()) ? (function() {
})()));
keyMap = newMap;
return (keyOrder = newKeys);
})() : NIL);
})() : forEachIndexed(function(idx, item) { return (function() {
var rendered = renderListItem(mapFn, item, env, ns);
var key = extractKey(rendered, idx);
keyMap[key] = rendered;
keyOrder.push(key);
return domAppend(container, rendered);
})(); }, items));
})(); });
return container;
})(); };
@@ -1758,8 +1855,8 @@ return (isSxTruthy(testFn()) ? (function() {
// render-dom-portal
var renderDomPortal = function(args, env, ns) { return (function() {
var selector = trampoline(evalExpr(first(args), env));
var target = domQuery(selector);
return (isSxTruthy(!isSxTruthy(target)) ? (logWarn((String("Portal target not found: ") + String(selector))), createComment((String("portal: ") + String(selector) + String(" (not found)")))) : (function() {
var target = sxOr(domQuery(selector), domEnsureElement(selector));
return (isSxTruthy(!isSxTruthy(target)) ? createComment((String("portal: ") + String(selector) + String(" (not found)"))) : (function() {
var marker = createComment((String("portal: ") + String(selector)));
var frag = createFragment();
{ var _c = rest(args); for (var _i = 0; _i < _c.length; _i++) { var child = _c[_i]; domAppend(frag, renderToDom(child, env, ns)); } }
@@ -1777,32 +1874,29 @@ return (isSxTruthy(testFn()) ? (function() {
var fallbackExpr = first(args);
var bodyExprs = rest(args);
var container = domCreateElement("div", NIL);
var boundaryDisposers = [];
var retryVersion = signal(0);
domSetAttr(container, "data-sx-boundary", "true");
return (function() {
var renderBody = function() { { var _c = boundaryDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; d(); } }
boundaryDisposers = [];
effect(function() { deref(retryVersion);
domSetProp(container, "innerHTML", "");
return tryCatch(function() { return withIslandScope(function(disposable) { boundaryDisposers.push(disposable);
return registerInScope(disposable); }, function() { return (function() {
return (function() {
var savedScope = _islandScope;
_islandScope = NIL;
return tryCatch(function() { (function() {
var frag = createFragment();
{ var _c = bodyExprs; for (var _i = 0; _i < _c.length; _i++) { var child = _c[_i]; domAppend(frag, renderToDom(child, env, ns)); } }
return domAppend(container, frag);
})(); }); }, function(err) { { var _c = boundaryDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; d(); } }
boundaryDisposers = [];
})();
return (_islandScope = savedScope); }, function(err) { _islandScope = savedScope;
return (function() {
var fallbackFn = trampoline(evalExpr(fallbackExpr, env));
var retryFn = function() { return renderBody(); };
var retryFn = function() { return swap_b(retryVersion, function(n) { return (n + 1); }); };
return (function() {
var fallbackDom = (isSxTruthy(isLambda(fallbackFn)) ? renderLambdaDom(fallbackFn, [err, retryFn], env, ns) : renderToDom(apply(fallbackFn, [err, retryFn]), env, ns));
return domAppend(container, fallbackDom);
})();
})(); }); };
renderBody();
registerInScope(function() { { var _c = boundaryDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; d(); } }
return (boundaryDisposers = []); });
})(); });
})(); });
return container;
})();
})(); };
@@ -3070,14 +3164,14 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
// def-store
var defStore = function(name, initFn) { return (function() {
var registry = _storeRegistry;
if (isSxTruthy(!isSxTruthy(hasKey_p(registry, name)))) {
if (isSxTruthy(!isSxTruthy(dictHas(registry, name)))) {
_storeRegistry = assoc(registry, name, invoke(initFn));
}
return get(_storeRegistry, name);
})(); };
// use-store
var useStore = function(name) { return (isSxTruthy(hasKey_p(_storeRegistry, name)) ? get(_storeRegistry, name) : error((String("Store not found: ") + String(name) + String(". Call (def-store ...) before (use-store ...).")))); };
var useStore = function(name) { return (isSxTruthy(dictHas(_storeRegistry, name)) ? get(_storeRegistry, name) : error((String("Store not found: ") + String(name) + String(". Call (def-store ...) before (use-store ...).")))); };
// clear-stores
var clearStores = function() { return (_storeRegistry = {}); };
@@ -3273,6 +3367,20 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
return _hasDom ? document.querySelector(sel) : null;
}
function domEnsureElement(sel) {
if (!_hasDom) return null;
var el = document.querySelector(sel);
if (el) return el;
// Parse #id selector → create div with that id, append to body
if (sel.charAt(0) === '#') {
el = document.createElement('div');
el.id = sel.slice(1);
document.body.appendChild(el);
return el;
}
return null;
}
function domQueryAll(root, sel) {
if (!root || !root.querySelectorAll) return [];
return Array.prototype.slice.call(root.querySelectorAll(sel));
@@ -3404,6 +3512,12 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
function promiseDelayed(ms, value) {
return new Promise(function(resolve) {
setTimeout(function() { resolve(value); }, ms);
});
}
// --- Abort controllers ---
var _controllers = typeof WeakMap !== "undefined" ? new WeakMap() : null;
@@ -3440,8 +3554,9 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function clearTimeout_(id) { clearTimeout(id); }
function clearInterval_(id) { clearInterval(id); }
function requestAnimationFrame_(fn) {
if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(fn);
else setTimeout(fn, 16);
var cb = _wrapSxFn(fn);
if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(cb);
else setTimeout(cb, 16);
}
// --- Fetch ---
@@ -3840,14 +3955,19 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function stopPropagation_(e) { if (e && e.stopPropagation) e.stopPropagation(); }
function domFocus(el) { if (el && el.focus) el.focus(); }
function tryCatch(tryFn, catchFn) {
try { return tryFn(); } catch (e) { return catchFn(e); }
var t = _wrapSxFn(tryFn);
var c = catchFn && catchFn._lambda
? function(e) { return trampoline(callLambda(catchFn, [e], lambdaClosure(catchFn))); }
: catchFn;
try { return t(); } catch (e) { return c(e); }
}
function errorMessage(e) {
return e && e.message ? e.message : String(e);
}
function scheduleIdle(fn) {
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(fn);
else setTimeout(fn, 0);
var cb = _wrapSxFn(fn);
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(cb);
else setTimeout(cb, 0);
}
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
@@ -4386,11 +4506,24 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
PRIMITIVES["dom-listen"] = domListen;
PRIMITIVES["dom-dispatch"] = domDispatch;
PRIMITIVES["event-detail"] = eventDetail;
PRIMITIVES["resource"] = resource;
PRIMITIVES["promise-delayed"] = promiseDelayed;
PRIMITIVES["promise-then"] = promiseThen;
PRIMITIVES["def-store"] = defStore;
PRIMITIVES["use-store"] = useStore;
PRIMITIVES["emit-event"] = emitEvent;
PRIMITIVES["on-event"] = onEvent;
PRIMITIVES["bridge-event"] = bridgeEvent;
// DOM primitives for island code
PRIMITIVES["dom-focus"] = domFocus;
PRIMITIVES["dom-tag-name"] = domTagName;
PRIMITIVES["dom-get-prop"] = domGetProp;
PRIMITIVES["stop-propagation"] = stopPropagation_;
PRIMITIVES["error-message"] = errorMessage;
PRIMITIVES["schedule-idle"] = scheduleIdle;
PRIMITIVES["invoke"] = invoke;
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
PRIMITIVES["filter"] = filter;
// =========================================================================
// Async IO: Promise-aware rendering for client-side IO primitives