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:
2026-03-14 00:04:38 +00:00
parent dcc73a68d5
commit f3a9f3ccc0
6 changed files with 121 additions and 169 deletions

View File

@@ -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); };

View File

@@ -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"];

View File

@@ -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"]
'''

View File

@@ -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

View File

@@ -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
;; --------------------------------------------------------------------------

View File

@@ -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)