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 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 01:13:21 +00:00
parent 90febbd91e
commit 5c4a8c8cc2
3 changed files with 161 additions and 1 deletions

View File

@@ -1107,6 +1107,48 @@
(reset! sig (dom-get-prop el "value")))))))) (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 ;; render-dom-portal — render children into a remote target element
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------

View File

@@ -156,6 +156,9 @@
(= name "reset") (step-sf-reset args env kont) (= name "reset") (step-sf-reset args env kont)
(= name "shift") (step-sf-shift 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 ;; Scoped effects
(= name "scope") (step-sf-scope args env kont) (= name "scope") (step-sf-scope args env kont)
(= name "provide") (step-sf-provide args env kont) (= name "provide") (step-sf-provide args env kont)
@@ -397,6 +400,55 @@
(make-cek-state body shift-env rest-kont)))))) (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 ;; 6. Function call step handler
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -702,6 +754,37 @@
(= ft "reset") (= ft "reset")
(make-cek-value value env rest-k) (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 --- ;; --- ScopeFrame: body result ---
(= ft "scope") (= ft "scope")
(let ((name (get frame "name")) (let ((name (get frame "name"))

View File

@@ -166,6 +166,18 @@
{:type "dynamic-wind" :phase phase {:type "dynamic-wind" :phase phase
:body-thunk body-thunk :after-thunk after-thunk :env env})) :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 ;; 3. Frame accessors
@@ -202,12 +214,35 @@
;; Returns (captured-frames remaining-kont). ;; Returns (captured-frames remaining-kont).
;; captured-frames: frames from top up to (not including) ResetFrame. ;; captured-frames: frames from top up to (not including) ResetFrame.
;; remaining-kont: frames after ResetFrame. ;; remaining-kont: frames after ResetFrame.
;; Stops at either "reset" or "reactive-reset" frames.
(define scan (define scan
(fn (k captured) (fn (k captured)
(if (empty? k) (if (empty? k)
(error "shift without enclosing reset") (error "shift without enclosing reset")
(let ((frame (first k))) (let ((frame (first k)))
(if (= (frame-type frame) "reset") (if (or (= (frame-type frame) "reset")
(= (frame-type frame) "reactive-reset"))
(list captured (rest k)) (list captured (rest k))
(scan (rest k) (append captured (list frame)))))))) (scan (rest k) (append captured (list frame))))))))
(scan kont (list)))) (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))))