Replace invoke with cek-call in reactive island primitives

All signal operations (computed, effect, batch, etc.) now dispatch
function calls through cek-call, which routes SX lambdas via cek-run
and native callables via apply. This replaces the invoke shim.

Key changes:
- cek.sx: add cek-call (defined before reactive-shift-deref), replace
  invoke in subscriber disposal and ReactiveResetFrame handler
- signals.sx: replace all 11 invoke calls with cek-call
- js.sx: fix octal escape in js-quote-string (char-from-code 0)
- platform_js.py: fix JS append to match Python (list concat semantics),
  add Continuation type guard in PLATFORM_CEK_JS, add scheduleIdle
  safety check, module ordering (cek before signals)
- platform_py.py: fix ident-char regex (remove [ ] from valid chars),
  module ordering (cek before signals)
- run_js_sx.py: emit PLATFORM_CEK_JS before transpiled spec files
- page-functions.sx: add cek and provide page functions for SX URLs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 10:11:48 +00:00
parent 30d9d4aa4c
commit 455e48df07
20 changed files with 911 additions and 600 deletions

View File

@@ -14,7 +14,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-14T01:23:35Z"; var SX_VERSION = "2026-03-14T10:06:04Z";
function isNil(x) { return x === NIL || x === null || x === undefined; } function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -414,7 +414,7 @@
PRIMITIVES["rest"] = function(c) { if (c && typeof c.slice !== "function") { console.error("[sx-debug] rest called on non-sliceable:", typeof c, c, new Error().stack); return []; } return c ? c.slice(1) : []; }; PRIMITIVES["rest"] = function(c) { if (c && typeof c.slice !== "function") { console.error("[sx-debug] rest called on non-sliceable:", typeof c, c, new Error().stack); return []; } return c ? c.slice(1) : []; };
PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; }; PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); }; PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); };
PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); }; PRIMITIVES["append"] = function(c, x) { return (c || []).concat(Array.isArray(x) ? x : [x]); };
PRIMITIVES["append!"] = function(arr, x) { arr.push(x); return arr; }; PRIMITIVES["append!"] = function(arr, x) { arr.push(x); return arr; };
PRIMITIVES["chunk-every"] = function(c, n) { PRIMITIVES["chunk-every"] = function(c, n) {
var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r; var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r;
@@ -744,10 +744,10 @@
// ========================================================================= // =========================================================================
// Character classification derived from the grammar: // Character classification derived from the grammar:
// ident-start → [a-zA-Z_~*+\-><=/!?&] // ident-start → [a-zA-Z_~*+\-><=/!?&]
// ident-char → ident-start + [0-9.:\/\[\]#,] // ident-char → ident-start + [0-9.:\/\#,]
var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/; var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/;
var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]/; var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/#,]/;
function isIdentStart(ch) { return _identStartRe.test(ch); } function isIdentStart(ch) { return _identStartRe.test(ch); }
function isIdentChar(ch) { return _identCharRe.test(ch); } function isIdentChar(ch) { return _identCharRe.test(ch); }
@@ -759,6 +759,35 @@
var charFromCode = PRIMITIVES["char-from-code"]; var charFromCode = PRIMITIVES["char-from-code"];
// =========================================================================
// Platform: CEK module — explicit CEK machine
// =========================================================================
// Continuation type (needed by CEK even without the tree-walk shift/reset extension)
if (typeof Continuation === "undefined") {
function Continuation(fn) { this.fn = fn; }
Continuation.prototype._continuation = true;
Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); };
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
}
// Standalone aliases for primitives used by cek.sx / frames.sx
var inc = PRIMITIVES["inc"];
var dec = PRIMITIVES["dec"];
var zip_pairs = PRIMITIVES["zip-pairs"];
var continuation_p = PRIMITIVES["continuation?"];
function makeCekContinuation(captured, restKont) {
var c = new Continuation(function(v) { return v !== undefined ? v : NIL; });
c._cek_data = {"captured": captured, "rest-kont": restKont};
return c;
}
function continuationData(c) {
return (c && c._cek_data) ? c._cek_data : {};
}
// === Transpiled from eval === // === Transpiled from eval ===
// trampoline // trampoline
@@ -1473,7 +1502,8 @@ if (isSxTruthy((pos >= lenSrc))) { return error("Unexpected end of input"); } el
if (isSxTruthy((ch == "("))) { pos = (pos + 1); if (isSxTruthy((ch == "("))) { pos = (pos + 1);
return readList(")"); } else if (isSxTruthy((ch == "["))) { pos = (pos + 1); return readList(")"); } else if (isSxTruthy((ch == "["))) { pos = (pos + 1);
return readList("]"); } else if (isSxTruthy((ch == "{"))) { pos = (pos + 1); return readList("]"); } else if (isSxTruthy((ch == "{"))) { pos = (pos + 1);
return readMap(); } else if (isSxTruthy((ch == "\""))) { return readString(); } else if (isSxTruthy((ch == ":"))) { return readKeyword(); } else if (isSxTruthy((ch == "`"))) { pos = (pos + 1); return readMap(); } else if (isSxTruthy((ch == "\""))) { return readString(); } else if (isSxTruthy((ch == ":"))) { return readKeyword(); } else if (isSxTruthy((ch == "'"))) { pos = (pos + 1);
return [makeSymbol("quote"), readExpr()]; } else if (isSxTruthy((ch == "`"))) { pos = (pos + 1);
return [makeSymbol("quasiquote"), readExpr()]; } else if (isSxTruthy((ch == ","))) { pos = (pos + 1); return [makeSymbol("quasiquote"), readExpr()]; } else if (isSxTruthy((ch == ","))) { pos = (pos + 1);
if (isSxTruthy((isSxTruthy((pos < lenSrc)) && (nth(source, pos) == "@")))) { pos = (pos + 1); if (isSxTruthy((isSxTruthy((pos < lenSrc)) && (nth(source, pos) == "@")))) { pos = (pos + 1);
return [makeSymbol("splice-unquote"), readExpr()]; } else { return [makeSymbol("unquote"), readExpr()]; } } else if (isSxTruthy((ch == "#"))) { pos = (pos + 1); return [makeSymbol("splice-unquote"), readExpr()]; } else { return [makeSymbol("unquote"), readExpr()]; } } else if (isSxTruthy((ch == "#"))) { pos = (pos + 1);
@@ -4385,224 +4415,6 @@ return scan(kont, []); };
})(); }; })(); };
// === Transpiled from signals (reactive signal runtime) ===
// make-signal
var makeSignal = function(value) { return {["__signal"]: true, ["value"]: value, ["subscribers"]: [], ["deps"]: []}; };
// signal?
var isSignal = function(x) { return (isSxTruthy(isDict(x)) && dictHas(x, "__signal")); };
// signal-value
var signalValue = function(s) { return get(s, "value"); };
// signal-set-value!
var signalSetValue = function(s, v) { return dictSet(s, "value", v); };
// signal-subscribers
var signalSubscribers = function(s) { return get(s, "subscribers"); };
// signal-add-sub!
var signalAddSub = function(s, f) { return (isSxTruthy(!isSxTruthy(contains(get(s, "subscribers"), f))) ? append_b(get(s, "subscribers"), f) : NIL); };
// signal-remove-sub!
var signalRemoveSub = function(s, f) { return dictSet(s, "subscribers", filter(function(sub) { return !isSxTruthy(isIdentical(sub, f)); }, get(s, "subscribers"))); };
// signal-deps
var signalDeps = function(s) { return get(s, "deps"); };
// signal-set-deps!
var signalSetDeps = function(s, deps) { return dictSet(s, "deps", deps); };
// signal
var signal = function(initialValue) { return makeSignal(initialValue); };
// deref
var deref = function(s) { return (isSxTruthy(!isSxTruthy(isSignal(s))) ? s : (function() {
var ctx = sxContext("sx-reactive", NIL);
if (isSxTruthy(ctx)) {
(function() {
var depList = get(ctx, "deps");
var notifyFn = get(ctx, "notify");
return (isSxTruthy(!isSxTruthy(contains(depList, s))) ? (append_b(depList, s), signalAddSub(s, notifyFn)) : NIL);
})();
}
return signalValue(s);
})()); };
// reset!
var reset_b = function(s, value) { return (isSxTruthy(isSignal(s)) ? (function() {
var old = signalValue(s);
return (isSxTruthy(!isSxTruthy(isIdentical(old, value))) ? (signalSetValue(s, value), notifySubscribers(s)) : NIL);
})() : NIL); };
// swap!
var swap_b = function(s, f) { var args = Array.prototype.slice.call(arguments, 2); return (isSxTruthy(isSignal(s)) ? (function() {
var old = signalValue(s);
var newVal = apply(f, cons(old, args));
return (isSxTruthy(!isSxTruthy(isIdentical(old, newVal))) ? (signalSetValue(s, newVal), notifySubscribers(s)) : NIL);
})() : NIL); };
// computed
var computed = function(computeFn) { return (function() {
var s = makeSignal(NIL);
var deps = [];
var computeCtx = NIL;
return (function() {
var recompute = function() { { var _c = signalDeps(s); for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; signalRemoveSub(dep, recompute); } }
signalSetDeps(s, []);
return (function() {
var ctx = {["deps"]: [], ["notify"]: recompute};
scopePush("sx-reactive", ctx);
return (function() {
var newVal = invoke(computeFn);
scopePop("sx-reactive");
signalSetDeps(s, get(ctx, "deps"));
return (function() {
var old = signalValue(s);
signalSetValue(s, newVal);
return (isSxTruthy(!isSxTruthy(isIdentical(old, newVal))) ? notifySubscribers(s) : NIL);
})();
})();
})(); };
recompute();
registerInScope(function() { return disposeComputed(s); });
return s;
})();
})(); };
// effect
var effect = function(effectFn) { return (function() {
var deps = [];
var disposed = false;
var cleanupFn = NIL;
return (function() {
var runEffect = function() { return (isSxTruthy(!isSxTruthy(disposed)) ? ((isSxTruthy(cleanupFn) ? invoke(cleanupFn) : NIL), forEach(function(dep) { return signalRemoveSub(dep, runEffect); }, deps), (deps = []), (function() {
var ctx = {["deps"]: [], ["notify"]: runEffect};
scopePush("sx-reactive", ctx);
return (function() {
var result = invoke(effectFn);
scopePop("sx-reactive");
deps = get(ctx, "deps");
return (isSxTruthy(isCallable(result)) ? (cleanupFn = result) : NIL);
})();
})()) : NIL); };
runEffect();
return (function() {
var disposeFn = function() { disposed = true;
if (isSxTruthy(cleanupFn)) {
invoke(cleanupFn);
}
{ var _c = deps; for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; signalRemoveSub(dep, runEffect); } }
return (deps = []); };
registerInScope(disposeFn);
return disposeFn;
})();
})();
})(); };
// *batch-depth*
var _batchDepth = 0;
// *batch-queue*
var _batchQueue = [];
// batch
var batch = function(thunk) { _batchDepth = (_batchDepth + 1);
invoke(thunk);
_batchDepth = (_batchDepth - 1);
return (isSxTruthy((_batchDepth == 0)) ? (function() {
var queue = _batchQueue;
_batchQueue = [];
return (function() {
var seen = [];
var pending = [];
{ var _c = queue; for (var _i = 0; _i < _c.length; _i++) { var s = _c[_i]; { var _c = signalSubscribers(s); for (var _i = 0; _i < _c.length; _i++) { var sub = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(seen, sub)))) {
seen.push(sub);
pending.push(sub);
} } } } }
return forEach(function(sub) { return sub(); }, pending);
})();
})() : NIL); };
// notify-subscribers
var notifySubscribers = function(s) { return (isSxTruthy((_batchDepth > 0)) ? (isSxTruthy(!isSxTruthy(contains(_batchQueue, s))) ? append_b(_batchQueue, s) : NIL) : flushSubscribers(s)); };
// flush-subscribers
var flushSubscribers = function(s) { return forEach(function(sub) { return sub(); }, signalSubscribers(s)); };
// dispose-computed
var disposeComputed = function(s) { return (isSxTruthy(isSignal(s)) ? (forEach(function(dep) { return signalRemoveSub(dep, NIL); }, signalDeps(s)), signalSetDeps(s, [])) : NIL); };
// with-island-scope
var withIslandScope = function(scopeFn, bodyFn) { scopePush("sx-island-scope", scopeFn);
return (function() {
var result = bodyFn();
scopePop("sx-island-scope");
return result;
})(); };
// register-in-scope
var registerInScope = function(disposable) { return (function() {
var collector = sxContext("sx-island-scope", NIL);
return (isSxTruthy(collector) ? invoke(collector, disposable) : NIL);
})(); };
// with-marsh-scope
var withMarshScope = function(marshEl, bodyFn) { return (function() {
var disposers = [];
withIslandScope(function(d) { return append_b(disposers, d); }, bodyFn);
return domSetData(marshEl, "sx-marsh-disposers", disposers);
})(); };
// dispose-marsh-scope
var disposeMarshScope = function(marshEl) { return (function() {
var disposers = domGetData(marshEl, "sx-marsh-disposers");
return (isSxTruthy(disposers) ? (forEach(function(d) { return invoke(d); }, disposers), domSetData(marshEl, "sx-marsh-disposers", NIL)) : NIL);
})(); };
// *store-registry*
var _storeRegistry = {};
// def-store
var defStore = function(name, initFn) { return (function() {
var registry = _storeRegistry;
if (isSxTruthy(!isSxTruthy(dictHas(registry, name)))) {
_storeRegistry = assoc(registry, name, invoke(initFn));
}
return get(_storeRegistry, name);
})(); };
// 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 = {}); };
// emit-event
var emitEvent = function(el, eventName, detail) { return domDispatch(el, eventName, detail); };
// on-event
var onEvent = function(el, eventName, handler) { return domListen(el, eventName, handler); };
// bridge-event
var bridgeEvent = function(el, eventName, targetSignal, transformFn) { return effect(function() { return (function() {
var remove = domListen(el, eventName, function(e) { return (function() {
var detail = eventDetail(e);
var newVal = (isSxTruthy(transformFn) ? invoke(transformFn, detail) : detail);
return reset_b(targetSignal, newVal);
})(); });
return remove;
})(); }); };
// resource
var resource = function(fetchFn) { return (function() {
var state = signal({["loading"]: true, ["data"]: NIL, ["error"]: NIL});
promiseThen(invoke(fetchFn), function(data) { return reset_b(state, {["loading"]: false, ["data"]: data, ["error"]: NIL}); }, function(err) { return reset_b(state, {["loading"]: false, ["data"]: NIL, ["error"]: err}); });
return state;
})(); };
// === Transpiled from cek (explicit CEK machine evaluator) === // === Transpiled from cek (explicit CEK machine evaluator) ===
// cek-run // cek-run
@@ -4743,6 +4555,12 @@ return (function() {
// step-sf-deref // step-sf-deref
var stepSfDeref = function(args, env, kont) { return makeCekState(first(args), env, kontPush(makeDerefFrame(env), kont)); }; var stepSfDeref = function(args, env, kont) { return makeCekState(first(args), env, kontPush(makeDerefFrame(env), kont)); };
// cek-call
var cekCall = function(f, args) { return (function() {
var a = (isSxTruthy(isNil(args)) ? [] : args);
return (isSxTruthy(isNil(f)) ? NIL : (isSxTruthy(isLambda(f)) ? cekRun(continueWithCall(f, a, {}, a, [])) : (isSxTruthy(isCallable(f)) ? apply(f, a) : NIL)));
})(); };
// reactive-shift-deref // reactive-shift-deref
var reactiveShiftDeref = function(sig, env, kont) { return (function() { var reactiveShiftDeref = function(sig, env, kont) { return (function() {
var scanResult = kontCaptureToReactiveReset(kont); var scanResult = kontCaptureToReactiveReset(kont);
@@ -4753,7 +4571,7 @@ return (function() {
return (function() { return (function() {
var subDisposers = []; var subDisposers = [];
return (function() { return (function() {
var subscriber = function() { { var _c = subDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; invoke(d); } } var subscriber = function() { { var _c = subDisposers; for (var _i = 0; _i < _c.length; _i++) { var d = _c[_i]; cekCall(d, NIL); } }
subDisposers = []; subDisposers = [];
return (function() { return (function() {
var newReset = makeReactiveResetFrame(env, updateFn, false); var newReset = makeReactiveResetFrame(env, updateFn, false);
@@ -4762,7 +4580,7 @@ return (function() {
})(); }; })(); };
signalAddSub(sig, subscriber); signalAddSub(sig, subscriber);
registerInScope(function() { signalRemoveSub(sig, subscriber); registerInScope(function() { signalRemoveSub(sig, subscriber);
return forEach(function(d) { return invoke(d); }, subDisposers); }); return forEach(function(d) { return cekCall(d, NIL); }, subDisposers); });
return (function() { return (function() {
var initialKont = concat(capturedFrames, [resetFrame], remainingKont); var initialKont = concat(capturedFrames, [resetFrame], remainingKont);
return makeCekValue(signalValue(sig), env, initialKont); return makeCekValue(signalValue(sig), env, initialKont);
@@ -4926,7 +4744,7 @@ return forEach(function(d) { return invoke(d); }, subDisposers); });
var updateFn = get(frame, "update-fn"); var updateFn = get(frame, "update-fn");
var first_p = get(frame, "first-render"); var first_p = get(frame, "first-render");
if (isSxTruthy((isSxTruthy(updateFn) && !isSxTruthy(first_p)))) { if (isSxTruthy((isSxTruthy(updateFn) && !isSxTruthy(first_p)))) {
invoke(updateFn, value); cekCall(updateFn, [value]);
} }
return makeCekValue(value, env, restK); return makeCekValue(value, env, restK);
})() : (isSxTruthy((ft == "scope")) ? (function() { })() : (isSxTruthy((ft == "scope")) ? (function() {
@@ -4980,6 +4798,224 @@ return forEach(function(d) { return invoke(d); }, subDisposers); });
var trampolineCek = function(val) { return (isSxTruthy(isThunk(val)) ? evalExprCek(thunkExpr(val), thunkEnv(val)) : val); }; var trampolineCek = function(val) { return (isSxTruthy(isThunk(val)) ? evalExprCek(thunkExpr(val), thunkEnv(val)) : val); };
// === Transpiled from signals (reactive signal runtime) ===
// make-signal
var makeSignal = function(value) { return {["__signal"]: true, ["value"]: value, ["subscribers"]: [], ["deps"]: []}; };
// signal?
var isSignal = function(x) { return (isSxTruthy(isDict(x)) && dictHas(x, "__signal")); };
// signal-value
var signalValue = function(s) { return get(s, "value"); };
// signal-set-value!
var signalSetValue = function(s, v) { return dictSet(s, "value", v); };
// signal-subscribers
var signalSubscribers = function(s) { return get(s, "subscribers"); };
// signal-add-sub!
var signalAddSub = function(s, f) { return (isSxTruthy(!isSxTruthy(contains(get(s, "subscribers"), f))) ? append_b(get(s, "subscribers"), f) : NIL); };
// signal-remove-sub!
var signalRemoveSub = function(s, f) { return dictSet(s, "subscribers", filter(function(sub) { return !isSxTruthy(isIdentical(sub, f)); }, get(s, "subscribers"))); };
// signal-deps
var signalDeps = function(s) { return get(s, "deps"); };
// signal-set-deps!
var signalSetDeps = function(s, deps) { return dictSet(s, "deps", deps); };
// signal
var signal = function(initialValue) { return makeSignal(initialValue); };
// deref
var deref = function(s) { return (isSxTruthy(!isSxTruthy(isSignal(s))) ? s : (function() {
var ctx = sxContext("sx-reactive", NIL);
if (isSxTruthy(ctx)) {
(function() {
var depList = get(ctx, "deps");
var notifyFn = get(ctx, "notify");
return (isSxTruthy(!isSxTruthy(contains(depList, s))) ? (append_b(depList, s), signalAddSub(s, notifyFn)) : NIL);
})();
}
return signalValue(s);
})()); };
// reset!
var reset_b = function(s, value) { return (isSxTruthy(isSignal(s)) ? (function() {
var old = signalValue(s);
return (isSxTruthy(!isSxTruthy(isIdentical(old, value))) ? (signalSetValue(s, value), notifySubscribers(s)) : NIL);
})() : NIL); };
// swap!
var swap_b = function(s, f) { var args = Array.prototype.slice.call(arguments, 2); return (isSxTruthy(isSignal(s)) ? (function() {
var old = signalValue(s);
var newVal = apply(f, cons(old, args));
return (isSxTruthy(!isSxTruthy(isIdentical(old, newVal))) ? (signalSetValue(s, newVal), notifySubscribers(s)) : NIL);
})() : NIL); };
// computed
var computed = function(computeFn) { return (function() {
var s = makeSignal(NIL);
var deps = [];
var computeCtx = NIL;
return (function() {
var recompute = function() { { var _c = signalDeps(s); for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; signalRemoveSub(dep, recompute); } }
signalSetDeps(s, []);
return (function() {
var ctx = {["deps"]: [], ["notify"]: recompute};
scopePush("sx-reactive", ctx);
return (function() {
var newVal = cekCall(computeFn, NIL);
scopePop("sx-reactive");
signalSetDeps(s, get(ctx, "deps"));
return (function() {
var old = signalValue(s);
signalSetValue(s, newVal);
return (isSxTruthy(!isSxTruthy(isIdentical(old, newVal))) ? notifySubscribers(s) : NIL);
})();
})();
})(); };
recompute();
registerInScope(function() { return disposeComputed(s); });
return s;
})();
})(); };
// effect
var effect = function(effectFn) { return (function() {
var deps = [];
var disposed = false;
var cleanupFn = NIL;
return (function() {
var runEffect = function() { return (isSxTruthy(!isSxTruthy(disposed)) ? ((isSxTruthy(cleanupFn) ? cekCall(cleanupFn, NIL) : NIL), forEach(function(dep) { return signalRemoveSub(dep, runEffect); }, deps), (deps = []), (function() {
var ctx = {["deps"]: [], ["notify"]: runEffect};
scopePush("sx-reactive", ctx);
return (function() {
var result = cekCall(effectFn, NIL);
scopePop("sx-reactive");
deps = get(ctx, "deps");
return (isSxTruthy(isCallable(result)) ? (cleanupFn = result) : NIL);
})();
})()) : NIL); };
runEffect();
return (function() {
var disposeFn = function() { disposed = true;
if (isSxTruthy(cleanupFn)) {
cekCall(cleanupFn, NIL);
}
{ var _c = deps; for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; signalRemoveSub(dep, runEffect); } }
return (deps = []); };
registerInScope(disposeFn);
return disposeFn;
})();
})();
})(); };
// *batch-depth*
var _batchDepth = 0;
// *batch-queue*
var _batchQueue = [];
// batch
var batch = function(thunk) { _batchDepth = (_batchDepth + 1);
cekCall(thunk, NIL);
_batchDepth = (_batchDepth - 1);
return (isSxTruthy((_batchDepth == 0)) ? (function() {
var queue = _batchQueue;
_batchQueue = [];
return (function() {
var seen = [];
var pending = [];
{ var _c = queue; for (var _i = 0; _i < _c.length; _i++) { var s = _c[_i]; { var _c = signalSubscribers(s); for (var _i = 0; _i < _c.length; _i++) { var sub = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(seen, sub)))) {
seen.push(sub);
pending.push(sub);
} } } } }
return forEach(function(sub) { return sub(); }, pending);
})();
})() : NIL); };
// notify-subscribers
var notifySubscribers = function(s) { return (isSxTruthy((_batchDepth > 0)) ? (isSxTruthy(!isSxTruthy(contains(_batchQueue, s))) ? append_b(_batchQueue, s) : NIL) : flushSubscribers(s)); };
// flush-subscribers
var flushSubscribers = function(s) { return forEach(function(sub) { return sub(); }, signalSubscribers(s)); };
// dispose-computed
var disposeComputed = function(s) { return (isSxTruthy(isSignal(s)) ? (forEach(function(dep) { return signalRemoveSub(dep, NIL); }, signalDeps(s)), signalSetDeps(s, [])) : NIL); };
// with-island-scope
var withIslandScope = function(scopeFn, bodyFn) { scopePush("sx-island-scope", scopeFn);
return (function() {
var result = bodyFn();
scopePop("sx-island-scope");
return result;
})(); };
// register-in-scope
var registerInScope = function(disposable) { return (function() {
var collector = sxContext("sx-island-scope", NIL);
return (isSxTruthy(collector) ? cekCall(collector, [disposable]) : NIL);
})(); };
// with-marsh-scope
var withMarshScope = function(marshEl, bodyFn) { return (function() {
var disposers = [];
withIslandScope(function(d) { return append_b(disposers, d); }, bodyFn);
return domSetData(marshEl, "sx-marsh-disposers", disposers);
})(); };
// dispose-marsh-scope
var disposeMarshScope = function(marshEl) { return (function() {
var disposers = domGetData(marshEl, "sx-marsh-disposers");
return (isSxTruthy(disposers) ? (forEach(function(d) { return cekCall(d, NIL); }, disposers), domSetData(marshEl, "sx-marsh-disposers", NIL)) : NIL);
})(); };
// *store-registry*
var _storeRegistry = {};
// def-store
var defStore = function(name, initFn) { return (function() {
var registry = _storeRegistry;
if (isSxTruthy(!isSxTruthy(dictHas(registry, name)))) {
_storeRegistry = assoc(registry, name, cekCall(initFn, NIL));
}
return get(_storeRegistry, name);
})(); };
// 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 = {}); };
// emit-event
var emitEvent = function(el, eventName, detail) { return domDispatch(el, eventName, detail); };
// on-event
var onEvent = function(el, eventName, handler) { return domListen(el, eventName, handler); };
// bridge-event
var bridgeEvent = function(el, eventName, targetSignal, transformFn) { return effect(function() { return (function() {
var remove = domListen(el, eventName, function(e) { return (function() {
var detail = eventDetail(e);
var newVal = (isSxTruthy(transformFn) ? cekCall(transformFn, [detail]) : detail);
return reset_b(targetSignal, newVal);
})(); });
return remove;
})(); }); };
// resource
var resource = function(fetchFn) { return (function() {
var state = signal({["loading"]: true, ["data"]: NIL, ["error"]: NIL});
promiseThen(cekCall(fetchFn, NIL), function(data) { return reset_b(state, {["loading"]: false, ["data"]: data, ["error"]: NIL}); }, function(err) { return reset_b(state, {["loading"]: false, ["data"]: NIL, ["error"]: err}); });
return state;
})(); };
// ========================================================================= // =========================================================================
// Platform interface — DOM adapter (browser-only) // Platform interface — DOM adapter (browser-only)
// ========================================================================= // =========================================================================
@@ -5787,6 +5823,10 @@ return forEach(function(d) { return invoke(d); }, subDisposers); });
} }
function scheduleIdle(fn) { function scheduleIdle(fn) {
var cb = _wrapSxFn(fn); var cb = _wrapSxFn(fn);
if (typeof cb !== "function") {
console.error("[sx-ref] scheduleIdle: callback not callable, fn type:", typeof fn, "fn:", fn, "_lambda:", fn && fn._lambda);
return;
}
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(cb); if (typeof requestIdleCallback !== "undefined") requestIdleCallback(cb);
else setTimeout(cb, 0); else setTimeout(cb, 0);
} }
@@ -5876,8 +5916,12 @@ return forEach(function(d) { return invoke(d); }, subDisposers); });
e.preventDefault(); e.preventDefault();
// Re-read href from element at click time (not closed-over value) // Re-read href from element at click time (not closed-over value)
var liveHref = el.getAttribute("href") || _href; var liveHref = el.getAttribute("href") || _href;
console.log("[sx-debug] bindBoostLink click:", liveHref, "el:", el.tagName, el.textContent.slice(0,30));
executeRequest(el, { method: "GET", url: liveHref }).then(function() { executeRequest(el, { method: "GET", url: liveHref }).then(function() {
console.log("[sx-debug] boost fetch OK, pushState:", liveHref);
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
}).catch(function(err) {
console.error("[sx-debug] boost fetch ERROR:", err);
}); });
}); });
} }
@@ -5902,21 +5946,25 @@ return forEach(function(d) { return invoke(d); }, subDisposers); });
// Re-read href from element at click time (not closed-over value) // Re-read href from element at click time (not closed-over value)
var liveHref = link.getAttribute("href") || _href; var liveHref = link.getAttribute("href") || _href;
var pathname = urlPathname(liveHref); var pathname = urlPathname(liveHref);
console.log("[sx-debug] bindClientRouteClick:", pathname, "el:", link.tagName, link.textContent.slice(0,30));
// Find target selector: sx-boost ancestor, explicit sx-target, or #main-panel // Find target selector: sx-boost ancestor, explicit sx-target, or #main-panel
var boostEl = link.closest("[sx-boost]"); var boostEl = link.closest("[sx-boost]");
var targetSel = boostEl ? boostEl.getAttribute("sx-boost") : null; var targetSel = boostEl ? boostEl.getAttribute("sx-boost") : null;
if (!targetSel || targetSel === "true") { if (!targetSel || targetSel === "true") {
targetSel = link.getAttribute("sx-target") || "#main-panel"; targetSel = link.getAttribute("sx-target") || "#main-panel";
} }
console.log("[sx-debug] targetSel:", targetSel, "trying client route...");
if (tryClientRoute(pathname, targetSel)) { if (tryClientRoute(pathname, targetSel)) {
console.log("[sx-debug] client route SUCCESS, pushState:", liveHref);
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
if (typeof window !== "undefined") window.scrollTo(0, 0); if (typeof window !== "undefined") window.scrollTo(0, 0);
} else { } else {
logInfo("sx:route server " + pathname); console.log("[sx-debug] client route FAILED, server fetch:", liveHref);
executeRequest(link, { method: "GET", url: liveHref }).then(function() { executeRequest(link, { method: "GET", url: liveHref }).then(function() {
console.log("[sx-debug] server fetch OK, pushState:", liveHref);
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
}).catch(function(err) { }).catch(function(err) {
logWarn("sx:route server fetch error: " + (err && err.message ? err.message : err)); console.error("[sx-debug] server fetch ERROR:", err);
}); });
} }
}); });
@@ -6290,27 +6338,6 @@ return forEach(function(d) { return invoke(d); }, subDisposers); });
// =========================================================================
// Platform: CEK module — explicit CEK machine
// =========================================================================
// Standalone aliases for primitives used by cek.sx / frames.sx
var inc = PRIMITIVES["inc"];
var dec = PRIMITIVES["dec"];
var zip_pairs = PRIMITIVES["zip-pairs"];
var continuation_p = PRIMITIVES["continuation?"];
function makeCekContinuation(captured, restKont) {
var c = new Continuation(function(v) { return v !== undefined ? v : NIL; });
c._cek_data = {"captured": captured, "rest-kont": restKont};
return c;
}
function continuationData(c) {
return (c && c._cek_data) ? c._cek_data : {};
}
// ========================================================================= // =========================================================================
// Post-transpilation fixups // Post-transpilation fixups
// ========================================================================= // =========================================================================

