From 5c4a8c8cc2080fc130d6ab155290829f486ad357 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 14 Mar 2026 01:13:21 +0000 Subject: [PATCH] Implement deref-as-shift: ReactiveResetFrame, DerefFrame, continuation capture frames.sx: ReactiveResetFrame + DerefFrame constructors, kont-capture-to-reactive-reset, has-reactive-reset-frame?. cek.sx: deref as CEK special form, step-sf-deref pushes DerefFrame, reactive-shift-deref captures continuation as signal subscriber, ReactiveResetFrame in step-continue calls update-fn on re-render. adapter-dom.sx: cek-reactive-text/cek-reactive-attr using cek-run with ReactiveResetFrame for implicit DOM bindings. Co-Authored-By: Claude Opus 4.6 --- shared/sx/ref/adapter-dom.sx | 42 ++++++++++++++++++ shared/sx/ref/cek.sx | 83 ++++++++++++++++++++++++++++++++++++ shared/sx/ref/frames.sx | 37 +++++++++++++++- 3 files changed, 161 insertions(+), 1 deletion(-) diff --git a/shared/sx/ref/adapter-dom.sx b/shared/sx/ref/adapter-dom.sx index 18a40fb..23cad39 100644 --- a/shared/sx/ref/adapter-dom.sx +++ b/shared/sx/ref/adapter-dom.sx @@ -1107,6 +1107,48 @@ (reset! sig (dom-get-prop el "value")))))))) +;; -------------------------------------------------------------------------- +;; CEK-based reactive rendering (opt-in, deref-as-shift) +;; -------------------------------------------------------------------------- +;; +;; When enabled, (deref sig) inside a reactive-reset boundary performs +;; continuation capture: "the rest of this expression" becomes the subscriber. +;; No explicit effect() wrapping needed for text/attr bindings. + +(define *use-cek-reactive* true) +(define enable-cek-reactive! (fn () (set! *use-cek-reactive* true))) + +;; cek-reactive-text — create a text node bound via continuation capture +(define cek-reactive-text :effects [render mutation] + (fn (expr env) + (let ((node (create-text-node "")) + (update-fn (fn (val) + (dom-set-text-content node (str val))))) + (let ((initial (cek-run + (make-cek-state expr env + (list (make-reactive-reset-frame env update-fn true)))))) + (dom-set-text-content node (str initial)) + node)))) + +;; cek-reactive-attr — bind an attribute via continuation capture +(define cek-reactive-attr :effects [render mutation] + (fn (el attr-name expr env) + (let ((update-fn (fn (val) + (cond + (or (nil? val) (= val false)) (dom-remove-attr el attr-name) + (= val true) (dom-set-attr el attr-name "") + :else (dom-set-attr el attr-name (str val)))))) + ;; Mark for morph protection + (let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") "")) + (updated (if (empty? existing) attr-name (str existing "," attr-name)))) + (dom-set-attr el "data-sx-reactive-attrs" updated)) + ;; Initial render via CEK with ReactiveResetFrame + (let ((initial (cek-run + (make-cek-state expr env + (list (make-reactive-reset-frame env update-fn true)))))) + (invoke update-fn initial))))) + + ;; -------------------------------------------------------------------------- ;; render-dom-portal — render children into a remote target element ;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/cek.sx b/shared/sx/ref/cek.sx index addbe7e..3a2c97d 100644 --- a/shared/sx/ref/cek.sx +++ b/shared/sx/ref/cek.sx @@ -156,6 +156,9 @@ (= name "reset") (step-sf-reset args env kont) (= name "shift") (step-sf-shift args env kont) + ;; Reactive deref-as-shift + (= name "deref") (step-sf-deref args env kont) + ;; Scoped effects (= name "scope") (step-sf-scope args env kont) (= name "provide") (step-sf-provide args env kont) @@ -397,6 +400,55 @@ (make-cek-state body shift-env rest-kont)))))) +;; deref: evaluate argument, push DerefFrame +(define step-sf-deref + (fn (args env kont) + (make-cek-state + (first args) env + (kont-push (make-deref-frame env) kont)))) + +;; reactive-shift-deref: the heart of deref-as-shift +;; When deref encounters a signal inside a reactive-reset boundary, +;; capture the continuation up to the reactive-reset as the subscriber. +(define reactive-shift-deref + (fn (sig env kont) + (let ((scan-result (kont-capture-to-reactive-reset kont)) + (captured-frames (first scan-result)) + (reset-frame (nth scan-result 1)) + (remaining-kont (nth scan-result 2)) + (update-fn (get reset-frame "update-fn"))) + ;; Sub-scope for nested subscriber cleanup on re-invocation + (let ((sub-disposers (list))) + (let ((subscriber + (fn () + ;; Dispose previous nested subscribers + (for-each (fn (d) (invoke d)) sub-disposers) + (set! sub-disposers (list)) + ;; Re-invoke: push fresh ReactiveResetFrame (first-render=false) + (let ((new-reset (make-reactive-reset-frame env update-fn false)) + (new-kont (concat captured-frames + (list new-reset) + remaining-kont))) + (with-island-scope + (fn (d) (append! sub-disposers d)) + (fn () + (cek-run + (make-cek-value (signal-value sig) env new-kont)))))))) + ;; Register subscriber + (signal-add-sub! sig subscriber) + ;; Register cleanup with island scope + (register-in-scope + (fn () + (signal-remove-sub! sig subscriber) + (for-each (fn (d) (invoke d)) sub-disposers))) + ;; Initial render: value flows through captured frames + reset (first-render=true) + ;; so the full expression completes normally + (let ((initial-kont (concat captured-frames + (list reset-frame) + remaining-kont))) + (make-cek-value (signal-value sig) env initial-kont))))))) + + ;; -------------------------------------------------------------------------- ;; 6. Function call step handler ;; -------------------------------------------------------------------------- @@ -702,6 +754,37 @@ (= ft "reset") (make-cek-value value env rest-k) + ;; --- DerefFrame: deref argument evaluated --- + (= ft "deref") + (let ((val value) + (fenv (get frame "env"))) + (if (not (signal? val)) + ;; Not a signal: pass through + (make-cek-value val fenv rest-k) + ;; Signal: check for ReactiveResetFrame + (if (has-reactive-reset-frame? rest-k) + ;; Perform reactive shift + (reactive-shift-deref val fenv rest-k) + ;; No reactive-reset: normal deref (scope-based tracking) + (do + (let ((ctx (context "sx-reactive" nil))) + (when ctx + (let ((dep-list (get ctx "deps")) + (notify-fn (get ctx "notify"))) + (when (not (contains? dep-list val)) + (append! dep-list val) + (signal-add-sub! val notify-fn))))) + (make-cek-value (signal-value val) fenv rest-k))))) + + ;; --- ReactiveResetFrame: expression completed --- + (= ft "reactive-reset") + (let ((update-fn (get frame "update-fn")) + (first? (get frame "first-render"))) + ;; On re-render (not first), call update-fn with new value + (when (and update-fn (not first?)) + (invoke update-fn value)) + (make-cek-value value env rest-k)) + ;; --- ScopeFrame: body result --- (= ft "scope") (let ((name (get frame "name")) diff --git a/shared/sx/ref/frames.sx b/shared/sx/ref/frames.sx index b7881bd..c6f597c 100644 --- a/shared/sx/ref/frames.sx +++ b/shared/sx/ref/frames.sx @@ -166,6 +166,18 @@ {:type "dynamic-wind" :phase phase :body-thunk body-thunk :after-thunk after-thunk :env env})) +;; ReactiveResetFrame: delimiter for reactive deref-as-shift +;; Carries an update-fn that gets called with new values on re-render. +(define make-reactive-reset-frame + (fn (env update-fn first-render?) + {:type "reactive-reset" :env env :update-fn update-fn + :first-render first-render?})) + +;; DerefFrame: awaiting evaluation of deref's argument +(define make-deref-frame + (fn (env) + {:type "deref" :env env})) + ;; -------------------------------------------------------------------------- ;; 3. Frame accessors @@ -202,12 +214,35 @@ ;; Returns (captured-frames remaining-kont). ;; captured-frames: frames from top up to (not including) ResetFrame. ;; remaining-kont: frames after ResetFrame. + ;; Stops at either "reset" or "reactive-reset" frames. (define scan (fn (k captured) (if (empty? k) (error "shift without enclosing reset") (let ((frame (first k))) - (if (= (frame-type frame) "reset") + (if (or (= (frame-type frame) "reset") + (= (frame-type frame) "reactive-reset")) (list captured (rest k)) (scan (rest k) (append captured (list frame)))))))) (scan kont (list)))) + +;; Check if a ReactiveResetFrame exists anywhere in the continuation +(define has-reactive-reset-frame? + (fn (kont) + (if (empty? kont) false + (if (= (frame-type (first kont)) "reactive-reset") true + (has-reactive-reset-frame? (rest kont)))))) + +;; Capture frames up to nearest ReactiveResetFrame. +;; Returns (captured-frames, reset-frame, remaining-kont). +(define kont-capture-to-reactive-reset + (fn (kont) + (define scan + (fn (k captured) + (if (empty? k) + (error "reactive deref without enclosing reactive-reset") + (let ((frame (first k))) + (if (= (frame-type frame) "reactive-reset") + (list captured frame (rest k)) + (scan (rest k) (append captured (list frame)))))))) + (scan kont (list))))