Collapse reactive islands into scopes: replace TrackingContext and *island-scope* with scope-push!/scope-pop!/context

Reactive tracking (deref/computed/effect dep discovery) and island lifecycle
now use the general scoped effects system instead of parallel infrastructure.
Two scope names: "sx-reactive" for tracking context, "sx-island-scope" for
island disposable collection. Eliminates ~98 net lines: _TrackingContext class,
7 tracking context platform functions (Python + JS), *island-scope* global,
and corresponding RENAME_MAP entries. All 20 signal tests pass (17 original +
3 new scope integration tests), plus CEK/continuation/type tests clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 23:09:09 +00:00
parent 1765216335
commit dcc73a68d5
11 changed files with 330 additions and 268 deletions

View File

@@ -9,6 +9,12 @@
;; layer (adapter-dom.sx) subscribes DOM nodes to signals. The server
;; adapter (adapter-html.sx) reads signal values without subscribing.
;;
;; 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
@@ -20,10 +26,10 @@
;; (signal-deps s) → list — dependency list (for computed)
;; (signal-set-deps! s deps) → void — set dependency list
;;
;; Global state required:
;; *tracking-context* → nil | Effect/Computed currently evaluating
;; (set-tracking-context! c) → void
;; (get-tracking-context) → context or nil
;; Scope-based tracking (replaces TrackingContext platform primitives):
;; (scope-push! "sx-reactive" {:deps (list) :notify fn}) → void
;; (scope-pop! "sx-reactive") → void
;; (context "sx-reactive" nil) → dict or nil
;;
;; Runtime callable dispatch:
;; (invoke f &rest args) → any — call f with args; handles both
@@ -58,12 +64,14 @@
(fn ((s :as any))
(if (not (signal? s))
s ;; non-signal values pass through
(let ((ctx (get-tracking-context)))
(let ((ctx (context "sx-reactive" nil)))
(when ctx
;; Register this signal as a dependency of the current context
(tracking-context-add-dep! ctx s)
;; Subscribe the context to this signal
(signal-add-sub! s (tracking-context-notify-fn ctx)))
(let ((dep-list (get ctx "deps"))
(notify-fn (get ctx "notify")))
(when (not (contains? dep-list s))
(append! dep-list s)
(signal-add-sub! s notify-fn))))
(signal-value s)))))
@@ -117,19 +125,18 @@
(signal-deps s))
(signal-set-deps! s (list))
;; Create tracking context for this computed
(let ((ctx (make-tracking-context recompute)))
(let ((prev (get-tracking-context)))
(set-tracking-context! ctx)
(let ((new-val (invoke compute-fn)))
(set-tracking-context! prev)
;; Save discovered deps
(signal-set-deps! s (tracking-context-deps ctx))
;; Update value + notify downstream
(let ((old (signal-value s)))
(signal-set-value! s new-val)
(when (not (identical? old new-val))
(notify-subscribers s)))))))))
;; Push scope-based tracking context for this computed
(let ((ctx (dict "deps" (list) "notify" recompute)))
(scope-push! "sx-reactive" ctx)
(let ((new-val (invoke compute-fn)))
(scope-pop! "sx-reactive")
;; Save discovered deps
(signal-set-deps! s (get ctx "deps"))
;; Update value + notify downstream
(let ((old (signal-value s)))
(signal-set-value! s new-val)
(when (not (identical? old new-val))
(notify-subscribers s))))))))
;; Initial computation
(recompute)
@@ -163,16 +170,15 @@
deps)
(set! deps (list))
;; Track new deps
(let ((ctx (make-tracking-context run-effect)))
(let ((prev (get-tracking-context)))
(set-tracking-context! ctx)
(let ((result (invoke effect-fn)))
(set-tracking-context! prev)
(set! deps (tracking-context-deps ctx))
;; If effect returns a function, it's the cleanup
(when (callable? result)
(set! cleanup-fn result)))))))))
;; Push scope-based tracking context
(let ((ctx (dict "deps" (list) "notify" run-effect)))
(scope-push! "sx-reactive" ctx)
(let ((result (invoke effect-fn)))
(scope-pop! "sx-reactive")
(set! deps (get ctx "deps"))
;; If effect returns a function, it's the cleanup
(when (callable? result)
(set! cleanup-fn result))))))))
;; Initial run
(run-effect)
@@ -246,19 +252,13 @@
;; --------------------------------------------------------------------------
;; 9. Tracking context
;; 9. Reactive tracking context
;; --------------------------------------------------------------------------
;;
;; A tracking context is an ephemeral object created during effect/computed
;; evaluation to discover signal dependencies. Platform must provide:
;;
;; (make-tracking-context notify-fn) → context
;; (tracking-context-deps ctx) → list of signals
;; (tracking-context-add-dep! ctx s) → void (adds s to ctx's dep list)
;; (tracking-context-notify-fn ctx) → the notify function
;;
;; These are platform primitives because the context is mutable state
;; that must be efficient (often a Set in the host language).
;; Tracking is now scope-based. computed/effect push a dict
;; {:deps (list) :notify fn} onto the "sx-reactive" scope stack via
;; scope-push!/scope-pop!. deref reads it via (context "sx-reactive" nil).
;; No platform primitives needed — uses the existing scope infrastructure.
;; --------------------------------------------------------------------------
@@ -284,25 +284,24 @@
;; When an island is created, all signals, effects, and computeds created
;; within it are tracked. When the island is removed from the DOM, they
;; are all disposed.
(define *island-scope* nil)
;;
;; Uses "sx-island-scope" scope name. The scope value is a collector
;; function (fn (disposable) ...) that appends to the island's disposer list.
(define with-island-scope :effects [mutation]
(fn ((scope-fn :as lambda) (body-fn :as lambda))
(let ((prev *island-scope*))
(set! *island-scope* scope-fn)
(let ((result (body-fn)))
(set! *island-scope* prev)
result))))
(scope-push! "sx-island-scope" scope-fn)
(let ((result (body-fn)))
(scope-pop! "sx-island-scope")
result)))
;; Hook into signal/effect/computed creation for scope tracking.
;; The platform's make-signal should call (register-in-scope s) if
;; *island-scope* is non-nil.
(define register-in-scope :effects [mutation]
(fn ((disposable :as lambda))
(when *island-scope*
(*island-scope* disposable))))
(let ((collector (context "sx-island-scope" nil)))
(when collector
(invoke collector disposable)))))
;; ==========================================================================