View File

@@ -39,10 +39,10 @@ def _load_declarations() -> None:
len(_DECLARED_PURE), len(_DECLARED_IO), len(_DECLARED_HELPERS), len(_DECLARED_PURE), len(_DECLARED_IO), len(_DECLARED_HELPERS),
) )
except Exception as e: except Exception as e:
logger.warning("Failed to load boundary declarations: %s", e) # Don't cache failure — parser may not be ready yet (circular import
_DECLARED_PURE = frozenset() # during startup). Will retry on next call. Validation functions
_DECLARED_IO = frozenset() # skip checks when declarations aren't loaded.
_DECLARED_HELPERS = {} logger.debug("Boundary declarations not ready yet: %s", e)
def _is_strict() -> bool: def _is_strict() -> bool:
@@ -63,7 +63,8 @@ def _report(message: str) -> None:
def validate_primitive(name: str) -> None: def validate_primitive(name: str) -> None:
"""Validate that a pure primitive is declared in primitives.sx.""" """Validate that a pure primitive is declared in primitives.sx."""
_load_declarations() _load_declarations()
assert _DECLARED_PURE is not None if _DECLARED_PURE is None:
return # Not ready yet (circular import during startup), skip
if name not in _DECLARED_PURE: if name not in _DECLARED_PURE:
_report(f"Undeclared pure primitive: {name!r}. Add to primitives.sx.") _report(f"Undeclared pure primitive: {name!r}. Add to primitives.sx.")
@@ -71,7 +72,8 @@ def validate_primitive(name: str) -> None:
def validate_io(name: str) -> None: def validate_io(name: str) -> None:
"""Validate that an I/O primitive is declared in boundary.sx or boundary-app.sx.""" """Validate that an I/O primitive is declared in boundary.sx or boundary-app.sx."""
_load_declarations() _load_declarations()
assert _DECLARED_IO is not None if _DECLARED_IO is None:
return # Not ready yet, skip
if name not in _DECLARED_IO: if name not in _DECLARED_IO:
_report( _report(
f"Undeclared I/O primitive: {name!r}. " f"Undeclared I/O primitive: {name!r}. "
@@ -82,7 +84,8 @@ def validate_io(name: str) -> None:
def validate_helper(service: str, name: str) -> None: def validate_helper(service: str, name: str) -> None:
"""Validate that a page helper is declared in {service}/sx/boundary.sx.""" """Validate that a page helper is declared in {service}/sx/boundary.sx."""
_load_declarations() _load_declarations()
assert _DECLARED_HELPERS is not None if _DECLARED_HELPERS is None:
return # Not ready yet, skip
svc_helpers = _DECLARED_HELPERS.get(service, frozenset()) svc_helpers = _DECLARED_HELPERS.get(service, frozenset())
if name not in svc_helpers: if name not in svc_helpers:
_report( _report(
@@ -129,17 +132,14 @@ def validate_boundary_value(value: Any, context: str = "") -> None:
def declared_pure() -> frozenset[str]: def declared_pure() -> frozenset[str]:
_load_declarations() _load_declarations()
assert _DECLARED_PURE is not None return _DECLARED_PURE or frozenset()
return _DECLARED_PURE
def declared_io() -> frozenset[str]: def declared_io() -> frozenset[str]:
_load_declarations() _load_declarations()
assert _DECLARED_IO is not None return _DECLARED_IO or frozenset()
return _DECLARED_IO
def declared_helpers() -> dict[str, frozenset[str]]: def declared_helpers() -> dict[str, frozenset[str]]:
_load_declarations() _load_declarations()
assert _DECLARED_HELPERS is not None return dict(_DECLARED_HELPERS) if _DECLARED_HELPERS else {}
return dict(_DECLARED_HELPERS)

View File

@@ -760,7 +760,16 @@ class PyEmitter:
self._current_cell_vars = old_cells | nested_set_vars self._current_cell_vars = old_cells | nested_set_vars
if is_async: if is_async:
self._in_async = True self._in_async = True
self._emit_body_stmts(body, lines, indent + 1) # Self-tail-recursive 0-param functions: wrap body in while True
if (not param_names and not is_async
and self._has_self_tail_call(body, name)):
lines.append(f"{pad} while True:")
old_loop = getattr(self, '_current_loop_name', None)
self._current_loop_name = name
self._emit_body_stmts(body, lines, indent + 2)
self._current_loop_name = old_loop
else:
self._emit_body_stmts(body, lines, indent + 1)
self._current_cell_vars = old_cells self._current_cell_vars = old_cells
self._in_async = old_async self._in_async = old_async
return "\n".join(lines) return "\n".join(lines)
@@ -799,14 +808,20 @@ class PyEmitter:
Handles let as local variable declarations, and returns the last Handles let as local variable declarations, and returns the last
expression. Control flow in tail position (if, cond, case, when) expression. Control flow in tail position (if, cond, case, when)
is flattened to if/elif statements with returns in each branch. is flattened to if/elif statements with returns in each branch.
Detects self-tail-recursive (define name (fn () ...)) followed by
(name) and emits as while True loop instead of recursive def.
""" """
pad = " " * indent pad = " " * indent
for i, expr in enumerate(body): idx = 0
is_last = (i == len(body) - 1) while idx < len(body):
expr = body[idx]
is_last = (idx == len(body) - 1)
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol): if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
name = expr[0].name name = expr[0].name
if name in ("let", "let*"): if name in ("let", "let*"):
self._emit_let_as_stmts(expr, lines, indent, is_last) self._emit_let_as_stmts(expr, lines, indent, is_last)
idx += 1
continue continue
if name in ("do", "begin"): if name in ("do", "begin"):
sub_body = expr[1:] sub_body = expr[1:]
@@ -815,15 +830,172 @@ class PyEmitter:
else: else:
for sub in sub_body: for sub in sub_body:
lines.append(self.emit_statement(sub, indent)) lines.append(self.emit_statement(sub, indent))
idx += 1
continue continue
# Detect self-tail-recursive loop pattern:
# (define loop-name (fn () body...))
# (loop-name)
# Emit as: while True: <body with self-calls as continue>
if (name == "define" and not is_last
and idx + 1 < len(body)):
loop_info = self._detect_tail_loop(expr, body[idx + 1])
if loop_info:
loop_name, fn_body = loop_info
remaining = body[idx + 2:]
# Only optimize if the function isn't called again later
if not self._name_in_exprs(loop_name, remaining):
self._emit_while_loop(loop_name, fn_body, lines, indent)
# Skip the invocation; emit remaining body
for j, rem in enumerate(remaining):
if j == len(remaining) - 1:
self._emit_return_expr(rem, lines, indent)
else:
self._emit_stmt_recursive(rem, lines, indent)
return
if is_last: if is_last:
self._emit_return_expr(expr, lines, indent) self._emit_return_expr(expr, lines, indent)
else: else:
self._emit_stmt_recursive(expr, lines, indent) self._emit_stmt_recursive(expr, lines, indent)
idx += 1
def _detect_tail_loop(self, define_expr, next_expr):
"""Detect pattern: (define name (fn () body...)) followed by (name).
Returns (loop_name, fn_body) if tail-recursive, else None.
The function must have 0 params and body must end with self-call
in all tail positions.
"""
# Extract name and fn from define
dname = define_expr[1].name if isinstance(define_expr[1], Symbol) else None
if not dname:
return None
# Skip :effects annotation
if (len(define_expr) >= 5 and isinstance(define_expr[2], Keyword)
and define_expr[2].name == "effects"):
val_expr = define_expr[4]
else:
val_expr = define_expr[2] if len(define_expr) > 2 else None
if not (isinstance(val_expr, list) and val_expr
and isinstance(val_expr[0], Symbol)
and val_expr[0].name in ("fn", "lambda")):
return None
params = val_expr[1]
if not isinstance(params, list) or len(params) != 0:
return None # Must be 0-param function
fn_body = val_expr[2:]
# Check next expression is (name) — invocation
if not (isinstance(next_expr, list) and len(next_expr) == 1
and isinstance(next_expr[0], Symbol)
and next_expr[0].name == dname):
return None
# Check that fn_body has self-call in tail position(s)
if not self._has_self_tail_call(fn_body, dname):
return None
return (dname, fn_body)
def _has_self_tail_call(self, body, name):
"""Check if body is safe for while-loop optimization.
Returns True only when ALL tail positions are either:
- self-calls (name) → will become continue
- nil/void returns → will become break
- error() calls → raise, don't return
- when blocks → implicit nil else is fine
No tail position may return a computed value, since while-loop
break discards return values.
"""
if not body:
return False
last = body[-1]
# Non-list terminal: nil is ok, anything else is a value return
if not isinstance(last, list) or not last:
return (last is None or last is SX_NIL
or (isinstance(last, Symbol) and last.name == "nil"))
head = last[0] if isinstance(last[0], Symbol) else None
if not head:
return False
# Direct self-call in tail position
if head.name == name and len(last) == 1:
return True
# error() — raises, safe
if head.name == "error":
return True
# if — ALL branches must be safe
if head.name == "if":
then_ok = self._has_self_tail_call(
[last[2]] if len(last) > 2 else [None], name)
else_ok = self._has_self_tail_call(
[last[3]] if len(last) > 3 else [None], name)
return then_ok and else_ok
# do/begin — check last expression
if head.name in ("do", "begin"):
return self._has_self_tail_call(last[1:], name)
# when — body must be safe (implicit nil else is ok)
if head.name == "when":
return self._has_self_tail_call(last[2:], name)
# let/let* — check body (skip bindings)
if head.name in ("let", "let*"):
return self._has_self_tail_call(last[2:], name)
# cond — ALL branches must be safe
if head.name == "cond":
clauses = last[1:]
is_scheme = (
all(isinstance(c, list) and len(c) == 2 for c in clauses)
and not any(isinstance(c, Keyword) for c in clauses)
)
if is_scheme:
for clause in clauses:
if not self._has_self_tail_call([clause[1]], name):
return False
return True
else:
i = 0
while i < len(clauses) - 1:
if not self._has_self_tail_call([clauses[i + 1]], name):
return False
i += 2
return True
return False
def _name_in_exprs(self, name, exprs):
"""Check if a symbol name appears anywhere in a list of expressions."""
for expr in exprs:
if isinstance(expr, Symbol) and expr.name == name:
return True
if isinstance(expr, list):
if self._name_in_exprs(name, expr):
return True
return False
def _emit_while_loop(self, loop_name, fn_body, lines, indent):
"""Emit a self-tail-recursive function body as a while True loop."""
pad = " " * indent
lines.append(f"{pad}while True:")
# Track the loop name so _emit_return_expr can emit 'continue'
old_loop = getattr(self, '_current_loop_name', None)
self._current_loop_name = loop_name
self._emit_body_stmts(fn_body, lines, indent + 1)
self._current_loop_name = old_loop
def _emit_nil_return(self, lines: list, indent: int) -> None:
"""Emit 'return NIL' or 'break' depending on while-loop context."""
pad = " " * indent
if getattr(self, '_current_loop_name', None):
lines.append(f"{pad}break")
else:
lines.append(f"{pad}return NIL")
def _emit_return_expr(self, expr, lines: list, indent: int) -> None: def _emit_return_expr(self, expr, lines: list, indent: int) -> None:
"""Emit an expression in return position, flattening control flow.""" """Emit an expression in return position, flattening control flow."""
pad = " " * indent pad = " " * indent
# Inside a while loop (self-tail-recursive define optimization):
# self-call → continue
loop_name = getattr(self, '_current_loop_name', None)
if loop_name:
if (isinstance(expr, list) and len(expr) == 1
and isinstance(expr[0], Symbol) and expr[0].name == loop_name):
lines.append(f"{pad}continue")
return
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol): if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
name = expr[0].name name = expr[0].name
if name == "if": if name == "if":
@@ -845,11 +1017,17 @@ class PyEmitter:
self._emit_body_stmts(expr[1:], lines, indent) self._emit_body_stmts(expr[1:], lines, indent)
return return
if name == "for-each": if name == "for-each":
# for-each in return position: emit as statement, return NIL # for-each in return position: emit as statement, then return/break
lines.append(self._emit_for_each_stmt(expr, indent)) lines.append(self._emit_for_each_stmt(expr, indent))
lines.append(f"{pad}return NIL") self._emit_nil_return(lines, indent)
return return
lines.append(f"{pad}return {self.emit(expr)}") if loop_name:
emitted = self.emit(expr)
if emitted != "NIL":
lines.append(f"{pad}{emitted}")
lines.append(f"{pad}break")
else:
lines.append(f"{pad}return {self.emit(expr)}")
def _emit_if_return(self, expr, lines: list, indent: int) -> None: def _emit_if_return(self, expr, lines: list, indent: int) -> None:
"""Emit if as statement with returns in each branch.""" """Emit if as statement with returns in each branch."""
@@ -860,7 +1038,7 @@ class PyEmitter:
lines.append(f"{pad}else:") lines.append(f"{pad}else:")
self._emit_return_expr(expr[3], lines, indent + 1) self._emit_return_expr(expr[3], lines, indent + 1)
else: else:
lines.append(f"{pad}return NIL") self._emit_nil_return(lines, indent)
def _emit_when_return(self, expr, lines: list, indent: int) -> None: def _emit_when_return(self, expr, lines: list, indent: int) -> None:
"""Emit when as statement with return in body, else return NIL.""" """Emit when as statement with return in body, else return NIL."""
@@ -873,7 +1051,7 @@ class PyEmitter:
for b in body_parts[:-1]: for b in body_parts[:-1]:
lines.append(self.emit_statement(b, indent + 1)) lines.append(self.emit_statement(b, indent + 1))
self._emit_return_expr(body_parts[-1], lines, indent + 1) self._emit_return_expr(body_parts[-1], lines, indent + 1)
lines.append(f"{pad}return NIL") self._emit_nil_return(lines, indent)
def _emit_cond_return(self, expr, lines: list, indent: int) -> None: def _emit_cond_return(self, expr, lines: list, indent: int) -> None:
"""Emit cond as if/elif/else with returns in each branch.""" """Emit cond as if/elif/else with returns in each branch."""
@@ -915,7 +1093,7 @@ class PyEmitter:
self._emit_return_expr(body, lines, indent + 1) self._emit_return_expr(body, lines, indent + 1)
i += 2 i += 2
if not has_else: if not has_else:
lines.append(f"{pad}return NIL") self._emit_nil_return(lines, indent)
def _emit_case_return(self, expr, lines: list, indent: int) -> None: def _emit_case_return(self, expr, lines: list, indent: int) -> None:
"""Emit case as if/elif/else with returns in each branch.""" """Emit case as if/elif/else with returns in each branch."""
@@ -940,7 +1118,7 @@ class PyEmitter:
self._emit_return_expr(body, lines, indent + 1) self._emit_return_expr(body, lines, indent + 1)
i += 2 i += 2
if not has_else: if not has_else:
lines.append(f"{pad}return NIL") self._emit_nil_return(lines, indent)
def _emit_let_as_stmts(self, expr, lines: list, indent: int, is_last: bool) -> None: def _emit_let_as_stmts(self, expr, lines: list, indent: int, is_last: bool) -> None:
"""Emit a let expression as local variable declarations.""" """Emit a let expression as local variable declarations."""

View File

@@ -20,17 +20,21 @@ logger = logging.getLogger("sx.boundary_parser")
# Allow standalone use (from bootstrappers) or in-project imports # Allow standalone use (from bootstrappers) or in-project imports
try: try:
from shared.sx.parser import parse_all
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
except ImportError: except ImportError:
import sys import sys
_HERE = os.path.dirname(os.path.abspath(__file__)) _HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", "..")) _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT) sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all
from shared.sx.types import Symbol, Keyword, NIL as SX_NIL from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
def _get_parse_all():
"""Lazy import to avoid circular dependency when parser.py loads sx_ref.py."""
from shared.sx.parser import parse_all
return parse_all
def _ref_dir() -> str: def _ref_dir() -> str:
return os.path.dirname(os.path.abspath(__file__)) return os.path.dirname(os.path.abspath(__file__))
@@ -81,7 +85,7 @@ def _extract_declarations(
Returns (io_names, {service: helper_names}). Returns (io_names, {service: helper_names}).
""" """
exprs = parse_all(source) exprs = _get_parse_all()(source)
io_names: set[str] = set() io_names: set[str] = set()
helpers: dict[str, set[str]] = {} helpers: dict[str, set[str]] = {}
@@ -144,7 +148,7 @@ def parse_primitives_sx() -> frozenset[str]:
def parse_primitives_by_module() -> dict[str, frozenset[str]]: def parse_primitives_by_module() -> dict[str, frozenset[str]]:
"""Parse primitives.sx and return primitives grouped by module.""" """Parse primitives.sx and return primitives grouped by module."""
source = _read_file("primitives.sx") source = _read_file("primitives.sx")
exprs = parse_all(source) exprs = _get_parse_all()(source)
modules: dict[str, set[str]] = {} modules: dict[str, set[str]] = {}
current_module = "_unscoped" current_module = "_unscoped"
@@ -204,7 +208,7 @@ def parse_primitive_param_types() -> dict[str, dict]:
type of the &rest parameter (or None if no &rest, or None if untyped &rest). type of the &rest parameter (or None if no &rest, or None if untyped &rest).
""" """
source = _read_file("primitives.sx") source = _read_file("primitives.sx")
exprs = parse_all(source) exprs = _get_parse_all()(source)
result: dict[str, dict] = {} result: dict[str, dict] = {}
for expr in exprs: for expr in exprs:
@@ -293,7 +297,7 @@ def parse_boundary_effects() -> dict[str, list[str]]:
Pure primitives from primitives.sx are not included (they have no effects). Pure primitives from primitives.sx are not included (they have no effects).
""" """
source = _read_file("boundary.sx") source = _read_file("boundary.sx")
exprs = parse_all(source) exprs = _get_parse_all()(source)
result: dict[str, list[str]] = {} result: dict[str, list[str]] = {}
_DECL_FORMS = { _DECL_FORMS = {
@@ -338,7 +342,7 @@ def parse_boundary_effects() -> dict[str, list[str]]:
def parse_boundary_types() -> frozenset[str]: def parse_boundary_types() -> frozenset[str]:
"""Parse boundary.sx and return the declared boundary type names.""" """Parse boundary.sx and return the declared boundary type names."""
source = _read_file("boundary.sx") source = _read_file("boundary.sx")
exprs = parse_all(source) exprs = _get_parse_all()(source)
for expr in exprs: for expr in exprs:
if (isinstance(expr, list) and len(expr) >= 2 if (isinstance(expr, list) and len(expr) >= 2
and isinstance(expr[0], Symbol) and isinstance(expr[0], Symbol)

View File

@@ -407,6 +407,16 @@
(first args) env (first args) env
(kont-push (make-deref-frame env) kont)))) (kont-push (make-deref-frame env) kont))))
;; cek-call — call a function via CEK (replaces invoke)
(define cek-call
(fn (f args)
(let ((a (if (nil? args) (list) args)))
(cond
(nil? f) nil
(lambda? f) (cek-run (continue-with-call f a (dict) a (list)))
(callable? f) (apply f a)
:else nil))))
;; reactive-shift-deref: the heart of deref-as-shift ;; reactive-shift-deref: the heart of deref-as-shift
;; When deref encounters a signal inside a reactive-reset boundary, ;; When deref encounters a signal inside a reactive-reset boundary,
;; capture the continuation up to the reactive-reset as the subscriber. ;; capture the continuation up to the reactive-reset as the subscriber.
@@ -422,7 +432,7 @@
(let ((subscriber (let ((subscriber
(fn () (fn ()
;; Dispose previous nested subscribers ;; Dispose previous nested subscribers
(for-each (fn (d) (invoke d)) sub-disposers) (for-each (fn (d) (cek-call d nil)) sub-disposers)
(set! sub-disposers (list)) (set! sub-disposers (list))
;; Re-invoke: push fresh ReactiveResetFrame (first-render=false) ;; Re-invoke: push fresh ReactiveResetFrame (first-render=false)
(let ((new-reset (make-reactive-reset-frame env update-fn false)) (let ((new-reset (make-reactive-reset-frame env update-fn false))
@@ -440,7 +450,7 @@
(register-in-scope (register-in-scope
(fn () (fn ()
(signal-remove-sub! sig subscriber) (signal-remove-sub! sig subscriber)
(for-each (fn (d) (invoke d)) sub-disposers))) (for-each (fn (d) (cek-call d nil)) sub-disposers)))
;; Initial render: value flows through captured frames + reset (first-render=true) ;; Initial render: value flows through captured frames + reset (first-render=true)
;; so the full expression completes normally ;; so the full expression completes normally
(let ((initial-kont (concat captured-frames (let ((initial-kont (concat captured-frames
@@ -782,7 +792,7 @@
(first? (get frame "first-render"))) (first? (get frame "first-render")))
;; On re-render (not first), call update-fn with new value ;; On re-render (not first), call update-fn with new value
(when (and update-fn (not first?)) (when (and update-fn (not first?))
(invoke update-fn value)) (cek-call update-fn (list value)))
(make-cek-value value env rest-k)) (make-cek-value value env rest-k))
;; --- ScopeFrame: body result --- ;; --- ScopeFrame: body result ---

View File

@@ -655,7 +655,7 @@
(fn ((s :as string)) (fn ((s :as string))
(str "\"" (str "\""
(replace (replace (replace (replace (replace (replace (replace (replace (replace (replace (replace (replace
s "\\" "\\\\") "\"" "\\\"") "\n" "\\n") "\r" "\\r") "\t" "\\t") "\0" "\\0") s "\\" "\\\\") "\"" "\\\"") "\n" "\\n") "\r" "\\r") "\t" "\\t") (char-from-code 0) "\\u0000")
"\""))) "\"")))

View File

@@ -25,6 +25,7 @@
;; comment → ';' to end of line (discarded) ;; comment → ';' to end of line (discarded)
;; ;;
;; Quote sugar: ;; Quote sugar:
;; 'expr → (quote expr)
;; `expr → (quasiquote expr) ;; `expr → (quasiquote expr)
;; ,expr → (unquote expr) ;; ,expr → (unquote expr)
;; ,@expr → (splice-unquote expr) ;; ,@expr → (splice-unquote expr)
@@ -267,6 +268,11 @@
(= ch ":") (= ch ":")
(read-keyword) (read-keyword)
;; Quote sugar
(= ch "'")
(do (set! pos (inc pos))
(list (make-symbol "quote") (read-expr)))
;; Quasiquote sugar ;; Quasiquote sugar
(= ch "`") (= ch "`")
(do (set! pos (inc pos)) (do (set! pos (inc pos))
@@ -395,7 +401,7 @@
;; True for: a-z A-Z _ ~ * + - > < = / ! ? & ;; True for: a-z A-Z _ ~ * + - > < = / ! ? &
;; ;;
;; (ident-char? ch) → boolean ;; (ident-char? ch) → boolean
;; True for: ident-start chars plus: 0-9 . : / [ ] # , ;; True for: ident-start chars plus: 0-9 . : / # ,
;; ;;
;; Constructors (provided by the SX runtime): ;; Constructors (provided by the SX runtime):
;; (make-symbol name) → Symbol value ;; (make-symbol name) → Symbol value

View File

@@ -52,7 +52,7 @@ SPEC_MODULES = {
# Explicit ordering for spec modules with dependencies. # Explicit ordering for spec modules with dependencies.
# Modules listed here are emitted in this order; any not listed use alphabetical. # Modules listed here are emitted in this order; any not listed use alphabetical.
SPEC_MODULE_ORDER = ["deps", "frames", "page-helpers", "router", "signals", "cek"] SPEC_MODULE_ORDER = ["deps", "frames", "page-helpers", "router", "cek", "signals"]
EXTENSION_NAMES = {"continuations"} EXTENSION_NAMES = {"continuations"}
@@ -1004,7 +1004,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
PRIMITIVES["rest"] = function(c) { if (c && typeof c.slice !== "function") { console.error("[sx-debug] rest called on non-sliceable:", typeof c, c, new Error().stack); return []; } return c ? c.slice(1) : []; }; PRIMITIVES["rest"] = function(c) { if (c && typeof c.slice !== "function") { console.error("[sx-debug] rest called on non-sliceable:", typeof c, c, new Error().stack); return []; } return c ? c.slice(1) : []; };
PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; }; PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); }; PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); };
PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); }; PRIMITIVES["append"] = function(c, x) { return (c || []).concat(Array.isArray(x) ? x : [x]); };
PRIMITIVES["append!"] = function(arr, x) { arr.push(x); return arr; }; PRIMITIVES["append!"] = function(arr, x) { arr.push(x); return arr; };
PRIMITIVES["chunk-every"] = function(c, n) { PRIMITIVES["chunk-every"] = function(c, n) {
var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r; var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r;
@@ -1487,6 +1487,14 @@ PLATFORM_CEK_JS = '''
// Platform: CEK module — explicit CEK machine // Platform: CEK module — explicit CEK machine
// ========================================================================= // =========================================================================
// Continuation type (needed by CEK even without the tree-walk shift/reset extension)
if (typeof Continuation === "undefined") {
function Continuation(fn) { this.fn = fn; }
Continuation.prototype._continuation = true;
Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); };
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
}
// Standalone aliases for primitives used by cek.sx / frames.sx // Standalone aliases for primitives used by cek.sx / frames.sx
var inc = PRIMITIVES["inc"]; var inc = PRIMITIVES["inc"];
var dec = PRIMITIVES["dec"]; var dec = PRIMITIVES["dec"];
@@ -1608,10 +1616,10 @@ PLATFORM_PARSER_JS = r"""
// ========================================================================= // =========================================================================
// Character classification derived from the grammar: // Character classification derived from the grammar:
// ident-start → [a-zA-Z_~*+\-><=/!?&] // ident-start → [a-zA-Z_~*+\-><=/!?&]
// ident-char → ident-start + [0-9.:\/\[\]#,] // ident-char → ident-start + [0-9.:\/\#,]
var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/; var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/;
var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]/; var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/#,]/;
function isIdentStart(ch) { return _identStartRe.test(ch); } function isIdentStart(ch) { return _identStartRe.test(ch); }
function isIdentChar(ch) { return _identCharRe.test(ch); } function isIdentChar(ch) { return _identCharRe.test(ch); }
@@ -2436,6 +2444,10 @@ PLATFORM_ORCHESTRATION_JS = """
} }
function scheduleIdle(fn) { function scheduleIdle(fn) {
var cb = _wrapSxFn(fn); var cb = _wrapSxFn(fn);
if (typeof cb !== "function") {
console.error("[sx-ref] scheduleIdle: callback not callable, fn type:", typeof fn, "fn:", fn, "_lambda:", fn && fn._lambda);
return;
}
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(cb); if (typeof requestIdleCallback !== "undefined") requestIdleCallback(cb);
else setTimeout(cb, 0); else setTimeout(cb, 0);
} }
@@ -2525,8 +2537,12 @@ PLATFORM_ORCHESTRATION_JS = """
e.preventDefault(); e.preventDefault();
// Re-read href from element at click time (not closed-over value) // Re-read href from element at click time (not closed-over value)
var liveHref = el.getAttribute("href") || _href; var liveHref = el.getAttribute("href") || _href;
console.log("[sx-debug] bindBoostLink click:", liveHref, "el:", el.tagName, el.textContent.slice(0,30));
executeRequest(el, { method: "GET", url: liveHref }).then(function() { executeRequest(el, { method: "GET", url: liveHref }).then(function() {
console.log("[sx-debug] boost fetch OK, pushState:", liveHref);
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
}).catch(function(err) {
console.error("[sx-debug] boost fetch ERROR:", err);
}); });
}); });
} }
@@ -2551,21 +2567,25 @@ PLATFORM_ORCHESTRATION_JS = """
// Re-read href from element at click time (not closed-over value) // Re-read href from element at click time (not closed-over value)
var liveHref = link.getAttribute("href") || _href; var liveHref = link.getAttribute("href") || _href;
var pathname = urlPathname(liveHref); var pathname = urlPathname(liveHref);
console.log("[sx-debug] bindClientRouteClick:", pathname, "el:", link.tagName, link.textContent.slice(0,30));
// Find target selector: sx-boost ancestor, explicit sx-target, or #main-panel // Find target selector: sx-boost ancestor, explicit sx-target, or #main-panel
var boostEl = link.closest("[sx-boost]"); var boostEl = link.closest("[sx-boost]");
var targetSel = boostEl ? boostEl.getAttribute("sx-boost") : null; var targetSel = boostEl ? boostEl.getAttribute("sx-boost") : null;
if (!targetSel || targetSel === "true") { if (!targetSel || targetSel === "true") {
targetSel = link.getAttribute("sx-target") || "#main-panel"; targetSel = link.getAttribute("sx-target") || "#main-panel";
} }
console.log("[sx-debug] targetSel:", targetSel, "trying client route...");
if (tryClientRoute(pathname, targetSel)) { if (tryClientRoute(pathname, targetSel)) {
console.log("[sx-debug] client route SUCCESS, pushState:", liveHref);
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
if (typeof window !== "undefined") window.scrollTo(0, 0); if (typeof window !== "undefined") window.scrollTo(0, 0);
} else { } else {
logInfo("sx:route server " + pathname); console.log("[sx-debug] client route FAILED, server fetch:", liveHref);
executeRequest(link, { method: "GET", url: liveHref }).then(function() { executeRequest(link, { method: "GET", url: liveHref }).then(function() {
console.log("[sx-debug] server fetch OK, pushState:", liveHref);
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {}
}).catch(function(err) { }).catch(function(err) {
logWarn("sx:route server fetch error: " + (err && err.message ? err.message : err)); console.error("[sx-debug] server fetch ERROR:", err);
}); });
} }
}); });

View File

@@ -1038,7 +1038,7 @@ PLATFORM_PARSER_PY = '''
import re as _re_parser import re as _re_parser
_IDENT_START_RE = _re_parser.compile(r"[a-zA-Z_~*+\\-><=/!?&]") _IDENT_START_RE = _re_parser.compile(r"[a-zA-Z_~*+\\-><=/!?&]")
_IDENT_CHAR_RE = _re_parser.compile(r"[a-zA-Z0-9_~*+\\-><=/!?.:&/\\[\\]#,]") _IDENT_CHAR_RE = _re_parser.compile(r"[a-zA-Z0-9_~*+\\-><=/!?.:&/#,]")
def ident_start_p(ch): def ident_start_p(ch):
@@ -1626,7 +1626,7 @@ SPEC_MODULES = {
# Explicit ordering for spec modules with dependencies. # Explicit ordering for spec modules with dependencies.
# Modules listed here are emitted in this order; any not listed use alphabetical. # Modules listed here are emitted in this order; any not listed use alphabetical.
SPEC_MODULE_ORDER = [ SPEC_MODULE_ORDER = [
"deps", "engine", "frames", "page-helpers", "router", "signals", "types", "cek", "deps", "engine", "frames", "page-helpers", "router", "cek", "signals", "types",
] ]
EXTENSION_NAMES = {"continuations"} EXTENSION_NAMES = {"continuations"}

View File

@@ -7,7 +7,7 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", "..")) _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT) sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all from shared.sx.ref.sx_ref import sx_parse as parse_all
from shared.sx.ref import sx_ref from shared.sx.ref import sx_ref
from shared.sx.ref.sx_ref import ( from shared.sx.ref.sx_ref import (
make_env, env_get, env_has, env_set, make_env, env_get, env_has, env_set,

View File

@@ -32,8 +32,8 @@ try:
finally: finally:
os.unlink(tmp.name) os.unlink(tmp.name)
from shared.sx.parser import parse_all
from shared.sx.types import NIL from shared.sx.types import NIL
parse_all = mod.sx_parse
# Use tree-walk evaluator for interpreting .sx test files. # Use tree-walk evaluator for interpreting .sx test files.
# CEK is now the default, but test runners need tree-walk so that # CEK is now the default, but test runners need tree-walk so that

View File

@@ -190,6 +190,10 @@ def compile_ref_to_js(
if has_parser: if has_parser:
parts.append(adapter_platform["parser"]) parts.append(adapter_platform["parser"])
# CEK platform aliases must come before transpiled cek.sx (which uses them)
if has_cek:
parts.append(PLATFORM_CEK_JS)
# Translate each spec file using js.sx # Translate each spec file using js.sx
for filename, label in sx_files: for filename, label in sx_files:
filepath = os.path.join(ref_dir, filename) filepath = os.path.join(ref_dir, filename)
@@ -216,9 +220,6 @@ def compile_ref_to_js(
if name in adapter_set and name in adapter_platform: if name in adapter_set and name in adapter_platform:
parts.append(adapter_platform[name]) parts.append(adapter_platform[name])
if has_cek:
parts.append(PLATFORM_CEK_JS)
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps, has_page_helpers)) parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps, has_page_helpers))
if has_cek: if has_cek:
parts.append(CEK_FIXUPS_JS) parts.append(CEK_FIXUPS_JS)

View File

@@ -12,7 +12,7 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", "..")) _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT) sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all from shared.sx.ref.sx_ref import sx_parse as parse_all
from shared.sx.ref import sx_ref from shared.sx.ref import sx_ref
from shared.sx.ref.sx_ref import make_env, scope_push, scope_pop, sx_context from shared.sx.ref.sx_ref import make_env, scope_push, scope_pop, sx_context
from shared.sx.types import NIL, Island, Lambda from shared.sx.types import NIL, Island, Lambda

View File

@@ -7,7 +7,7 @@ _HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", "..")) _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT) sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all from shared.sx.ref.sx_ref import sx_parse as parse_all
from shared.sx.ref import sx_ref from shared.sx.ref import sx_ref
from shared.sx.ref.sx_ref import make_env, env_get, env_has, env_set from shared.sx.ref.sx_ref import make_env, env_get, env_has, env_set
from shared.sx.types import NIL, Component from shared.sx.types import NIL, Component

View File

@@ -23,14 +23,11 @@
;; (scope-pop! "sx-reactive") → void ;; (scope-pop! "sx-reactive") → void
;; (context "sx-reactive" nil) → dict or nil ;; (context "sx-reactive" nil) → dict or nil
;; ;;
;; Runtime callable dispatch: ;; CEK callable dispatch:
;; (invoke f &rest args) → any — call f with args; handles both ;; (cek-call f args) → any — call f with args list via CEK.
;; native host functions AND SX lambdas ;; Dispatches through cek-run for SX
;; from runtime-evaluated code (islands). ;; lambdas, apply for native callables.
;; Transpiled code emits direct calls ;; Defined in cek.sx.
;; f(args) which fail on SX lambdas.
;; invoke goes through the evaluator's
;; dispatch (call-fn) so either works.
;; ;;
;; ========================================================================== ;; ==========================================================================
@@ -150,7 +147,7 @@
;; Push scope-based tracking context for this computed ;; Push scope-based tracking context for this computed
(let ((ctx (dict "deps" (list) "notify" recompute))) (let ((ctx (dict "deps" (list) "notify" recompute)))
(scope-push! "sx-reactive" ctx) (scope-push! "sx-reactive" ctx)
(let ((new-val (invoke compute-fn))) (let ((new-val (cek-call compute-fn nil)))
(scope-pop! "sx-reactive") (scope-pop! "sx-reactive")
;; Save discovered deps ;; Save discovered deps
(signal-set-deps! s (get ctx "deps")) (signal-set-deps! s (get ctx "deps"))
@@ -184,7 +181,7 @@
(fn () (fn ()
(when (not disposed) (when (not disposed)
;; Run previous cleanup if any ;; Run previous cleanup if any
(when cleanup-fn (invoke cleanup-fn)) (when cleanup-fn (cek-call cleanup-fn nil))
;; Unsubscribe from old deps ;; Unsubscribe from old deps
(for-each (for-each
@@ -195,7 +192,7 @@
;; Push scope-based tracking context ;; Push scope-based tracking context
(let ((ctx (dict "deps" (list) "notify" run-effect))) (let ((ctx (dict "deps" (list) "notify" run-effect)))
(scope-push! "sx-reactive" ctx) (scope-push! "sx-reactive" ctx)
(let ((result (invoke effect-fn))) (let ((result (cek-call effect-fn nil)))
(scope-pop! "sx-reactive") (scope-pop! "sx-reactive")
(set! deps (get ctx "deps")) (set! deps (get ctx "deps"))
;; If effect returns a function, it's the cleanup ;; If effect returns a function, it's the cleanup
@@ -209,7 +206,7 @@
(let ((dispose-fn (let ((dispose-fn
(fn () (fn ()
(set! disposed true) (set! disposed true)
(when cleanup-fn (invoke cleanup-fn)) (when cleanup-fn (cek-call cleanup-fn nil))
(for-each (for-each
(fn ((dep :as signal)) (signal-remove-sub! dep run-effect)) (fn ((dep :as signal)) (signal-remove-sub! dep run-effect))
deps) deps)
@@ -232,7 +229,7 @@
(define batch :effects [mutation] (define batch :effects [mutation]
(fn ((thunk :as lambda)) (fn ((thunk :as lambda))
(set! *batch-depth* (+ *batch-depth* 1)) (set! *batch-depth* (+ *batch-depth* 1))
(invoke thunk) (cek-call thunk nil)
(set! *batch-depth* (- *batch-depth* 1)) (set! *batch-depth* (- *batch-depth* 1))
(when (= *batch-depth* 0) (when (= *batch-depth* 0)
(let ((queue *batch-queue*)) (let ((queue *batch-queue*))
@@ -323,7 +320,7 @@
(fn ((disposable :as lambda)) (fn ((disposable :as lambda))
(let ((collector (context "sx-island-scope" nil))) (let ((collector (context "sx-island-scope" nil)))
(when collector (when collector
(invoke collector disposable))))) (cek-call collector (list disposable))))))
;; ========================================================================== ;; ==========================================================================
@@ -362,7 +359,7 @@
;; Parent island scope and sibling marshes are unaffected. ;; Parent island scope and sibling marshes are unaffected.
(let ((disposers (dom-get-data marsh-el "sx-marsh-disposers"))) (let ((disposers (dom-get-data marsh-el "sx-marsh-disposers")))
(when disposers (when disposers
(for-each (fn ((d :as lambda)) (invoke d)) disposers) (for-each (fn ((d :as lambda)) (cek-call d nil)) disposers)
(dom-set-data marsh-el "sx-marsh-disposers" nil))))) (dom-set-data marsh-el "sx-marsh-disposers" nil)))))
@@ -384,7 +381,7 @@
(let ((registry *store-registry*)) (let ((registry *store-registry*))
;; Only create the store once — subsequent calls return existing ;; Only create the store once — subsequent calls return existing
(when (not (has-key? registry name)) (when (not (has-key? registry name))
(set! *store-registry* (assoc registry name (invoke init-fn)))) (set! *store-registry* (assoc registry name (cek-call init-fn nil))))
(get *store-registry* name)))) (get *store-registry* name))))
(define use-store :effects [] (define use-store :effects []
@@ -443,7 +440,7 @@
(fn (e) (fn (e)
(let ((detail (event-detail e)) (let ((detail (event-detail e))
(new-val (if transform-fn (new-val (if transform-fn
(invoke transform-fn detail) (cek-call transform-fn (list detail))
detail))) detail)))
(reset! target-signal new-val)))))) (reset! target-signal new-val))))))
;; Return cleanup — removes listener on dispose/re-run ;; Return cleanup — removes listener on dispose/re-run
@@ -474,7 +471,7 @@
(fn ((fetch-fn :as lambda)) (fn ((fetch-fn :as lambda))
(let ((state (signal (dict "loading" true "data" nil "error" nil)))) (let ((state (signal (dict "loading" true "data" nil "error" nil))))
;; Kick off the async operation ;; Kick off the async operation
(promise-then (invoke fetch-fn) (promise-then (cek-call fetch-fn nil)
(fn (data) (reset! state (dict "loading" false "data" data "error" nil))) (fn (data) (reset! state (dict "loading" false "data" data "error" nil)))
(fn (err) (reset! state (dict "loading" false "data" nil "error" err)))) (fn (err) (reset! state (dict "loading" false "data" nil "error" err))))
state))) state)))

View File

@@ -952,7 +952,7 @@ char_from_code = PRIMITIVES["char-from-code"]
import re as _re_parser import re as _re_parser
_IDENT_START_RE = _re_parser.compile(r"[a-zA-Z_~*+\-><=/!?&]") _IDENT_START_RE = _re_parser.compile(r"[a-zA-Z_~*+\-><=/!?&]")
_IDENT_CHAR_RE = _re_parser.compile(r"[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]") _IDENT_CHAR_RE = _re_parser.compile(r"[a-zA-Z0-9_~*+\-><=/!?.:&/#,]")
def ident_start_p(ch): def ident_start_p(ch):
@@ -2179,36 +2179,39 @@ def sx_parse(source):
_cells['pos'] = 0 _cells['pos'] = 0
len_src = len(source) len_src = len(source)
def skip_comment(): def skip_comment():
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (not sx_truthy((nth(source, _cells['pos']) == '\n'))))): while True:
_cells['pos'] = (_cells['pos'] + 1) if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (not sx_truthy((nth(source, _cells['pos']) == '\n'))))):
return skip_comment() _cells['pos'] = (_cells['pos'] + 1)
return NIL continue
break
def skip_ws(): def skip_ws():
if sx_truthy((_cells['pos'] < len_src)): while True:
ch = nth(source, _cells['pos']) if sx_truthy((_cells['pos'] < len_src)):
if sx_truthy(((ch == ' ') if sx_truthy((ch == ' ')) else ((ch == '\t') if sx_truthy((ch == '\t')) else ((ch == '\n') if sx_truthy((ch == '\n')) else (ch == '\r'))))): ch = nth(source, _cells['pos'])
_cells['pos'] = (_cells['pos'] + 1) if sx_truthy(((ch == ' ') if sx_truthy((ch == ' ')) else ((ch == '\t') if sx_truthy((ch == '\t')) else ((ch == '\n') if sx_truthy((ch == '\n')) else (ch == '\r'))))):
return skip_ws() _cells['pos'] = (_cells['pos'] + 1)
elif sx_truthy((ch == ';')): continue
_cells['pos'] = (_cells['pos'] + 1) elif sx_truthy((ch == ';')):
skip_comment() _cells['pos'] = (_cells['pos'] + 1)
return skip_ws() skip_comment()
else: continue
return NIL else:
return NIL break
break
def hex_digit_value(ch): def hex_digit_value(ch):
return index_of('0123456789abcdef', lower(ch)) return index_of('0123456789abcdef', lower(ch))
def read_string(): def read_string():
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
_cells['buf'] = '' _cells['buf'] = ''
def read_str_loop(): while True:
if sx_truthy((_cells['pos'] >= len_src)): if sx_truthy((_cells['pos'] >= len_src)):
return error('Unterminated string') error('Unterminated string')
break
else: else:
ch = nth(source, _cells['pos']) ch = nth(source, _cells['pos'])
if sx_truthy((ch == '"')): if sx_truthy((ch == '"')):
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
return NIL break
elif sx_truthy((ch == '\\')): elif sx_truthy((ch == '\\')):
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
esc = nth(source, _cells['pos']) esc = nth(source, _cells['pos'])
@@ -2223,25 +2226,23 @@ def sx_parse(source):
d3 = hex_digit_value(nth(source, _cells['pos'])) d3 = hex_digit_value(nth(source, _cells['pos']))
_ = _sx_cell_set(_cells, 'pos', (_cells['pos'] + 1)) _ = _sx_cell_set(_cells, 'pos', (_cells['pos'] + 1))
_cells['buf'] = sx_str(_cells['buf'], char_from_code(((d0 * 4096) + (d1 * 256)))) _cells['buf'] = sx_str(_cells['buf'], char_from_code(((d0 * 4096) + (d1 * 256))))
return read_str_loop() continue
else: else:
_cells['buf'] = sx_str(_cells['buf'], ('\n' if sx_truthy((esc == 'n')) else ('\t' if sx_truthy((esc == 't')) else ('\r' if sx_truthy((esc == 'r')) else esc)))) _cells['buf'] = sx_str(_cells['buf'], ('\n' if sx_truthy((esc == 'n')) else ('\t' if sx_truthy((esc == 't')) else ('\r' if sx_truthy((esc == 'r')) else esc))))
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
return read_str_loop() continue
else: else:
_cells['buf'] = sx_str(_cells['buf'], ch) _cells['buf'] = sx_str(_cells['buf'], ch)
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
return read_str_loop() continue
read_str_loop()
return _cells['buf'] return _cells['buf']
def read_ident(): def read_ident():
start = _cells['pos'] start = _cells['pos']
def read_ident_loop(): while True:
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else ident_char_p(nth(source, _cells['pos'])))): if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else ident_char_p(nth(source, _cells['pos'])))):
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
return read_ident_loop() continue
return NIL break
read_ident_loop()
return slice(source, start, _cells['pos']) return slice(source, start, _cells['pos'])
def read_keyword(): def read_keyword():
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
@@ -2251,10 +2252,11 @@ def sx_parse(source):
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (nth(source, _cells['pos']) == '-'))): if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (nth(source, _cells['pos']) == '-'))):
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
def read_digits(): def read_digits():
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (lambda c: ((c >= '0') if not sx_truthy((c >= '0')) else (c <= '9')))(nth(source, _cells['pos'])))): while True:
_cells['pos'] = (_cells['pos'] + 1) if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (lambda c: ((c >= '0') if not sx_truthy((c >= '0')) else (c <= '9')))(nth(source, _cells['pos'])))):
return read_digits() _cells['pos'] = (_cells['pos'] + 1)
return NIL continue
break
read_digits() read_digits()
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (nth(source, _cells['pos']) == '.'))): if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (nth(source, _cells['pos']) == '.'))):
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
@@ -2277,52 +2279,52 @@ def sx_parse(source):
return make_symbol(name) return make_symbol(name)
def read_list(close_ch): def read_list(close_ch):
items = [] items = []
def read_list_loop(): while True:
skip_ws() skip_ws()
if sx_truthy((_cells['pos'] >= len_src)): if sx_truthy((_cells['pos'] >= len_src)):
return error('Unterminated list') error('Unterminated list')
break
else: else:
if sx_truthy((nth(source, _cells['pos']) == close_ch)): if sx_truthy((nth(source, _cells['pos']) == close_ch)):
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
return NIL break
else: else:
items.append(read_expr()) items.append(read_expr())
return read_list_loop() continue
read_list_loop()
return items return items
def read_map(): def read_map():
result = {} result = {}
def read_map_loop(): while True:
skip_ws() skip_ws()
if sx_truthy((_cells['pos'] >= len_src)): if sx_truthy((_cells['pos'] >= len_src)):
return error('Unterminated map') error('Unterminated map')
break
else: else:
if sx_truthy((nth(source, _cells['pos']) == '}')): if sx_truthy((nth(source, _cells['pos']) == '}')):
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
return NIL break
else: else:
key_expr = read_expr() key_expr = read_expr()
key_str = (keyword_name(key_expr) if sx_truthy((type_of(key_expr) == 'keyword')) else sx_str(key_expr)) key_str = (keyword_name(key_expr) if sx_truthy((type_of(key_expr) == 'keyword')) else sx_str(key_expr))
val_expr = read_expr() val_expr = read_expr()
result[key_str] = val_expr result[key_str] = val_expr
return read_map_loop() continue
read_map_loop()
return result return result
def read_raw_string(): def read_raw_string():
_cells['buf'] = '' _cells['buf'] = ''
def raw_loop(): while True:
if sx_truthy((_cells['pos'] >= len_src)): if sx_truthy((_cells['pos'] >= len_src)):
return error('Unterminated raw string') error('Unterminated raw string')
break
else: else:
ch = nth(source, _cells['pos']) ch = nth(source, _cells['pos'])
if sx_truthy((ch == '|')): if sx_truthy((ch == '|')):
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
return NIL break
else: else:
_cells['buf'] = sx_str(_cells['buf'], ch) _cells['buf'] = sx_str(_cells['buf'], ch)
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
return raw_loop() continue
raw_loop()
return _cells['buf'] return _cells['buf']
def read_expr(): def read_expr():
skip_ws() skip_ws()
@@ -2343,6 +2345,9 @@ def sx_parse(source):
return read_string() return read_string()
elif sx_truthy((ch == ':')): elif sx_truthy((ch == ':')):
return read_keyword() return read_keyword()
elif sx_truthy((ch == "'")):
_cells['pos'] = (_cells['pos'] + 1)
return [make_symbol('quote'), read_expr()]
elif sx_truthy((ch == '`')): elif sx_truthy((ch == '`')):
_cells['pos'] = (_cells['pos'] + 1) _cells['pos'] = (_cells['pos'] + 1)
return [make_symbol('quasiquote'), read_expr()] return [make_symbol('quasiquote'), read_expr()]
@@ -2388,13 +2393,12 @@ def sx_parse(source):
else: else:
return error(sx_str('Unexpected character: ', ch)) return error(sx_str('Unexpected character: ', ch))
exprs = [] exprs = []
def parse_loop(): while True:
skip_ws() skip_ws()
if sx_truthy((_cells['pos'] < len_src)): if sx_truthy((_cells['pos'] < len_src)):
exprs.append(read_expr()) exprs.append(read_expr())
return parse_loop() continue
return NIL break
parse_loop()
return exprs return exprs
# sx-serialize # sx-serialize
@@ -3862,234 +3866,6 @@ def prepare_url_expr(url_path, env):
return auto_quote_unknowns(expr, env) return auto_quote_unknowns(expr, env)
# === Transpiled from signals (reactive signal runtime) ===
# make-signal
def make_signal(value):
return {'__signal': True, 'value': value, 'subscribers': [], 'deps': []}
# signal?
def is_signal(x):
return (dict_p(x) if not sx_truthy(dict_p(x)) else has_key_p(x, '__signal'))
# signal-value
def signal_value(s):
return get(s, 'value')
# signal-set-value!
def signal_set_value(s, v):
return _sx_dict_set(s, 'value', v)
# signal-subscribers
def signal_subscribers(s):
return get(s, 'subscribers')
# signal-add-sub!
def signal_add_sub(s, f):
if sx_truthy((not sx_truthy(contains_p(get(s, 'subscribers'), f)))):
return _sx_append(get(s, 'subscribers'), f)
return NIL
# signal-remove-sub!
def signal_remove_sub(s, f):
return _sx_dict_set(s, 'subscribers', filter(lambda sub: (not sx_truthy(is_identical(sub, f))), get(s, 'subscribers')))
# signal-deps
def signal_deps(s):
return get(s, 'deps')
# signal-set-deps!
def signal_set_deps(s, deps):
return _sx_dict_set(s, 'deps', deps)
# signal
def signal(initial_value):
return make_signal(initial_value)
# deref
def deref(s):
if sx_truthy((not sx_truthy(is_signal(s)))):
return s
else:
ctx = sx_context('sx-reactive', NIL)
if sx_truthy(ctx):
dep_list = get(ctx, 'deps')
notify_fn = get(ctx, 'notify')
if sx_truthy((not sx_truthy(contains_p(dep_list, s)))):
dep_list.append(s)
signal_add_sub(s, notify_fn)
return signal_value(s)
# reset!
def reset_b(s, value):
if sx_truthy(is_signal(s)):
old = signal_value(s)
if sx_truthy((not sx_truthy(is_identical(old, value)))):
signal_set_value(s, value)
return notify_subscribers(s)
return NIL
return NIL
# swap!
def swap_b(s, f, *args):
if sx_truthy(is_signal(s)):
old = signal_value(s)
new_val = apply(f, cons(old, args))
if sx_truthy((not sx_truthy(is_identical(old, new_val)))):
signal_set_value(s, new_val)
return notify_subscribers(s)
return NIL
return NIL
# computed
def computed(compute_fn):
s = make_signal(NIL)
deps = []
compute_ctx = NIL
recompute = _sx_fn(lambda : (
for_each(lambda dep: signal_remove_sub(dep, recompute), signal_deps(s)),
signal_set_deps(s, []),
(lambda ctx: _sx_begin(scope_push('sx-reactive', ctx), (lambda new_val: _sx_begin(scope_pop('sx-reactive'), signal_set_deps(s, get(ctx, 'deps')), (lambda old: _sx_begin(signal_set_value(s, new_val), (notify_subscribers(s) if sx_truthy((not sx_truthy(is_identical(old, new_val)))) else NIL)))(signal_value(s))))(invoke(compute_fn))))({'deps': [], 'notify': recompute})
)[-1])
recompute()
register_in_scope(lambda : dispose_computed(s))
return s
# effect
def effect(effect_fn):
_cells = {}
_cells['deps'] = []
_cells['disposed'] = False
_cells['cleanup_fn'] = NIL
run_effect = lambda : (_sx_begin((invoke(_cells['cleanup_fn']) if sx_truthy(_cells['cleanup_fn']) else NIL), for_each(lambda dep: signal_remove_sub(dep, run_effect), _cells['deps']), _sx_cell_set(_cells, 'deps', []), (lambda ctx: _sx_begin(scope_push('sx-reactive', ctx), (lambda result: _sx_begin(scope_pop('sx-reactive'), _sx_cell_set(_cells, 'deps', get(ctx, 'deps')), (_sx_cell_set(_cells, 'cleanup_fn', result) if sx_truthy(is_callable(result)) else NIL)))(invoke(effect_fn))))({'deps': [], 'notify': run_effect})) if sx_truthy((not sx_truthy(_cells['disposed']))) else NIL)
run_effect()
dispose_fn = _sx_fn(lambda : (
_sx_cell_set(_cells, 'disposed', True),
(invoke(_cells['cleanup_fn']) if sx_truthy(_cells['cleanup_fn']) else NIL),
for_each(lambda dep: signal_remove_sub(dep, run_effect), _cells['deps']),
_sx_cell_set(_cells, 'deps', [])
)[-1])
register_in_scope(dispose_fn)
return dispose_fn
# *batch-depth*
_batch_depth = 0
# *batch-queue*
_batch_queue = []
# batch
def batch(thunk):
_batch_depth = (_batch_depth + 1)
invoke(thunk)
_batch_depth = (_batch_depth - 1)
if sx_truthy((_batch_depth == 0)):
queue = _batch_queue
_batch_queue = []
seen = []
pending = []
for s in queue:
for sub in signal_subscribers(s):
if sx_truthy((not sx_truthy(contains_p(seen, sub)))):
seen.append(sub)
pending.append(sub)
for sub in pending:
sub()
return NIL
return NIL
# notify-subscribers
def notify_subscribers(s):
if sx_truthy((_batch_depth > 0)):
if sx_truthy((not sx_truthy(contains_p(_batch_queue, s)))):
return _sx_append(_batch_queue, s)
return NIL
else:
return flush_subscribers(s)
# flush-subscribers
def flush_subscribers(s):
for sub in signal_subscribers(s):
sub()
return NIL
# dispose-computed
def dispose_computed(s):
if sx_truthy(is_signal(s)):
for dep in signal_deps(s):
signal_remove_sub(dep, NIL)
return signal_set_deps(s, [])
return NIL
# with-island-scope
def with_island_scope(scope_fn, body_fn):
scope_push('sx-island-scope', scope_fn)
result = body_fn()
scope_pop('sx-island-scope')
return result
# register-in-scope
def register_in_scope(disposable):
collector = sx_context('sx-island-scope', NIL)
if sx_truthy(collector):
return invoke(collector, disposable)
return NIL
# with-marsh-scope
def with_marsh_scope(marsh_el, body_fn):
disposers = []
with_island_scope(lambda d: _sx_append(disposers, d), body_fn)
return dom_set_data(marsh_el, 'sx-marsh-disposers', disposers)
# dispose-marsh-scope
def dispose_marsh_scope(marsh_el):
disposers = dom_get_data(marsh_el, 'sx-marsh-disposers')
if sx_truthy(disposers):
for d in disposers:
invoke(d)
return dom_set_data(marsh_el, 'sx-marsh-disposers', NIL)
return NIL
# *store-registry*
_store_registry = {}
# def-store
def def_store(name, init_fn):
registry = _store_registry
if sx_truthy((not sx_truthy(has_key_p(registry, name)))):
_store_registry = assoc(registry, name, invoke(init_fn))
return get(_store_registry, name)
# use-store
def use_store(name):
if sx_truthy(has_key_p(_store_registry, name)):
return get(_store_registry, name)
else:
return error(sx_str('Store not found: ', name, '. Call (def-store ...) before (use-store ...).'))
# clear-stores
def clear_stores():
return _sx_cell_set(_cells, '_store_registry', {})
# emit-event
def emit_event(el, event_name, detail):
return dom_dispatch(el, event_name, detail)
# on-event
def on_event(el, event_name, handler):
return dom_listen(el, event_name, handler)
# bridge-event
def bridge_event(el, event_name, target_signal, transform_fn):
return effect(lambda : (lambda remove: remove)(dom_listen(el, event_name, lambda e: (lambda detail: (lambda new_val: reset_b(target_signal, new_val))((invoke(transform_fn, detail) if sx_truthy(transform_fn) else detail)))(event_detail(e)))))
# resource
def resource(fetch_fn):
state = signal({'loading': True, 'data': NIL, 'error': NIL})
promise_then(invoke(fetch_fn), lambda data: reset_b(state, {'loading': False, 'data': data, 'error': NIL}), lambda err: reset_b(state, {'loading': False, 'data': NIL, 'error': err}))
return state
# === Transpiled from cek (explicit CEK machine evaluator) === # === Transpiled from cek (explicit CEK machine evaluator) ===
# cek-run # cek-run
@@ -4371,6 +4147,18 @@ def step_sf_shift(args, env, kont):
def step_sf_deref(args, env, kont): def step_sf_deref(args, env, kont):
return make_cek_state(first(args), env, kont_push(make_deref_frame(env), kont)) return make_cek_state(first(args), env, kont_push(make_deref_frame(env), kont))
# cek-call
def cek_call(f, args):
a = ([] if sx_truthy(is_nil(args)) else args)
if sx_truthy(is_nil(f)):
return NIL
elif sx_truthy(is_lambda(f)):
return cek_run(continue_with_call(f, a, {}, a, []))
elif sx_truthy(is_callable(f)):
return apply(f, a)
else:
return NIL
# reactive-shift-deref # reactive-shift-deref
def reactive_shift_deref(sig, env, kont): def reactive_shift_deref(sig, env, kont):
_cells = {} _cells = {}
@@ -4381,14 +4169,14 @@ def reactive_shift_deref(sig, env, kont):
update_fn = get(reset_frame, 'update-fn') update_fn = get(reset_frame, 'update-fn')
_cells['sub_disposers'] = [] _cells['sub_disposers'] = []
subscriber = _sx_fn(lambda : ( subscriber = _sx_fn(lambda : (
for_each(lambda d: invoke(d), _cells['sub_disposers']), for_each(lambda d: cek_call(d, NIL), _cells['sub_disposers']),
_sx_cell_set(_cells, 'sub_disposers', []), _sx_cell_set(_cells, 'sub_disposers', []),
(lambda new_reset: (lambda new_kont: with_island_scope(lambda d: _sx_append(_cells['sub_disposers'], d), lambda : cek_run(make_cek_value(signal_value(sig), env, new_kont))))(concat(captured_frames, [new_reset], remaining_kont)))(make_reactive_reset_frame(env, update_fn, False)) (lambda new_reset: (lambda new_kont: with_island_scope(lambda d: _sx_append(_cells['sub_disposers'], d), lambda : cek_run(make_cek_value(signal_value(sig), env, new_kont))))(concat(captured_frames, [new_reset], remaining_kont)))(make_reactive_reset_frame(env, update_fn, False))
)[-1]) )[-1])
signal_add_sub(sig, subscriber) signal_add_sub(sig, subscriber)
register_in_scope(_sx_fn(lambda : ( register_in_scope(_sx_fn(lambda : (
signal_remove_sub(sig, subscriber), signal_remove_sub(sig, subscriber),
for_each(lambda d: invoke(d), _cells['sub_disposers']) for_each(lambda d: cek_call(d, NIL), _cells['sub_disposers'])
)[-1])) )[-1]))
initial_kont = concat(captured_frames, [reset_frame], remaining_kont) initial_kont = concat(captured_frames, [reset_frame], remaining_kont)
return make_cek_value(signal_value(sig), env, initial_kont) return make_cek_value(signal_value(sig), env, initial_kont)
@@ -4610,7 +4398,7 @@ def step_continue(state):
update_fn = get(frame, 'update-fn') update_fn = get(frame, 'update-fn')
first_p = get(frame, 'first-render') first_p = get(frame, 'first-render')
if sx_truthy((update_fn if not sx_truthy(update_fn) else (not sx_truthy(first_p)))): if sx_truthy((update_fn if not sx_truthy(update_fn) else (not sx_truthy(first_p)))):
invoke(update_fn, value) cek_call(update_fn, [value])
return make_cek_value(value, env, rest_k) return make_cek_value(value, env, rest_k)
elif sx_truthy((ft == 'scope')): elif sx_truthy((ft == 'scope')):
name = get(frame, 'name') name = get(frame, 'name')
@@ -4686,6 +4474,234 @@ def trampoline_cek(val):
return val return val
# === Transpiled from signals (reactive signal runtime) ===
# make-signal
def make_signal(value):
return {'__signal': True, 'value': value, 'subscribers': [], 'deps': []}
# signal?
def is_signal(x):
return (dict_p(x) if not sx_truthy(dict_p(x)) else has_key_p(x, '__signal'))
# signal-value
def signal_value(s):
return get(s, 'value')
# signal-set-value!
def signal_set_value(s, v):
return _sx_dict_set(s, 'value', v)
# signal-subscribers
def signal_subscribers(s):
return get(s, 'subscribers')
# signal-add-sub!
def signal_add_sub(s, f):
if sx_truthy((not sx_truthy(contains_p(get(s, 'subscribers'), f)))):
return _sx_append(get(s, 'subscribers'), f)
return NIL
# signal-remove-sub!
def signal_remove_sub(s, f):
return _sx_dict_set(s, 'subscribers', filter(lambda sub: (not sx_truthy(is_identical(sub, f))), get(s, 'subscribers')))
# signal-deps
def signal_deps(s):
return get(s, 'deps')
# signal-set-deps!
def signal_set_deps(s, deps):
return _sx_dict_set(s, 'deps', deps)
# signal
def signal(initial_value):
return make_signal(initial_value)
# deref
def deref(s):
if sx_truthy((not sx_truthy(is_signal(s)))):
return s
else:
ctx = sx_context('sx-reactive', NIL)
if sx_truthy(ctx):
dep_list = get(ctx, 'deps')
notify_fn = get(ctx, 'notify')
if sx_truthy((not sx_truthy(contains_p(dep_list, s)))):
dep_list.append(s)
signal_add_sub(s, notify_fn)
return signal_value(s)
# reset!
def reset_b(s, value):
if sx_truthy(is_signal(s)):
old = signal_value(s)
if sx_truthy((not sx_truthy(is_identical(old, value)))):
signal_set_value(s, value)
return notify_subscribers(s)
return NIL
return NIL
# swap!
def swap_b(s, f, *args):
if sx_truthy(is_signal(s)):
old = signal_value(s)
new_val = apply(f, cons(old, args))
if sx_truthy((not sx_truthy(is_identical(old, new_val)))):
signal_set_value(s, new_val)
return notify_subscribers(s)
return NIL
return NIL
# computed
def computed(compute_fn):
s = make_signal(NIL)
deps = []
compute_ctx = NIL
recompute = _sx_fn(lambda : (
for_each(lambda dep: signal_remove_sub(dep, recompute), signal_deps(s)),
signal_set_deps(s, []),
(lambda ctx: _sx_begin(scope_push('sx-reactive', ctx), (lambda new_val: _sx_begin(scope_pop('sx-reactive'), signal_set_deps(s, get(ctx, 'deps')), (lambda old: _sx_begin(signal_set_value(s, new_val), (notify_subscribers(s) if sx_truthy((not sx_truthy(is_identical(old, new_val)))) else NIL)))(signal_value(s))))(cek_call(compute_fn, NIL))))({'deps': [], 'notify': recompute})
)[-1])
recompute()
register_in_scope(lambda : dispose_computed(s))
return s
# effect
def effect(effect_fn):
_cells = {}
_cells['deps'] = []
_cells['disposed'] = False
_cells['cleanup_fn'] = NIL
run_effect = lambda : (_sx_begin((cek_call(_cells['cleanup_fn'], NIL) if sx_truthy(_cells['cleanup_fn']) else NIL), for_each(lambda dep: signal_remove_sub(dep, run_effect), _cells['deps']), _sx_cell_set(_cells, 'deps', []), (lambda ctx: _sx_begin(scope_push('sx-reactive', ctx), (lambda result: _sx_begin(scope_pop('sx-reactive'), _sx_cell_set(_cells, 'deps', get(ctx, 'deps')), (_sx_cell_set(_cells, 'cleanup_fn', result) if sx_truthy(is_callable(result)) else NIL)))(cek_call(effect_fn, NIL))))({'deps': [], 'notify': run_effect})) if sx_truthy((not sx_truthy(_cells['disposed']))) else NIL)
run_effect()
dispose_fn = _sx_fn(lambda : (
_sx_cell_set(_cells, 'disposed', True),
(cek_call(_cells['cleanup_fn'], NIL) if sx_truthy(_cells['cleanup_fn']) else NIL),
for_each(lambda dep: signal_remove_sub(dep, run_effect), _cells['deps']),
_sx_cell_set(_cells, 'deps', [])
)[-1])
register_in_scope(dispose_fn)
return dispose_fn
# *batch-depth*
_batch_depth = 0
# *batch-queue*
_batch_queue = []
# batch
def batch(thunk):
_batch_depth = (_batch_depth + 1)
cek_call(thunk, NIL)
_batch_depth = (_batch_depth - 1)
if sx_truthy((_batch_depth == 0)):
queue = _batch_queue
_batch_queue = []
seen = []
pending = []
for s in queue:
for sub in signal_subscribers(s):
if sx_truthy((not sx_truthy(contains_p(seen, sub)))):
seen.append(sub)
pending.append(sub)
for sub in pending:
sub()
return NIL
return NIL
# notify-subscribers
def notify_subscribers(s):
if sx_truthy((_batch_depth > 0)):
if sx_truthy((not sx_truthy(contains_p(_batch_queue, s)))):
return _sx_append(_batch_queue, s)
return NIL
else:
return flush_subscribers(s)
# flush-subscribers
def flush_subscribers(s):
for sub in signal_subscribers(s):
sub()
return NIL
# dispose-computed
def dispose_computed(s):
if sx_truthy(is_signal(s)):
for dep in signal_deps(s):
signal_remove_sub(dep, NIL)
return signal_set_deps(s, [])
return NIL
# with-island-scope
def with_island_scope(scope_fn, body_fn):
scope_push('sx-island-scope', scope_fn)
result = body_fn()
scope_pop('sx-island-scope')
return result
# register-in-scope
def register_in_scope(disposable):
collector = sx_context('sx-island-scope', NIL)
if sx_truthy(collector):
return cek_call(collector, [disposable])
return NIL
# with-marsh-scope
def with_marsh_scope(marsh_el, body_fn):
disposers = []
with_island_scope(lambda d: _sx_append(disposers, d), body_fn)
return dom_set_data(marsh_el, 'sx-marsh-disposers', disposers)
# dispose-marsh-scope
def dispose_marsh_scope(marsh_el):
disposers = dom_get_data(marsh_el, 'sx-marsh-disposers')
if sx_truthy(disposers):
for d in disposers:
cek_call(d, NIL)
return dom_set_data(marsh_el, 'sx-marsh-disposers', NIL)
return NIL
# *store-registry*
_store_registry = {}
# def-store
def def_store(name, init_fn):
registry = _store_registry
if sx_truthy((not sx_truthy(has_key_p(registry, name)))):
_store_registry = assoc(registry, name, cek_call(init_fn, NIL))
return get(_store_registry, name)
# use-store
def use_store(name):
if sx_truthy(has_key_p(_store_registry, name)):
return get(_store_registry, name)
else:
return error(sx_str('Store not found: ', name, '. Call (def-store ...) before (use-store ...).'))
# clear-stores
def clear_stores():
return _sx_cell_set(_cells, '_store_registry', {})
# emit-event
def emit_event(el, event_name, detail):
return dom_dispatch(el, event_name, detail)
# on-event
def on_event(el, event_name, handler):
return dom_listen(el, event_name, handler)
# bridge-event
def bridge_event(el, event_name, target_signal, transform_fn):
return effect(lambda : (lambda remove: remove)(dom_listen(el, event_name, lambda e: (lambda detail: (lambda new_val: reset_b(target_signal, new_val))((cek_call(transform_fn, [detail]) if sx_truthy(transform_fn) else detail)))(event_detail(e)))))
# resource
def resource(fetch_fn):
state = signal({'loading': True, 'data': NIL, 'error': NIL})
promise_then(cek_call(fetch_fn, NIL), lambda data: reset_b(state, {'loading': False, 'data': data, 'error': NIL}), lambda err: reset_b(state, {'loading': False, 'data': NIL, 'error': err}))
return state
# === Transpiled from adapter-async === # === Transpiled from adapter-async ===
# async-render # async-render

View File

@@ -401,6 +401,43 @@ class EvalError(Exception):
pass pass
# ---------------------------------------------------------------------------
# SxExpr
# ---------------------------------------------------------------------------
class SxExpr(str):
"""Pre-built sx source that serialize() outputs unquoted.
``SxExpr`` is a ``str`` subclass, so it works everywhere a plain
string does (join, startswith, f-strings, isinstance checks). The
only difference: ``serialize()`` emits it unquoted instead of
wrapping it in double-quotes.
Use this to nest sx call strings inside other sx_call() invocations
without them being quoted as strings::
sx_call("parent", child=sx_call("child", x=1))
# => (~parent :child (~child :x 1))
"""
def __new__(cls, source: str = "") -> "SxExpr":
return str.__new__(cls, source)
@property
def source(self) -> str:
"""The raw SX source string (backward compat)."""
return str.__str__(self)
def __repr__(self) -> str:
return f"SxExpr({str.__repr__(self)})"
def __add__(self, other: object) -> "SxExpr":
return SxExpr(str.__add__(self, str(other)))
def __radd__(self, other: object) -> "SxExpr":
return SxExpr(str.__add__(str(other), self))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Type alias # Type alias
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -236,7 +236,9 @@
(dict :label "Deref as Shift" :href "/sx/(etc.(plan.cek-reactive))" (dict :label "Deref as Shift" :href "/sx/(etc.(plan.cek-reactive))"
:summary "Phase B: replace explicit effect wrapping with implicit continuation capture. Deref inside reactive-reset performs shift, capturing the rest of the expression as the subscriber.") :summary "Phase B: replace explicit effect wrapping with implicit continuation capture. Deref inside reactive-reset performs shift, capturing the rest of the expression as the subscriber.")
(dict :label "Reactive Runtime" :href "/sx/(etc.(plan.reactive-runtime))" (dict :label "Reactive Runtime" :href "/sx/(etc.(plan.reactive-runtime))"
:summary "Seven feature layers — ref, foreign FFI, state machines, commands with undo/redo, render loops, keyed lists, client-first app shell. Zero new platform primitives."))) :summary "Seven feature layers — ref, foreign FFI, state machines, commands with undo/redo, render loops, keyed lists, client-first app shell. Zero new platform primitives.")
(dict :label "Rust/WASM Host" :href "/sx/(etc.(plan.rust-wasm-host))"
:summary "Bootstrap the SX spec to Rust, compile to WASM, replace sx-browser.js. Shared platform layer for DOM, phased rollout from parse to full parity.")))
(define reactive-islands-nav-items (list (define reactive-islands-nav-items (list
(dict :label "Overview" :href "/sx/(geography.(reactive))" (dict :label "Overview" :href "/sx/(geography.(reactive))"

View File

@@ -60,6 +60,18 @@
"phase2" '(~reactive-islands/phase2/reactive-islands-phase2-content) "phase2" '(~reactive-islands/phase2/reactive-islands-phase2-content)
:else '(~reactive-islands/index/reactive-islands-index-content))))) :else '(~reactive-islands/index/reactive-islands-index-content)))))
(define cek
(fn (slug)
(if (nil? slug)
'(~geography/cek/cek-content)
(case slug
"demo" '(~geography/cek/cek-demo-content)
:else '(~geography/cek/cek-content)))))
(define provide
(fn (content)
(if (nil? content) '(~geography/provide-content) content)))
(define scopes (define scopes
(fn (content) (fn (content)
(if (nil? content) '(~geography/scopes-content) content))) (if (nil? content) '(~geography/scopes-content) content)))

View File

@@ -580,6 +580,7 @@
"foundations" (~plans/foundations/plan-foundations-content) "foundations" (~plans/foundations/plan-foundations-content)
"cek-reactive" (~plans/cek-reactive/plan-cek-reactive-content) "cek-reactive" (~plans/cek-reactive/plan-cek-reactive-content)
"reactive-runtime" (~plans/reactive-runtime/plan-reactive-runtime-content) "reactive-runtime" (~plans/reactive-runtime/plan-reactive-runtime-content)
"rust-wasm-host" (~plans/rust-wasm-host/plan-rust-wasm-host-content)
:else (~plans/index/plans-index-content)))) :else (~plans/index/plans-index-content))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------