diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index dd29465..d063988 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-13T22:49:51Z"; + var SX_VERSION = "2026-03-13T23:56:11Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -54,13 +54,6 @@ } Island.prototype._island = true; - function SxSignal(value) { - this.value = value; - this.subscribers = []; - this.deps = []; - } - SxSignal.prototype._signal = true; - function Macro(params, restParam, body, closure, name) { this.params = params; this.restParam = restParam; @@ -115,7 +108,6 @@ if (x._lambda) return "lambda"; if (x._component) return "component"; if (x._island) return "island"; - if (x._signal) return "signal"; if (x._spread) return "spread"; if (x._macro) return "macro"; if (x._raw) return "raw-html"; @@ -226,17 +218,6 @@ return new Island(name, params, hasChildren, body, merge(env)); } - // Signal platform - function makeSignal(value) { return new SxSignal(value); } - function isSignal(x) { return x != null && x._signal === true; } - function signalValue(s) { return s.value; } - function signalSetValue(s, v) { s.value = v; } - function signalSubscribers(s) { return s.subscribers.slice(); } - function signalAddSub(s, fn) { if (s.subscribers.indexOf(fn) < 0) s.subscribers.push(fn); } - function signalRemoveSub(s, fn) { var i = s.subscribers.indexOf(fn); if (i >= 0) s.subscribers.splice(i, 1); } - function signalDeps(s) { return s.deps.slice(); } - function signalSetDeps(s, deps) { s.deps = Array.isArray(deps) ? deps.slice() : []; } - // invoke — call any callable (native fn or SX lambda) with args. // Transpiled code emits direct calls f(args) which fail on SX lambdas // from runtime-evaluated island bodies. invoke dispatches correctly. @@ -554,6 +535,9 @@ } function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; } + // Predicate aliases used by transpiled code + var isDict = PRIMITIVES["dict?"]; + // List primitives used directly by transpiled code var len = PRIMITIVES["len"]; var first = PRIMITIVES["first"]; @@ -4244,6 +4228,33 @@ callExpr.push(dictGet(kwargs, k)); } } // === 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); }; diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py index 75d5533..957b7ad 100644 --- a/shared/sx/ref/platform_js.py +++ b/shared/sx/ref/platform_js.py @@ -851,13 +851,6 @@ PREAMBLE = '''\ } Island.prototype._island = true; - function SxSignal(value) { - this.value = value; - this.subscribers = []; - this.deps = []; - } - SxSignal.prototype._signal = true; - function Macro(params, restParam, body, closure, name) { this.params = params; this.restParam = restParam; @@ -1141,7 +1134,6 @@ PLATFORM_JS_PRE = ''' if (x._lambda) return "lambda"; if (x._component) return "component"; if (x._island) return "island"; - if (x._signal) return "signal"; if (x._spread) return "spread"; if (x._macro) return "macro"; if (x._raw) return "raw-html"; @@ -1252,17 +1244,6 @@ PLATFORM_JS_PRE = ''' return new Island(name, params, hasChildren, body, merge(env)); } - // Signal platform - function makeSignal(value) { return new SxSignal(value); } - function isSignal(x) { return x != null && x._signal === true; } - function signalValue(s) { return s.value; } - function signalSetValue(s, v) { s.value = v; } - function signalSubscribers(s) { return s.subscribers.slice(); } - function signalAddSub(s, fn) { if (s.subscribers.indexOf(fn) < 0) s.subscribers.push(fn); } - function signalRemoveSub(s, fn) { var i = s.subscribers.indexOf(fn); if (i >= 0) s.subscribers.splice(i, 1); } - function signalDeps(s) { return s.deps.slice(); } - function signalSetDeps(s, deps) { s.deps = Array.isArray(deps) ? deps.slice() : []; } - // invoke — call any callable (native fn or SX lambda) with args. // Transpiled code emits direct calls f(args) which fail on SX lambdas // from runtime-evaluated island bodies. invoke dispatches correctly. @@ -1376,6 +1357,9 @@ PLATFORM_JS_POST = ''' } function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; } + // Predicate aliases used by transpiled code + var isDict = PRIMITIVES["dict?"]; + // List primitives used directly by transpiled code var len = PRIMITIVES["len"]; var first = PRIMITIVES["first"]; diff --git a/shared/sx/ref/platform_py.py b/shared/sx/ref/platform_py.py index d3ef8e7..3078b2f 100644 --- a/shared/sx/ref/platform_py.py +++ b/shared/sx/ref/platform_py.py @@ -225,8 +225,6 @@ def type_of(x): return "component" if isinstance(x, Island): return "island" - if isinstance(x, _Signal): - return "signal" if isinstance(x, _Spread): return "spread" if isinstance(x, Macro): @@ -468,58 +466,6 @@ def is_identical(a, b): return a is b -# ------------------------------------------------------------------------- -# Signal platform -- reactive state primitives -# ------------------------------------------------------------------------- - -class _Signal: - """Reactive signal container.""" - __slots__ = ("value", "subscribers", "deps") - def __init__(self, value): - self.value = value - self.subscribers = [] - self.deps = [] - - -def make_signal(value): - return _Signal(value) - - -def is_signal(x): - return isinstance(x, _Signal) - - -def signal_value(s): - return s.value if isinstance(s, _Signal) else s - - -def signal_set_value(s, v): - if isinstance(s, _Signal): - s.value = v - - -def signal_subscribers(s): - return list(s.subscribers) if isinstance(s, _Signal) else [] - - -def signal_add_sub(s, fn): - if isinstance(s, _Signal) and fn not in s.subscribers: - s.subscribers.append(fn) - - -def signal_remove_sub(s, fn): - if isinstance(s, _Signal) and fn in s.subscribers: - s.subscribers.remove(fn) - - -def signal_deps(s): - return list(s.deps) if isinstance(s, _Signal) else [] - - -def signal_set_deps(s, deps): - if isinstance(s, _Signal): - s.deps = list(deps) if isinstance(deps, list) else [] - def invoke(f, *args): """Call f with args — handles both native callables and SX lambdas. @@ -1117,6 +1063,7 @@ replace = PRIMITIVES["replace"] parse_int = PRIMITIVES["parse-int"] upper = PRIMITIVES["upper"] has_key_p = PRIMITIVES["has-key?"] +dict_p = PRIMITIVES["dict?"] dissoc = PRIMITIVES["dissoc"] index_of = PRIMITIVES["index-of"] ''' diff --git a/shared/sx/ref/run_signal_tests.py b/shared/sx/ref/run_signal_tests.py index 5332096..89a5eea 100644 --- a/shared/sx/ref/run_signal_tests.py +++ b/shared/sx/ref/run_signal_tests.py @@ -87,16 +87,8 @@ env["report-fail"] = _report_fail env["push-suite"] = _push_suite env["pop-suite"] = _pop_suite -# Signal platform primitives -env["signal?"] = sx_ref.is_signal -env["make-signal"] = sx_ref.make_signal -env["signal-value"] = sx_ref.signal_value -env["signal-set-value!"] = sx_ref.signal_set_value -env["signal-subscribers"] = sx_ref.signal_subscribers -env["signal-add-sub!"] = sx_ref.signal_add_sub -env["signal-remove-sub!"] = sx_ref.signal_remove_sub -env["signal-deps"] = sx_ref.signal_deps -env["signal-set-deps!"] = sx_ref.signal_set_deps +# Signal functions are now pure SX (transpiled into sx_ref.py from signals.sx) +# Wire both low-level dict-based signal functions and high-level API env["identical?"] = sx_ref.is_identical env["island?"] = lambda x: isinstance(x, Island) @@ -105,6 +97,17 @@ env["scope-push!"] = scope_push env["scope-pop!"] = scope_pop env["context"] = sx_context +# Low-level signal functions (now pure SX, transpiled from signals.sx) +env["make-signal"] = sx_ref.make_signal +env["signal?"] = sx_ref.is_signal +env["signal-value"] = sx_ref.signal_value +env["signal-set-value!"] = sx_ref.signal_set_value +env["signal-subscribers"] = sx_ref.signal_subscribers +env["signal-add-sub!"] = sx_ref.signal_add_sub +env["signal-remove-sub!"] = sx_ref.signal_remove_sub +env["signal-deps"] = sx_ref.signal_deps +env["signal-set-deps!"] = sx_ref.signal_set_deps + # Bootstrapped signal functions from sx_ref.py env["signal"] = sx_ref.signal env["deref"] = sx_ref.deref diff --git a/shared/sx/ref/signals.sx b/shared/sx/ref/signals.sx index bba2a02..01e1a91 100644 --- a/shared/sx/ref/signals.sx +++ b/shared/sx/ref/signals.sx @@ -9,24 +9,16 @@ ;; layer (adapter-dom.sx) subscribes DOM nodes to signals. The server ;; adapter (adapter-html.sx) reads signal values without subscribing. ;; +;; Signals are plain dicts with a "__signal" marker key. No platform +;; primitives needed — all signal operations are pure SX. +;; ;; Reactive tracking and island lifecycle use the general scoped effects ;; system (scope-push!/scope-pop!/context) instead of separate globals. ;; Two scope names: ;; "sx-reactive" — tracking context for computed/effect dep discovery ;; "sx-island-scope" — island disposable collector ;; -;; Platform interface required: -;; (make-signal value) → Signal — create signal container -;; (signal? x) → boolean — type predicate -;; (signal-value s) → any — read current value (no tracking) -;; (signal-set-value! s v) → void — write value (no notification) -;; (signal-subscribers s) → list — list of subscriber fns -;; (signal-add-sub! s fn) → void — add subscriber -;; (signal-remove-sub! s fn) → void — remove subscriber -;; (signal-deps s) → list — dependency list (for computed) -;; (signal-set-deps! s deps) → void — set dependency list -;; -;; Scope-based tracking (replaces TrackingContext platform primitives): +;; Scope-based tracking: ;; (scope-push! "sx-reactive" {:deps (list) :notify fn}) → void ;; (scope-pop! "sx-reactive") → void ;; (context "sx-reactive" nil) → dict or nil @@ -43,6 +35,36 @@ ;; ========================================================================== +;; -------------------------------------------------------------------------- +;; Signal container — plain dict with marker key +;; -------------------------------------------------------------------------- +;; +;; A signal is a dict: {"__signal" true, "value" v, "subscribers" [], "deps" []} +;; type-of returns "dict". Use signal? to distinguish from regular dicts. + +(define make-signal (fn (value) + (dict "__signal" true "value" value "subscribers" (list) "deps" (list)))) + +(define signal? (fn (x) + (and (dict? x) (has-key? x "__signal")))) + +(define signal-value (fn (s) (get s "value"))) +(define signal-set-value! (fn (s v) (dict-set! s "value" v))) +(define signal-subscribers (fn (s) (get s "subscribers"))) + +(define signal-add-sub! (fn (s f) + (when (not (contains? (get s "subscribers") f)) + (append! (get s "subscribers") f)))) + +(define signal-remove-sub! (fn (s f) + (dict-set! s "subscribers" + (filter (fn (sub) (not (identical? sub f))) + (get s "subscribers"))))) + +(define signal-deps (fn (s) (get s "deps"))) +(define signal-set-deps! (fn (s deps) (dict-set! s "deps" deps))) + + ;; -------------------------------------------------------------------------- ;; 1. signal — create a reactive container ;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index d813b71..bbbf179 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -184,8 +184,6 @@ def type_of(x): return "component" if isinstance(x, Island): return "island" - if isinstance(x, _Signal): - return "signal" if isinstance(x, _Spread): return "spread" if isinstance(x, Macro): @@ -427,58 +425,6 @@ def is_identical(a, b): return a is b -# ------------------------------------------------------------------------- -# Signal platform -- reactive state primitives -# ------------------------------------------------------------------------- - -class _Signal: - """Reactive signal container.""" - __slots__ = ("value", "subscribers", "deps") - def __init__(self, value): - self.value = value - self.subscribers = [] - self.deps = [] - - -def make_signal(value): - return _Signal(value) - - -def is_signal(x): - return isinstance(x, _Signal) - - -def signal_value(s): - return s.value if isinstance(s, _Signal) else s - - -def signal_set_value(s, v): - if isinstance(s, _Signal): - s.value = v - - -def signal_subscribers(s): - return list(s.subscribers) if isinstance(s, _Signal) else [] - - -def signal_add_sub(s, fn): - if isinstance(s, _Signal) and fn not in s.subscribers: - s.subscribers.append(fn) - - -def signal_remove_sub(s, fn): - if isinstance(s, _Signal) and fn in s.subscribers: - s.subscribers.remove(fn) - - -def signal_deps(s): - return list(s.deps) if isinstance(s, _Signal) else [] - - -def signal_set_deps(s, deps): - if isinstance(s, _Signal): - s.deps = list(deps) if isinstance(deps, list) else [] - def invoke(f, *args): """Call f with args — handles both native callables and SX lambdas. @@ -1037,6 +983,7 @@ replace = PRIMITIVES["replace"] parse_int = PRIMITIVES["parse-int"] upper = PRIMITIVES["upper"] has_key_p = PRIMITIVES["has-key?"] +dict_p = PRIMITIVES["dict?"] dissoc = PRIMITIVES["dissoc"] index_of = PRIMITIVES["index-of"] @@ -3456,6 +3403,44 @@ def prepare_url_expr(url_path, 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)