Collapse signal platform primitives into pure SX dicts
Replace _Signal class (Python) and SxSignal constructor (JS) with plain dicts keyed by "__signal". Nine platform accessor functions become ~20 lines of pure SX in signals.sx. type-of returns "dict" for signals; signal? is now a structural predicate (dict? + has-key?). Net: -168 lines platform, +120 lines SX. Zero platform primitives for reactivity — signals compile to any host via the bootstrappers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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); };
|
||||
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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"]
|
||||
'''
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user