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
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:
@@ -102,9 +102,11 @@ class JSEmitter:
|
||||
# Map SX names to JS names
|
||||
return self._mangle(name)
|
||||
|
||||
def _mangle(self, name: str) -> str:
|
||||
"""Convert SX identifier to valid JS identifier."""
|
||||
RENAMES = {
|
||||
# Explicit SX→JS name mappings. Auto-mangle (kebab→camelCase, ?→_p, !→_b)
|
||||
# is only a fallback. Every platform symbol used in spec .sx files MUST have
|
||||
# an entry here — relying on auto-mangle is fragile and has caused runtime
|
||||
# errors (e.g. has-key? → hasKey_p instead of dictHas).
|
||||
RENAMES = {
|
||||
"nil": "NIL",
|
||||
"true": "true",
|
||||
"false": "false",
|
||||
@@ -328,6 +330,7 @@ class JSEmitter:
|
||||
"dom-listen": "domListen",
|
||||
"event-detail": "eventDetail",
|
||||
"dom-query": "domQuery",
|
||||
"dom-ensure-element": "domEnsureElement",
|
||||
"dom-query-all": "domQueryAll",
|
||||
"dom-tag-name": "domTagName",
|
||||
"create-comment": "createComment",
|
||||
@@ -338,6 +341,7 @@ class JSEmitter:
|
||||
"dom-get-data": "domGetData",
|
||||
"json-parse": "jsonParse",
|
||||
"dict-has?": "dictHas",
|
||||
"has-key?": "dictHas",
|
||||
"dict-delete!": "dictDelete",
|
||||
"process-bindings": "processBindings",
|
||||
"eval-cond": "evalCond",
|
||||
@@ -413,6 +417,7 @@ class JSEmitter:
|
||||
"promise-resolve": "promiseResolve",
|
||||
"promise-then": "promiseThen",
|
||||
"promise-catch": "promiseCatch",
|
||||
"promise-delayed": "promiseDelayed",
|
||||
"abort-previous": "abortPrevious",
|
||||
"track-controller": "trackController",
|
||||
"new-abort-controller": "newAbortController",
|
||||
@@ -590,9 +595,12 @@ class JSEmitter:
|
||||
"match-route": "matchRoute",
|
||||
"find-matching-route": "findMatchingRoute",
|
||||
"for-each-indexed": "forEachIndexed",
|
||||
}
|
||||
if name in RENAMES:
|
||||
return RENAMES[name]
|
||||
}
|
||||
|
||||
def _mangle(self, name: str) -> str:
|
||||
"""Convert SX identifier to valid JS identifier."""
|
||||
if name in self.RENAMES:
|
||||
return self.RENAMES[name]
|
||||
# General mangling: replace - with camelCase, ? with _p, ! with _b
|
||||
result = name
|
||||
if result.endswith("?"):
|
||||
@@ -2196,6 +2204,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
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": '''
|
||||
@@ -2219,6 +2228,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
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]);
|
||||
@@ -2254,6 +2264,12 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
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": '''
|
||||
@@ -2276,6 +2292,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
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]; }
|
||||
@@ -2906,6 +2923,20 @@ PLATFORM_DOM_JS = """
|
||||
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));
|
||||
@@ -3039,6 +3070,12 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
|
||||
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;
|
||||
@@ -3075,8 +3112,9 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
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 ---
|
||||
@@ -3475,14 +3513,19 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
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; }
|
||||
|
||||
@@ -4028,11 +4071,24 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False):
|
||||
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;''')
|
||||
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;''')
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user