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:
@@ -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
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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))))
|
||||
|
||||
Reference in New Issue
Block a user