Skip to main content

cranpose_core/
recompose_scope.rs

1use std::{
2    cell::{Cell, RefCell},
3    hash::{Hash, Hasher},
4    rc::{Rc, Weak},
5};
6
7use smallvec::SmallVec;
8
9#[cfg(feature = "inspection")]
10use crate::source_trace;
11use crate::{
12    AnchorId, Composer, LocalStackSnapshot, NodeId, NodeIdCell, PackedU64, RetentionMode,
13    RuntimeHandle, ScopeId, SlotsHost, StateId, collections::map::HashSet, composer, runtime,
14    thread_state,
15};
16
17// Whether a scope's body ran to its end or was skipped.
18#[derive(Clone, Copy, PartialEq, Eq)]
19pub(crate) enum BodyRun {
20    Ran,
21    Skipped,
22}
23
24impl From<bool> for BodyRun {
25    fn from(ran: bool) -> Self {
26        if ran { Self::Ran } else { Self::Skipped }
27    }
28}
29
30fn scope_activity_epoch() -> u64 {
31    thread_state::cells(|cells| cells.scope_activity_epoch.get())
32}
33
34fn note_scope_activity_change() {
35    thread_state::cells(|cells| {
36        let epoch = &cells.scope_activity_epoch;
37        epoch.set(epoch.get() + 1);
38    });
39}
40
41/// Runs `read` on what `cell` holds and puts it back: a scope's links are
42/// read this way rather than through a `RefCell`, whose borrow flag would
43/// take a word in every scope of a composition. `read` must not reach the
44/// same cell again.
45fn read_cell<T: Default, R>(cell: &Cell<T>, read: impl FnOnce(&T) -> R) -> R {
46    let value = cell.take();
47    let result = read(&value);
48    cell.set(value);
49    result
50}
51
52/// Like [`read_cell`], for a change to what `cell` holds.
53fn update_cell<T: Default, R>(cell: &Cell<T>, update: impl FnOnce(&mut T) -> R) -> R {
54    let mut value = cell.take();
55    let result = update(&mut value);
56    cell.set(value);
57    result
58}
59
60/// How a scope is tied to the scope its lifetime follows.
61#[derive(Clone, Copy, PartialEq, Eq)]
62enum OwnerKind {
63    /// The scope it composes under, its recomposition ancestor.
64    Parent,
65    /// For the root of a slot table composed for another scope's content,
66    /// that scope, which is no recomposition ancestor.
67    Lifetime,
68}
69
70/// The scope a scope's lifetime follows, and how it is tied to it.
71type OwnerLink = Cell<Option<(OwnerKind, Weak<RecomposeScopeInner>)>>;
72
73// A hand-written body that recomposes a scope.
74type RecomposeClosure = Box<dyn FnMut(&Composer)>;
75
76// A scope's yes-or-no states. Each takes a bit of `ScopeFlags`.
77#[derive(Clone, Copy)]
78enum ScopeFlag {
79    Invalid,
80    Enqueued,
81    Active,
82    ComposedOnce,
83    PendingRecompose,
84    // A hand-written recompose closure is running: it is out of
85    // `recompose_closure` meanwhile, and the scope still recomposes what
86    // reads in it.
87    Recomposing,
88    ForceReuse,
89    ForceRecompose,
90    Derivation,
91    // Something other than a state invalidated the scope.
92    UnknownInvalidationSource,
93    // The scope's first body waits for a later frame, in pausable content.
94    Paused,
95    // The scope belongs to pausable content that has not completed: an
96    // invalidation waits until it completes.
97    Pending,
98}
99
100impl ScopeFlag {
101    const fn bit(self) -> u16 {
102        match self {
103            Self::Invalid => 1,
104            Self::Enqueued => 1 << 1,
105            Self::Active => 1 << 2,
106            Self::ComposedOnce => 1 << 3,
107            Self::PendingRecompose => 1 << 4,
108            Self::Recomposing => 1 << 5,
109            Self::ForceReuse => 1 << 6,
110            Self::ForceRecompose => 1 << 7,
111            Self::Derivation => 1 << 8,
112            Self::UnknownInvalidationSource => 1 << 9,
113            Self::Paused => 1 << 10,
114            Self::Pending => 1 << 11,
115        }
116    }
117}
118
119// A scope's flags in one cell, a bit each: a `Cell<bool>` each would take a
120// byte of every scope.
121struct ScopeFlags(Cell<u16>);
122
123impl ScopeFlags {
124    fn get(&self, flag: ScopeFlag) -> bool {
125        self.0.get() & flag.bit() != 0
126    }
127
128    fn set(&self, flag: ScopeFlag, on: bool) {
129        let bits = self.0.get();
130        self.0.set(if on {
131            bits | flag.bit()
132        } else {
133            bits & !flag.bit()
134        });
135    }
136
137    // Whether `a` or `b` is set, in one test.
138    fn either(&self, a: ScopeFlag, b: ScopeFlag) -> bool {
139        self.0.get() & (a.bit() | b.bit()) != 0
140    }
141
142    // Sets `flag` and says whether it was set.
143    fn replace(&self, flag: ScopeFlag, on: bool) -> bool {
144        let was = self.get(flag);
145        self.set(flag, on);
146        was
147    }
148}
149
150pub(crate) struct RecomposeScopeInner {
151    runtime: runtime::ScopeRuntime,
152    flags: ScopeFlags,
153    active_epoch: Cell<PackedU64>,
154    deactivations: Cell<PackedU64>,
155    retention_mode: Cell<RetentionMode>,
156    parent_hint: NodeIdCell,
157    group_anchor: Cell<AnchorId>,
158    // What runs the scope's body again: a `#[composable]` function's
159    // generated recompose function, or a hand-written closure when the
160    // function is `None`.
161    recompose: Cell<Option<fn(&Composer)>>,
162    recompose_closure: Cell<Option<RecomposeClosure>>,
163    owner: OwnerLink,
164    local_stack: Cell<LocalStackSnapshot>,
165    #[cfg(feature = "inspection")]
166    source_trace: RefCell<Rc<[source_trace::SourceLocation]>>,
167    #[cfg(all(feature = "inspection", debug_assertions))]
168    pub(crate) recompositions: std::cell::OnceCell<source_trace::RecompositionCounter>,
169    slots_storage_key: Cell<usize>,
170    slots_runtime_state: Cell<Option<Weak<composer::ComposerRuntimeState>>>,
171    /// The states the scope is subscribed to: what its last body read, in
172    /// read order.
173    state_subscriptions: RefCell<StateIds>,
174    /// What the body running now read so far, once it left the order of
175    /// `state_subscriptions`: none while it reads in order.
176    run_reads: Cell<Option<Box<StateIds>>>,
177    /// How many subscriptions the body running now read in order, or
178    /// [`DIVERGED`] or [`NOT_READING`].
179    read_cursor: Cell<u32>,
180    invalidation_sources: Cell<StateIds>,
181}
182
183type StateIds = SmallVec<[StateId; 2]>;
184
185/// What [`RecomposeScope::note_read`] made of a read.
186pub(crate) enum ScopeRead {
187    /// The read repeats the last body's, so the state watches the scope.
188    Watched,
189    /// The read went into the running body's reads; the state may not watch
190    /// the scope yet.
191    Collected,
192    /// No body of the scope is running.
193    NotReading,
194}
195
196/// A scope's `read_cursor` while no body of it runs.
197const NOT_READING: u32 = u32::MAX;
198/// A scope's `read_cursor` once the running body left the order of the reads
199/// before it.
200const DIVERGED: u32 = u32::MAX - 1;
201
202enum ScopeOwner {
203    Root,
204    Live(Rc<RecomposeScopeInner>),
205    Dropped,
206}
207
208impl RecomposeScopeInner {
209    fn owner(&self) -> ScopeOwner {
210        read_cell(&self.owner, |owner| {
211            owner.as_ref().map_or(ScopeOwner::Root, |(_, owner)| {
212                owner
213                    .upgrade()
214                    .map_or(ScopeOwner::Dropped, ScopeOwner::Live)
215            })
216        })
217    }
218
219    fn is_effectively_active(&self, epoch: u64) -> bool {
220        if self.active_epoch.get().get() == epoch {
221            return true;
222        }
223        if !self.flags.get(ScopeFlag::Active) {
224            return false;
225        }
226        let active = match self.owner() {
227            ScopeOwner::Root => true,
228            ScopeOwner::Dropped => false,
229            ScopeOwner::Live(owner) => owner.is_effectively_active(epoch),
230        };
231        if active {
232            self.active_epoch.set(PackedU64::new(epoch));
233        }
234        active
235    }
236
237    fn new(runtime: runtime::ScopeRuntime) -> Self {
238        runtime.increment_live_recompose_scope_count();
239        Self {
240            runtime,
241            flags: ScopeFlags(Cell::new(ScopeFlag::Active.bit())),
242            active_epoch: Cell::default(),
243            deactivations: Cell::default(),
244            retention_mode: Cell::new(RetentionMode::DisposeWhenInactive),
245            parent_hint: NodeIdCell::new(None),
246            group_anchor: Cell::new(AnchorId::INVALID),
247            recompose: Cell::new(None),
248            recompose_closure: Cell::new(None),
249            owner: Cell::new(None),
250            local_stack: Cell::new(None),
251            #[cfg(feature = "inspection")]
252            source_trace: RefCell::new(Rc::from([])),
253            #[cfg(all(feature = "inspection", debug_assertions))]
254            recompositions: std::cell::OnceCell::new(),
255            slots_storage_key: Cell::new(0),
256            slots_runtime_state: Cell::new(None),
257            state_subscriptions: RefCell::new(StateIds::new()),
258            run_reads: Cell::new(None),
259            read_cursor: Cell::new(NOT_READING),
260            invalidation_sources: Cell::new(StateIds::new()),
261        }
262    }
263
264    fn id(&self) -> ScopeId {
265        std::ptr::from_ref(self).addr()
266    }
267}
268
269fn push_unique_state_id(ids: &mut StateIds, state_id: StateId) {
270    if !ids.contains(&state_id) {
271        ids.push(state_id);
272    }
273}
274
275impl Drop for RecomposeScopeInner {
276    fn drop(&mut self) {
277        note_scope_activity_change();
278        let id = self.id();
279        self.runtime.decrement_live_recompose_scope_count();
280        let subscriptions = std::mem::take(self.state_subscriptions.get_mut());
281        let run_reads = self.run_reads.take().map(|reads| *reads);
282        for state_id in subscriptions
283            .into_iter()
284            .chain(run_reads.into_iter().flatten())
285        {
286            self.runtime.unregister_state_scope(state_id, id);
287        }
288        #[cfg(debug_assertions)]
289        {
290            thread_state::try_runtimes(|state| {
291                state.scope_labels.borrow_mut().remove(&id);
292                state.scope_invalidation_sources.borrow_mut().remove(&id);
293            });
294        }
295        if self.flags.replace(ScopeFlag::Enqueued, false) {
296            self.runtime.mark_scope_recomposed();
297        }
298    }
299}
300
301/// Size of the registry of live recompose scopes, for leak checks.
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
303pub struct RecomposeScopeRegistryDebugStats {
304    /// Scopes registered.
305    pub len: usize,
306    /// Capacity reserved for the registry.
307    pub capacity: usize,
308}
309
310/// The unit of recomposition. State read inside a scope invalidates it, and a recomposition runs
311/// only the content of the invalid scopes. Clones refer to the same scope.
312#[derive(Clone)]
313pub struct RecomposeScope {
314    pub(crate) inner: Rc<RecomposeScopeInner>,
315}
316
317impl PartialEq for RecomposeScope {
318    fn eq(&self, other: &Self) -> bool {
319        Rc::ptr_eq(&self.inner, &other.inner)
320    }
321}
322
323impl Eq for RecomposeScope {}
324
325impl Hash for RecomposeScope {
326    fn hash<H: Hasher>(&self, state: &mut H) {
327        self.id().hash(state);
328    }
329}
330
331impl RecomposeScope {
332    /// Marks this scope as one that only recomputes a derived state: running
333    /// it changes nothing a composition shows unless the value it writes
334    /// invalidates the scopes that read it.
335    pub(crate) fn mark_derivation(&self) {
336        self.inner.flags.set(ScopeFlag::Derivation, true);
337    }
338
339    pub(crate) fn is_derivation(&self) -> bool {
340        self.inner.flags.get(ScopeFlag::Derivation)
341    }
342
343    pub(crate) fn new(runtime: RuntimeHandle) -> Self {
344        Self {
345            inner: Rc::new(RecomposeScopeInner::new(runtime.scope_runtime())),
346        }
347    }
348
349    pub(crate) fn downgrade(&self) -> Weak<RecomposeScopeInner> {
350        Rc::downgrade(&self.inner)
351    }
352
353    pub(crate) fn upgrade(weak: &Weak<RecomposeScopeInner>) -> Option<Self> {
354        weak.upgrade().map(|inner| Self { inner })
355    }
356
357    /// Returns an id that is unique among the live scopes.
358    pub fn id(&self) -> ScopeId {
359        self.inner.id()
360    }
361
362    /// Returns `true` if state that the scope read has changed since it last recomposed.
363    pub fn is_invalid(&self) -> bool {
364        self.inner.flags.get(ScopeFlag::Invalid)
365    }
366
367    pub(crate) fn is_enqueued(&self) -> bool {
368        self.inner.flags.get(ScopeFlag::Enqueued)
369    }
370
371    /// Returns `false` between `deactivate` and `reactivate`.
372    pub fn is_active(&self) -> bool {
373        self.inner.flags.get(ScopeFlag::Active)
374    }
375
376    /// Total deactivations along this scope's owner chain. A retained slot
377    /// composition records this at compose time; a later mismatch means some
378    /// enclosing composition was deactivated (its owned effects cancelled)
379    /// since the slot last composed, so the retained content must recompose
380    /// once to restart them — no flag on the slot's own scopes carries that
381    /// trace, because deactivation walks stop at slot-host boundaries.
382    pub fn owner_chain_deactivation_epoch(&self) -> u64 {
383        let mut total = self.inner.deactivations.get().get();
384        let mut owner = self.inner.owner();
385        while let ScopeOwner::Live(scope) = owner {
386            total = total.wrapping_add(scope.deactivations.get().get());
387            owner = scope.owner();
388        }
389        total
390    }
391
392    pub(crate) fn is_effectively_active(&self) -> bool {
393        self.inner.is_effectively_active(scope_activity_epoch())
394    }
395
396    pub(crate) fn record_state_subscription(&self, state_id: StateId) {
397        push_unique_state_id(&mut self.inner.state_subscriptions.borrow_mut(), state_id);
398    }
399
400    /// Starts collecting the states a body of the scope reads.
401    pub(crate) fn begin_reads(&self) {
402        self.inner.read_cursor.set(0);
403    }
404
405    /// Notes that the body running now read `state_id`. While the body reads
406    /// what the last one read, in the same order, a read only moves the
407    /// cursor along the subscriptions, and the state is known to watch the
408    /// scope already; the first read off that order copies the reads so far
409    /// into `run_reads`.
410    #[inline]
411    pub(crate) fn note_read(&self, state_id: StateId) -> ScopeRead {
412        let cursor = self.inner.read_cursor.get();
413        if cursor == NOT_READING {
414            return ScopeRead::NotReading;
415        }
416        if cursor == DIVERGED {
417            update_cell(&self.inner.run_reads, |reads| {
418                if let Some(reads) = reads
419                    && reads.last() != Some(&state_id)
420                {
421                    reads.push(state_id);
422                }
423            });
424            return ScopeRead::Collected;
425        }
426        let at = cursor as usize;
427        let subscribed = self.inner.state_subscriptions.borrow();
428        if subscribed.get(at) == Some(&state_id) {
429            self.inner.read_cursor.set(cursor + 1);
430            return ScopeRead::Watched;
431        }
432        if at > 0 && subscribed[at - 1] == state_id {
433            return ScopeRead::Watched;
434        }
435        let mut reads = StateIds::from_slice(&subscribed[..at]);
436        reads.push(state_id);
437        self.inner.run_reads.set(Some(Box::new(reads)));
438        self.inner.read_cursor.set(DIVERGED);
439        ScopeRead::Collected
440    }
441
442    // Keeps the subscriptions the scope has: its body runs this pass without
443    // the reads that made them, as a skipped body does.
444    pub(crate) fn keep_reads(&self) {
445        self.inner.read_cursor.set(NOT_READING);
446    }
447
448    /// Ends collecting reads. A body that ran makes its reads the scope's
449    /// subscriptions and drops the states it did not read again, as Compose
450    /// does; a skipped or unfinished one keeps the subscriptions it had.
451    #[inline]
452    pub(crate) fn end_reads(&self, run: BodyRun) {
453        let ran = run == BodyRun::Ran;
454        let cursor = self.inner.read_cursor.replace(NOT_READING);
455        // A skipped body that read in order keeps the subscriptions as they
456        // are.
457        if cursor == NOT_READING || (!ran && cursor != DIVERGED) {
458            return;
459        }
460        self.settle_reads(cursor, ran);
461    }
462
463    /// The part of [`Self::end_reads`] for a body that ran or left the
464    /// order of its reads: `cursor` is where the read cursor stood.
465    #[inline(never)]
466    fn settle_reads(&self, cursor: u32, ran: bool) {
467        let id = self.id();
468        let mut subscribed = self.inner.state_subscriptions.borrow_mut();
469        if cursor != DIVERGED {
470            // The body read a prefix of the subscriptions, in order.
471            let read = cursor as usize;
472            if ran && read < subscribed.len() {
473                for &state_id in &subscribed[read..] {
474                    if !subscribed[..read].contains(&state_id) {
475                        self.inner.runtime.forget_state_scope(state_id, id);
476                    }
477                }
478                subscribed.truncate(read);
479            }
480            return;
481        }
482        let Some(reads) = self.inner.run_reads.take() else {
483            return;
484        };
485        if ran {
486            for &state_id in subscribed.iter() {
487                if !reads.contains(&state_id) {
488                    self.inner.runtime.forget_state_scope(state_id, id);
489                }
490            }
491            *subscribed = *reads;
492        } else {
493            for state_id in *reads {
494                push_unique_state_id(&mut subscribed, state_id);
495            }
496        }
497    }
498
499    fn record_unknown_invalidation_source(&self) {
500        self.inner
501            .flags
502            .set(ScopeFlag::UnknownInvalidationSource, true);
503        update_cell(&self.inner.invalidation_sources, StateIds::clear);
504    }
505
506    fn record_state_invalidation_source(&self, state_id: StateId) {
507        if !self.inner.flags.get(ScopeFlag::UnknownInvalidationSource) {
508            update_cell(&self.inner.invalidation_sources, |ids| {
509                push_unique_state_id(ids, state_id);
510            });
511        }
512    }
513
514    fn enqueue_invalidation(&self) {
515        self.inner.flags.set(ScopeFlag::Invalid, true);
516        if self.is_effectively_active() && self.may_queue() {
517            self.queue_recompose();
518        }
519    }
520
521    // Whether the scope may join the recompose queue: it is not in it, and
522    // its pausable content, if any, completed.
523    fn may_queue(&self) -> bool {
524        !self
525            .inner
526            .flags
527            .either(ScopeFlag::Enqueued, ScopeFlag::Pending)
528    }
529
530    fn queue_recompose(&self) {
531        self.inner.flags.set(ScopeFlag::Enqueued, true);
532        self.inner.runtime.register_invalid_scope(self.downgrade());
533    }
534
535    pub(crate) fn invalidate(&self) {
536        self.record_unknown_invalidation_source();
537        self.enqueue_invalidation();
538    }
539
540    pub(crate) fn invalidate_from_state(&self, state_id: StateId) {
541        self.record_state_invalidation_source(state_id);
542        if self.inner.read_cursor.get() != NOT_READING {
543            self.note_running_write(state_id);
544        }
545        self.enqueue_invalidation();
546    }
547
548    // A write to `state_id` while the scope's body runs. After a read of it
549    // in this run, the body composed from the value the write replaced: it
550    // composes again once it closes, as Compose's does after a backward
551    // write. A write before any read, as a provider's, needs no second run.
552    #[inline(never)]
553    fn note_running_write(&self, state_id: StateId) {
554        let cursor = self.inner.read_cursor.get();
555        let read = if cursor == DIVERGED {
556            read_cell(&self.inner.run_reads, |reads| {
557                reads
558                    .as_ref()
559                    .is_some_and(|reads| reads.contains(&state_id))
560            })
561        } else {
562            self.inner.state_subscriptions.borrow()[..cursor as usize].contains(&state_id)
563        };
564        if read {
565            self.request_pending_recompose();
566        }
567    }
568
569    pub(crate) fn mark_recomposed(&self) {
570        // An invalidation notes its source and sets `invalid` together, so a
571        // scope nothing invalidated has no source to forget.
572        if self.inner.flags.replace(ScopeFlag::Invalid, false) {
573            self.inner
574                .flags
575                .set(ScopeFlag::UnknownInvalidationSource, false);
576            update_cell(&self.inner.invalidation_sources, StateIds::clear);
577        }
578        self.inner.flags.set(ScopeFlag::ForceReuse, false);
579        self.inner.flags.set(ScopeFlag::ForceRecompose, false);
580        if self.inner.flags.replace(ScopeFlag::Enqueued, false) {
581            self.inner.runtime.mark_scope_recomposed();
582        }
583        let pending = self.inner.flags.replace(ScopeFlag::PendingRecompose, false);
584        if pending {
585            if self.inner.flags.get(ScopeFlag::Active) {
586                self.invalidate();
587            } else {
588                self.inner.flags.set(ScopeFlag::Invalid, true);
589            }
590        }
591    }
592
593    /// Registers `callback` as the code that recomposes the scope when state
594    /// its body read changes.
595    pub(crate) fn set_recompose_callback<F>(&self, callback: F)
596    where
597        F: FnMut(&Composer) + 'static,
598    {
599        self.note_recompose_source();
600        self.inner.recompose.set(None);
601        self.inner.recompose_closure.set(Some(Box::new(callback)));
602    }
603
604    // Registers `body` as the function that recomposes the scope; the same
605    // function stays as it is.
606    #[inline]
607    pub(crate) fn set_recompose_fn(&self, body: fn(&Composer)) {
608        let same = self
609            .inner
610            .recompose
611            .get()
612            .is_some_and(|current| std::ptr::fn_addr_eq(current, body));
613        if !same {
614            self.set_recompose(body);
615        }
616    }
617
618    #[inline(never)]
619    fn set_recompose(&self, body: fn(&Composer)) {
620        self.note_recompose_source();
621        self.inner.recompose.set(Some(body));
622        self.inner.recompose_closure.set(None);
623    }
624
625    fn note_recompose_source(&self) {
626        #[cfg(feature = "inspection")]
627        self.inner
628            .source_trace
629            .replace(source_trace::current_source_trace());
630    }
631
632    // Kept out of line: the ESP32-S3 hot-code list places it in IRAM.
633    #[inline(never)]
634    pub(crate) fn run_recompose(&self, composer: &Composer) -> bool {
635        #[cfg(feature = "inspection")]
636        let _source_context = source_trace::restore_source_trace(&self.inner.source_trace.borrow());
637        let Some(body) = self.inner.recompose.get() else {
638            return self.run_recompose_closure(composer);
639        };
640        self.begin_reads();
641        body(composer);
642        self.end_reads(BodyRun::Ran);
643        true
644    }
645
646    #[inline(never)]
647    fn run_recompose_closure(&self, composer: &Composer) -> bool {
648        let Some(mut closure) = self.inner.recompose_closure.take() else {
649            return false;
650        };
651        self.begin_reads();
652        self.inner.flags.set(ScopeFlag::Recomposing, true);
653        closure(composer);
654        self.inner.flags.set(ScopeFlag::Recomposing, false);
655        self.end_reads(BodyRun::Ran);
656        // The closure may have stored a new one for the scope while it ran.
657        update_cell(&self.inner.recompose_closure, |slot| {
658            if slot.is_none() {
659                *slot = Some(closure);
660            }
661        });
662        true
663    }
664
665    pub(crate) fn has_recompose_callback(&self) -> bool {
666        self.inner.recompose.get().is_some()
667            || self.inner.flags.get(ScopeFlag::Recomposing)
668            || read_cell(&self.inner.recompose_closure, Option::is_some)
669    }
670
671    /// How many scopes this one composes under.
672    pub(crate) fn depth(&self) -> usize {
673        let mut depth = 0;
674        let mut current = self.parent_scope();
675        while let Some(scope) = current {
676            depth += 1;
677            current = scope.parent_scope();
678        }
679        depth
680    }
681
682    pub(crate) fn snapshot_locals(&self, stack: &LocalStackSnapshot) {
683        update_cell(&self.inner.local_stack, |locals| {
684            let unchanged = match (&*locals, stack) {
685                (Some(current), Some(stack)) => Rc::ptr_eq(current, stack),
686                (current, stack) => current.is_none() && stack.is_none(),
687            };
688            if !unchanged {
689                locals.clone_from(stack);
690            }
691        });
692    }
693
694    pub(crate) fn local_stack(&self) -> LocalStackSnapshot {
695        read_cell(&self.inner.local_stack, Clone::clone)
696    }
697
698    pub(crate) fn set_parent_hint(&self, parent: Option<NodeId>) {
699        self.inner.parent_hint.set(parent);
700    }
701
702    /// Ties the scope to the scope its lifetime follows, or to none.
703    fn set_owner(&self, kind: OwnerKind, owner: Option<&Self>) {
704        update_cell(&self.inner.owner, |link| {
705            let current = link
706                .as_ref()
707                .map(|(kind, owner)| (*kind, Weak::as_ptr(owner)));
708            if current != owner.map(|owner| (kind, Rc::as_ptr(&owner.inner))) {
709                *link = owner.map(|owner| (kind, owner.downgrade()));
710                note_scope_activity_change();
711            }
712        });
713    }
714
715    /// Makes `parent` the scope this one composes under, or makes it a root.
716    pub(crate) fn set_parent_scope(&self, parent: Option<&Self>) {
717        self.set_owner(OwnerKind::Parent, parent);
718    }
719
720    fn owner_of_kind(&self, kind: OwnerKind) -> Option<Self> {
721        read_cell(&self.inner.owner, |owner| {
722            owner
723                .as_ref()
724                .filter(|(owner_kind, _)| *owner_kind == kind)
725                .and_then(|(_, owner)| Self::upgrade(owner))
726        })
727    }
728
729    pub(crate) fn parent_scope(&self) -> Option<Self> {
730        self.owner_of_kind(OwnerKind::Parent)
731    }
732
733    /// Makes this scope a root whose lifetime follows `owner`, or a plain
734    /// root.
735    pub(crate) fn set_lifetime_owner_scope(&self, owner: Option<&Self>) {
736        self.set_owner(OwnerKind::Lifetime, owner);
737    }
738
739    #[cfg(test)]
740    pub(crate) fn lifetime_owner_scope(&self) -> Option<Self> {
741        self.owner_of_kind(OwnerKind::Lifetime)
742    }
743
744    pub(crate) fn callback_promotion_target(&self) -> Option<Self> {
745        let mut current = self.parent_scope();
746        while let Some(scope) = current {
747            if scope.has_recompose_callback() {
748                return Some(scope);
749            }
750            current = scope.parent_scope();
751        }
752        None
753    }
754
755    pub(crate) fn parent_hint(&self) -> Option<NodeId> {
756        self.inner.parent_hint.get()
757    }
758
759    pub(crate) fn group_anchor(&self) -> AnchorId {
760        self.inner.group_anchor.get()
761    }
762
763    pub(crate) fn set_group_anchor(&self, anchor: AnchorId) {
764        self.inner.group_anchor.set(anchor);
765    }
766
767    pub(crate) fn set_slots_host(&self, host: &SlotsHost) {
768        let storage_key = host.storage_key();
769        update_cell(&self.inner.slots_runtime_state, |runtime_state| {
770            if self.inner.slots_storage_key.get() == storage_key
771                && runtime_state.as_ref().map(Weak::as_ptr) == host.runtime_state_ptr()
772            {
773                return;
774            }
775            self.inner.slots_storage_key.set(storage_key);
776            *runtime_state = host.runtime_state().map(|state| Rc::downgrade(&state));
777        });
778    }
779
780    pub(crate) fn slots_storage_key(&self) -> Option<usize> {
781        let key = self.inner.slots_storage_key.get();
782        (key != 0).then_some(key)
783    }
784
785    /// What decides the slot host a recomposition of this scope runs in: its
786    /// table's storage key and the runtime state that table belongs to.
787    pub(crate) fn slots_host_identity(&self) -> (usize, *const composer::ComposerRuntimeState) {
788        let state = read_cell(&self.inner.slots_runtime_state, |state| {
789            state.as_ref().map_or(std::ptr::null(), Weak::as_ptr)
790        });
791        (self.inner.slots_storage_key.get(), state)
792    }
793
794    pub(crate) fn slots_runtime_state(&self) -> Option<Rc<composer::ComposerRuntimeState>> {
795        read_cell(&self.inner.slots_runtime_state, |state| {
796            state.as_ref().and_then(Weak::upgrade)
797        })
798    }
799
800    /// Marks the scope inactive, as when its content is retained or held for reuse. An inactive
801    /// scope is not queued for recomposition until it is reactivated.
802    pub fn deactivate(&self) {
803        if !self.inner.flags.replace(ScopeFlag::Active, false) {
804            return;
805        }
806        note_scope_activity_change();
807        self.inner
808            .deactivations
809            .set(PackedU64::new(self.inner.deactivations.get().get() + 1));
810        if self.inner.flags.replace(ScopeFlag::Enqueued, false) {
811            self.inner.runtime.mark_scope_recomposed();
812        }
813    }
814
815    pub(crate) fn defer_until_reactivated(&self) {
816        if self.inner.flags.replace(ScopeFlag::Enqueued, false) {
817            self.inner.runtime.mark_scope_recomposed();
818        }
819    }
820
821    /// Marks the scope active again and queues it for recomposition if it was invalidated while
822    /// inactive.
823    pub fn reactivate(&self) {
824        self.inner.flags.set(ScopeFlag::Active, true);
825        if self.inner.flags.get(ScopeFlag::Invalid)
826            && self.is_effectively_active()
827            && self.may_queue()
828        {
829            self.queue_recompose();
830        }
831    }
832
833    /// Makes the next `should_recompose` return `false` even if the scope is invalid. Cancels a
834    /// pending `force_recompose`.
835    pub fn force_reuse(&self) {
836        self.inner.flags.set(ScopeFlag::ForceReuse, true);
837        self.inner.flags.set(ScopeFlag::ForceRecompose, false);
838        self.inner.flags.set(ScopeFlag::PendingRecompose, true);
839    }
840
841    pub(crate) fn request_pending_recompose(&self) {
842        self.inner.flags.set(ScopeFlag::PendingRecompose, true);
843    }
844
845    /// Makes the next `should_recompose` return `true` even if the scope is not invalid. Cancels a
846    /// pending `force_reuse`.
847    pub fn force_recompose(&self) {
848        self.inner.flags.set(ScopeFlag::ForceRecompose, true);
849        self.inner.flags.set(ScopeFlag::ForceReuse, false);
850        self.inner.flags.set(ScopeFlag::PendingRecompose, false);
851    }
852
853    pub(crate) fn set_retention_mode(&self, mode: RetentionMode) {
854        self.inner.retention_mode.set(mode);
855    }
856
857    pub(crate) fn retention_mode(&self) -> RetentionMode {
858        self.inner.retention_mode.get()
859    }
860
861    /// Returns whether the scope's content has to run now: `true` after `force_recompose` or when
862    /// the scope is invalid, and `false` after `force_reuse`. A forced answer is used up by the
863    /// call.
864    pub fn should_recompose(&self) -> bool {
865        if self.inner.flags.replace(ScopeFlag::ForceRecompose, false) {
866            self.inner.flags.set(ScopeFlag::ForceReuse, false);
867            return true;
868        }
869        if self.inner.flags.replace(ScopeFlag::ForceReuse, false) {
870            return false;
871        }
872        self.is_invalid()
873    }
874
875    /// Returns `true` once the scope has composed its content at least once.
876    pub fn has_composed_once(&self) -> bool {
877        self.inner.flags.get(ScopeFlag::ComposedOnce)
878    }
879
880    pub(crate) fn mark_composed_once(&self) {
881        self.inner.flags.set(ScopeFlag::ComposedOnce, true);
882    }
883
884    // Whether the scope's first body waits for a later frame.
885    pub(crate) fn is_paused(&self) -> bool {
886        self.inner.flags.get(ScopeFlag::Paused)
887    }
888
889    // Sets whether the scope's first body waits, and says whether it waited
890    // already.
891    pub(crate) fn replace_paused(&self, paused: bool) -> bool {
892        self.inner.flags.replace(ScopeFlag::Paused, paused)
893    }
894
895    // Whether the scope belongs to pausable content that has not completed.
896    pub(crate) fn is_pending(&self) -> bool {
897        self.inner.flags.get(ScopeFlag::Pending)
898    }
899
900    // Holds the scope's invalidations until its pausable content completes,
901    // or, with `false`, queues the invalidation that waited.
902    pub(crate) fn set_pending(&self, pending: bool) {
903        self.inner.flags.set(ScopeFlag::Pending, pending);
904        if !pending && self.is_invalid() {
905            self.enqueue_invalidation();
906        }
907    }
908
909    // Whether a change of the state `state_id` alone invalidated the scope.
910    pub(crate) fn invalidated_only_by_state(&self, state_id: StateId) -> bool {
911        !self.inner.flags.get(ScopeFlag::UnknownInvalidationSource)
912            && read_cell(&self.inner.invalidation_sources, |sources| {
913                !sources.is_empty() && sources.iter().all(|source| *source == state_id)
914            })
915    }
916
917    pub(crate) fn invalidated_only_by(&self, allowed_sources: &HashSet<StateId>) -> Option<bool> {
918        if self.inner.flags.get(ScopeFlag::UnknownInvalidationSource) {
919            return None;
920        }
921        read_cell(&self.inner.invalidation_sources, |sources| {
922            (!sources.is_empty()).then(|| {
923                sources
924                    .iter()
925                    .all(|source| allowed_sources.contains(source))
926            })
927        })
928    }
929}
930
931#[cfg(test)]
932impl RecomposeScope {
933    pub(crate) fn new_for_test(runtime: RuntimeHandle) -> Self {
934        Self::new(runtime)
935    }
936}
937
938/// Options that a composable group passes to its recompose scope.
939#[derive(Debug, Clone, Copy, Default)]
940pub struct RecomposeOptions {
941    /// Skips the scope's body on the next composition even if it is invalid.
942    pub force_reuse: bool,
943    /// Runs the scope's body on the next composition even if it is not invalid.
944    pub force_recompose: bool,
945    /// What happens to the scope's content when it leaves the composition.
946    pub retention: RetentionMode,
947}