Skip to main content

cranpose_core/
composer.rs

1use std::{
2    any::Any,
3    cell::{Cell, RefCell, RefMut},
4    hash::Hash,
5    marker::PhantomData,
6    rc::{Rc, Weak},
7};
8
9use smallvec::SmallVec;
10
11use crate::{
12    AnchorId, Applier, ApplierGuard, ApplierHost, COMMAND_FLUSH_THRESHOLD, ChildList, Command,
13    CommandQueue, CompositionLocal, DirtyBubble, Key, LocalKey, LocalStackSnapshot,
14    LocalStateEntry, MutableState, Node, NodeError, NodeId, Owned, ProvidedValue, RecomposeOptions,
15    RecomposeScope, RecomposeScopeInner, RecycledNode, RetentionMode, RetentionPolicy,
16    RuntimeHandle, ScopeId, SlotId, SlotPassOutcome, SlotTable, SlotsHost, SnapshotStateList,
17    SnapshotStateMap, StaticCompositionLocal, StaticLocalEntry, SubcomposeState,
18    collections::map::{HashMap, HashSet},
19    composer_context, explicit_group_key_seed,
20    retention::{RetainKey, RetentionManager},
21    runtime,
22    slot::{FinishGroupResult, GroupStart, GroupStartKind, PayloadKind, RootNodeIds, ValueSlotId},
23};
24
25/// A typed reference to a value stored in a slot, made by `Composer::use_value_slot`. It is valid
26/// for the composition pass that made it, and `Composer::with_slot_value` reads through it.
27pub struct ValueSlotHandle<'pass, T: 'static> {
28    slot: ValueSlotId,
29    record: Option<usize>,
30    _pass: PhantomData<&'pass Composer>,
31    _value: PhantomData<fn() -> T>,
32}
33
34impl<T: 'static> Copy for ValueSlotHandle<'_, T> {}
35
36impl<T: 'static> Clone for ValueSlotHandle<'_, T> {
37    fn clone(&self) -> Self {
38        *self
39    }
40}
41
42impl<T: 'static> ValueSlotHandle<'_, T> {
43    pub(crate) fn new(slot: ValueSlotId, record: Option<usize>) -> Self {
44        Self {
45            slot,
46            record,
47            _pass: PhantomData,
48            _value: PhantomData,
49        }
50    }
51}
52
53fn slots_storage_key(host: &Rc<SlotsHost>) -> usize {
54    host.storage_key()
55}
56
57fn bind_slots_host_to_runtime_state(
58    state: &Rc<ComposerRuntimeState>,
59    host: &Rc<SlotsHost>,
60) -> Rc<SlotsHost> {
61    if let Some(bound_state) = host.runtime_state() {
62        if Rc::ptr_eq(&bound_state, state) {
63            state.bind_slots_host(host);
64            return Rc::clone(host);
65        }
66        drop(bound_state);
67        if host.rebind_orphaned_runtime_state(state) {
68            state.bind_slots_host(host);
69            return Rc::clone(host);
70        }
71        log::error!(
72            "slot host already belongs to a different composer runtime state; using a fresh slot host"
73        );
74        let replacement = Rc::new(SlotsHost::new(SlotTable::new()));
75        state.bind_slots_host(&replacement);
76        return replacement;
77    }
78    state.bind_slots_host(host);
79    Rc::clone(host)
80}
81
82struct GroupEntry {
83    key: crate::slot::GroupKey,
84    /// Boxed: a restore is rare, and the entry moves on every call.
85    restored: Option<Box<crate::slot::DetachedSubtree>>,
86    placeholder_for: Option<crate::slot::GroupKey>,
87}
88
89struct GroupScopeEntry<'a> {
90    options: RecomposeOptions,
91    start_kind: GroupStartKind,
92    host: &'a SlotsHost,
93    group: crate::slot::ActiveGroupId,
94    parent_hint: Option<NodeId>,
95}
96
97struct SlotHostPassGuard {
98    core: Rc<ComposerCore>,
99    host: Rc<SlotsHost>,
100    /// The branch fold depth the pass began at.
101    fold_base: usize,
102    active: bool,
103}
104
105impl SlotHostPassGuard {
106    fn close(&mut self) {
107        if !self.active {
108            return;
109        }
110        if self.host.has_active_pass() {
111            self.host.abandon_active_pass();
112        }
113        match self.core.slot_hosts.borrow_mut().pop() {
114            Some(host) if Rc::ptr_eq(&host, &self.host) => {}
115            Some(_) => {
116                log::error!("slot host stack mismatch while closing slot host pass");
117            }
118            None => {
119                log::error!("slot host stack underflow while closing slot host pass");
120            }
121        }
122        self.core.branch_folds.end_pass(self.fold_base);
123        self.active = false;
124    }
125}
126
127impl Drop for SlotHostPassGuard {
128    fn drop(&mut self) {
129        self.close();
130    }
131}
132
133pub(crate) struct PendingMovable {
134    pub(crate) key: crate::slot::GroupKey,
135    pub(crate) placeholder: AnchorId,
136    pub(crate) parent_scope: Option<Weak<RecomposeScopeInner>>,
137}
138
139fn reparent_restored_scopes(
140    host: &SlotsHost,
141    group: crate::slot::ActiveGroupId,
142    old_hint: Option<NodeId>,
143    parent_hint: Option<NodeId>,
144) {
145    host.with_write_session(|slots| {
146        slots.for_each_subtree_scope(group, |scope| {
147            if scope.parent_hint() == old_hint {
148                scope.set_parent_hint(parent_hint);
149            }
150            scope.reactivate();
151        });
152    });
153}
154
155fn movable_retain_key(id: Key) -> RetainKey {
156    RetainKey::for_group(
157        None,
158        crate::slot::GroupKey::new(crate::slot::MOVABLE_STATIC_KEY, Some(id), 0),
159    )
160}
161
162pub(crate) struct ComposerRuntimeState {
163    retention_by_host: RefCell<HashMap<usize, RetentionManager>>,
164    pending_movables_by_host: RefCell<HashMap<usize, Vec<PendingMovable>>>,
165    retention_policy: Cell<RetentionPolicy>,
166    live_hosts: RefCell<HashMap<usize, std::rc::Weak<SlotsHost>>>,
167    applier_host: RefCell<Option<std::rc::Weak<dyn ApplierHost>>>,
168    branch_folds: Rc<crate::slot::BranchFolds>,
169}
170
171/// How many applied command queues a thread keeps for its passes.
172const SPARE_COMMAND_QUEUES: usize = 8;
173
174/// The most commands a kept queue has room for. A larger one grew in a
175/// spike such as the first composition, and keeping it would hold its memory.
176const SPARE_COMMAND_CAPACITY: usize = 1024;
177
178fn spare_commands() -> CommandQueue {
179    crate::thread_state::try_runtimes(|state| state.spare_commands.borrow_mut().pop())
180        .flatten()
181        .unwrap_or_default()
182}
183
184/// The stacks a composer grows while it composes, kept empty between passes
185/// so the next composer on this thread starts with their storage.
186#[derive(Default)]
187pub(crate) struct ComposerStacks {
188    slot_hosts: Vec<Rc<SlotsHost>>,
189    parent_stack: Vec<ParentFrame>,
190    scope_stack: Vec<RecomposeScope>,
191}
192
193/// The most entries a kept stack has room for; a larger one grew in a spike
194/// such as a deep first composition.
195const SPARE_STACK_CAPACITY: usize = 256;
196
197fn spare_composer_stacks() -> ComposerStacks {
198    crate::thread_state::try_runtimes(|state| state.spare_composer_stacks.take())
199        .flatten()
200        .unwrap_or_default()
201}
202
203/// Keeps a dropped composer's emptied stacks for the next composer.
204fn recycle_composer_stacks(mut stacks: ComposerStacks) {
205    let ComposerStacks {
206        slot_hosts,
207        parent_stack,
208        scope_stack,
209    } = &mut stacks;
210    if slot_hosts.capacity() > SPARE_STACK_CAPACITY
211        || parent_stack.capacity() > SPARE_STACK_CAPACITY
212        || scope_stack.capacity() > SPARE_STACK_CAPACITY
213    {
214        return;
215    }
216    slot_hosts.clear();
217    parent_stack.clear();
218    scope_stack.clear();
219    crate::thread_state::try_runtimes(|state| state.spare_composer_stacks.set(Some(stacks)));
220}
221
222/// Keeps an applied, empty command queue for the next pass on this thread.
223pub(crate) fn recycle_commands(commands: CommandQueue) {
224    if commands.capacity() > SPARE_COMMAND_CAPACITY {
225        return;
226    }
227    crate::thread_state::try_runtimes(|state| {
228        let mut spare = state.spare_commands.borrow_mut();
229        if spare.len() < SPARE_COMMAND_QUEUES {
230            spare.push(commands);
231        }
232    });
233}
234
235impl Default for ComposerRuntimeState {
236    fn default() -> Self {
237        Self {
238            retention_by_host: RefCell::new(HashMap::default()),
239            pending_movables_by_host: RefCell::new(HashMap::default()),
240            retention_policy: Cell::new(RetentionPolicy::default()),
241            live_hosts: RefCell::new(HashMap::default()),
242            applier_host: RefCell::new(None),
243            branch_folds: Rc::default(),
244        }
245    }
246}
247
248impl ComposerRuntimeState {
249    pub(crate) fn clear_host_storage_key(&self, host_key: usize, table: &mut SlotTable) {
250        self.retention_by_host.borrow_mut().remove(&host_key);
251        self.pending_movables_by_host.borrow_mut().remove(&host_key);
252        self.live_hosts.borrow_mut().remove(&host_key);
253        table.release_scopes();
254    }
255
256    pub(crate) fn force_recompose_retained_scopes(&self, host_key: usize) {
257        if let Some(retention) = self.retention_by_host.borrow().get(&host_key) {
258            for scope in retention
259                .subtrees()
260                .flat_map(crate::slot::DetachedSubtree::scopes)
261            {
262                scope.force_recompose();
263            }
264        }
265    }
266
267    pub(crate) fn bind_applier_host(&self, applier: &Rc<dyn ApplierHost>) {
268        *self.applier_host.borrow_mut() = Some(Rc::downgrade(applier));
269    }
270
271    pub(crate) fn has_live_applier_host(&self) -> bool {
272        self.applier_host
273            .borrow()
274            .as_ref()
275            .and_then(std::rc::Weak::upgrade)
276            .is_some()
277    }
278
279    pub(crate) fn bind_slots_host(self: &Rc<Self>, host: &Rc<SlotsHost>) {
280        host.bind_runtime_state(self);
281        self.live_hosts
282            .borrow_mut()
283            .insert(host.storage_key(), Rc::downgrade(host));
284    }
285
286    pub(crate) fn retains_any(&self) -> bool {
287        !self.retention_by_host.borrow().is_empty()
288    }
289
290    pub(crate) fn set_retention_policy(&self, policy: RetentionPolicy) {
291        self.retention_policy.set(policy);
292    }
293
294    pub(crate) fn retention_policy(&self) -> RetentionPolicy {
295        self.retention_policy.get()
296    }
297
298    pub(crate) fn take_retained(
299        &self,
300        host: &Rc<SlotsHost>,
301        key: RetainKey,
302        preflight: impl FnOnce(&mut crate::slot::DetachedSubtree) -> bool,
303    ) -> Option<crate::slot::DetachedSubtree> {
304        let host_key = slots_storage_key(host);
305        let mut retention = self.retention_by_host.borrow_mut();
306        let subtree = retention
307            .get_mut(&host_key)?
308            .take_after_restore_preflight(key, preflight);
309        if retention
310            .get(&host_key)
311            .is_some_and(|manager| manager.is_empty() && manager.evictions_total() == 0)
312        {
313            retention.remove(&host_key);
314        }
315        subtree
316    }
317
318    pub(crate) fn insert_retained(
319        &self,
320        host: &Rc<SlotsHost>,
321        key: RetainKey,
322        subtree: crate::slot::DetachedSubtree,
323        pinned: bool,
324    ) -> Vec<crate::slot::DetachedSubtree> {
325        let policy = self.retention_policy();
326        let mut retention_by_host = self.retention_by_host.borrow_mut();
327        let manager = retention_by_host
328            .entry(slots_storage_key(host))
329            .or_insert_with(|| RetentionManager::new(policy));
330        manager.set_policy(policy);
331        if pinned {
332            manager.insert_pinned(key, subtree)
333        } else {
334            manager.insert(key, subtree)
335        }
336    }
337
338    pub(crate) fn take_retained_movable(
339        &self,
340        id: Key,
341    ) -> Option<(Rc<SlotsHost>, crate::slot::DetachedSubtree)> {
342        self.take_retained_movable_by_key(movable_retain_key(id))
343    }
344
345    fn take_retained_movable_by_key(
346        &self,
347        key: RetainKey,
348    ) -> Option<(Rc<SlotsHost>, crate::slot::DetachedSubtree)> {
349        let mut retention_by_host = self.retention_by_host.borrow_mut();
350        let (host_key, manager) = retention_by_host
351            .iter_mut()
352            .find(|(_, manager)| manager.contains(key))?;
353        let host = self.host_for_storage_key(*host_key)?;
354        let subtree = manager.take(key)?;
355        Some((host, subtree))
356    }
357
358    /// Takes a movable's retained content out of a slot table other than the
359    /// one composing, which is how it crosses a subcomposition.
360    pub(crate) fn take_retained_movable_elsewhere(
361        &self,
362        besides: &Rc<SlotsHost>,
363        key: RetainKey,
364    ) -> Option<(Rc<SlotsHost>, crate::slot::DetachedSubtree)> {
365        let besides_key = slots_storage_key(besides);
366        let mut retention_by_host = self.retention_by_host.borrow_mut();
367        let (host_key, manager) = retention_by_host
368            .iter_mut()
369            .find(|(host_key, manager)| **host_key != besides_key && manager.contains(key))?;
370        let host = self.host_for_storage_key(*host_key)?;
371        let subtree = manager.take(key)?;
372        Some((host, subtree))
373    }
374
375    /// Whether a movable's content is retained in any slot table, which is
376    /// what a site waiting for it has to know: the content may have been let
377    /// go by a parent composed in another host, such as a subcomposition.
378    pub(crate) fn movable_retained_anywhere(&self, key: RetainKey) -> bool {
379        self.retention_by_host
380            .borrow()
381            .values()
382            .any(|manager| manager.contains(key))
383    }
384
385    /// The slot table a movable's content is attached to right now, if any.
386    pub(crate) fn host_holding_movable(&self, id: Key) -> Option<Rc<SlotsHost>> {
387        self.live_hosts
388            .borrow()
389            .values()
390            .filter_map(std::rc::Weak::upgrade)
391            .find(|host| host.borrow().movable_is_attached(id))
392    }
393
394    pub(crate) fn record_pending_movable(&self, host: &Rc<SlotsHost>, pending: PendingMovable) {
395        let mut by_host = self.pending_movables_by_host.borrow_mut();
396        let sites = by_host.entry(slots_storage_key(host)).or_default();
397        if sites
398            .iter()
399            .all(|site| site.placeholder != pending.placeholder)
400        {
401            sites.push(pending);
402        }
403    }
404
405    pub(crate) fn take_pending_movables(&self, host: &Rc<SlotsHost>) -> Vec<PendingMovable> {
406        self.pending_movables_by_host
407            .borrow_mut()
408            .remove(&slots_storage_key(host))
409            .unwrap_or_default()
410    }
411
412    /// Every slot table with a site still waiting for movable content. The
413    /// content may be let go by a parent composed in another table, so a pass
414    /// that ends has to look wider than itself.
415    pub(crate) fn hosts_awaiting_movables(&self) -> Vec<Rc<SlotsHost>> {
416        let waiting = self
417            .pending_movables_by_host
418            .borrow()
419            .iter()
420            .filter(|(_, sites)| !sites.is_empty())
421            .map(|(host_key, _)| *host_key)
422            .collect::<Vec<_>>();
423        waiting
424            .into_iter()
425            .filter_map(|host_key| self.host_for_storage_key(host_key))
426            .collect()
427    }
428
429    pub(crate) fn keep_pending_movables(&self, host: &Rc<SlotsHost>, sites: Vec<PendingMovable>) {
430        if sites.is_empty() {
431            return;
432        }
433        self.pending_movables_by_host
434            .borrow_mut()
435            .insert(slots_storage_key(host), sites);
436    }
437
438    pub(crate) fn advance_retention_pass(
439        &self,
440        host: &Rc<SlotsHost>,
441    ) -> Vec<crate::slot::DetachedSubtree> {
442        let host_key = slots_storage_key(host);
443        let policy = self.retention_policy();
444        let mut retention_by_host = self.retention_by_host.borrow_mut();
445        let Some(manager) = retention_by_host.get_mut(&host_key) else {
446            return Vec::new();
447        };
448        manager.set_policy(policy);
449        manager.advance_pass()
450    }
451
452    pub(crate) fn fill_slot_debug_snapshot(
453        &self,
454        host: &SlotsHost,
455        snapshot: &mut crate::SlotDebugSnapshot,
456    ) {
457        let retention = self.retention_debug_stats(host.storage_key());
458        snapshot.retained_subtree_count = retention.subtree_count;
459        snapshot.retained_group_count = retention.group_count;
460        snapshot.retained_payload_count = retention.payload_count;
461        snapshot.retained_node_count = retention.node_count;
462        snapshot.retained_scope_count = retention.scope_count;
463    }
464
465    pub(crate) fn slot_retention_debug_stats(
466        &self,
467        host: &SlotsHost,
468    ) -> crate::slot::SlotRetentionDebugStats {
469        let retention = self.retention_debug_stats(host.storage_key());
470        crate::slot::SlotRetentionDebugStats {
471            retained_subtree_count: retention.subtree_count,
472            retained_group_count: retention.group_count,
473            retained_payload_count: retention.payload_count,
474            retained_node_count: retention.node_count,
475            retained_scope_count: retention.scope_count,
476            retained_anchor_count: retention.anchor_count,
477            retained_heap_bytes: retention.heap_bytes,
478            retained_evictions_total: retention.evictions_total,
479        }
480    }
481
482    pub(crate) fn compact_table_identity_storage_for_host(
483        &self,
484        host: &SlotsHost,
485        table: &mut SlotTable,
486        compact_anchors: bool,
487        compact_payloads: bool,
488    ) {
489        if !compact_anchors && !compact_payloads {
490            return;
491        }
492
493        let host_key = host.storage_key();
494        let mut retention = self.retention_by_host.borrow_mut();
495        if let Some(retained) = retention.get_mut(&host_key) {
496            if compact_anchors {
497                table.compact_anchor_registry_storage(Some(&mut *retained));
498            }
499            if compact_payloads {
500                table.compact_payload_anchor_registry_storage(Some(&mut *retained));
501            }
502        } else {
503            if compact_anchors {
504                table.compact_anchor_registry_storage(None);
505            }
506            if compact_payloads {
507                table.compact_payload_anchor_registry_storage(None);
508            }
509        }
510    }
511
512    pub(crate) fn clear_host(&self, host: &SlotsHost, table: &mut SlotTable) {
513        let host_key = host.storage_key();
514        debug_assert!(
515            self.host_retention_is_empty(host),
516            "host retention must be drained before clearing host ownership"
517        );
518        self.clear_host_storage_key(host_key, table);
519    }
520
521    fn deactivate_and_queue_subtrees(
522        &self,
523        retention: RetentionManager,
524        table: &mut SlotTable,
525        lifecycle: &mut crate::slot::SlotLifecycleCoordinator,
526    ) {
527        for subtree in retention.into_subtrees() {
528            for scope in subtree.scopes() {
529                scope.deactivate();
530            }
531            table.invalidate_detached_subtree_anchors(&subtree);
532            lifecycle.queue_subtree_disposal(subtree);
533        }
534    }
535
536    pub(crate) fn dispose_retained_subtrees_for_host(
537        &self,
538        host_key: usize,
539        table: &mut SlotTable,
540        lifecycle: &mut crate::slot::SlotLifecycleCoordinator,
541    ) -> Result<(), NodeError> {
542        let retention = self.retention_by_host.borrow_mut().remove(&host_key);
543        let applier_host = self
544            .applier_host
545            .borrow()
546            .as_ref()
547            .and_then(std::rc::Weak::upgrade);
548        if let Some(applier_host) = applier_host {
549            let mut disposal = crate::NodeDisposal::default();
550            if let Some(retention) = retention {
551                for subtree in retention.into_subtrees() {
552                    subtree.scopes().for_each(RecomposeScope::deactivate);
553                    table.invalidate_detached_subtree_anchors(&subtree);
554                    disposal.retain_subtree(subtree);
555                }
556            }
557            applier_host.dispose_nodes(disposal)?;
558        } else if let Some(retention) = retention {
559            self.deactivate_and_queue_subtrees(retention, table, lifecycle);
560        }
561        Ok(())
562    }
563
564    pub(crate) fn abandon_retained_subtrees_for_host(
565        &self,
566        host_key: usize,
567        table: &mut SlotTable,
568        lifecycle: &mut crate::slot::SlotLifecycleCoordinator,
569    ) {
570        let Some(retention) = self.retention_by_host.borrow_mut().remove(&host_key) else {
571            self.clear_host_storage_key(host_key, table);
572            return;
573        };
574        self.deactivate_and_queue_subtrees(retention, table, lifecycle);
575        self.clear_host_storage_key(host_key, table);
576    }
577
578    pub(crate) fn host_retention_is_empty(&self, host: &SlotsHost) -> bool {
579        self.retention_by_host
580            .borrow()
581            .get(&host.storage_key())
582            .is_none_or(RetentionManager::is_empty)
583    }
584
585    #[cfg(any(test, debug_assertions))]
586    pub(crate) fn debug_verify_host(&self, host: &SlotsHost, table: &SlotTable) {
587        if let Some(retention) = self.retention_by_host.borrow().get(&host.storage_key()) {
588            retention.debug_verify(table);
589        }
590    }
591
592    #[cfg(test)]
593    pub(crate) fn validate_host_retention(
594        &self,
595        host: &SlotsHost,
596        table: &SlotTable,
597    ) -> Result<(), crate::slot::SlotInvariantError> {
598        if let Some(retention) = self.retention_by_host.borrow().get(&host.storage_key()) {
599            retention.validate(table)?;
600        }
601        Ok(())
602    }
603
604    pub(crate) fn host_for_storage_key(&self, storage_key: usize) -> Option<Rc<SlotsHost>> {
605        self.live_hosts
606            .borrow()
607            .get(&storage_key)
608            .and_then(std::rc::Weak::upgrade)
609    }
610
611    #[cold]
612    #[inline(never)]
613    fn retention_debug_stats(&self, host_key: usize) -> crate::retention::RetentionDebugStats {
614        self.retention_by_host
615            .borrow()
616            .get(&host_key)
617            .map(RetentionManager::debug_stats)
618            .unwrap_or_default()
619    }
620}
621
622pub(crate) struct ParentFrame {
623    pub(crate) id: NodeId,
624    pub(crate) previous: ChildList,
625    pub(crate) new_children: ChildList,
626    pub(crate) new_children_membership: Option<HashSet<NodeId>>,
627    pub(crate) attach_mode: ParentAttachMode,
628    pub(crate) synthetic_root: bool,
629}
630
631#[derive(Clone, Copy)]
632pub(crate) enum InitialParentFrame {
633    SyntheticRoot,
634    RealParent,
635}
636
637const LARGE_DEFERRED_CHILD_TRACKING_THRESHOLD: usize = 16;
638
639#[derive(Clone, Copy, Debug, PartialEq, Eq)]
640pub(crate) enum ParentAttachMode {
641    ImmediateAppend,
642    DeferredSync,
643}
644
645#[derive(Default)]
646pub(crate) struct SubcomposeFrame {
647    pub(crate) nodes: Vec<NodeId>,
648    pub(crate) scopes: Vec<RecomposeScope>,
649}
650
651/// The values one provider supplies, over the frames of the providers around
652/// it.
653pub(crate) struct LocalFrame {
654    values: SmallVec<[(LocalKey, Rc<dyn Any>); 2]>,
655    parent: LocalStackSnapshot,
656}
657
658/// The entry the innermost provider in `stack` supplies for `key`.
659fn provided_entry(stack: &LocalStackSnapshot, key: &LocalKey) -> Option<Rc<dyn Any>> {
660    let mut frame = stack.as_deref();
661    while let Some(current) = frame {
662        if let Some((_, entry)) = current.values.iter().find(|(provided, _)| provided == key) {
663            return Some(Rc::clone(entry));
664        }
665        frame = current.parent.as_deref();
666    }
667    None
668}
669
670pub(crate) struct ComposerCore {
671    pub(crate) shared_state: Rc<ComposerRuntimeState>,
672    /// The branch fold stack of the runtime state, which its nested
673    /// composers share.
674    pub(crate) branch_folds: Rc<crate::slot::BranchFolds>,
675    pub(crate) slots: Rc<SlotsHost>,
676    slot_hosts: RefCell<Vec<Rc<SlotsHost>>>,
677    pub(crate) applier: Rc<dyn ApplierHost>,
678    pub(crate) runtime: RuntimeHandle,
679    pub(crate) parent_stack: RefCell<Vec<ParentFrame>>,
680    pub(crate) subcompose_stack: RefCell<Vec<SubcomposeFrame>>,
681    pub(crate) root: Cell<Option<NodeId>>,
682    pub(crate) commands: RefCell<CommandQueue>,
683    pub(crate) scope_stack: RefCell<Vec<RecomposeScope>>,
684    subcomposition_owner_scope: RefCell<Option<RecomposeScope>>,
685    pub(crate) local_stack: RefCell<LocalStackSnapshot>,
686    pub(crate) side_effects: RefCell<Vec<Box<dyn FnOnce()>>>,
687    pub(crate) pending_scope_options: RefCell<Option<RecomposeOptions>>,
688    pub(crate) phase: Cell<crate::Phase>,
689    pub(crate) recompose_parent_hint: Cell<Option<NodeId>>,
690    pub(crate) recompose_child_cursor: Cell<crate::recompose::RecomposeChildCursor>,
691    pub(crate) root_render_requested: Cell<bool>,
692    /// Whether a static composition local provided around what composes
693    /// now changed: every group opened meanwhile runs its body.
694    locals_changed: Cell<bool>,
695    pub(crate) _not_send: PhantomData<*const ()>,
696}
697
698/// The composition context inherited by work that is composed in another slot
699/// host. Besides composition locals, this carries the source owner scope so a
700/// secondary tree cannot outlive the composition that supplied its callbacks.
701#[derive(Clone)]
702pub struct CapturedCompositionContext {
703    locals: LocalStackSnapshot,
704    owner_scope: Option<Weak<RecomposeScopeInner>>,
705    locals_changed: bool,
706}
707
708impl CapturedCompositionContext {
709    /// Whether a static composition local provided around the capture site
710    /// changed in the composition that captured this context: what composes
711    /// under it, subcompositions included, must compose again without
712    /// skipping.
713    pub fn locals_changed(&self) -> bool {
714        self.locals_changed
715    }
716
717    /// Total deactivations along the capturing scope's owner chain right now;
718    /// see [`crate::RecomposeScope::owner_chain_deactivation_epoch`]. Zero
719    /// when the context has no owner scope or it is gone.
720    pub fn owner_chain_deactivation_epoch(&self) -> u64 {
721        self.owner_scope
722            .as_ref()
723            .and_then(Weak::upgrade)
724            .map_or(0, |inner| {
725                crate::RecomposeScope { inner }.owner_chain_deactivation_epoch()
726            })
727    }
728}
729
730fn take_subcompose_frame(core: &ComposerCore, operation: &str) -> SubcomposeFrame {
731    match core.subcompose_stack.borrow_mut().pop() {
732        Some(frame) => frame,
733        None => {
734            log::error!("subcompose stack underflow while finishing {operation}");
735            SubcomposeFrame::default()
736        }
737    }
738}
739
740struct SubcomposeStackGuard {
741    core: Rc<ComposerCore>,
742    leaked: bool,
743}
744
745impl Drop for SubcomposeStackGuard {
746    fn drop(&mut self) {
747        if !self.leaked {
748            self.core.subcompose_stack.borrow_mut().pop();
749        }
750    }
751}
752
753impl Drop for ComposerCore {
754    /// A pass that queued nothing never takes its queue: hand its storage
755    /// back for the next pass.
756    fn drop(&mut self) {
757        let commands = std::mem::take(self.commands.get_mut());
758        if commands.len() == 0 && commands.capacity() > 0 {
759            recycle_commands(commands);
760        }
761        recycle_composer_stacks(ComposerStacks {
762            slot_hosts: std::mem::take(self.slot_hosts.get_mut()),
763            parent_stack: std::mem::take(self.parent_stack.get_mut()),
764            scope_stack: std::mem::take(self.scope_stack.get_mut()),
765        });
766    }
767}
768
769impl ComposerCore {
770    pub(crate) fn new(
771        shared_state: Rc<ComposerRuntimeState>,
772        slots: Rc<SlotsHost>,
773        applier: Rc<dyn ApplierHost>,
774        runtime: RuntimeHandle,
775        root: Option<NodeId>,
776        initial_parent_frame: InitialParentFrame,
777    ) -> Self {
778        let ComposerStacks {
779            slot_hosts,
780            mut parent_stack,
781            scope_stack,
782        } = spare_composer_stacks();
783        if let Some(root_id) = root {
784            parent_stack.push(ParentFrame {
785                id: root_id,
786                previous: ChildList::new(),
787                new_children: ChildList::new(),
788                new_children_membership: None,
789                attach_mode: ParentAttachMode::DeferredSync,
790                synthetic_root: matches!(initial_parent_frame, InitialParentFrame::SyntheticRoot),
791            });
792        }
793
794        let commands = spare_commands();
795        Self {
796            branch_folds: Rc::clone(&shared_state.branch_folds),
797            shared_state,
798            slots,
799            slot_hosts: RefCell::new(slot_hosts),
800            applier,
801            runtime,
802            parent_stack: RefCell::new(parent_stack),
803            subcompose_stack: RefCell::new(Vec::new()),
804            root: Cell::new(root),
805            commands: RefCell::new(commands),
806            scope_stack: RefCell::new(scope_stack),
807            subcomposition_owner_scope: RefCell::new(None),
808            local_stack: RefCell::new(None),
809            side_effects: RefCell::new(Vec::new()),
810            pending_scope_options: RefCell::new(None),
811            phase: Cell::new(crate::Phase::Compose),
812            recompose_parent_hint: Cell::new(None),
813            recompose_child_cursor: Cell::new(crate::recompose::RecomposeChildCursor::Unknown),
814            root_render_requested: Cell::new(false),
815            locals_changed: Cell::new(false),
816            _not_send: PhantomData,
817        }
818    }
819}
820
821/// The state of one composition pass. It writes the slot table and queues node changes for the
822/// applier. Clones share the same pass. Composable functions reach the current composer through
823/// `with_current_composer`.
824#[derive(Clone)]
825pub struct Composer {
826    pub(crate) core: Rc<ComposerCore>,
827}
828
829/// Keeps a branch's key in the fold of the groups composed inside it, for
830/// code `#[composable]` expands to.
831#[doc(hidden)]
832pub struct BranchGroupGuard<'a> {
833    folds: &'a crate::slot::BranchFolds,
834    token: usize,
835}
836
837impl Drop for BranchGroupGuard<'_> {
838    #[inline]
839    fn drop(&mut self) {
840        self.folds.close(self.token);
841    }
842}
843
844/// A [`BranchGroupGuard`] for code that reaches the composer through the
845/// thread's current one, as a content closure does.
846#[doc(hidden)]
847pub struct ScopedBranchGroupGuard {
848    folds: Rc<crate::slot::BranchFolds>,
849    token: usize,
850}
851
852impl ScopedBranchGroupGuard {
853    #[inline]
854    pub(crate) fn open(folds: &Rc<crate::slot::BranchFolds>, key: Key) -> Self {
855        Self {
856            token: folds.push(key),
857            folds: Rc::clone(folds),
858        }
859    }
860}
861
862impl Drop for ScopedBranchGroupGuard {
863    #[inline]
864    fn drop(&mut self) {
865        self.folds.close(self.token);
866    }
867}
868
869/// A group a composable call opened; [`ComposableGroup::close`] ends it
870/// after the call's body.
871#[doc(hidden)]
872pub struct ComposableGroup<'a> {
873    composer: &'a Composer,
874    host: Rc<SlotsHost>,
875    scope: RecomposeScope,
876    pass: Option<SlotHostPassGuard>,
877    runs_body: bool,
878    completed: bool,
879    skipped: Cell<bool>,
880}
881
882impl ComposableGroup<'_> {
883    /// The recompose scope of the group.
884    pub fn scope(&self) -> &RecomposeScope {
885        &self.scope
886    }
887
888    /// Skips the group's body: what it composed last stays. The call
889    /// composes nothing more in the group before closing it.
890    pub fn skip(&self) {
891        self.skipped.set(true);
892    }
893
894    /// Ends the group after its body composed.
895    pub fn close(mut self) {
896        self.completed = true;
897    }
898}
899
900impl Drop for ComposableGroup<'_> {
901    fn drop(&mut self) {
902        if self.completed {
903            self.scope.mark_composed_once();
904        }
905        if self.skipped.get() {
906            self.composer
907                .close_skipped_group_in_active_pass(&self.host, &self.scope);
908        } else {
909            self.composer
910                .close_group_in_active_pass(&self.host, &self.scope);
911        }
912        if let Some(mut pass) = self.pass.take() {
913            if self.completed
914                && let Err(err) = self.composer.finish_slot_host_pass(&pass.host)
915            {
916                log::error!("slot host pass finalization failed: {err}");
917            }
918            pass.close();
919        }
920    }
921}
922
923pub(crate) enum EmittedNode {
924    Fresh(Box<dyn Node>),
925    Recycled(RecycledNode),
926}
927
928impl Composer {
929    pub(crate) fn new_with_shared_state(
930        shared_state: Rc<ComposerRuntimeState>,
931        slots: Rc<SlotsHost>,
932        applier: Rc<dyn ApplierHost>,
933        runtime: RuntimeHandle,
934        root: Option<NodeId>,
935    ) -> Self {
936        Self::new_with_shared_state_with_parent_frame(
937            shared_state,
938            slots,
939            applier,
940            runtime,
941            root,
942            InitialParentFrame::SyntheticRoot,
943        )
944    }
945
946    fn new_with_shared_state_with_parent_frame(
947        shared_state: Rc<ComposerRuntimeState>,
948        slots: Rc<SlotsHost>,
949        applier: Rc<dyn ApplierHost>,
950        runtime: RuntimeHandle,
951        root: Option<NodeId>,
952        initial_parent_frame: InitialParentFrame,
953    ) -> Self {
954        shared_state.bind_applier_host(&applier);
955        let slots = bind_slots_host_to_runtime_state(&shared_state, &slots);
956        let core = Rc::new(ComposerCore::new(
957            shared_state,
958            slots,
959            applier,
960            runtime,
961            root,
962            initial_parent_frame,
963        ));
964        Self { core }
965    }
966
967    /// Creates a composer that writes into `slots` and queues node changes for `applier`. Top-level
968    /// nodes attach under `root`, or have no parent when `root` is `None`.
969    pub fn new(
970        slots: Rc<SlotsHost>,
971        applier: Rc<dyn ApplierHost>,
972        runtime: RuntimeHandle,
973        root: Option<NodeId>,
974    ) -> Self {
975        Self::new_with_shared_state_with_parent_frame(
976            slots
977                .runtime_state()
978                .unwrap_or_else(|| Rc::new(ComposerRuntimeState::default())),
979            slots,
980            applier,
981            runtime,
982            root,
983            InitialParentFrame::RealParent,
984        )
985    }
986
987    pub(crate) fn from_core(core: Rc<ComposerCore>) -> Self {
988        Self { core }
989    }
990
991    pub(crate) fn clone_core(&self) -> Rc<ComposerCore> {
992        Rc::clone(&self.core)
993    }
994
995    pub(crate) fn request_root_render(&self) {
996        self.core.root_render_requested.set(true);
997    }
998
999    pub(crate) fn take_root_render_request(&self) -> bool {
1000        self.core.root_render_requested.replace(false)
1001    }
1002
1003    /// Returns the slot table host that receives this composer's writes: the innermost host set up
1004    /// for a subcomposition, otherwise the composition's own.
1005    pub fn active_slots_host(&self) -> Rc<SlotsHost> {
1006        self.core
1007            .slot_hosts
1008            .borrow()
1009            .last()
1010            .cloned()
1011            .unwrap_or_else(|| Rc::clone(&self.core.slots))
1012    }
1013
1014    fn with_active_slots_host<R>(&self, f: impl FnOnce(&SlotsHost) -> R) -> R {
1015        let hosts = self.core.slot_hosts.borrow();
1016        f(hosts.last().unwrap_or(&self.core.slots))
1017    }
1018
1019    pub(crate) fn with_slots<R>(&self, f: impl FnOnce(&SlotTable) -> R) -> R {
1020        self.with_active_slots_host(|host| f(&host.borrow()))
1021    }
1022
1023    pub(crate) fn with_slots_mut<R>(&self, f: impl FnOnce(&mut SlotTable) -> R) -> R {
1024        self.with_active_slots_host(|host| f(&mut host.borrow_mut()))
1025    }
1026
1027    pub(crate) fn with_slot_session_mut<R>(
1028        &self,
1029        f: impl FnOnce(&mut crate::slot::SlotWriteSession<'_>) -> R,
1030    ) -> R {
1031        self.with_active_slots_host(|host| host.with_write_session(f))
1032    }
1033
1034    pub(crate) fn try_with_slot_host_pass<R>(
1035        &self,
1036        slots: Rc<SlotsHost>,
1037        mode: crate::slot::SlotPassMode,
1038        f: impl FnOnce(&Composer) -> R,
1039    ) -> Result<(R, SlotPassOutcome), NodeError> {
1040        let mut guard = self.begin_slot_host_pass(&slots, mode);
1041        let result = f(self);
1042        let outcome = self.finish_slot_host_pass(&guard.host)?;
1043        guard.close();
1044        Ok((result, outcome))
1045    }
1046
1047    pub(crate) fn with_slot_host_pass<R>(
1048        &self,
1049        slots: Rc<SlotsHost>,
1050        mode: crate::slot::SlotPassMode,
1051        f: impl FnOnce(&Composer) -> R,
1052    ) -> (R, SlotPassOutcome) {
1053        let mut guard = self.begin_slot_host_pass(&slots, mode);
1054        let result = f(self);
1055        let outcome = match self.finish_slot_host_pass(&guard.host) {
1056            Ok(outcome) => outcome,
1057            Err(err) => {
1058                log::error!("slot host pass finalization failed: {err}");
1059                SlotPassOutcome::default()
1060            }
1061        };
1062        guard.close();
1063        (result, outcome)
1064    }
1065
1066    pub(crate) fn with_slot_override<R>(
1067        &self,
1068        slots: Rc<SlotsHost>,
1069        f: impl FnOnce(&Composer) -> R,
1070    ) -> (R, SlotPassOutcome) {
1071        self.with_slot_host_pass(slots, crate::slot::SlotPassMode::Compose, f)
1072    }
1073
1074    fn begin_slot_host_pass(
1075        &self,
1076        slots: &Rc<SlotsHost>,
1077        mode: crate::slot::SlotPassMode,
1078    ) -> SlotHostPassGuard {
1079        let slots = bind_slots_host_to_runtime_state(&self.core.shared_state, slots);
1080        let fold_base = self.core.branch_folds.len();
1081        slots.begin_pass(mode, &self.core.branch_folds);
1082        {
1083            let mut stack = self.core.slot_hosts.borrow_mut();
1084            if let Some(parent) = stack.last()
1085                && !Rc::ptr_eq(parent, &slots)
1086            {
1087                parent.note_nested_host(&slots);
1088            }
1089            stack.push(Rc::clone(&slots));
1090        }
1091        SlotHostPassGuard {
1092            core: self.clone_core(),
1093            host: slots,
1094            fold_base,
1095            active: true,
1096        }
1097    }
1098
1099    fn finish_slot_host_pass(&self, slots: &Rc<SlotsHost>) -> Result<SlotPassOutcome, NodeError> {
1100        let finished = {
1101            let mut applier = self.core.applier.borrow_dyn();
1102            slots.finish_pass(&mut *applier)
1103        }?;
1104        self.handle_detached_children_in_host(slots, None, finished.detached_root_children)?;
1105        self.wake_sites_whose_movable_arrived();
1106        self.evict_retained_subtrees_for_host(slots)?;
1107        slots.complete_pass_cleanup(&finished.outcome);
1108        Ok(finished.outcome)
1109    }
1110
1111    fn wake_sites_whose_movable_arrived(&self) {
1112        for host in self.core.shared_state.hosts_awaiting_movables() {
1113            self.wake_sites_in_host(&host);
1114        }
1115    }
1116
1117    fn wake_sites_in_host(&self, slots: &Rc<SlotsHost>) {
1118        let pending = self.core.shared_state.take_pending_movables(slots);
1119        if pending.is_empty() {
1120            return;
1121        }
1122        let mut waiting = Vec::new();
1123        for site in pending {
1124            if !slots.borrow().group_is_active(site.placeholder) {
1125                continue;
1126            }
1127            let retain_key = RetainKey::for_group(None, site.key);
1128            if !self.core.shared_state.movable_retained_anywhere(retain_key) {
1129                waiting.push(site);
1130                continue;
1131            }
1132            match site.parent_scope.as_ref().and_then(RecomposeScope::upgrade) {
1133                Some(scope) => {
1134                    scope.force_recompose();
1135                    scope.invalidate();
1136                }
1137                None => log::error!(
1138                    "movable content {:?} arrived for a site whose scope is gone",
1139                    site.key
1140                ),
1141            }
1142        }
1143        self.core.shared_state.keep_pending_movables(slots, waiting);
1144    }
1145
1146    pub(crate) fn forget_movables(&self, ids: &[Key]) -> Result<(), NodeError> {
1147        for id in ids {
1148            let Some((host, subtree)) = self.core.shared_state.take_retained_movable(*id) else {
1149                continue;
1150            };
1151            self.dispose_detached_subtree_in_host(&host, subtree)?;
1152            host.flush_pending_drops();
1153        }
1154        Ok(())
1155    }
1156
1157    pub(crate) fn parent_stack(&self) -> RefMut<'_, Vec<ParentFrame>> {
1158        self.core.parent_stack.borrow_mut()
1159    }
1160
1161    pub(crate) fn current_parent_hint(&self) -> Option<NodeId> {
1162        let stack = self.core.parent_stack.borrow();
1163        let stack_hint = stack
1164            .last()
1165            .and_then(|frame| (!frame.synthetic_root).then_some(frame.id));
1166        stack_hint.or_else(|| self.core.recompose_parent_hint.get())
1167    }
1168
1169    pub(crate) fn subcompose_stack(&self) -> RefMut<'_, Vec<SubcomposeFrame>> {
1170        self.core.subcompose_stack.borrow_mut()
1171    }
1172
1173    pub(crate) fn commands_mut(&self) -> RefMut<'_, CommandQueue> {
1174        self.core.commands.borrow_mut()
1175    }
1176
1177    pub(crate) fn enqueue_semantics_invalidation(&self, id: NodeId) {
1178        self.commands_mut().push(Command::BubbleDirty {
1179            node_id: id,
1180            bubble: DirtyBubble::SEMANTICS,
1181        });
1182    }
1183
1184    pub(crate) fn scope_stack(&self) -> RefMut<'_, Vec<RecomposeScope>> {
1185        self.core.scope_stack.borrow_mut()
1186    }
1187
1188    pub(crate) fn local_stack(&self) -> RefMut<'_, LocalStackSnapshot> {
1189        self.core.local_stack.borrow_mut()
1190    }
1191
1192    pub(crate) fn current_local_stack(&self) -> LocalStackSnapshot {
1193        self.core.local_stack.borrow().clone()
1194    }
1195
1196    pub(crate) fn side_effects_mut(&self) -> RefMut<'_, Vec<Box<dyn FnOnce()>>> {
1197        self.core.side_effects.borrow_mut()
1198    }
1199
1200    fn pending_scope_options(&self) -> RefMut<'_, Option<RecomposeOptions>> {
1201        self.core.pending_scope_options.borrow_mut()
1202    }
1203
1204    pub(crate) fn borrow_applier(&self) -> ApplierGuard<'_, dyn Applier> {
1205        self.core.applier.borrow_dyn()
1206    }
1207
1208    /// Records nodes whose retained subtrees a reused subcompose slot just
1209    /// rebound to new content.
1210    ///
1211    /// The rebinding recomposes inline during measure, so every repass it
1212    /// schedules is consumed by the layout pass already running — nothing
1213    /// else survives to tell the scoped scene update these subtrees changed,
1214    /// and a translate-only update would keep their stale layers (measured:
1215    /// a 60pt lazy scroll presented the old row's text at the new row's
1216    /// position). The structural-change set is the one channel drained
1217    /// after layout, so the rebound children ride it.
1218    pub fn record_rebound_slot_children(&self, children: &[NodeId]) {
1219        let mut applier = self.borrow_applier();
1220        for &child in children {
1221            applier.record_structural_change(child);
1222        }
1223    }
1224
1225    /// Registers a virtual node in the Applier.
1226    ///
1227    /// Assigns the node's ID so its layout registry and invalidation metadata
1228    /// belong to the node stored in the applier.
1229    pub fn register_virtual_node(
1230        &self,
1231        node_id: NodeId,
1232        node: Box<dyn Node>,
1233    ) -> Result<(), NodeError> {
1234        let mut applier = self.borrow_applier();
1235        applier.insert_with_id(node_id, node)?;
1236        applier.get_mut(node_id)?.set_node_id(node_id);
1237        Ok(())
1238    }
1239
1240    /// Detaches and disposes subcomposition roots evicted from their reuse pool.
1241    /// Applies pending composition commands before releasing the roots and their descendants.
1242    ///
1243    /// ```no_run
1244    /// # use cranpose_core::{Composer, NodeError, SubcomposeState};
1245    /// # fn finish(composer: &Composer, state: &mut SubcomposeState) -> Result<(), NodeError> {
1246    /// let evicted = state.finish_pass();
1247    /// composer.dispose_subcomposed_nodes(evicted)?;
1248    /// # Ok(())
1249    /// # }
1250    /// ```
1251    pub fn dispose_subcomposed_nodes(
1252        &self,
1253        disposal: crate::subcompose::SubcomposeDisposal,
1254    ) -> Result<(), NodeError> {
1255        if disposal.nodes.is_empty() && disposal.slot_hosts.is_empty() {
1256            return Ok(());
1257        }
1258        self.apply_pending_commands()?;
1259        let disposal = crate::NodeDisposal::from_subcomposed(disposal, &*self.borrow_applier());
1260        self.core.applier.dispose_nodes(disposal)
1261    }
1262
1263    /// Checks if a node has no parent (is a root node).
1264    /// Used by SubcomposeMeasureScope to filter subcompose results.
1265    pub fn node_has_no_parent(&self, node_id: NodeId) -> bool {
1266        let mut applier = self.borrow_applier();
1267        match applier.get_mut(node_id) {
1268            Ok(node) => node.parent().is_none(),
1269            Err(_) => true,
1270        }
1271    }
1272
1273    /// Returns a node's parent, or `None` for a root node.
1274    ///
1275    /// Returns an error when the node is no longer present in the applier.
1276    pub fn node_parent(&self, node_id: NodeId) -> Result<Option<NodeId>, NodeError> {
1277        self.borrow_applier()
1278            .get_mut(node_id)
1279            .map(|node| node.parent())
1280    }
1281
1282    /// Gets the children of a node from the Applier.
1283    ///
1284    /// This is used by SubcomposeLayoutNode to get children of virtual nodes
1285    /// directly from the Applier, where insert_child commands have been applied.
1286    pub fn get_node_children(&self, node_id: NodeId) -> SmallVec<[NodeId; 8]> {
1287        let mut applier = self.borrow_applier();
1288        match applier.get_mut(node_id) {
1289            Ok(node) => {
1290                let mut children = SmallVec::<[NodeId; 8]>::new();
1291                node.collect_children_into(&mut children);
1292                children
1293            }
1294            Err(_) => SmallVec::<[NodeId; 8]>::new(),
1295        }
1296    }
1297
1298    /// Whether any node needs measurement or placement before its cached layout can be reused.
1299    ///
1300    /// ```no_run
1301    /// # use cranpose_core::{Composer, NodeId};
1302    /// # fn cache_is_current(composer: &Composer, children: &[NodeId]) -> bool {
1303    /// !composer.nodes_need_relayout(children.iter().copied())
1304    /// # }
1305    /// ```
1306    pub fn nodes_need_relayout(&self, node_ids: impl IntoIterator<Item = NodeId>) -> bool {
1307        let mut applier = self.borrow_applier();
1308        node_ids.into_iter().any(|node_id| {
1309            applier
1310                .get_mut(node_id)
1311                .is_ok_and(|node| node.layout_dirty())
1312        })
1313    }
1314
1315    /// Records a child node in the current parent frame's expected children list.
1316    ///
1317    /// Used by SubcomposeLayout's `perform_subcompose` to register virtual nodes
1318    /// with the outer composer's parent frame. This ensures that the `pop_parent`
1319    /// call at the end of `subcompose_slot` generates a correct `SyncChildren`
1320    /// command that preserves (rather than removes) the virtual nodes.
1321    ///
1322    /// Without this, `pop_parent` would generate `SyncChildren { expected: [] }`,
1323    /// which removes all virtual nodes and their subtrees from the applier.
1324    pub fn record_subcompose_child(&self, child_id: NodeId) {
1325        let mut parent_stack = self.parent_stack();
1326        if let Some(frame) = parent_stack.last_mut()
1327            && matches!(frame.attach_mode, ParentAttachMode::DeferredSync)
1328        {
1329            if let Some(membership) = frame.new_children_membership.as_mut() {
1330                if membership.insert(child_id) {
1331                    frame.new_children.push(child_id);
1332                }
1333            } else if frame.new_children.len() >= LARGE_DEFERRED_CHILD_TRACKING_THRESHOLD {
1334                let mut membership = HashSet::default();
1335                membership.reserve(frame.new_children.len() + 1);
1336                membership.extend(frame.new_children.iter().copied());
1337                if membership.insert(child_id) {
1338                    frame.new_children.push(child_id);
1339                }
1340                frame.new_children_membership = Some(membership);
1341            } else if !frame.new_children.contains(&child_id) {
1342                frame.new_children.push(child_id);
1343            }
1344        }
1345    }
1346
1347    /// Clears all children of a node in the Applier.
1348    ///
1349    /// This is used by SubcomposeLayoutNode when reusing a virtual node for
1350    /// different content. Without clearing, old children remain attached,
1351    /// causing duplicate/interleaved items in lazy lists after scrolling.
1352    pub fn clear_node_children(&self, node_id: NodeId) {
1353        let mut applier = self.borrow_applier();
1354        if let Ok(node) = applier.get_mut(node_id) {
1355            node.update_children(&[]);
1356        }
1357    }
1358
1359    /// Runs `f` with this composer as the current one and its runtime as the active runtime. Both
1360    /// are restored when `f` returns.
1361    pub fn install<R>(&self, f: impl FnOnce(&Composer) -> R) -> R {
1362        let _composer_guard = composer_context::enter(self);
1363        runtime::push_active_runtime(&self.core.runtime);
1364        struct Guard;
1365        impl Drop for Guard {
1366            fn drop(&mut self) {
1367                runtime::pop_active_runtime();
1368            }
1369        }
1370        let guard = Guard;
1371        let result = f(self);
1372        drop(guard);
1373        result
1374    }
1375
1376    pub(crate) fn flush_pending_commands_if_large(&self) -> Result<(), NodeError> {
1377        let queued = self.core.commands.borrow().len();
1378        if queued < COMMAND_FLUSH_THRESHOLD {
1379            return Ok(());
1380        }
1381        self.apply_pending_commands()
1382    }
1383
1384    fn resolve_group_entry(
1385        &self,
1386        host: &Rc<SlotsHost>,
1387        seed: crate::slot::GroupKeySeed,
1388        parent_scope_id: Option<ScopeId>,
1389    ) -> GroupEntry {
1390        let key = host.with_write_session(|slots| slots.reserve_group_key(seed));
1391        let retain_key = RetainKey::for_group(parent_scope_id, key);
1392        let restored = self
1393            .core
1394            .shared_state
1395            .take_retained(host, retain_key, |subtree| {
1396                host.with_write_session(|slots| slots.retained_restore_ready(key, subtree))
1397            })
1398            .or_else(|| self.take_movable_from_another_table(host, retain_key, key))
1399            .map(Box::new);
1400        if restored.is_some() || !key.is_movable() {
1401            return GroupEntry {
1402                key,
1403                restored,
1404                placeholder_for: None,
1405            };
1406        }
1407        let attached_elsewhere = self.movable_attached_elsewhere(host, key);
1408        if !attached_elsewhere {
1409            return GroupEntry {
1410                key,
1411                restored: None,
1412                placeholder_for: None,
1413            };
1414        }
1415        let id = key.explicit_key.unwrap_or_default();
1416        let placeholder = host.with_write_session(|slots| {
1417            slots.reserve_group_key(crate::slot::GroupKeySeed::movable_placeholder(id))
1418        });
1419        GroupEntry {
1420            key: placeholder,
1421            restored: None,
1422            placeholder_for: Some(key),
1423        }
1424    }
1425
1426    /// Movable content that was let go by a parent composed in another slot
1427    /// table — a `SubcomposeLayout`'s, or the one a subcomposition owns — is
1428    /// retained there, not here. Bring it over: the table that held it lets
1429    /// go of its anchors, and this one issues its own.
1430    fn take_movable_from_another_table(
1431        &self,
1432        host: &Rc<SlotsHost>,
1433        retain_key: RetainKey,
1434        key: crate::slot::GroupKey,
1435    ) -> Option<crate::slot::DetachedSubtree> {
1436        if !key.is_movable() {
1437            return None;
1438        }
1439        let (source, mut subtree) = self
1440            .core
1441            .shared_state
1442            .take_retained_movable_elsewhere(host, retain_key)?;
1443        source
1444            .borrow_mut()
1445            .invalidate_detached_subtree_anchors(&subtree);
1446        if host.with_write_session(|slots| slots.retained_restore_ready(key, &mut subtree)) {
1447            return Some(subtree);
1448        }
1449        log::error!(
1450            "movable content {key:?} could not be taken over by the slot table that asked for it"
1451        );
1452        if let Err(error) = self.dispose_detached_subtree_in_host(host, subtree) {
1453            log::error!("disposing movable content that could not move failed: {error}");
1454        }
1455        None
1456    }
1457
1458    /// Whether the movable's content is attached to some parent that is not
1459    /// the one composing, in this slot table or in another.
1460    fn movable_attached_elsewhere(&self, host: &Rc<SlotsHost>, key: crate::slot::GroupKey) -> bool {
1461        if host.with_write_session(|slots| slots.movable_attached_elsewhere(key)) {
1462            return true;
1463        }
1464        let Some(id) = key.movable_id() else {
1465            return false;
1466        };
1467        self.core
1468            .shared_state
1469            .host_holding_movable(id)
1470            .is_some_and(|holder| !Rc::ptr_eq(&holder, host))
1471    }
1472
1473    fn scope_for_started_group(
1474        &self,
1475        host: &SlotsHost,
1476        group: crate::slot::ActiveGroupId,
1477        scope: Option<RecomposeScope>,
1478    ) -> RecomposeScope {
1479        if let Some(scope) = scope {
1480            return scope;
1481        }
1482        let scope = RecomposeScope::new(self.runtime_handle());
1483        host.with_write_session(|slots| slots.set_group_scope(group, scope.clone()));
1484        scope
1485    }
1486
1487    fn enter_group_scope(&self, scope_ref: &RecomposeScope, entry: GroupScopeEntry<'_>) {
1488        let GroupScopeEntry {
1489            options,
1490            start_kind,
1491            host,
1492            group,
1493            parent_hint,
1494        } = entry;
1495        // A group entered while its scope is inactive is reused content, as
1496        // when a lazy list gives a recycled row to another item: like Compose,
1497        // it composes again rather than skipping, which reactivates the
1498        // scopes beneath it.
1499        let reused = !scope_ref.is_active() && scope_ref.has_composed_once();
1500        scope_ref.reactivate();
1501        {
1502            let mut stack = self.scope_stack();
1503            let parent_scope = stack.last();
1504            scope_ref.set_parent_scope(parent_scope);
1505            if parent_scope.is_none() {
1506                scope_ref.set_lifetime_owner_scope(
1507                    self.core.subcomposition_owner_scope.borrow().as_ref(),
1508                );
1509            } else {
1510                scope_ref.set_lifetime_owner_scope(None);
1511            }
1512            stack.push(scope_ref.clone());
1513        }
1514        scope_ref.set_retention_mode(options.retention);
1515
1516        // Content under a changed static composition local runs every body,
1517        // as restored content does.
1518        let restored = matches!(start_kind, GroupStartKind::Restored);
1519        let recompose = options.force_recompose | self.core.locals_changed.get() | restored;
1520        if recompose | (reused & !options.force_reuse) {
1521            scope_ref.force_recompose();
1522        } else if options.force_reuse {
1523            scope_ref.force_reuse();
1524        }
1525
1526        scope_ref.set_slots_host(host);
1527
1528        {
1529            let mut stack = self.subcompose_stack();
1530            if let Some(frame) = stack.last_mut() {
1531                frame.scopes.push(scope_ref.clone());
1532            }
1533        }
1534
1535        scope_ref.snapshot_locals(&self.core.local_stack.borrow());
1536        if restored {
1537            reparent_restored_scopes(host, group, scope_ref.parent_hint(), parent_hint);
1538        }
1539        scope_ref.set_parent_hint(parent_hint);
1540    }
1541
1542    fn start_group(
1543        &self,
1544        host: &Rc<SlotsHost>,
1545        seed: crate::slot::GroupKeySeed,
1546        parent_node: Option<NodeId>,
1547    ) -> (
1548        GroupStart<crate::slot::ActiveGroupId>,
1549        Option<crate::slot::GroupKey>,
1550    ) {
1551        if !self.core.shared_state.retains_any() {
1552            let started = host.with_write_session(|slots| {
1553                let key = slots.reserve_group_key(seed);
1554                (!key.is_movable()).then(|| slots.begin_group(key, None, parent_node))
1555            });
1556            if let Some(started) = started {
1557                return (started, None);
1558            }
1559        }
1560        let parent_scope_id = self
1561            .core
1562            .scope_stack
1563            .borrow()
1564            .last()
1565            .map(RecomposeScope::id);
1566        let GroupEntry {
1567            key,
1568            restored,
1569            placeholder_for,
1570        } = self.resolve_group_entry(host, seed, parent_scope_id);
1571        let started =
1572            host.with_write_session(|slots| slots.begin_group(key, restored, parent_node));
1573        (started, placeholder_for)
1574    }
1575
1576    #[inline(never)]
1577    fn open_group_in_active_pass(
1578        &self,
1579        host: &Rc<SlotsHost>,
1580        key: crate::slot::GroupKeySeed,
1581    ) -> (RecomposeScope, bool) {
1582        let options = self.pending_scope_options().take().unwrap_or_default();
1583        let parent_node = self.current_parent_hint();
1584        let (
1585            GroupStart {
1586                group,
1587                anchor,
1588                scope,
1589                kind,
1590            },
1591            placeholder_for,
1592        ) = self.start_group(host, key, parent_node);
1593        let scope_ref = self.scope_for_started_group(host, group, scope);
1594        if let Some(movable_key) = placeholder_for {
1595            self.core.shared_state.record_pending_movable(
1596                host,
1597                PendingMovable {
1598                    key: movable_key,
1599                    placeholder: anchor,
1600                    parent_scope: self
1601                        .core
1602                        .scope_stack
1603                        .borrow()
1604                        .last()
1605                        .map(RecomposeScope::downgrade),
1606                },
1607            );
1608        }
1609        self.enter_group_scope(
1610            &scope_ref,
1611            GroupScopeEntry {
1612                options,
1613                start_kind: kind,
1614                host,
1615                group,
1616                parent_hint: parent_node,
1617            },
1618        );
1619        (scope_ref, placeholder_for.is_none())
1620    }
1621
1622    #[inline(never)]
1623    fn close_group_in_active_pass(&self, host: &Rc<SlotsHost>, scope: &RecomposeScope) {
1624        let result = host.with_write_session(|slots| {
1625            let result = slots.finish_group_body();
1626            slots.end_group();
1627            result
1628        });
1629        self.close_finished_group(host, scope, result);
1630        self.settle_closed_group(scope);
1631    }
1632
1633    /// Closes a group whose body was skipped, in one slot session: its root
1634    /// nodes stay attached where they were.
1635    #[inline(never)]
1636    fn close_skipped_group_in_active_pass(&self, host: &Rc<SlotsHost>, scope: &RecomposeScope) {
1637        #[expect(
1638            clippy::redundant_closure_for_method_calls,
1639            reason = "the method path is not general over the session lifetime"
1640        )]
1641        let root_nodes = host.with_write_session(|slots| slots.skip_and_end_group());
1642        self.attach_root_nodes(root_nodes);
1643        self.pop_closed_scope(scope);
1644        self.settle_closed_group(scope);
1645    }
1646
1647    fn settle_closed_group(&self, scope: &RecomposeScope) {
1648        scope.mark_recomposed();
1649        if let Err(err) = self.flush_pending_commands_if_large() {
1650            log::error!("mid-composition command flush failed: {err}");
1651        }
1652    }
1653
1654    #[inline(never)]
1655    fn open_group(&self, key: crate::slot::GroupKeySeed) -> ComposableGroup<'_> {
1656        let host = self.active_slots_host();
1657        let (host, pass) = if host.has_active_pass() {
1658            (host, None)
1659        } else {
1660            let pass = self.begin_slot_host_pass(&host, crate::slot::SlotPassMode::Compose);
1661            (self.active_slots_host(), Some(pass))
1662        };
1663        let (scope, runs_body) = self.open_group_in_active_pass(&host, key);
1664        ComposableGroup {
1665            composer: self,
1666            host,
1667            scope,
1668            pass,
1669            runs_body,
1670            completed: false,
1671            skipped: Cell::new(false),
1672        }
1673    }
1674
1675    #[doc(hidden)]
1676    pub fn __open_composable_group(&self, key: Key) -> ComposableGroup<'_> {
1677        self.open_group(crate::slot::GroupKeySeed::unkeyed(key))
1678    }
1679
1680    pub(crate) fn with_group_seed<R>(
1681        &self,
1682        key: crate::slot::GroupKeySeed,
1683        f: impl FnOnce(&Composer) -> R,
1684    ) -> R {
1685        let group = self.open_group(key);
1686        debug_assert!(group.runs_body, "only movable content waits for a body");
1687        let result = f(self);
1688        group.close();
1689        result
1690    }
1691
1692    pub(crate) fn with_movable_group(&self, id: Key, f: impl FnOnce(&Composer)) {
1693        let group = self.open_group(crate::slot::GroupKeySeed::movable(id));
1694        if group.runs_body {
1695            f(self);
1696        }
1697        group.close();
1698    }
1699
1700    /// Runs `f` in a group identified by `key`. Slots and nodes that `f` creates belong to the
1701    /// group.
1702    pub fn with_group<R>(&self, key: Key, f: impl FnOnce(&Composer) -> R) -> R {
1703        self.with_group_seed(crate::slot::GroupKeySeed::unkeyed(key), f)
1704    }
1705
1706    /// Runs `f` in a group identified by `key` that is retained when it leaves the composition,
1707    /// with the recompose `options` applied to its scope. A retained group returns with its state
1708    /// and nodes when `key` is composed again.
1709    pub fn cranpose_with_reuse<R>(
1710        &self,
1711        key: Key,
1712        mut options: RecomposeOptions,
1713        f: impl FnOnce(&Composer) -> R,
1714    ) -> R {
1715        options.retention = RetentionMode::RetainWhenInactive;
1716        self.pending_scope_options().replace(options);
1717        self.with_group(key, f)
1718    }
1719
1720    /// Runs `f` in a group identified by the hash of `key`, so state and nodes follow the key when
1721    /// siblings move. Compose's `key`.
1722    #[track_caller]
1723    pub fn with_key<K: Hash, R>(&self, key: &K, f: impl FnOnce(&Composer) -> R) -> R {
1724        let seed = explicit_group_key_seed(key, std::panic::Location::caller());
1725        self.with_group_seed(seed, f)
1726    }
1727
1728    #[doc(hidden)]
1729    pub fn __branch_group_deferred(&self, key: Key) -> BranchGroupGuard<'_> {
1730        let folds = &*self.core.branch_folds;
1731        BranchGroupGuard {
1732            token: folds.push(key),
1733            folds,
1734        }
1735    }
1736
1737    fn dispose_detached_nodes(&self, nodes: impl IntoIterator<Item = NodeId>) {
1738        for node_id in nodes {
1739            self.commands_mut().push(Command::callback(move |applier| {
1740                crate::slot::dispose_detached_node_now(applier, node_id)
1741            }));
1742        }
1743    }
1744
1745    fn detached_root_parent_commands(
1746        &self,
1747        subtree: &crate::slot::DetachedSubtree,
1748        context: &'static str,
1749    ) -> Result<Vec<(NodeId, Option<NodeId>)>, NodeError> {
1750        let mut root_nodes = Vec::new();
1751        subtree.collect_root_nodes_checked_into(&mut root_nodes, context);
1752        let mut roots = Vec::with_capacity(root_nodes.len());
1753        for root in root_nodes {
1754            let parent_id = {
1755                let mut applier = self.borrow_applier();
1756                applier.get_mut(root)?.parent()
1757            };
1758            roots.push((root, parent_id));
1759        }
1760        Ok(roots)
1761    }
1762
1763    fn retain_detached_subtree_in_host(
1764        &self,
1765        slots_host: &Rc<SlotsHost>,
1766        parent_scope: Option<ScopeId>,
1767        subtree: crate::slot::DetachedSubtree,
1768    ) -> Result<(), NodeError> {
1769        let Some(root_key) = subtree.root_key_checked() else {
1770            log::error!("retention rejected detached subtree without a root group");
1771            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1772            return Ok(());
1773        };
1774        let root_detaches = self.detached_root_parent_commands(&subtree, "retention")?;
1775        subtree.scopes().for_each(RecomposeScope::deactivate);
1776        for (root, parent_id) in root_detaches {
1777            if let Some(parent_id) = parent_id {
1778                self.commands_mut().push(Command::DetachChild {
1779                    parent_id,
1780                    child_id: root,
1781                });
1782            }
1783        }
1784        let evicted = self.core.shared_state.insert_retained(
1785            slots_host,
1786            RetainKey::for_group(parent_scope, root_key),
1787            subtree,
1788            root_key.is_movable(),
1789        );
1790        for subtree in evicted {
1791            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1792        }
1793        Ok(())
1794    }
1795
1796    fn evict_retained_subtrees_for_host(
1797        &self,
1798        slots_host: &Rc<SlotsHost>,
1799    ) -> Result<(), NodeError> {
1800        let evicted = self.core.shared_state.advance_retention_pass(slots_host);
1801        for subtree in evicted {
1802            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1803        }
1804        Ok(())
1805    }
1806
1807    fn dispose_detached_subtree_in_host(
1808        &self,
1809        slots_host: &Rc<SlotsHost>,
1810        subtree: crate::slot::DetachedSubtree,
1811    ) -> Result<(), NodeError> {
1812        let root_nodes = self
1813            .detached_root_parent_commands(&subtree, "disposal")?
1814            .into_iter()
1815            .map(|(root, _)| root);
1816        subtree.scopes().for_each(RecomposeScope::deactivate);
1817        self.dispose_detached_nodes(root_nodes);
1818        slots_host.with_table_and_lifecycle_mut(|table, lifecycle| {
1819            table.invalidate_detached_subtree_anchors(&subtree);
1820            lifecycle.queue_subtree_disposal(subtree);
1821        });
1822        Ok(())
1823    }
1824
1825    fn handle_detached_children_in_host(
1826        &self,
1827        slots_host: &Rc<SlotsHost>,
1828        parent_scope: Option<ScopeId>,
1829        detached: Vec<crate::slot::DetachedSubtree>,
1830    ) -> Result<(), NodeError> {
1831        for mut subtree in detached {
1832            for movable in subtree.split_off_nested_movables() {
1833                self.retain_detached_subtree_in_host(slots_host, None, movable)?;
1834            }
1835            if subtree
1836                .root_key_checked()
1837                .is_some_and(crate::slot::GroupKey::is_movable)
1838            {
1839                self.retain_detached_subtree_in_host(slots_host, None, subtree)?;
1840                continue;
1841            }
1842            let retention_mode = subtree
1843                .root_scope()
1844                .map(RecomposeScope::retention_mode)
1845                .unwrap_or_default();
1846            match retention_mode {
1847                RetentionMode::DisposeWhenInactive => {
1848                    self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1849                }
1850                RetentionMode::RetainWhenInactive => {
1851                    self.retain_detached_subtree_in_host(slots_host, parent_scope, subtree)?;
1852                }
1853            }
1854        }
1855        Ok(())
1856    }
1857
1858    fn handle_finished_group_result(
1859        &self,
1860        host: &Rc<SlotsHost>,
1861        parent_scope: Option<ScopeId>,
1862        result: FinishGroupResult,
1863    ) {
1864        let FinishGroupResult {
1865            detached_children,
1866            direct_nodes,
1867            root_nodes,
1868            was_skipped,
1869        } = result;
1870        if was_skipped {
1871            self.attach_root_nodes(root_nodes);
1872        }
1873        self.dispose_detached_nodes(direct_nodes);
1874        if detached_children.is_empty() {
1875            return;
1876        }
1877        if let Err(err) =
1878            self.handle_detached_children_in_host(host, parent_scope, detached_children)
1879        {
1880            log::error!("detached subtree handling failed while closing a group: {err}");
1881        }
1882    }
1883
1884    pub(crate) fn close_current_group_body_for_scope(&self, scope: &RecomposeScope) {
1885        self.close_group_body(&self.active_slots_host(), scope);
1886    }
1887
1888    fn close_group_body(&self, host: &Rc<SlotsHost>, scope: &RecomposeScope) {
1889        #[expect(
1890            clippy::redundant_closure_for_method_calls,
1891            reason = "the method path is not general over the session lifetime"
1892        )]
1893        let result = host.with_write_session(|slots| slots.finish_group_body());
1894        self.close_finished_group(host, scope, result);
1895    }
1896
1897    fn close_finished_group(
1898        &self,
1899        host: &Rc<SlotsHost>,
1900        scope: &RecomposeScope,
1901        result: FinishGroupResult,
1902    ) {
1903        self.handle_finished_group_result(host, Some(scope.id()), result);
1904        self.pop_closed_scope(scope);
1905    }
1906
1907    fn pop_closed_scope(&self, scope: &RecomposeScope) {
1908        if let Some(popped) = self.scope_stack().pop() {
1909            debug_assert_eq!(
1910                popped.id(),
1911                scope.id(),
1912                "closed scope must match the active scope stack"
1913            );
1914        } else {
1915            log::error!("scope stack underflow while closing scope {}", scope.id());
1916        }
1917    }
1918
1919    /// Returns the value `init` made on the first composition at this call site. Later compositions
1920    /// return the same value until the call site leaves the composition.
1921    #[track_caller]
1922    pub fn remember<T: 'static>(&self, init: impl FnOnce() -> T) -> Owned<T> {
1923        self.remember_at(crate::caller_location_key(), init)
1924    }
1925
1926    #[doc(hidden)]
1927    pub fn remember_at<T: 'static>(
1928        &self,
1929        source: crate::Key,
1930        init: impl FnOnce() -> T,
1931    ) -> Owned<T> {
1932        self.with_slot_session_mut(|slots| {
1933            slots.remember_with_kind(PayloadKind::Remember, source, init)
1934        })
1935    }
1936
1937    #[track_caller]
1938    pub(crate) fn remember_internal<T: 'static>(
1939        &self,
1940        source_salt: crate::Key,
1941        init: impl FnOnce() -> T,
1942    ) -> Owned<T> {
1943        let source = crate::caller_location_key() ^ source_salt;
1944        self.with_slot_session_mut(|slots| {
1945            slots.remember_with_kind(PayloadKind::Internal, source, init)
1946        })
1947    }
1948
1949    #[track_caller]
1950    pub(crate) fn remember_effect<T: Default + 'static>(&self) -> Owned<T> {
1951        let source = crate::caller_location_key();
1952        self.with_slot_session_mut(|slots| slots.remember_effect::<T>(source))
1953    }
1954
1955    /// Reserves the next slot at the call site for a `T` that `init` makes on the first
1956    /// composition. Returns a handle for reading the value through `with_slot_value`.
1957    #[track_caller]
1958    pub fn use_value_slot<T: 'static>(&self, init: impl FnOnce() -> T) -> ValueSlotHandle<'_, T> {
1959        let source = crate::caller_location_key();
1960        let (slot, record) = self.with_slot_session_mut(|slots| {
1961            slots.located_value_slot(PayloadKind::Internal, source, init)
1962        });
1963        ValueSlotHandle::new(slot, record)
1964    }
1965
1966    #[doc(hidden)]
1967    pub fn __use_param_slot<T: 'static>(
1968        &self,
1969        source: Key,
1970        init: impl FnOnce() -> T,
1971    ) -> ValueSlotHandle<'_, T> {
1972        let (slot, record) = self.with_slot_session_mut(|slots| {
1973            slots.located_value_slot(PayloadKind::Param, source, init)
1974        });
1975        ValueSlotHandle::new(slot, record)
1976    }
1977
1978    /// Finds the call's next parameter slot and runs `update` on its value,
1979    /// with one slot lookup for both.
1980    #[doc(hidden)]
1981    pub fn __update_param_slot<T: 'static, R>(
1982        &self,
1983        source: Key,
1984        init: impl FnOnce() -> T,
1985        update: impl FnOnce(&mut T) -> R,
1986    ) -> (ValueSlotHandle<'_, T>, R) {
1987        self.update_value_slot(PayloadKind::Param, source, init, update)
1988    }
1989
1990    /// Finds the call's return slot and runs `read` on its value, with one
1991    /// slot lookup for both.
1992    #[doc(hidden)]
1993    pub fn __update_return_slot<T: 'static, R>(
1994        &self,
1995        source: Key,
1996        init: impl FnOnce() -> T,
1997        read: impl FnOnce(&mut T) -> R,
1998    ) -> (ValueSlotHandle<'_, T>, R) {
1999        self.update_value_slot(PayloadKind::Return, source, init, read)
2000    }
2001
2002    fn update_value_slot<T: 'static, R>(
2003        &self,
2004        kind: PayloadKind,
2005        source: Key,
2006        init: impl FnOnce() -> T,
2007        update: impl FnOnce(&mut T) -> R,
2008    ) -> (ValueSlotHandle<'_, T>, R) {
2009        self.with_slot_session_mut(|slots| {
2010            let (slot, record, value) = slots.value_slot_and_value(kind, source, init);
2011            (ValueSlotHandle::new(slot, record), update(value))
2012        })
2013    }
2014
2015    #[doc(hidden)]
2016    #[track_caller]
2017    pub fn __use_return_slot<T: 'static>(
2018        &self,
2019        init: impl FnOnce() -> T,
2020    ) -> ValueSlotHandle<'_, T> {
2021        let source = crate::caller_location_key();
2022        let (slot, record) = self.with_slot_session_mut(|slots| {
2023            slots.located_value_slot(PayloadKind::Return, source, init)
2024        });
2025        ValueSlotHandle::new(slot, record)
2026    }
2027
2028    #[doc(hidden)]
2029    pub fn __invalidate_return_consumer_scope(&self) {
2030        let Some(scope) = self.current_recompose_scope() else {
2031            self.request_root_render();
2032            return;
2033        };
2034
2035        if let Some(target) = scope.callback_promotion_target() {
2036            target.invalidate();
2037        } else {
2038            self.request_root_render();
2039        }
2040    }
2041
2042    /// Runs `f` with a shared reference to the value in the slot that `handle` names.
2043    pub fn with_slot_value<'pass, T: 'static, R>(
2044        &'pass self,
2045        handle: ValueSlotHandle<'pass, T>,
2046        f: impl FnOnce(&T) -> R,
2047    ) -> R {
2048        self.with_slots(|slots| f(slots.value_at(handle.slot, handle.record)))
2049    }
2050
2051    /// Runs `f` with a mutable reference to the value in the slot that `handle` names.
2052    pub fn with_slot_value_mut<'pass, T: 'static, R>(
2053        &'pass self,
2054        handle: ValueSlotHandle<'pass, T>,
2055        f: impl FnOnce(&mut T) -> R,
2056    ) -> R {
2057        self.with_slots_mut(|slots| f(slots.value_at_mut(handle.slot, handle.record)))
2058    }
2059
2060    /// Creates a [`MutableState`] holding `initial`, tied to this composer's runtime. It is not
2061    /// remembered, so each call makes a new state.
2062    pub fn mutable_state_of<T: Clone + 'static>(&self, initial: T) -> MutableState<T> {
2063        MutableState::with_runtime(initial, self.runtime_handle())
2064    }
2065
2066    /// Creates a snapshot-aware list holding `values`, tied to this composer's runtime. It is not
2067    /// remembered, so each call makes a new list.
2068    pub fn mutable_state_list_of<T, I>(&self, values: I) -> SnapshotStateList<T>
2069    where
2070        T: Clone + 'static,
2071        I: IntoIterator<Item = T>,
2072    {
2073        SnapshotStateList::with_runtime(values, self.runtime_handle())
2074    }
2075
2076    /// Creates a snapshot-aware map holding `pairs`, tied to this composer's runtime. It is not
2077    /// remembered, so each call makes a new map.
2078    pub fn mutable_state_map_of<K, V, I>(&self, pairs: I) -> SnapshotStateMap<K, V>
2079    where
2080        K: Clone + Eq + Hash + 'static,
2081        V: Clone + 'static,
2082        I: IntoIterator<Item = (K, V)>,
2083    {
2084        SnapshotStateMap::with_runtime(pairs, self.runtime_handle())
2085    }
2086
2087    /// Returns the value of `local` from the innermost enclosing provider, or the local's default
2088    /// value when no provider is in scope.
2089    pub fn read_composition_local<T: Clone + 'static>(&self, local: &CompositionLocal<T>) -> T {
2090        self.provided_local::<LocalStateEntry<T>>(&local.key)
2091            .map_or_else(|| local.default_value(), |entry| entry.value())
2092    }
2093
2094    /// Returns the value of `local` from the innermost enclosing provider, or the local's default
2095    /// value when no provider is in scope.
2096    pub fn read_static_composition_local<T: Clone + 'static>(
2097        &self,
2098        local: &StaticCompositionLocal<T>,
2099    ) -> T {
2100        self.provided_local::<StaticLocalEntry<T>>(&local.key)
2101            .map_or_else(|| local.default_value(), |entry| entry.value())
2102    }
2103
2104    /// The innermost provided entry for `key`, when it is an `E`.
2105    fn provided_local<E: 'static>(&self, key: &LocalKey) -> Option<Rc<E>> {
2106        let entry = provided_entry(&self.core.local_stack.borrow(), key)?;
2107        entry
2108            .downcast::<E>()
2109            .inspect_err(|_| log::error!("composition local entry type mismatch for key {key:?}"))
2110            .ok()
2111    }
2112
2113    /// Returns the recompose scope being composed, or `None` outside any scope.
2114    pub fn current_recompose_scope(&self) -> Option<RecomposeScope> {
2115        self.core.scope_stack.borrow().last().cloned()
2116    }
2117
2118    pub(crate) fn current_state_invalidation_scope(&self) -> Option<RecomposeScope> {
2119        let stack = self.core.scope_stack.borrow();
2120        stack
2121            .iter()
2122            .rev()
2123            .find(|scope| scope.has_recompose_callback())
2124            .cloned()
2125            .or_else(|| stack.last().cloned())
2126    }
2127
2128    /// Returns the phase of the frame in which this composer is used.
2129    pub fn phase(&self) -> crate::Phase {
2130        self.core.phase.get()
2131    }
2132
2133    pub(crate) fn set_phase(&self, phase: crate::Phase) {
2134        self.core.phase.set(phase);
2135    }
2136
2137    /// Switches the composer to `phase`.
2138    pub fn enter_phase(&self, phase: crate::Phase) {
2139        self.set_phase(phase);
2140    }
2141
2142    pub(crate) fn subcompose<R>(
2143        &self,
2144        state: &mut SubcomposeState,
2145        slot_id: SlotId,
2146        content: impl FnOnce(&Composer) -> R,
2147    ) -> (R, Vec<NodeId>) {
2148        match self.phase() {
2149            crate::Phase::Measure | crate::Phase::Layout => {}
2150            current => panic!(
2151                "subcompose() may only be called during measure or layout; current phase: {current:?}"
2152            ),
2153        }
2154
2155        self.subcompose_stack().push(SubcomposeFrame::default());
2156        let mut guard = SubcomposeStackGuard {
2157            core: self.clone_core(),
2158            leaked: false,
2159        };
2160
2161        let slot_host = state.get_or_create_slots(slot_id);
2162        let (result, _) = self.with_slot_override(slot_host, |composer| {
2163            composer.with_group(slot_id.raw(), |composer| content(composer))
2164        });
2165
2166        let frame = {
2167            let frame = take_subcompose_frame(&guard.core, "subcompose");
2168            guard.leaked = true;
2169            frame
2170        };
2171        let nodes = frame.nodes;
2172        let scopes = frame.scopes;
2173        state.register_active(slot_id, &nodes, &scopes);
2174        (result, nodes)
2175    }
2176
2177    /// Composes `content` in the slot `slot_id` of `state` and returns its result with the ids of
2178    /// the nodes it produced that have no parent. Panics outside the measure and layout phases.
2179    pub fn subcompose_measurement<R>(
2180        &self,
2181        state: &mut SubcomposeState,
2182        slot_id: SlotId,
2183        content: impl FnOnce(&Composer) -> R,
2184    ) -> (R, Vec<NodeId>) {
2185        let (result, nodes) = self.subcompose(state, slot_id, content);
2186        let roots = nodes
2187            .into_iter()
2188            .filter(|&id| self.node_has_no_parent(id))
2189            .collect();
2190
2191        (result, roots)
2192    }
2193
2194    fn spin_up_subcompose_core(
2195        &self,
2196        slots: &Rc<SlotsHost>,
2197        root: Option<NodeId>,
2198        runtime_handle: &RuntimeHandle,
2199        locals: LocalStackSnapshot,
2200    ) -> Rc<ComposerCore> {
2201        let phase = self.phase();
2202        let shared_state = slots
2203            .runtime_state()
2204            .unwrap_or_else(|| Rc::clone(&self.core.shared_state));
2205        let core = Rc::new(ComposerCore::new(
2206            shared_state,
2207            Rc::clone(slots),
2208            Rc::clone(&self.core.applier),
2209            runtime_handle.clone(),
2210            root,
2211            InitialParentFrame::RealParent,
2212        ));
2213        core.phase.set(phase);
2214        core.locals_changed.set(self.core.locals_changed.get());
2215        *core.local_stack.borrow_mut() = locals;
2216        core
2217    }
2218
2219    /// Applies a subcomposition's commands. The pass holds a state teardown
2220    /// scope from its start until this returns, as a root pass does: the
2221    /// states a removed group remembered stay readable until the nodes it
2222    /// emitted are detached, whose modifier nodes may read them.
2223    fn flush_subcompose_pass(
2224        &self,
2225        mut commands: CommandQueue,
2226        runtime_handle: &RuntimeHandle,
2227        compact_applier: bool,
2228        side_effects: Vec<Box<dyn FnOnce()>>,
2229    ) -> Result<(), NodeError> {
2230        {
2231            let mut applier = self.borrow_applier();
2232            commands.apply(&mut *applier)?;
2233            for update in runtime_handle.take_updates() {
2234                update.apply(&mut *applier)?;
2235            }
2236        }
2237        recycle_commands(commands);
2238        if compact_applier {
2239            self.core.applier.compact();
2240            self.core.applier.borrow_dyn().clear_recycled_nodes();
2241        }
2242        composer_context::without_composer(|| {
2243            for effect in side_effects {
2244                effect();
2245            }
2246        });
2247        Ok(())
2248    }
2249
2250    /// Composes `f` as an independent pass over the separate slot table `slots` and applies its
2251    /// node changes before returning. Top-level nodes from `f` attach under `root`, if any. The
2252    /// pass sees the composition locals in scope at the call.
2253    pub fn subcompose_in<R>(
2254        &self,
2255        slots: &Rc<SlotsHost>,
2256        root: Option<NodeId>,
2257        f: impl FnOnce(&Composer) -> R,
2258    ) -> Result<R, NodeError> {
2259        let _teardown = crate::runtime::enter_state_teardown_scope();
2260        let runtime_handle = self.runtime_handle();
2261        let locals = self.current_local_stack();
2262        let core = self.spin_up_subcompose_core(slots, root, &runtime_handle, locals);
2263        let composer = Composer::from_core(core);
2264        let (result, commands, side_effects, compact_applier) = composer.install(|composer| {
2265            let (output, outcome) = composer.try_with_slot_host_pass(
2266                Rc::clone(slots),
2267                crate::slot::SlotPassMode::Compose,
2268                |composer| f(composer),
2269            )?;
2270            let commands = composer.take_commands();
2271            let side_effects = composer.take_side_effects();
2272            Ok((output, commands, side_effects, outcome.compacted))
2273        })?;
2274        self.flush_subcompose_pass(commands, &runtime_handle, compact_applier, side_effects)?;
2275        Ok(result)
2276    }
2277
2278    /// Captures the composition context at the current point so work composed
2279    /// in another slot host inherits both locals and source ownership.
2280    ///
2281    /// A `SubcomposeLayout` captures this while it is being composed and replays
2282    /// it while subcomposing off the measure pass, so content that is
2283    /// subcomposed during layout observes the same composition locals as the
2284    /// `SubcomposeLayout` call site — matching Jetpack Compose, where a
2285    /// subcomposition inherits the composition locals of the layout that
2286    /// created it rather than whatever happens to be in scope during measure
2287    /// (which, after composition unwinds, no longer carries ancestor providers).
2288    pub fn capture_composition_context(&self) -> CapturedCompositionContext {
2289        CapturedCompositionContext {
2290            locals: self.current_local_stack(),
2291            owner_scope: self
2292                .current_recompose_scope()
2293                .map(|scope| scope.downgrade()),
2294            locals_changed: self.core.locals_changed.get(),
2295        }
2296    }
2297
2298    /// Runs `f` as content under a static composition local that changed
2299    /// when `changed`: every group it opens runs its body, as under the
2300    /// provider of a changed static local.
2301    pub fn with_locals_changed<R>(&self, changed: bool, f: impl FnOnce(&Composer) -> R) -> R {
2302        let outer = self.core.locals_changed.get();
2303        self.core.locals_changed.set(outer || changed);
2304        let result = f(self);
2305        self.core.locals_changed.set(outer);
2306        result
2307    }
2308
2309    /// Subcomposes content using an isolated SlotsHost without resetting it.
2310    /// Unlike `subcompose_in`, this preserves existing slot state across calls,
2311    /// allowing efficient reuse during measurement passes. This is critical for
2312    /// lazy lists where items need stable slot positions.
2313    pub fn subcompose_slot<R>(
2314        &self,
2315        slots: &Rc<SlotsHost>,
2316        root: Option<NodeId>,
2317        f: impl FnOnce(&Composer) -> R,
2318    ) -> Result<(R, Vec<RecomposeScope>), NodeError> {
2319        let context = self.capture_composition_context();
2320        self.subcompose_slot_with_context(slots, root, &context, f)
2321    }
2322
2323    /// Like [`Composer::subcompose_slot`], but uses a context captured at the
2324    /// source composition site. This is required for measure-time composition,
2325    /// where the source scope is no longer on the active stack.
2326    pub fn subcompose_slot_with_context<R>(
2327        &self,
2328        slots: &Rc<SlotsHost>,
2329        root: Option<NodeId>,
2330        context: &CapturedCompositionContext,
2331        f: impl FnOnce(&Composer) -> R,
2332    ) -> Result<(R, Vec<RecomposeScope>), NodeError> {
2333        let _teardown = crate::runtime::enter_state_teardown_scope();
2334        let runtime_handle = self.runtime_handle();
2335        let locals = context.locals.clone();
2336        let core = self.spin_up_subcompose_core(slots, root, &runtime_handle, locals);
2337        *core.subcomposition_owner_scope.borrow_mut() = context
2338            .owner_scope
2339            .as_ref()
2340            .and_then(RecomposeScope::upgrade);
2341        let composer = Composer::from_core(core);
2342        composer.subcompose_stack().push(SubcomposeFrame::default());
2343        let mut guard = SubcomposeStackGuard {
2344            core: composer.clone_core(),
2345            leaked: false,
2346        };
2347        let root_group_key = crate::location_key!();
2348        let (result, commands, side_effects, compact_applier) = composer.install(|composer| {
2349            let (output, outcome) = composer.try_with_slot_host_pass(
2350                Rc::clone(slots),
2351                crate::slot::SlotPassMode::Compose,
2352                |composer| {
2353                    let output = composer.with_group(root_group_key, |composer| f(composer));
2354                    if root.is_some() {
2355                        composer.pop_parent();
2356                    }
2357                    output
2358                },
2359            )?;
2360            let commands = composer.take_commands();
2361            let side_effects = composer.take_side_effects();
2362            Ok((output, commands, side_effects, outcome.compacted))
2363        })?;
2364        let frame = {
2365            let frame = take_subcompose_frame(&guard.core, "subcompose_slot");
2366            guard.leaked = true;
2367            frame
2368        };
2369
2370        self.flush_subcompose_pass(commands, &runtime_handle, compact_applier, side_effects)?;
2371        Ok((result, frame.scopes))
2372    }
2373
2374    fn attach_root_nodes(&self, root_nodes: RootNodeIds) {
2375        for id in root_nodes {
2376            self.attach_to_parent(id);
2377        }
2378    }
2379
2380    /// Skips the currently open group without running it again. Its nodes and remembered values
2381    /// stay as the last composition left them.
2382    pub fn skip_current_group(&self) {
2383        #[expect(
2384            clippy::redundant_closure_for_method_calls,
2385            reason = "the method path is not general over the session lifetime"
2386        )]
2387        self.with_slot_session_mut(|slots| slots.skip_group());
2388    }
2389
2390    /// Returns a handle to the runtime of this composer.
2391    pub fn runtime_handle(&self) -> RuntimeHandle {
2392        self.core.runtime.clone()
2393    }
2394
2395    /// Registers `callback` as the code that recomposes the current scope when state it read
2396    /// changes. Does nothing outside a recompose scope.
2397    pub fn set_recompose_callback<F>(&self, callback: F)
2398    where
2399        F: FnMut(&Composer) + 'static,
2400    {
2401        let stateless = std::mem::size_of::<F>() == 0;
2402        if stateless && self.current_scope_reruns(std::any::TypeId::of::<F>()) {
2403            return;
2404        }
2405        let stateless_body = stateless.then_some(std::any::TypeId::of::<F> as fn() -> _);
2406        self.set_recompose_callback_boxed(Box::new(callback), stateless_body);
2407    }
2408
2409    fn current_scope_reruns(&self, body: std::any::TypeId) -> bool {
2410        self.core
2411            .scope_stack
2412            .borrow()
2413            .last()
2414            .is_some_and(|scope| scope.reruns_stateless(body))
2415    }
2416
2417    #[inline(never)]
2418    fn set_recompose_callback_boxed(
2419        &self,
2420        callback: Box<dyn FnMut(&Composer)>,
2421        stateless: Option<fn() -> std::any::TypeId>,
2422    ) {
2423        if let Some(scope) = self.current_recompose_scope() {
2424            scope.set_boxed_recompose(callback, stateless);
2425        }
2426    }
2427
2428    /// Like `set_recompose_callback` for a plain function pointer.
2429    pub fn set_recompose_fn(&self, callback: fn(&Composer)) {
2430        self.set_recompose_callback_boxed(Box::new(callback), None);
2431    }
2432
2433    /// Runs `f` with `provided` in scope over the locals around it; of values
2434    /// for one local, the last wins.
2435    pub fn with_composition_locals<R>(
2436        &self,
2437        provided: impl IntoIterator<Item = ProvidedValue>,
2438        site: crate::Key,
2439        f: impl FnOnce(&Composer) -> R,
2440    ) -> R {
2441        let provided: SmallVec<[ProvidedValue; 2]> = provided.into_iter().collect();
2442        if provided.is_empty() {
2443            return f(self);
2444        }
2445        let mut values = SmallVec::<[(LocalKey, Rc<dyn Any>); 2]>::new();
2446        let mut static_changed = false;
2447        for value in provided.into_iter().rev() {
2448            if values.iter().any(|(key, _)| key == value.key()) {
2449                continue;
2450            }
2451            let (key, entry, changed) = value.into_entry(self, site);
2452            static_changed |= changed;
2453            values.push((key, entry));
2454        }
2455        let parent = self.current_local_stack();
2456        *self.local_stack() = Some(Rc::new(LocalFrame {
2457            values,
2458            parent: parent.clone(),
2459        }));
2460        let result = self.with_locals_changed(static_changed, f);
2461        *self.local_stack() = parent;
2462        result
2463    }
2464}