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"))))))))
;; --------------------------------------------------------------------------
;; 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
;; --------------------------------------------------------------------------

View File

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

View File

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