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