Skip to main content

cranpose_core/
composer.rs

1use crate::collections::map::{HashMap, HashSet};
2use crate::retention::{RetainKey, RetentionManager};
3use crate::slot::{FinishGroupResult, PayloadKind};
4use crate::slot::{GroupStart, GroupStartKind, ValueSlotId};
5use crate::{
6    composer_context, empty_local_stack, explicit_group_key_seed, runtime, Applier, ApplierHost,
7    ChildList, Command, CommandQueue, CompositionLocal, DirtyBubble, Key, LocalKey,
8    LocalStackSnapshot, LocalStateEntry, MutableState, Node, NodeError, NodeId, Owned,
9    ProvidedValue, RecomposeOptions, RecomposeScope, RecycledNode, RetentionMode, RetentionPolicy,
10    RuntimeHandle, ScopeId, SlotId, SlotPassOutcome, SlotTable, SlotsHost, SnapshotStateList,
11    SnapshotStateMap, SnapshotStateObserver, StaticCompositionLocal, StaticLocalEntry,
12    SubcomposeState, COMMAND_FLUSH_THRESHOLD,
13};
14use smallvec::SmallVec;
15use std::any::Any;
16use std::cell::{Cell, RefCell, RefMut};
17use std::hash::Hash;
18use std::marker::PhantomData;
19use std::rc::Rc;
20
21pub struct ValueSlotHandle<'pass, T: 'static> {
22    slot: ValueSlotId,
23    _pass: PhantomData<&'pass Composer>,
24    _value: PhantomData<fn() -> T>,
25}
26
27impl<T: 'static> Copy for ValueSlotHandle<'_, T> {}
28
29impl<T: 'static> Clone for ValueSlotHandle<'_, T> {
30    fn clone(&self) -> Self {
31        *self
32    }
33}
34
35impl<T: 'static> ValueSlotHandle<'_, T> {
36    pub(crate) fn new(slot: ValueSlotId) -> Self {
37        Self {
38            slot,
39            _pass: PhantomData,
40            _value: PhantomData,
41        }
42    }
43
44    pub(crate) fn slot(self) -> ValueSlotId {
45        self.slot
46    }
47}
48
49fn slots_storage_key(host: &Rc<SlotsHost>) -> usize {
50    host.storage_key()
51}
52
53fn bind_slots_host_to_runtime_state(
54    state: &Rc<ComposerRuntimeState>,
55    host: &Rc<SlotsHost>,
56) -> Rc<SlotsHost> {
57    if let Some(bound_state) = host.runtime_state() {
58        if Rc::ptr_eq(&bound_state, state) {
59            state.bind_slots_host(host);
60            return Rc::clone(host);
61        }
62        drop(bound_state);
63        if host.rebind_orphaned_runtime_state(state) {
64            state.bind_slots_host(host);
65            return Rc::clone(host);
66        }
67        log::error!(
68            "slot host already belongs to a different composer runtime state; using a fresh slot host"
69        );
70        let replacement = Rc::new(SlotsHost::new(SlotTable::new()));
71        state.bind_slots_host(&replacement);
72        return replacement;
73    }
74    state.bind_slots_host(host);
75    Rc::clone(host)
76}
77
78struct SlotHostPassGuard {
79    core: Rc<ComposerCore>,
80    host: Rc<SlotsHost>,
81    active: bool,
82}
83
84impl SlotHostPassGuard {
85    fn close(&mut self) {
86        if !self.active {
87            return;
88        }
89        if self.host.has_active_pass() {
90            self.host.abandon_active_pass();
91        }
92        match self.core.slot_hosts.borrow_mut().pop() {
93            Some(host) if Rc::ptr_eq(&host, &self.host) => {}
94            Some(_) => {
95                log::error!("slot host stack mismatch while closing slot host pass");
96            }
97            None => {
98                log::error!("slot host stack underflow while closing slot host pass");
99            }
100        }
101        self.active = false;
102    }
103}
104
105impl Drop for SlotHostPassGuard {
106    fn drop(&mut self) {
107        self.close();
108    }
109}
110
111pub(crate) struct ComposerRuntimeState {
112    scope_registry: RefCell<HashMap<ScopeId, RecomposeScope>>,
113    retention_by_host: RefCell<HashMap<usize, RetentionManager>>,
114    retention_policy: Cell<RetentionPolicy>,
115    live_hosts: RefCell<HashMap<usize, std::rc::Weak<SlotsHost>>>,
116    applier_host: RefCell<Option<std::rc::Weak<dyn ApplierHost>>>,
117}
118
119impl Default for ComposerRuntimeState {
120    fn default() -> Self {
121        Self {
122            scope_registry: RefCell::new(HashMap::default()),
123            retention_by_host: RefCell::new(HashMap::default()),
124            retention_policy: Cell::new(RetentionPolicy::default()),
125            live_hosts: RefCell::new(HashMap::default()),
126            applier_host: RefCell::new(None),
127        }
128    }
129}
130
131impl ComposerRuntimeState {
132    pub(crate) fn clear_host_storage_key(&self, host_key: usize) {
133        self.retention_by_host.borrow_mut().remove(&host_key);
134        self.live_hosts.borrow_mut().remove(&host_key);
135        self.scope_registry
136            .borrow_mut()
137            .retain(|_, scope| scope.slots_storage_key() != Some(host_key));
138    }
139
140    pub(crate) fn bind_applier_host(&self, applier: &Rc<dyn ApplierHost>) {
141        *self.applier_host.borrow_mut() = Some(Rc::downgrade(applier));
142    }
143
144    pub(crate) fn has_live_applier_host(&self) -> bool {
145        self.applier_host
146            .borrow()
147            .as_ref()
148            .and_then(std::rc::Weak::upgrade)
149            .is_some()
150    }
151
152    pub(crate) fn bind_slots_host(self: &Rc<Self>, host: &Rc<SlotsHost>) {
153        host.bind_runtime_state(self);
154        self.live_hosts
155            .borrow_mut()
156            .insert(host.storage_key(), Rc::downgrade(host));
157    }
158
159    pub(crate) fn scope_for_id(&self, scope_id: ScopeId) -> Option<RecomposeScope> {
160        self.scope_registry.borrow().get(&scope_id).cloned()
161    }
162
163    pub(crate) fn register_scope(&self, scope: &RecomposeScope) {
164        self.scope_registry
165            .borrow_mut()
166            .insert(scope.id(), scope.clone());
167    }
168
169    pub(crate) fn remove_scope(&self, scope_id: ScopeId) -> Option<RecomposeScope> {
170        self.scope_registry.borrow_mut().remove(&scope_id)
171    }
172
173    pub(crate) fn set_retention_policy(&self, policy: RetentionPolicy) {
174        self.retention_policy.set(policy);
175    }
176
177    pub(crate) fn retention_policy(&self) -> RetentionPolicy {
178        self.retention_policy.get()
179    }
180
181    pub(crate) fn scope_registry_len(&self) -> usize {
182        self.scope_registry.borrow().len()
183    }
184
185    pub(crate) fn take_retained(
186        &self,
187        host: &Rc<SlotsHost>,
188        key: RetainKey,
189        preflight: impl FnOnce(&crate::slot::DetachedSubtree) -> bool,
190    ) -> Option<crate::slot::DetachedSubtree> {
191        let host_key = slots_storage_key(host);
192        let mut retention = self.retention_by_host.borrow_mut();
193        let subtree = retention
194            .get_mut(&host_key)?
195            .take_after_restore_preflight(key, preflight);
196        if retention
197            .get(&host_key)
198            .is_some_and(|manager| manager.is_empty() && manager.evictions_total() == 0)
199        {
200            retention.remove(&host_key);
201        }
202        subtree
203    }
204
205    pub(crate) fn insert_retained(
206        &self,
207        host: &Rc<SlotsHost>,
208        key: RetainKey,
209        subtree: crate::slot::DetachedSubtree,
210    ) -> Vec<crate::slot::DetachedSubtree> {
211        let policy = self.retention_policy();
212        let mut retention_by_host = self.retention_by_host.borrow_mut();
213        let manager = retention_by_host
214            .entry(slots_storage_key(host))
215            .or_insert_with(|| RetentionManager::new(policy));
216        manager.set_policy(policy);
217        manager.insert(key, subtree)
218    }
219
220    pub(crate) fn advance_retention_pass(
221        &self,
222        host: &Rc<SlotsHost>,
223    ) -> Vec<crate::slot::DetachedSubtree> {
224        let host_key = slots_storage_key(host);
225        let policy = self.retention_policy();
226        let mut retention_by_host = self.retention_by_host.borrow_mut();
227        let Some(manager) = retention_by_host.get_mut(&host_key) else {
228            return Vec::new();
229        };
230        manager.set_policy(policy);
231        manager.advance_pass()
232    }
233
234    pub(crate) fn fill_slot_debug_snapshot(
235        &self,
236        host: &SlotsHost,
237        snapshot: &mut crate::SlotDebugSnapshot,
238    ) {
239        let retention = self.retention_debug_stats(host.storage_key());
240        snapshot.runtime_scope_registry_count = Some(self.scope_registry_len());
241        snapshot.retained_subtree_count = retention.subtree_count;
242        snapshot.retained_group_count = retention.group_count;
243        snapshot.retained_payload_count = retention.payload_count;
244        snapshot.retained_node_count = retention.node_count;
245        snapshot.retained_scope_count = retention.scope_count;
246    }
247
248    pub(crate) fn slot_retention_debug_stats(
249        &self,
250        host: &SlotsHost,
251    ) -> crate::slot::SlotRetentionDebugStats {
252        let retention = self.retention_debug_stats(host.storage_key());
253        crate::slot::SlotRetentionDebugStats {
254            retained_subtree_count: retention.subtree_count,
255            retained_group_count: retention.group_count,
256            retained_payload_count: retention.payload_count,
257            retained_node_count: retention.node_count,
258            retained_scope_count: retention.scope_count,
259            retained_anchor_count: retention.anchor_count,
260            retained_heap_bytes: retention.heap_bytes,
261            retained_evictions_total: retention.evictions_total,
262        }
263    }
264
265    pub(crate) fn compact_table_identity_storage_for_host(
266        &self,
267        host: &SlotsHost,
268        table: &mut SlotTable,
269        compact_anchors: bool,
270        compact_payloads: bool,
271    ) {
272        if !compact_anchors && !compact_payloads {
273            return;
274        }
275
276        let host_key = host.storage_key();
277        let mut retention = self.retention_by_host.borrow_mut();
278        if let Some(retained) = retention.get_mut(&host_key) {
279            if compact_anchors {
280                table.compact_anchor_registry_storage(Some(&mut *retained));
281            }
282            if compact_payloads {
283                table.compact_payload_anchor_registry_storage(Some(&mut *retained));
284            }
285        } else {
286            if compact_anchors {
287                table.compact_anchor_registry_storage(None);
288            }
289            if compact_payloads {
290                table.compact_payload_anchor_registry_storage(None);
291            }
292        }
293    }
294
295    pub(crate) fn clear_host(&self, host: &SlotsHost) {
296        let host_key = host.storage_key();
297        debug_assert!(
298            self.host_retention_is_empty(host),
299            "host retention must be drained before clearing host ownership"
300        );
301        self.clear_host_storage_key(host_key);
302    }
303
304    pub(crate) fn dispose_retained_subtrees_for_host(
305        &self,
306        host_key: usize,
307        table: &mut SlotTable,
308        lifecycle: &mut crate::slot::SlotLifecycleCoordinator,
309    ) -> Result<(), NodeError> {
310        let applier_host = self
311            .applier_host
312            .borrow()
313            .as_ref()
314            .and_then(std::rc::Weak::upgrade);
315        if let Some(applier_host) = applier_host.as_ref() {
316            let retention_by_host = self.retention_by_host.borrow();
317            let Some(retention) = retention_by_host.get(&host_key) else {
318                return Ok(());
319            };
320            let mut applier = applier_host.borrow_dyn();
321            for subtree in retention.subtrees() {
322                crate::slot::dispose_detached_subtree_now(&mut *applier, subtree)?;
323            }
324        }
325        let Some(retention) = self.retention_by_host.borrow_mut().remove(&host_key) else {
326            return Ok(());
327        };
328        for subtree in retention.into_subtrees() {
329            for scope_id in subtree.scope_ids() {
330                if let Some(scope) = self.remove_scope(scope_id) {
331                    scope.deactivate();
332                }
333            }
334            table.invalidate_detached_subtree_anchors(&subtree);
335            lifecycle.queue_subtree_disposal(subtree);
336        }
337        Ok(())
338    }
339
340    pub(crate) fn abandon_retained_subtrees_for_host(
341        &self,
342        host_key: usize,
343        table: &mut SlotTable,
344        lifecycle: &mut crate::slot::SlotLifecycleCoordinator,
345    ) {
346        let Some(retention) = self.retention_by_host.borrow_mut().remove(&host_key) else {
347            self.clear_host_storage_key(host_key);
348            return;
349        };
350        for subtree in retention.into_subtrees() {
351            for scope_id in subtree.scope_ids() {
352                if let Some(scope) = self.remove_scope(scope_id) {
353                    scope.deactivate();
354                }
355            }
356            table.invalidate_detached_subtree_anchors(&subtree);
357            lifecycle.queue_subtree_disposal(subtree);
358        }
359        self.clear_host_storage_key(host_key);
360    }
361
362    pub(crate) fn host_retention_is_empty(&self, host: &SlotsHost) -> bool {
363        self.retention_by_host
364            .borrow()
365            .get(&host.storage_key())
366            .is_none_or(RetentionManager::is_empty)
367    }
368
369    #[cfg(any(test, debug_assertions))]
370    pub(crate) fn debug_verify_host(&self, host: &SlotsHost, table: &SlotTable) {
371        if let Some(retention) = self.retention_by_host.borrow().get(&host.storage_key()) {
372            retention.debug_verify(table);
373        }
374    }
375
376    #[cfg(test)]
377    pub(crate) fn validate_host_retention(
378        &self,
379        host: &SlotsHost,
380        table: &SlotTable,
381    ) -> Result<(), crate::slot::SlotInvariantError> {
382        if let Some(retention) = self.retention_by_host.borrow().get(&host.storage_key()) {
383            retention.validate(table)?;
384        }
385        Ok(())
386    }
387
388    pub(crate) fn host_for_storage_key(&self, storage_key: usize) -> Option<Rc<SlotsHost>> {
389        self.live_hosts
390            .borrow()
391            .get(&storage_key)
392            .and_then(std::rc::Weak::upgrade)
393    }
394
395    fn retention_debug_stats(&self, host_key: usize) -> crate::retention::RetentionDebugStats {
396        self.retention_by_host
397            .borrow()
398            .get(&host_key)
399            .map(RetentionManager::debug_stats)
400            .unwrap_or_default()
401    }
402}
403
404pub(crate) struct ParentFrame {
405    pub(crate) id: NodeId,
406    pub(crate) previous: ChildList,
407    pub(crate) new_children: ChildList,
408    pub(crate) new_children_membership: Option<HashSet<NodeId>>,
409    pub(crate) attach_mode: ParentAttachMode,
410    pub(crate) synthetic_root: bool,
411}
412
413#[derive(Clone, Copy)]
414pub(crate) enum InitialParentFrame {
415    SyntheticRoot,
416    RealParent,
417}
418
419const LARGE_DEFERRED_CHILD_TRACKING_THRESHOLD: usize = 16;
420
421#[derive(Clone, Copy, Debug, PartialEq, Eq)]
422pub(crate) enum ParentAttachMode {
423    ImmediateAppend,
424    DeferredSync,
425}
426
427#[derive(Default)]
428pub(crate) struct SubcomposeFrame {
429    pub(crate) nodes: Vec<NodeId>,
430    pub(crate) scopes: Vec<RecomposeScope>,
431}
432
433#[derive(Default, Clone)]
434pub(crate) struct LocalContext {
435    pub(crate) values: HashMap<LocalKey, Rc<dyn Any>>,
436}
437
438pub(crate) struct ComposerCore {
439    pub(crate) shared_state: Rc<ComposerRuntimeState>,
440    pub(crate) slots: Rc<SlotsHost>,
441    slot_hosts: RefCell<Vec<Rc<SlotsHost>>>,
442    pub(crate) applier: Rc<dyn ApplierHost>,
443    pub(crate) runtime: RuntimeHandle,
444    pub(crate) observer: SnapshotStateObserver,
445    pub(crate) parent_stack: RefCell<Vec<ParentFrame>>,
446    pub(crate) subcompose_stack: RefCell<Vec<SubcomposeFrame>>,
447    pub(crate) root: Cell<Option<NodeId>>,
448    pub(crate) commands: RefCell<CommandQueue>,
449    pub(crate) scope_stack: RefCell<Vec<RecomposeScope>>,
450    pub(crate) local_stack: RefCell<LocalStackSnapshot>,
451    pub(crate) side_effects: RefCell<Vec<Box<dyn FnOnce()>>>,
452    pub(crate) pending_scope_options: RefCell<Option<RecomposeOptions>>,
453    pub(crate) phase: Cell<crate::Phase>,
454    pub(crate) last_node_reused: Cell<Option<bool>>,
455    pub(crate) recranpose_parent_hint: Cell<Option<NodeId>>,
456    pub(crate) root_render_requested: Cell<bool>,
457    pub(crate) _not_send: PhantomData<*const ()>,
458}
459
460/// An opaque snapshot of the composition-local values in scope at the point it
461/// was captured (see [`Composer::capture_composition_locals`]). Cheap to clone
462/// (an `Rc` internally); replayed by [`Composer::subcompose_slot_with_locals`].
463#[derive(Clone)]
464pub struct CapturedCompositionLocals(LocalStackSnapshot);
465
466fn take_subcompose_frame(core: &ComposerCore, operation: &str) -> SubcomposeFrame {
467    match core.subcompose_stack.borrow_mut().pop() {
468        Some(frame) => frame,
469        None => {
470            log::error!("subcompose stack underflow while finishing {operation}");
471            SubcomposeFrame::default()
472        }
473    }
474}
475
476impl ComposerCore {
477    pub(crate) fn new(
478        shared_state: Rc<ComposerRuntimeState>,
479        slots: Rc<SlotsHost>,
480        applier: Rc<dyn ApplierHost>,
481        runtime: RuntimeHandle,
482        observer: SnapshotStateObserver,
483        root: Option<NodeId>,
484        initial_parent_frame: InitialParentFrame,
485    ) -> Self {
486        let parent_stack = if let Some(root_id) = root {
487            vec![ParentFrame {
488                id: root_id,
489                previous: ChildList::new(),
490                new_children: ChildList::new(),
491                new_children_membership: None,
492                attach_mode: ParentAttachMode::DeferredSync,
493                synthetic_root: matches!(initial_parent_frame, InitialParentFrame::SyntheticRoot),
494            }]
495        } else {
496            Vec::new()
497        };
498
499        Self {
500            shared_state,
501            slots,
502            slot_hosts: RefCell::new(Vec::new()),
503            applier,
504            runtime,
505            observer,
506            parent_stack: RefCell::new(parent_stack),
507            subcompose_stack: RefCell::new(Vec::new()),
508            root: Cell::new(root),
509            commands: RefCell::new(CommandQueue::default()),
510            scope_stack: RefCell::new(Vec::new()),
511            local_stack: RefCell::new(empty_local_stack()),
512            side_effects: RefCell::new(Vec::new()),
513            pending_scope_options: RefCell::new(None),
514            phase: Cell::new(crate::Phase::Compose),
515            last_node_reused: Cell::new(None),
516            recranpose_parent_hint: Cell::new(None),
517            root_render_requested: Cell::new(false),
518            _not_send: PhantomData,
519        }
520    }
521}
522
523#[derive(Clone)]
524pub struct Composer {
525    pub(crate) core: Rc<ComposerCore>,
526}
527
528pub(crate) enum EmittedNode {
529    Fresh(Box<dyn Node>),
530    Recycled(RecycledNode),
531}
532
533impl Composer {
534    pub(crate) fn new_with_shared_state(
535        shared_state: Rc<ComposerRuntimeState>,
536        slots: Rc<SlotsHost>,
537        applier: Rc<dyn ApplierHost>,
538        runtime: RuntimeHandle,
539        observer: SnapshotStateObserver,
540        root: Option<NodeId>,
541    ) -> Self {
542        Self::new_with_shared_state_with_parent_frame(
543            shared_state,
544            slots,
545            applier,
546            runtime,
547            observer,
548            root,
549            InitialParentFrame::SyntheticRoot,
550        )
551    }
552
553    fn new_with_shared_state_with_parent_frame(
554        shared_state: Rc<ComposerRuntimeState>,
555        slots: Rc<SlotsHost>,
556        applier: Rc<dyn ApplierHost>,
557        runtime: RuntimeHandle,
558        observer: SnapshotStateObserver,
559        root: Option<NodeId>,
560        initial_parent_frame: InitialParentFrame,
561    ) -> Self {
562        shared_state.bind_applier_host(&applier);
563        let slots = bind_slots_host_to_runtime_state(&shared_state, &slots);
564        let core = Rc::new(ComposerCore::new(
565            shared_state,
566            slots,
567            applier,
568            runtime,
569            observer,
570            root,
571            initial_parent_frame,
572        ));
573        Self { core }
574    }
575
576    pub fn new(
577        slots: Rc<SlotsHost>,
578        applier: Rc<dyn ApplierHost>,
579        runtime: RuntimeHandle,
580        observer: SnapshotStateObserver,
581        root: Option<NodeId>,
582    ) -> Self {
583        Self::new_with_shared_state_with_parent_frame(
584            slots
585                .runtime_state()
586                .unwrap_or_else(|| Rc::new(ComposerRuntimeState::default())),
587            slots,
588            applier,
589            runtime,
590            observer,
591            root,
592            InitialParentFrame::RealParent,
593        )
594    }
595
596    pub(crate) fn from_core(core: Rc<ComposerCore>) -> Self {
597        Self { core }
598    }
599
600    pub(crate) fn clone_core(&self) -> Rc<ComposerCore> {
601        Rc::clone(&self.core)
602    }
603
604    fn observer(&self) -> SnapshotStateObserver {
605        self.core.observer.clone()
606    }
607
608    pub(crate) fn request_root_render(&self) {
609        self.core.root_render_requested.set(true);
610    }
611
612    pub(crate) fn take_root_render_request(&self) -> bool {
613        self.core.root_render_requested.replace(false)
614    }
615
616    pub(crate) fn observe_scope<R>(&self, scope: &RecomposeScope, block: impl FnOnce() -> R) -> R {
617        let observer = self.observer();
618        let scope_clone = scope.clone();
619        observer.observe_reads(scope_clone, move |scope_ref| scope_ref.invalidate(), block)
620    }
621
622    pub(crate) fn active_slots_host(&self) -> Rc<SlotsHost> {
623        self.core
624            .slot_hosts
625            .borrow()
626            .last()
627            .cloned()
628            .unwrap_or_else(|| Rc::clone(&self.core.slots))
629    }
630
631    pub(crate) fn with_slots<R>(&self, f: impl FnOnce(&SlotTable) -> R) -> R {
632        let host = self.active_slots_host();
633        let slots = host.borrow();
634        f(&slots)
635    }
636
637    pub(crate) fn with_slots_mut<R>(&self, f: impl FnOnce(&mut SlotTable) -> R) -> R {
638        let host = self.active_slots_host();
639        let mut slots = host.borrow_mut();
640        f(&mut slots)
641    }
642
643    pub(crate) fn with_slot_session_mut<R>(
644        &self,
645        f: impl FnOnce(&mut crate::slot::SlotWriteSession<'_>) -> R,
646    ) -> R {
647        self.active_slots_host().with_write_session(f)
648    }
649
650    pub(crate) fn try_with_slot_host_pass<R>(
651        &self,
652        slots: Rc<SlotsHost>,
653        mode: crate::slot::SlotPassMode,
654        f: impl FnOnce(&Composer) -> R,
655    ) -> Result<(R, SlotPassOutcome), NodeError> {
656        let mut guard = self.begin_slot_host_pass(&slots, mode);
657        let result = f(self);
658        let outcome = self.finish_slot_host_pass(&guard.host)?;
659        guard.close();
660        Ok((result, outcome))
661    }
662
663    pub(crate) fn with_slot_host_pass<R>(
664        &self,
665        slots: Rc<SlotsHost>,
666        mode: crate::slot::SlotPassMode,
667        f: impl FnOnce(&Composer) -> R,
668    ) -> (R, SlotPassOutcome) {
669        let mut guard = self.begin_slot_host_pass(&slots, mode);
670        let result = f(self);
671        let outcome = match self.finish_slot_host_pass(&guard.host) {
672            Ok(outcome) => outcome,
673            Err(err) => {
674                log::error!("slot host pass finalization failed: {err}");
675                SlotPassOutcome::default()
676            }
677        };
678        guard.close();
679        (result, outcome)
680    }
681
682    pub(crate) fn with_slot_override<R>(
683        &self,
684        slots: Rc<SlotsHost>,
685        f: impl FnOnce(&Composer) -> R,
686    ) -> (R, SlotPassOutcome) {
687        self.with_slot_host_pass(slots, crate::slot::SlotPassMode::Compose, f)
688    }
689
690    fn begin_slot_host_pass(
691        &self,
692        slots: &Rc<SlotsHost>,
693        mode: crate::slot::SlotPassMode,
694    ) -> SlotHostPassGuard {
695        let slots = bind_slots_host_to_runtime_state(&self.core.shared_state, slots);
696        slots.begin_pass(mode);
697        self.core.slot_hosts.borrow_mut().push(Rc::clone(&slots));
698        SlotHostPassGuard {
699            core: self.clone_core(),
700            host: slots,
701            active: true,
702        }
703    }
704
705    fn finish_slot_host_pass(&self, slots: &Rc<SlotsHost>) -> Result<SlotPassOutcome, NodeError> {
706        let finished = {
707            let mut applier = self.core.applier.borrow_dyn();
708            slots.finish_pass(&mut *applier)
709        }?;
710        self.handle_detached_children_in_host(slots, None, finished.detached_root_children)?;
711        self.evict_retained_subtrees_for_host(slots)?;
712        slots.complete_pass_cleanup(&finished.outcome);
713        Ok(finished.outcome)
714    }
715
716    pub(crate) fn parent_stack(&self) -> RefMut<'_, Vec<ParentFrame>> {
717        self.core.parent_stack.borrow_mut()
718    }
719
720    fn current_parent_hint(&self) -> Option<NodeId> {
721        let stack = self.core.parent_stack.borrow();
722        let stack_hint = stack
723            .last()
724            .and_then(|frame| (!frame.synthetic_root).then_some(frame.id));
725        stack_hint.or_else(|| self.core.recranpose_parent_hint.get())
726    }
727
728    pub(crate) fn subcompose_stack(&self) -> RefMut<'_, Vec<SubcomposeFrame>> {
729        self.core.subcompose_stack.borrow_mut()
730    }
731
732    pub(crate) fn commands_mut(&self) -> RefMut<'_, CommandQueue> {
733        self.core.commands.borrow_mut()
734    }
735
736    pub(crate) fn enqueue_semantics_invalidation(&self, id: NodeId) {
737        self.commands_mut().push(Command::BubbleDirty {
738            node_id: id,
739            bubble: DirtyBubble::SEMANTICS,
740        });
741    }
742
743    pub(crate) fn scope_stack(&self) -> RefMut<'_, Vec<RecomposeScope>> {
744        self.core.scope_stack.borrow_mut()
745    }
746
747    fn scope_for_id(&self, scope_id: ScopeId) -> Option<RecomposeScope> {
748        self.core.shared_state.scope_for_id(scope_id)
749    }
750
751    fn register_scope(&self, scope: &RecomposeScope) {
752        self.core.shared_state.register_scope(scope);
753    }
754
755    fn remove_scope(&self, scope_id: ScopeId) -> Option<RecomposeScope> {
756        self.core.shared_state.remove_scope(scope_id)
757    }
758
759    pub(crate) fn local_stack(&self) -> RefMut<'_, LocalStackSnapshot> {
760        self.core.local_stack.borrow_mut()
761    }
762
763    pub(crate) fn current_local_stack(&self) -> LocalStackSnapshot {
764        self.core.local_stack.borrow().clone()
765    }
766
767    pub(crate) fn side_effects_mut(&self) -> RefMut<'_, Vec<Box<dyn FnOnce()>>> {
768        self.core.side_effects.borrow_mut()
769    }
770
771    fn pending_scope_options(&self) -> RefMut<'_, Option<RecomposeOptions>> {
772        self.core.pending_scope_options.borrow_mut()
773    }
774
775    pub(crate) fn borrow_applier(&self) -> RefMut<'_, dyn Applier> {
776        self.core.applier.borrow_dyn()
777    }
778
779    /// Registers a virtual node in the Applier.
780    ///
781    /// This is used by SubcomposeLayoutNode to register virtual container nodes
782    /// so that subsequent insert_child commands can find them and attach children.
783    /// Without this, virtual nodes would only exist in SubcomposeLayoutNodeInner.virtual_nodes
784    /// and applier.get_mut(virtual_node_id) would fail, breaking child attachment.
785    pub fn register_virtual_node(
786        &self,
787        node_id: NodeId,
788        node: Box<dyn Node>,
789    ) -> Result<(), NodeError> {
790        let mut applier = self.borrow_applier();
791        applier.insert_with_id(node_id, node)
792    }
793
794    /// Checks if a node has no parent (is a root node).
795    /// Used by SubcomposeMeasureScope to filter subcompose results.
796    pub fn node_has_no_parent(&self, node_id: NodeId) -> bool {
797        let mut applier = self.borrow_applier();
798        match applier.get_mut(node_id) {
799            Ok(node) => node.parent().is_none(),
800            Err(_) => true,
801        }
802    }
803
804    /// Gets the children of a node from the Applier.
805    ///
806    /// This is used by SubcomposeLayoutNode to get children of virtual nodes
807    /// directly from the Applier, where insert_child commands have been applied.
808    pub fn get_node_children(&self, node_id: NodeId) -> SmallVec<[NodeId; 8]> {
809        let mut applier = self.borrow_applier();
810        match applier.get_mut(node_id) {
811            Ok(node) => {
812                let mut children = SmallVec::<[NodeId; 8]>::new();
813                node.collect_children_into(&mut children);
814                children
815            }
816            Err(_) => SmallVec::<[NodeId; 8]>::new(),
817        }
818    }
819
820    pub fn nodes_need_measure(&self, node_ids: &[NodeId]) -> bool {
821        let mut applier = self.borrow_applier();
822        node_ids.iter().any(|node_id| {
823            applier
824                .get_mut(*node_id)
825                .is_ok_and(|node| node.needs_measure())
826        })
827    }
828
829    /// Records a child node in the current parent frame's expected children list.
830    ///
831    /// Used by SubcomposeLayout's `perform_subcompose` to register virtual nodes
832    /// with the outer composer's parent frame. This ensures that the `pop_parent`
833    /// call at the end of `subcompose_slot` generates a correct `SyncChildren`
834    /// command that preserves (rather than removes) the virtual nodes.
835    ///
836    /// Without this, `pop_parent` would generate `SyncChildren { expected: [] }`,
837    /// which removes all virtual nodes and their subtrees from the applier.
838    pub fn record_subcompose_child(&self, child_id: NodeId) {
839        let mut parent_stack = self.parent_stack();
840        if let Some(frame) = parent_stack.last_mut() {
841            if matches!(frame.attach_mode, ParentAttachMode::DeferredSync) {
842                if let Some(membership) = frame.new_children_membership.as_mut() {
843                    if membership.insert(child_id) {
844                        frame.new_children.push(child_id);
845                    }
846                } else if frame.new_children.len() >= LARGE_DEFERRED_CHILD_TRACKING_THRESHOLD {
847                    let mut membership = HashSet::default();
848                    membership.reserve(frame.new_children.len() + 1);
849                    membership.extend(frame.new_children.iter().copied());
850                    if membership.insert(child_id) {
851                        frame.new_children.push(child_id);
852                    }
853                    frame.new_children_membership = Some(membership);
854                } else if !frame.new_children.contains(&child_id) {
855                    frame.new_children.push(child_id);
856                }
857            }
858        }
859    }
860
861    /// Clears all children of a node in the Applier.
862    ///
863    /// This is used by SubcomposeLayoutNode when reusing a virtual node for
864    /// different content. Without clearing, old children remain attached,
865    /// causing duplicate/interleaved items in lazy lists after scrolling.
866    pub fn clear_node_children(&self, node_id: NodeId) {
867        let mut applier = self.borrow_applier();
868        if let Ok(node) = applier.get_mut(node_id) {
869            node.update_children(&[]);
870        }
871    }
872
873    pub fn install<R>(&self, f: impl FnOnce(&Composer) -> R) -> R {
874        let _composer_guard = composer_context::enter(self);
875        runtime::push_active_runtime(&self.core.runtime);
876        struct Guard;
877        impl Drop for Guard {
878            fn drop(&mut self) {
879                runtime::pop_active_runtime();
880            }
881        }
882        let guard = Guard;
883        let result = f(self);
884        drop(guard);
885        result
886    }
887
888    pub(crate) fn flush_pending_commands_if_large(&self) -> Result<(), NodeError> {
889        let queued = self.core.commands.borrow().len();
890        if queued < COMMAND_FLUSH_THRESHOLD {
891            return Ok(());
892        }
893        self.apply_pending_commands()
894    }
895
896    /// Thin monomorphic shim: the group machinery lives in
897    /// [`Self::with_group_in_active_pass_dyn`] so it is compiled once instead
898    /// of once per composable call site (real apps have thousands).
899    fn with_group_in_active_pass<R>(
900        &self,
901        key: crate::slot::GroupKeySeed,
902        f: impl FnOnce(&Composer) -> R,
903    ) -> R {
904        let mut f = Some(f);
905        let mut result = None;
906        self.with_group_in_active_pass_dyn(key, &mut |composer| {
907            let f = f.take().expect("group body must run at most once");
908            result = Some(f(composer));
909        });
910        result.expect("group body must run exactly once")
911    }
912
913    /// `inline(never)`: with fat LTO this single-caller body would otherwise
914    /// be inlined back into every monomorphic shim, recreating the per-call-site
915    /// code explosion this split exists to prevent.
916    #[inline(never)]
917    fn with_group_in_active_pass_dyn(
918        &self,
919        key: crate::slot::GroupKeySeed,
920        f: &mut dyn FnMut(&Composer),
921    ) {
922        struct GroupGuard {
923            composer: Composer,
924            scope: RecomposeScope,
925        }
926
927        impl Drop for GroupGuard {
928            fn drop(&mut self) {
929                self.composer
930                    .close_current_group_body_for_scope(&self.scope);
931                self.scope.mark_recomposed();
932                self.composer
933                    .with_slot_session_mut(|slots| slots.end_group());
934                if let Err(err) = self.composer.flush_pending_commands_if_large() {
935                    log::error!("mid-composition command flush failed: {err}");
936                }
937            }
938        }
939
940        let parent_scope = self.current_recranpose_scope();
941        let options = self.pending_scope_options().take().unwrap_or_default();
942        let parent_scope_id = parent_scope.as_ref().map(RecomposeScope::id);
943        let reserved_key = self.with_slot_session_mut(|slots| slots.preview_group_key(key));
944        let host = self.active_slots_host();
945        let restored = self.core.shared_state.take_retained(
946            &host,
947            RetainKey {
948                parent_scope: parent_scope_id,
949                key: reserved_key,
950            },
951            |subtree| {
952                self.with_slot_session_mut(|slots| {
953                    slots.retained_restore_ready(reserved_key, subtree)
954                })
955            },
956        );
957        let (group, start_scope_id, start_kind) = self.with_slot_session_mut(|slots| {
958            let GroupStart {
959                group,
960                scope_id,
961                kind,
962                ..
963            } = slots.begin_group(reserved_key, restored);
964            (group, scope_id, kind)
965        });
966        let scope_ref =
967            if let Some(scope) = start_scope_id.and_then(|scope_id| self.scope_for_id(scope_id)) {
968                scope
969            } else {
970                let scope = RecomposeScope::new(self.runtime_handle());
971                self.register_scope(&scope);
972                self.with_slot_session_mut(|slots| slots.set_group_scope(group, scope.id()));
973                scope
974            };
975
976        scope_ref.reactivate();
977        scope_ref.set_parent_scope(parent_scope);
978        scope_ref.set_retention_mode(options.retention);
979
980        if options.force_recompose {
981            scope_ref.force_recompose();
982        } else if options.force_reuse {
983            scope_ref.force_reuse();
984        }
985        if matches!(start_kind, GroupStartKind::Restored) {
986            scope_ref.force_recompose();
987        }
988
989        scope_ref.set_slots_host(&host);
990
991        {
992            let mut stack = self.scope_stack();
993            stack.push(scope_ref.clone());
994        }
995
996        {
997            let mut stack = self.subcompose_stack();
998            if let Some(frame) = stack.last_mut() {
999                frame.scopes.push(scope_ref.clone());
1000            }
1001        }
1002
1003        scope_ref.snapshot_locals(self.current_local_stack());
1004        {
1005            let parent_hint = self.current_parent_hint();
1006            scope_ref.set_parent_hint(parent_hint);
1007        }
1008
1009        let guard = GroupGuard {
1010            composer: self.clone(),
1011            scope: scope_ref.clone(),
1012        };
1013        self.observe_scope(&scope_ref, || f(self));
1014        scope_ref.mark_composed_once();
1015        drop(guard);
1016    }
1017
1018    pub(crate) fn with_group_seed<R>(
1019        &self,
1020        key: crate::slot::GroupKeySeed,
1021        f: impl FnOnce(&Composer) -> R,
1022    ) -> R {
1023        let host = self.active_slots_host();
1024        if host.has_active_pass() {
1025            return self.with_group_in_active_pass(key, f);
1026        }
1027        let (result, _) =
1028            self.with_slot_host_pass(host, crate::slot::SlotPassMode::Compose, |composer| {
1029                composer.with_group_in_active_pass(key, f)
1030            });
1031        result
1032    }
1033
1034    pub fn with_group<R>(&self, key: Key, f: impl FnOnce(&Composer) -> R) -> R {
1035        self.with_group_seed(crate::slot::GroupKeySeed::unkeyed(key), f)
1036    }
1037
1038    pub fn cranpose_with_reuse<R>(
1039        &self,
1040        key: Key,
1041        mut options: RecomposeOptions,
1042        f: impl FnOnce(&Composer) -> R,
1043    ) -> R {
1044        options.retention = RetentionMode::RetainWhenInactive;
1045        self.pending_scope_options().replace(options);
1046        self.with_group(key, f)
1047    }
1048
1049    #[track_caller]
1050    pub fn with_key<K: Hash, R>(&self, key: &K, f: impl FnOnce(&Composer) -> R) -> R {
1051        let seed = explicit_group_key_seed(key, std::panic::Location::caller());
1052        self.with_group_seed(seed, f)
1053    }
1054
1055    fn dispose_detached_nodes(&self, nodes: impl IntoIterator<Item = NodeId>) {
1056        for node_id in nodes {
1057            self.commands_mut().push(Command::callback(move |applier| {
1058                crate::slot::dispose_detached_node_now(applier, node_id)
1059            }));
1060        }
1061    }
1062
1063    fn deactivate_scope_ids(&self, scope_ids: impl IntoIterator<Item = ScopeId>) {
1064        for scope_id in scope_ids {
1065            if let Some(scope) = self.scope_for_id(scope_id) {
1066                scope.deactivate();
1067            }
1068        }
1069    }
1070
1071    fn dispose_scope_ids(&self, scope_ids: impl IntoIterator<Item = ScopeId>) {
1072        for scope_id in scope_ids {
1073            if let Some(scope) = self.remove_scope(scope_id) {
1074                scope.deactivate();
1075            }
1076        }
1077    }
1078
1079    fn detached_root_parent_commands(
1080        &self,
1081        subtree: &crate::slot::DetachedSubtree,
1082        context: &'static str,
1083    ) -> Result<Vec<(NodeId, Option<NodeId>)>, NodeError> {
1084        let mut root_nodes = Vec::new();
1085        subtree.collect_root_nodes_checked_into(&mut root_nodes, context);
1086        let mut roots = Vec::with_capacity(root_nodes.len());
1087        for root in root_nodes {
1088            let parent_id = {
1089                let mut applier = self.borrow_applier();
1090                applier.get_mut(root)?.parent()
1091            };
1092            roots.push((root, parent_id));
1093        }
1094        Ok(roots)
1095    }
1096
1097    fn retain_detached_subtree_in_host(
1098        &self,
1099        slots_host: &Rc<SlotsHost>,
1100        parent_scope: Option<ScopeId>,
1101        subtree: crate::slot::DetachedSubtree,
1102    ) -> Result<(), NodeError> {
1103        // Retention and disposal must preserve the slot lifecycle contract in
1104        // docs/SLOT_TABLE_LIFECYCLE.md across the slot table, applier, and scope
1105        // registry.
1106        let Some(root_key) = subtree.root_key_checked() else {
1107            log::error!("retention rejected detached subtree without a root group");
1108            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1109            return Ok(());
1110        };
1111        let root_detaches = self.detached_root_parent_commands(&subtree, "retention")?;
1112        self.deactivate_scope_ids(subtree.scope_ids_iter());
1113        for (root, parent_id) in root_detaches {
1114            if let Some(parent_id) = parent_id {
1115                self.commands_mut().push(Command::DetachChild {
1116                    parent_id,
1117                    child_id: root,
1118                });
1119            }
1120        }
1121        let evicted = self.core.shared_state.insert_retained(
1122            slots_host,
1123            RetainKey {
1124                parent_scope,
1125                key: root_key,
1126            },
1127            subtree,
1128        );
1129        for subtree in evicted {
1130            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1131        }
1132        Ok(())
1133    }
1134
1135    fn evict_retained_subtrees_for_host(
1136        &self,
1137        slots_host: &Rc<SlotsHost>,
1138    ) -> Result<(), NodeError> {
1139        let evicted = self.core.shared_state.advance_retention_pass(slots_host);
1140        for subtree in evicted {
1141            self.dispose_detached_subtree_in_host(slots_host, subtree)?;
1142        }
1143        Ok(())
1144    }
1145
1146    fn dispose_detached_subtree_in_host(
1147        &self,
1148        slots_host: &Rc<SlotsHost>,
1149        subtree: crate::slot::DetachedSubtree,
1150    ) -> Result<(), NodeError> {
1151        let root_nodes = self
1152            .detached_root_parent_commands(&subtree, "disposal")?
1153            .into_iter()
1154            .map(|(root, _)| root);
1155        self.dispose_scope_ids(subtree.scope_ids_iter());
1156        self.dispose_detached_nodes(root_nodes);
1157        slots_host.with_table_and_lifecycle_mut(|table, lifecycle| {
1158            table.invalidate_detached_subtree_anchors(&subtree);
1159            lifecycle.queue_subtree_disposal(subtree);
1160        });
1161        Ok(())
1162    }
1163
1164    fn handle_detached_children_in_host(
1165        &self,
1166        slots_host: &Rc<SlotsHost>,
1167        parent_scope: Option<ScopeId>,
1168        detached: Vec<crate::slot::DetachedSubtree>,
1169    ) -> Result<(), NodeError> {
1170        for subtree in detached {
1171            let retention_mode = subtree
1172                .root_scope_id()
1173                .and_then(|scope_id| self.scope_for_id(scope_id))
1174                .map(|scope| scope.retention_mode())
1175                .unwrap_or_default();
1176            match retention_mode {
1177                RetentionMode::DisposeWhenInactive => {
1178                    self.dispose_detached_subtree_in_host(slots_host, subtree)?
1179                }
1180                RetentionMode::RetainWhenInactive => {
1181                    self.retain_detached_subtree_in_host(slots_host, parent_scope, subtree)?
1182                }
1183            }
1184        }
1185        Ok(())
1186    }
1187
1188    fn handle_detached_children(
1189        &self,
1190        parent_scope: Option<ScopeId>,
1191        detached: Vec<crate::slot::DetachedSubtree>,
1192    ) {
1193        let host = self.active_slots_host();
1194        if let Err(err) = self.handle_detached_children_in_host(&host, parent_scope, detached) {
1195            log::error!("detached subtree handling failed while closing a group: {err}");
1196        }
1197    }
1198
1199    fn handle_finished_group_result(
1200        &self,
1201        parent_scope: Option<ScopeId>,
1202        result: FinishGroupResult,
1203    ) {
1204        let FinishGroupResult {
1205            detached_children,
1206            direct_nodes,
1207            root_nodes,
1208            was_skipped,
1209        } = result;
1210        if was_skipped {
1211            self.attach_root_nodes(root_nodes);
1212        }
1213        self.dispose_detached_nodes(direct_nodes);
1214        self.handle_detached_children(parent_scope, detached_children);
1215    }
1216
1217    pub(crate) fn close_current_group_body_for_scope(&self, scope: &RecomposeScope) {
1218        let result = self.with_slot_session_mut(|slots| slots.finish_group_body());
1219        self.handle_finished_group_result(Some(scope.id()), result);
1220        if let Some(popped) = self.scope_stack().pop() {
1221            debug_assert_eq!(
1222                popped.id(),
1223                scope.id(),
1224                "closed scope must match the active scope stack"
1225            );
1226        } else {
1227            log::error!("scope stack underflow while closing scope {}", scope.id());
1228        }
1229    }
1230
1231    pub fn remember<T: 'static>(&self, init: impl FnOnce() -> T) -> Owned<T> {
1232        self.remember_with_kind(PayloadKind::Remember, init)
1233    }
1234
1235    pub(crate) fn remember_internal<T: 'static>(&self, init: impl FnOnce() -> T) -> Owned<T> {
1236        self.remember_with_kind(PayloadKind::Internal, init)
1237    }
1238
1239    pub(crate) fn remember_effect<T: 'static>(&self, init: impl FnOnce() -> T) -> Owned<T> {
1240        self.remember_with_kind(PayloadKind::Effect, init)
1241    }
1242
1243    fn remember_with_kind<T: 'static>(
1244        &self,
1245        kind: PayloadKind,
1246        init: impl FnOnce() -> T,
1247    ) -> Owned<T> {
1248        self.with_slot_session_mut(|slots| slots.remember_with_kind(kind, init))
1249    }
1250
1251    pub fn use_value_slot<'pass, T: 'static>(
1252        &'pass self,
1253        init: impl FnOnce() -> T,
1254    ) -> ValueSlotHandle<'pass, T> {
1255        let slot = self
1256            .with_slot_session_mut(|slots| slots.value_slot_with_kind(PayloadKind::Internal, init));
1257        ValueSlotHandle::new(slot)
1258    }
1259
1260    #[doc(hidden)]
1261    pub fn __use_param_slot<'pass, T: 'static>(
1262        &'pass self,
1263        init: impl FnOnce() -> T,
1264    ) -> ValueSlotHandle<'pass, T> {
1265        let slot = self
1266            .with_slot_session_mut(|slots| slots.value_slot_with_kind(PayloadKind::Param, init));
1267        ValueSlotHandle::new(slot)
1268    }
1269
1270    #[doc(hidden)]
1271    pub fn __use_return_slot<'pass, T: 'static>(
1272        &'pass self,
1273        init: impl FnOnce() -> T,
1274    ) -> ValueSlotHandle<'pass, T> {
1275        let slot = self
1276            .with_slot_session_mut(|slots| slots.value_slot_with_kind(PayloadKind::Return, init));
1277        ValueSlotHandle::new(slot)
1278    }
1279
1280    #[doc(hidden)]
1281    pub fn __invalidate_return_consumer_scope(&self) {
1282        let Some(scope) = self.current_recranpose_scope() else {
1283            self.request_root_render();
1284            return;
1285        };
1286
1287        if let Some(target) = scope.callback_promotion_target() {
1288            target.invalidate();
1289        } else {
1290            self.request_root_render();
1291        }
1292    }
1293
1294    pub fn with_slot_value<'pass, T: 'static, R>(
1295        &'pass self,
1296        handle: ValueSlotHandle<'pass, T>,
1297        f: impl FnOnce(&T) -> R,
1298    ) -> R {
1299        self.with_slots(|slots| f(slots.read_value(handle.slot())))
1300    }
1301
1302    pub fn with_slot_value_mut<'pass, T: 'static, R>(
1303        &'pass self,
1304        handle: ValueSlotHandle<'pass, T>,
1305        f: impl FnOnce(&mut T) -> R,
1306    ) -> R {
1307        self.with_slots_mut(|slots| f(slots.read_value_mut(handle.slot())))
1308    }
1309
1310    pub fn mutable_state_of<T: Clone + 'static>(&self, initial: T) -> MutableState<T> {
1311        MutableState::with_runtime(initial, self.runtime_handle())
1312    }
1313
1314    pub fn mutable_state_list_of<T, I>(&self, values: I) -> SnapshotStateList<T>
1315    where
1316        T: Clone + 'static,
1317        I: IntoIterator<Item = T>,
1318    {
1319        SnapshotStateList::with_runtime(values, self.runtime_handle())
1320    }
1321
1322    pub fn mutable_state_map_of<K, V, I>(&self, pairs: I) -> SnapshotStateMap<K, V>
1323    where
1324        K: Clone + Eq + Hash + 'static,
1325        V: Clone + 'static,
1326        I: IntoIterator<Item = (K, V)>,
1327    {
1328        SnapshotStateMap::with_runtime(pairs, self.runtime_handle())
1329    }
1330
1331    pub fn read_composition_local<T: Clone + 'static>(&self, local: &CompositionLocal<T>) -> T {
1332        let stack = self.core.local_stack.borrow();
1333        for context in stack.iter().rev() {
1334            if let Some(entry) = context.values.get(&local.key) {
1335                match entry.clone().downcast::<LocalStateEntry<T>>() {
1336                    Ok(typed) => return typed.value(),
1337                    Err(_) => {
1338                        log::error!(
1339                            "composition local entry type mismatch for key {}",
1340                            local.key
1341                        );
1342                        return local.default_value();
1343                    }
1344                }
1345            }
1346        }
1347        local.default_value()
1348    }
1349
1350    pub fn read_static_composition_local<T: Clone + 'static>(
1351        &self,
1352        local: &StaticCompositionLocal<T>,
1353    ) -> T {
1354        let stack = self.core.local_stack.borrow();
1355        for context in stack.iter().rev() {
1356            if let Some(entry) = context.values.get(&local.key) {
1357                match entry.clone().downcast::<StaticLocalEntry<T>>() {
1358                    Ok(typed) => return typed.value(),
1359                    Err(_) => {
1360                        log::error!(
1361                            "static composition local entry type mismatch for key {}",
1362                            local.key
1363                        );
1364                        return local.default_value();
1365                    }
1366                }
1367            }
1368        }
1369        local.default_value()
1370    }
1371
1372    pub fn current_recranpose_scope(&self) -> Option<RecomposeScope> {
1373        self.core.scope_stack.borrow().last().cloned()
1374    }
1375
1376    pub(crate) fn current_state_invalidation_scope(&self) -> Option<RecomposeScope> {
1377        let stack = self.core.scope_stack.borrow();
1378        stack
1379            .iter()
1380            .rev()
1381            .find(|scope| scope.has_recompose_callback())
1382            .cloned()
1383            .or_else(|| stack.last().cloned())
1384    }
1385
1386    pub fn phase(&self) -> crate::Phase {
1387        self.core.phase.get()
1388    }
1389
1390    pub(crate) fn set_phase(&self, phase: crate::Phase) {
1391        self.core.phase.set(phase);
1392    }
1393
1394    pub fn enter_phase(&self, phase: crate::Phase) {
1395        self.set_phase(phase);
1396    }
1397
1398    pub(crate) fn subcompose<R>(
1399        &self,
1400        state: &mut SubcomposeState,
1401        slot_id: SlotId,
1402        content: impl FnOnce(&Composer) -> R,
1403    ) -> (R, Vec<NodeId>) {
1404        match self.phase() {
1405            crate::Phase::Measure | crate::Phase::Layout => {}
1406            current => panic!(
1407                "subcompose() may only be called during measure or layout; current phase: {:?}",
1408                current
1409            ),
1410        }
1411
1412        self.subcompose_stack().push(SubcomposeFrame::default());
1413        struct StackGuard {
1414            core: Rc<ComposerCore>,
1415            leaked: bool,
1416        }
1417        impl Drop for StackGuard {
1418            fn drop(&mut self) {
1419                if !self.leaked {
1420                    self.core.subcompose_stack.borrow_mut().pop();
1421                }
1422            }
1423        }
1424        let mut guard = StackGuard {
1425            core: self.clone_core(),
1426            leaked: false,
1427        };
1428
1429        let slot_host = state.get_or_create_slots(slot_id);
1430        let (result, _) = self.with_slot_override(slot_host.clone(), |composer| {
1431            composer.with_group(slot_id.raw(), |composer| content(composer))
1432        });
1433
1434        let frame = {
1435            let frame = take_subcompose_frame(&guard.core, "subcompose");
1436            guard.leaked = true;
1437            frame
1438        };
1439        let nodes = frame.nodes;
1440        let scopes = frame.scopes;
1441        state.register_active(slot_id, &nodes, &scopes);
1442        (result, nodes)
1443    }
1444
1445    pub fn subcompose_measurement<R>(
1446        &self,
1447        state: &mut SubcomposeState,
1448        slot_id: SlotId,
1449        content: impl FnOnce(&Composer) -> R,
1450    ) -> (R, Vec<NodeId>) {
1451        let (result, nodes) = self.subcompose(state, slot_id, content);
1452        let roots = nodes
1453            .into_iter()
1454            .filter(|&id| self.node_has_no_parent(id))
1455            .collect();
1456
1457        (result, roots)
1458    }
1459
1460    pub fn subcompose_in<R>(
1461        &self,
1462        slots: &Rc<SlotsHost>,
1463        root: Option<NodeId>,
1464        f: impl FnOnce(&Composer) -> R,
1465    ) -> Result<R, NodeError> {
1466        let runtime_handle = self.runtime_handle();
1467        let phase = self.phase();
1468        let locals = self.current_local_stack();
1469        let shared_state = slots
1470            .runtime_state()
1471            .unwrap_or_else(|| Rc::clone(&self.core.shared_state));
1472        let core = Rc::new(ComposerCore::new(
1473            shared_state,
1474            Rc::clone(slots),
1475            Rc::clone(&self.core.applier),
1476            runtime_handle.clone(),
1477            self.observer(),
1478            root,
1479            InitialParentFrame::RealParent,
1480        ));
1481        core.phase.set(phase);
1482        *core.local_stack.borrow_mut() = locals;
1483        let composer = Composer::from_core(core);
1484        let (result, commands, side_effects, compact_applier) = composer.install(|composer| {
1485            let (output, outcome) = composer.try_with_slot_host_pass(
1486                Rc::clone(slots),
1487                crate::slot::SlotPassMode::Compose,
1488                |composer| f(composer),
1489            )?;
1490            let commands = composer.take_commands();
1491            let side_effects = composer.take_side_effects();
1492            Ok((output, commands, side_effects, outcome.compacted))
1493        })?;
1494        {
1495            let mut applier = self.borrow_applier();
1496            commands.apply(&mut *applier)?;
1497            for update in runtime_handle.take_updates() {
1498                update.apply(&mut *applier)?;
1499            }
1500        }
1501        if compact_applier {
1502            self.core.applier.compact();
1503            self.core.applier.borrow_dyn().clear_recycled_nodes();
1504        }
1505        runtime_handle.drain_ui();
1506        for effect in side_effects {
1507            effect();
1508        }
1509        runtime_handle.drain_ui();
1510        Ok(result)
1511    }
1512
1513    /// Captures the composition-local values in scope at the current point of
1514    /// composition, as an opaque snapshot that can later be replayed with
1515    /// [`Composer::subcompose_slot_with_locals`].
1516    ///
1517    /// A `SubcomposeLayout` captures this while it is being composed and replays
1518    /// it while subcomposing off the measure pass, so content that is
1519    /// subcomposed during layout observes the same composition locals as the
1520    /// `SubcomposeLayout` call site — matching Jetpack Compose, where a
1521    /// subcomposition inherits the composition locals of the layout that
1522    /// created it rather than whatever happens to be in scope during measure
1523    /// (which, after composition unwinds, no longer carries ancestor providers).
1524    pub fn capture_composition_locals(&self) -> CapturedCompositionLocals {
1525        CapturedCompositionLocals(self.current_local_stack())
1526    }
1527
1528    /// Subcomposes content using an isolated SlotsHost without resetting it.
1529    /// Unlike `subcompose_in`, this preserves existing slot state across calls,
1530    /// allowing efficient reuse during measurement passes. This is critical for
1531    /// lazy lists where items need stable slot positions.
1532    pub fn subcompose_slot<R>(
1533        &self,
1534        slots: &Rc<SlotsHost>,
1535        root: Option<NodeId>,
1536        f: impl FnOnce(&Composer) -> R,
1537    ) -> Result<(R, Vec<RecomposeScope>), NodeError> {
1538        self.subcompose_slot_with_locals(slots, root, None, f)
1539    }
1540
1541    /// Like [`Composer::subcompose_slot`], but seeds the subcomposition with a
1542    /// composition-local snapshot captured earlier (see
1543    /// [`Composer::capture_composition_locals`]). When `locals` is `None` the
1544    /// locals in scope right now are used, preserving the plain behavior.
1545    pub fn subcompose_slot_with_locals<R>(
1546        &self,
1547        slots: &Rc<SlotsHost>,
1548        root: Option<NodeId>,
1549        locals: Option<&CapturedCompositionLocals>,
1550        f: impl FnOnce(&Composer) -> R,
1551    ) -> Result<(R, Vec<RecomposeScope>), NodeError> {
1552        let runtime_handle = self.runtime_handle();
1553        let phase = self.phase();
1554        let locals = match locals {
1555            Some(captured) => captured.0.clone(),
1556            None => self.current_local_stack(),
1557        };
1558        let shared_state = slots
1559            .runtime_state()
1560            .unwrap_or_else(|| Rc::clone(&self.core.shared_state));
1561        let core = Rc::new(ComposerCore::new(
1562            shared_state,
1563            Rc::clone(slots),
1564            Rc::clone(&self.core.applier),
1565            runtime_handle.clone(),
1566            self.observer(),
1567            root,
1568            InitialParentFrame::RealParent,
1569        ));
1570        core.phase.set(phase);
1571        *core.local_stack.borrow_mut() = locals;
1572        let composer = Composer::from_core(core);
1573        composer.subcompose_stack().push(SubcomposeFrame::default());
1574        struct StackGuard {
1575            core: Rc<ComposerCore>,
1576            leaked: bool,
1577        }
1578        impl Drop for StackGuard {
1579            fn drop(&mut self) {
1580                if !self.leaked {
1581                    self.core.subcompose_stack.borrow_mut().pop();
1582                }
1583            }
1584        }
1585        let mut guard = StackGuard {
1586            core: composer.clone_core(),
1587            leaked: false,
1588        };
1589        let root_group_key = crate::location_key(file!(), line!(), column!());
1590        let (result, commands, side_effects, compact_applier) = composer.install(|composer| {
1591            let (output, outcome) = composer.try_with_slot_host_pass(
1592                Rc::clone(slots),
1593                crate::slot::SlotPassMode::Compose,
1594                |composer| {
1595                    let output = composer.with_group(root_group_key, |composer| f(composer));
1596                    if root.is_some() {
1597                        composer.pop_parent();
1598                    }
1599                    output
1600                },
1601            )?;
1602            let commands = composer.take_commands();
1603            let side_effects = composer.take_side_effects();
1604            Ok((output, commands, side_effects, outcome.compacted))
1605        })?;
1606        let frame = {
1607            let frame = take_subcompose_frame(&guard.core, "subcompose_slot");
1608            guard.leaked = true;
1609            frame
1610        };
1611
1612        {
1613            let mut applier = self.borrow_applier();
1614            commands.apply(&mut *applier)?;
1615            for update in runtime_handle.take_updates() {
1616                update.apply(&mut *applier)?;
1617            }
1618        }
1619        if compact_applier {
1620            self.core.applier.compact();
1621            self.core.applier.borrow_dyn().clear_recycled_nodes();
1622        }
1623        runtime_handle.drain_ui();
1624        for effect in side_effects {
1625            effect();
1626        }
1627        runtime_handle.drain_ui();
1628        Ok((result, frame.scopes))
1629    }
1630
1631    fn attach_root_nodes(&self, root_nodes: Vec<NodeId>) {
1632        for id in root_nodes {
1633            self.attach_to_parent_with_mode(id, true);
1634        }
1635    }
1636
1637    pub fn skip_current_group(&self) {
1638        self.with_slot_session_mut(|slots| slots.skip_group());
1639    }
1640
1641    pub fn runtime_handle(&self) -> RuntimeHandle {
1642        self.core.runtime.clone()
1643    }
1644
1645    pub fn set_recranpose_callback<F>(&self, callback: F)
1646    where
1647        F: FnMut(&Composer) + 'static,
1648    {
1649        self.set_recranpose_callback_boxed(Box::new(callback));
1650    }
1651
1652    /// Monomorphic core (`inline(never)` so fat LTO keeps one copy): the
1653    /// observer wiring here used to be re-instantiated for every composable
1654    /// call site through the generic entry point above.
1655    #[inline(never)]
1656    fn set_recranpose_callback_boxed(&self, mut callback: Box<dyn FnMut(&Composer)>) {
1657        if let Some(scope) = self.current_recranpose_scope() {
1658            let observer = self.observer();
1659            let scope_weak = scope.downgrade();
1660            scope.set_recompose(Box::new(move |composer: &Composer| {
1661                if let Some(inner) = scope_weak.upgrade() {
1662                    let scope_instance = RecomposeScope { inner };
1663                    observer.observe_reads(
1664                        scope_instance.clone(),
1665                        move |scope_ref| scope_ref.invalidate(),
1666                        || {
1667                            callback(composer);
1668                        },
1669                    );
1670                }
1671            }));
1672        }
1673    }
1674
1675    pub fn set_recranpose_fn(&self, callback: fn(&Composer)) {
1676        if let Some(scope) = self.current_recranpose_scope() {
1677            scope.set_recompose_fn(callback);
1678        }
1679    }
1680
1681    pub fn with_composition_locals<R>(
1682        &self,
1683        provided: Vec<ProvidedValue>,
1684        f: impl FnOnce(&Composer) -> R,
1685    ) -> R {
1686        if provided.is_empty() {
1687            return f(self);
1688        }
1689        let mut context = LocalContext::default();
1690        for value in provided {
1691            let (key, entry) = value.into_entry(self);
1692            context.values.insert(key, entry);
1693        }
1694        {
1695            let mut stack = self.local_stack();
1696            Rc::make_mut(&mut *stack).push(context);
1697        }
1698        let result = f(self);
1699        {
1700            let mut stack = self.local_stack();
1701            Rc::make_mut(&mut *stack).pop();
1702        }
1703        result
1704    }
1705}