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:
@@ -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
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 ---
|
||||||
|
|||||||
@@ -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")
|
||||||
"\"")))
|
"\"")))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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))"
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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))))
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user