Skip to main content

cranpose_core/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(unsafe_code)]
3
4pub extern crate self as cranpose_core;
5
6mod callbacks;
7mod composer;
8pub mod composer_context;
9mod composition;
10mod composition_locals;
11pub mod concurrency;
12mod debug_trace;
13mod effect_key;
14mod emit;
15pub mod env_flags;
16#[cfg(any(feature = "internal", test))]
17mod frame_clock;
18mod hooks;
19mod launched_effect;
20pub mod owned;
21pub mod platform;
22mod recompose;
23mod retention;
24pub mod runtime;
25mod slot;
26pub mod snapshot_double_index_heap;
27pub mod snapshot_id_set;
28pub mod snapshot_pinning;
29pub mod snapshot_state_observer;
30pub mod snapshot_v2;
31mod snapshot_weak_set;
32mod state;
33pub mod subcompose;
34
35#[cfg(feature = "internal")]
36#[doc(hidden)]
37pub mod internal {
38    pub use crate::frame_clock::{FrameCallbackRegistration, FrameClock};
39}
40pub use callbacks::{CallbackHolder, CallbackHolder1, ParamSlot, ParamState, ReturnSlot};
41pub use composer::{BranchGroupGuard, CapturedCompositionContext, Composer, ValueSlotHandle};
42pub(crate) use composer::{ComposerCore, EmittedNode, ParentAttachMode, ParentFrame};
43pub use composition::{Composition, ROOT_RENDER_REPLAY_LIMIT};
44pub use composition_locals::{
45    CompositionLocal, CompositionLocalProvider, ProvidedValue, StaticCompositionLocal,
46    compositionLocalOf, compositionLocalOfWithPolicy, staticCompositionLocalOf,
47};
48pub(crate) use composition_locals::{LocalStateEntry, StaticLocalEntry};
49pub use concurrency::{
50    CollectEvents, CoroutineScope, Delay, EventChannel, EventSender, EventStream, EventStreamNext,
51    ProduceScope, collectAsState, delay, interval, launchBlocking, produceState,
52    rememberCoroutineScope, rememberEventStream, spawn_ui_task, withBlocking,
53};
54#[doc(hidden)]
55pub use debug_trace::{
56    debug_label_current_scope, debug_live_recompose_scope_count,
57    debug_recompose_scope_registry_stats, debug_scope_invalidation_sources, debug_scope_label,
58};
59pub use hooks::{
60    derivedStateOf, mutableStateList, mutableStateListOf, mutableStateMap, mutableStateMapOf,
61    mutableStateOf, ownedMutableStateOf, remember, rememberKeyed, rememberMutableStateOf,
62    rememberMutableStateOfNeverEqual, rememberUpdatedState, try_mutableStateOf,
63};
64#[cfg(feature = "internal")]
65#[doc(hidden)]
66pub use hooks::{withFrameMillis, withFrameNanos};
67pub use launched_effect::{
68    __launched_effect_async_impl, __launched_effect_impl, CancelToken, LaunchedEffectScope,
69    TaskSite,
70};
71pub use owned::Owned;
72pub use platform::{Clock, RuntimeScheduler, SchedulerRef, scheduler_ref};
73pub use retention::{RetentionBudget, RetentionEvictionPolicy, RetentionMode, RetentionPolicy};
74#[doc(hidden)]
75pub use runtime::{
76    DefaultScheduler, Runtime, RuntimeHandle, StateId, TaskHandle, UiDispatcher,
77    current_runtime_handle, label_next_ui_task, schedule_frame, schedule_node_update,
78};
79pub use slot::{
80    SlotDebugAnchor, SlotDebugEntry, SlotDebugEntryKind, SlotDebugGroup, SlotDebugScope,
81    SlotDebugSnapshot, SlotRetentionDebugStats, SlotTable, SlotTableDebugStats,
82    SlotTableLocalDebugStats, SlotTableMutationDebugStats,
83};
84#[doc(hidden)]
85pub use snapshot_state_observer::SnapshotStateObserver;
86
87/// Runs the provided closure inside a mutable snapshot and applies the result.
88///
89/// Use this function when you need to update `MutableState` from outside the
90/// composition or layout phase, typically in event handlers or async tasks.
91///
92/// # Why is this needed?
93/// Cranpose uses a snapshot system (MVCC) to isolate state changes. Modifications
94/// made to `MutableState` are only visible to the current thread's active snapshot.
95/// To make changes visible to the rest of the system (and trigger recomposition),
96/// they must be "applied" by committing the snapshot. This helper handles that
97/// lifecycle for you.
98///
99/// # Example
100///
101/// ```ignore
102/// // Inside a button click handler
103/// run_in_mutable_snapshot(|| {
104///     count.set(count.value() + 1);
105/// });
106/// ```
107///
108/// # Important
109/// ALL UI event handlers (keyboard, mouse, touch, animations) that modify state
110/// MUST use this function or [`dispatch_ui_event`].
111pub fn run_in_mutable_snapshot<T>(block: impl FnOnce() -> T) -> Result<T, &'static str> {
112    let snapshot = snapshot_v2::take_mutable_snapshot(None, None);
113
114    let _applied_guard = AppliedSnapshotFlagGuard::enter();
115    let value = snapshot.enter(block);
116
117    match snapshot.apply() {
118        snapshot_v2::SnapshotApplyResult::Success => Ok(value),
119        snapshot_v2::SnapshotApplyResult::Failure => Err("Snapshot apply failed"),
120    }
121}
122
123struct AppliedSnapshotFlagGuard {
124    previous: bool,
125}
126
127impl AppliedSnapshotFlagGuard {
128    fn enter() -> Self {
129        let previous = IN_APPLIED_SNAPSHOT.with(|flag| {
130            let previous = flag.get();
131            flag.set(true);
132            previous
133        });
134        Self { previous }
135    }
136}
137
138impl Drop for AppliedSnapshotFlagGuard {
139    fn drop(&mut self) {
140        IN_APPLIED_SNAPSHOT.with(|flag| flag.set(self.previous));
141    }
142}
143
144/// Dispatches a UI event in a proper snapshot context.
145///
146/// This is a convenience wrapper around [`run_in_mutable_snapshot`] that returns
147/// `Option<T>` instead of `Result<T, &str>`.
148///
149/// # Example
150/// ```ignore
151/// // In a keyboard event handler:
152/// dispatch_ui_event(|| {
153///     text_field_state.edit(|buffer| {
154///         buffer.insert("a");
155///     });
156/// });
157/// ```
158pub fn dispatch_ui_event<T>(block: impl FnOnce() -> T) -> Option<T> {
159    run_in_mutable_snapshot(block).ok()
160}
161
162thread_local! {
163    pub(crate) static IN_EVENT_HANDLER: Cell<bool> = const { Cell::new(false) };
164    pub(crate) static IN_APPLIED_SNAPSHOT: Cell<bool> = const { Cell::new(false) };
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
168pub struct CompositionPassDebugStats {
169    pub commands_len: usize,
170    pub commands_cap: usize,
171    pub command_payload_len_bytes: usize,
172    pub command_payload_cap_bytes: usize,
173    pub sync_children_len: usize,
174    pub sync_children_cap: usize,
175    pub sync_child_ids_len: usize,
176    pub sync_child_ids_cap: usize,
177    pub side_effects_len: usize,
178    pub side_effects_cap: usize,
179}
180
181#[must_use]
182pub struct EventHandlerScopeGuard {
183    previous: bool,
184}
185
186impl Drop for EventHandlerScopeGuard {
187    fn drop(&mut self) {
188        IN_EVENT_HANDLER.with(|flag| flag.set(self.previous));
189    }
190}
191
192pub fn enter_event_handler_scope() -> EventHandlerScopeGuard {
193    let previous = IN_EVENT_HANDLER.with(|flag| {
194        let previous = flag.get();
195        flag.set(true);
196        previous
197    });
198    EventHandlerScopeGuard { previous }
199}
200
201/// Returns true if currently in an event handler context.
202pub fn in_event_handler() -> bool {
203    IN_EVENT_HANDLER.with(|c| c.get())
204}
205
206/// Returns true if currently in an applied snapshot context.
207pub fn in_applied_snapshot() -> bool {
208    IN_APPLIED_SNAPSHOT.with(|c| c.get())
209}
210
211use std::{
212    any::{Any, TypeId},
213    cell::{Cell, Ref, RefCell, RefMut},
214    cmp::Reverse,
215    collections::BinaryHeap,
216    hash::{Hash, Hasher},
217    ops::{Deref, DerefMut},
218    rc::{Rc, Weak},
219};
220
221#[cfg(test)]
222pub use runtime::{TestRuntime, TestScheduler};
223use smallvec::SmallVec;
224
225use crate::collections::map::{HashMap, HashSet};
226
227pub type Key = u64;
228pub type NodeId = usize;
229
230#[cfg(any(test, debug_assertions))]
231#[derive(Clone, Debug, PartialEq, Eq)]
232struct LocationKeyDebugInfo {
233    file: String,
234    line: u32,
235    column: u32,
236}
237
238#[cfg(any(test, debug_assertions))]
239thread_local! {
240    static LOCATION_KEY_REGISTRY: RefCell<HashMap<Key, LocationKeyDebugInfo>> =
241        RefCell::new(HashMap::default());
242    static LOCATION_KEY_COLLISION_COUNT: Cell<usize> = const { Cell::new(0) };
243}
244
245#[cfg(any(test, debug_assertions))]
246fn register_location_key_debug_info(key: Key, file: &str, line: u32, column: u32) {
247    let info = LocationKeyDebugInfo {
248        file: file.to_owned(),
249        line,
250        column,
251    };
252    let collision = LOCATION_KEY_REGISTRY.with(|registry| {
253        let mut registry = registry.borrow_mut();
254        match registry.entry(key) {
255            std::collections::hash_map::Entry::Vacant(entry) => {
256                entry.insert(info);
257                None
258            }
259            std::collections::hash_map::Entry::Occupied(entry) => {
260                let existing = entry.get();
261                (existing != &info).then(|| (existing.clone(), info))
262            }
263        }
264    });
265    if let Some((existing, incoming)) = collision {
266        LOCATION_KEY_COLLISION_COUNT.with(|count| {
267            count.set(count.get().saturating_add(1));
268        });
269        log::error!("location key collision: key={key} first={existing:?} second={incoming:?}");
270    }
271}
272
273#[cfg(all(debug_assertions, not(test)))]
274fn location_key_diagnostics_enabled() -> bool {
275    crate::env_flag!("CRANPOSE_LOCATION_KEY_DIAGNOSTICS")
276}
277
278#[cfg(test)]
279pub(crate) fn register_location_key_debug_info_for_test(
280    key: Key,
281    file: &str,
282    line: u32,
283    column: u32,
284) {
285    register_location_key_debug_info(key, file, line, column);
286}
287
288#[cfg(test)]
289pub(crate) fn location_key_debug_collision_count_for_test() -> usize {
290    LOCATION_KEY_COLLISION_COUNT.with(Cell::get)
291}
292
293#[cfg(test)]
294pub(crate) fn location_key_debug_info_for_test(key: Key) -> Option<LocationKeyDebugInfo> {
295    LOCATION_KEY_REGISTRY.with(|registry| registry.borrow().get(&key).cloned())
296}
297
298#[cfg(test)]
299pub(crate) fn slot_validation_diagnostics_enabled() -> bool {
300    true
301}
302
303#[cfg(all(debug_assertions, not(test)))]
304pub(crate) fn slot_validation_diagnostics_enabled() -> bool {
305    crate::env_flag!("CRANPOSE_VALIDATE_SLOTS")
306}
307
308fn source_location_key(file: &str, line: u32, column: u32) -> Key {
309    avalanche_location_key(source_location_hash(file, line, column))
310}
311
312fn source_location_hash(file: &str, line: u32, column: u32) -> u64 {
313    let mut hash = 0xcbf2_9ce4_8422_2325u64;
314    hash = fnv1a_location_key_bytes(hash, file.as_bytes());
315    hash = fnv1a_location_key_bytes(hash, &[0xff]);
316    hash = fnv1a_location_key_bytes(hash, &line.to_le_bytes());
317    hash = fnv1a_location_key_bytes(hash, &[0xfe]);
318    hash = fnv1a_location_key_bytes(hash, &column.to_le_bytes());
319    hash
320}
321
322fn fnv1a_location_key_bytes(mut hash: u64, bytes: &[u8]) -> u64 {
323    for byte in bytes {
324        hash ^= u64::from(*byte);
325        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
326    }
327    hash
328}
329
330fn avalanche_location_key(mut value: u64) -> u64 {
331    value ^= value >> 33;
332    value = value.wrapping_mul(0xff51_afd7_ed55_8ccd);
333    value ^= value >> 33;
334    value = value.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
335    value ^ (value >> 33)
336}
337
338#[doc(hidden)]
339#[track_caller]
340pub fn caller_location_key() -> Key {
341    let caller = std::panic::Location::caller();
342    location_key(caller.file(), caller.line(), caller.column())
343}
344
345#[doc(hidden)]
346#[track_caller]
347pub fn composable_identity_key(definition: Key) -> Key {
348    (definition.wrapping_mul(0x0000_0100_0000_01b3) ^ caller_location_key())
349        .wrapping_mul(0x0000_0100_0000_01b3)
350}
351
352#[doc(hidden)]
353pub fn composable_definition_key(
354    file: &str,
355    line: u32,
356    column: u32,
357    marker: std::any::TypeId,
358) -> Key {
359    let mut hasher = std::collections::hash_map::DefaultHasher::new();
360    std::hash::Hash::hash(&marker, &mut hasher);
361    location_key(file, line, column) ^ avalanche_location_key(std::hash::Hasher::finish(&hasher))
362}
363
364pub fn location_key(file: &str, line: u32, column: u32) -> Key {
365    let key = source_location_key(file, line, column);
366    #[cfg(test)]
367    register_location_key_debug_info(key, file, line, column);
368    #[cfg(all(debug_assertions, not(test)))]
369    if location_key_diagnostics_enabled() {
370        register_location_key_debug_info(key, file, line, column);
371    }
372    key
373}
374
375#[doc(hidden)]
376pub fn __branch_group_scope_deferred(key: Key) -> Option<BranchGroupGuard> {
377    with_current_composer_opt(|composer| composer.__branch_group_deferred(key))
378}
379
380#[doc(hidden)]
381pub fn branch_location_key(file: &str, line: u32, column: u32, branch: u32) -> Key {
382    let mut hash = source_location_hash(file, line, column);
383    hash = fnv1a_location_key_bytes(hash, &[0xfd]);
384    hash = fnv1a_location_key_bytes(hash, &branch.to_le_bytes());
385    let key = avalanche_location_key(hash);
386    #[cfg(test)]
387    register_location_key_debug_info(key, file, line, column);
388    #[cfg(all(debug_assertions, not(test)))]
389    if location_key_diagnostics_enabled() {
390        register_location_key_debug_info(key, file, line, column);
391    }
392    key
393}
394
395/// Stable identifier for a slot in the slot table.
396///
397/// Anchors provide positional stability: they maintain their identity even when
398/// the slot table is reorganized (e.g., during conditional rendering or group moves).
399/// This prevents effect states from being prematurely removed during recomposition.
400#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default)]
401pub struct AnchorId {
402    id: u32,
403    generation: u32,
404}
405
406impl AnchorId {
407    pub(crate) const INVALID: AnchorId = AnchorId {
408        id: 0,
409        generation: 0,
410    };
411
412    pub(crate) fn new(id: usize) -> Self {
413        Self {
414            id: crate::slot::checked_usize_to_u32(id, "anchor id"),
415            generation: 1,
416        }
417    }
418
419    /// Check if this anchor is valid (non-zero).
420    pub fn is_valid(&self) -> bool {
421        self.id != 0
422    }
423}
424
425pub(crate) type ScopeId = usize;
426pub(crate) type FrameCallbackId = u64;
427type LocalStackSnapshot = Rc<Vec<composer::LocalContext>>;
428
429#[derive(Clone)]
430pub(crate) struct LocalKey(Rc<()>);
431
432impl LocalKey {
433    fn new() -> Self {
434        Self(Rc::new(()))
435    }
436
437    pub(crate) fn entry_source(&self) -> Key {
438        avalanche_location_key(Rc::as_ptr(&self.0) as usize as u64)
439    }
440}
441
442impl std::fmt::Debug for LocalKey {
443    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444        f.debug_tuple("LocalKey")
445            .field(&(Rc::as_ptr(&self.0) as usize))
446            .finish()
447    }
448}
449
450impl PartialEq for LocalKey {
451    fn eq(&self, other: &Self) -> bool {
452        Rc::ptr_eq(&self.0, &other.0)
453    }
454}
455
456impl Eq for LocalKey {}
457
458impl Hash for LocalKey {
459    fn hash<H: Hasher>(&self, state: &mut H) {
460        Rc::as_ptr(&self.0).hash(state);
461    }
462}
463
464thread_local! {
465    static EMPTY_LOCAL_STACK: LocalStackSnapshot = Rc::new(Vec::new());
466    #[cfg(debug_assertions)]
467    static DEBUG_SCOPE_LABELS: RefCell<HashMap<usize, &'static str>> = RefCell::new(HashMap::default());
468    #[cfg(debug_assertions)]
469    static DEBUG_SCOPE_INVALIDATION_SOURCES: RefCell<HashMap<usize, HashSet<String>>> =
470        RefCell::new(HashMap::default());
471    #[cfg(all(test, debug_assertions))]
472    static DEBUG_SCOPE_TRACKING_OVERRIDE: Cell<Option<bool>> = const { Cell::new(None) };
473}
474
475fn empty_local_stack() -> LocalStackSnapshot {
476    EMPTY_LOCAL_STACK.with(Rc::clone)
477}
478
479enum RecomposeCallback {
480    Static(fn(&Composer)),
481    Dynamic(Box<dyn FnMut(&Composer) + 'static>),
482}
483
484pub(crate) struct RecomposeScopeInner {
485    runtime: RuntimeHandle,
486    invalid: Cell<bool>,
487    enqueued: Cell<bool>,
488    active: Cell<bool>,
489    deactivations: Cell<u64>,
490    composed_once: Cell<bool>,
491    pending_recompose: Cell<bool>,
492    force_reuse: Cell<bool>,
493    force_recompose: Cell<bool>,
494    retention_mode: Cell<RetentionMode>,
495    parent_hint: Cell<Option<NodeId>>,
496    recompose: RefCell<Option<RecomposeCallback>>,
497    parent_scope: RefCell<Option<Weak<RecomposeScopeInner>>>,
498    lifetime_owner_scope: RefCell<Option<Weak<RecomposeScopeInner>>>,
499    local_stack: RefCell<LocalStackSnapshot>,
500    slots_storage_key: Cell<usize>,
501    slots_runtime_state: RefCell<Option<std::rc::Weak<crate::composer::ComposerRuntimeState>>>,
502    state_subscriptions: RefCell<HashSet<StateId>>,
503    invalidation_sources: RefCell<Option<HashSet<StateId>>>,
504}
505
506impl RecomposeScopeInner {
507    fn new(runtime: RuntimeHandle) -> Self {
508        runtime.increment_live_recompose_scope_count();
509        Self {
510            runtime,
511            invalid: Cell::new(false),
512            enqueued: Cell::new(false),
513            active: Cell::new(true),
514            deactivations: Cell::new(0),
515            composed_once: Cell::new(false),
516            pending_recompose: Cell::new(false),
517            force_reuse: Cell::new(false),
518            force_recompose: Cell::new(false),
519            retention_mode: Cell::new(RetentionMode::DisposeWhenInactive),
520            parent_hint: Cell::new(None),
521            recompose: RefCell::new(None),
522            parent_scope: RefCell::new(None),
523            lifetime_owner_scope: RefCell::new(None),
524            local_stack: RefCell::new(empty_local_stack()),
525            slots_storage_key: Cell::new(0),
526            slots_runtime_state: RefCell::new(None),
527            state_subscriptions: RefCell::new(HashSet::default()),
528            invalidation_sources: RefCell::new(Some(HashSet::default())),
529        }
530    }
531
532    fn id(&self) -> ScopeId {
533        std::ptr::from_ref(self).addr()
534    }
535}
536
537impl Drop for RecomposeScopeInner {
538    fn drop(&mut self) {
539        let id = self.id();
540        self.runtime.decrement_live_recompose_scope_count();
541        let subscriptions = std::mem::take(self.state_subscriptions.get_mut());
542        for state_id in subscriptions {
543            self.runtime.unregister_state_scope(state_id, id);
544        }
545        #[cfg(debug_assertions)]
546        {
547            let _ = DEBUG_SCOPE_LABELS.try_with(|labels| {
548                labels.borrow_mut().remove(&id);
549            });
550            let _ = DEBUG_SCOPE_INVALIDATION_SOURCES.try_with(|sources| {
551                sources.borrow_mut().remove(&id);
552            });
553        }
554        if self.enqueued.replace(false) {
555            self.runtime.mark_scope_recomposed(id);
556        }
557    }
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
561pub struct RecomposeScopeRegistryDebugStats {
562    pub len: usize,
563    pub capacity: usize,
564}
565
566#[derive(Clone)]
567pub struct RecomposeScope {
568    inner: Rc<RecomposeScopeInner>,
569}
570
571impl PartialEq for RecomposeScope {
572    fn eq(&self, other: &Self) -> bool {
573        Rc::ptr_eq(&self.inner, &other.inner)
574    }
575}
576
577impl Eq for RecomposeScope {}
578
579impl Hash for RecomposeScope {
580    fn hash<H: Hasher>(&self, state: &mut H) {
581        self.id().hash(state);
582    }
583}
584
585impl RecomposeScope {
586    fn new(runtime: RuntimeHandle) -> Self {
587        Self {
588            inner: Rc::new(RecomposeScopeInner::new(runtime)),
589        }
590    }
591
592    pub(crate) fn downgrade(&self) -> Weak<RecomposeScopeInner> {
593        Rc::downgrade(&self.inner)
594    }
595
596    pub fn id(&self) -> ScopeId {
597        self.inner.id()
598    }
599
600    pub fn is_invalid(&self) -> bool {
601        self.inner.invalid.get()
602    }
603
604    pub fn is_active(&self) -> bool {
605        self.inner.active.get()
606    }
607
608    /// Total deactivations along this scope's owner chain. A retained slot
609    /// composition records this at compose time; a later mismatch means some
610    /// enclosing composition was deactivated (its owned effects cancelled)
611    /// since the slot last composed, so the retained content must recompose
612    /// once to restart them — no flag on the slot's own scopes carries that
613    /// trace, because deactivation walks stop at slot-host boundaries.
614    pub fn owner_chain_deactivation_epoch(&self) -> u64 {
615        let mut total = 0u64;
616        let mut current = Some(self.clone());
617        while let Some(scope) = current {
618            total = total.wrapping_add(scope.inner.deactivations.get());
619            let structural_parent = scope.inner.parent_scope.borrow().clone();
620            let lifetime_owner = scope.inner.lifetime_owner_scope.borrow().clone();
621            let next = structural_parent.or(lifetime_owner);
622            current = next
623                .and_then(|parent| parent.upgrade())
624                .map(|inner| RecomposeScope { inner });
625        }
626        total
627    }
628
629    pub(crate) fn is_effectively_active(&self) -> bool {
630        let mut current = Some(self.clone());
631        while let Some(scope) = current {
632            if !scope.is_active() {
633                return false;
634            }
635            let structural_parent = scope.inner.parent_scope.borrow().clone();
636            let lifetime_owner = scope.inner.lifetime_owner_scope.borrow().clone();
637            let next = structural_parent.or(lifetime_owner);
638            current = match next {
639                Some(parent) => {
640                    let Some(inner) = parent.upgrade() else {
641                        return false;
642                    };
643                    Some(RecomposeScope { inner })
644                }
645                None => None,
646            };
647        }
648        true
649    }
650
651    fn record_state_subscription(&self, state_id: StateId) {
652        self.inner.state_subscriptions.borrow_mut().insert(state_id);
653    }
654
655    fn record_unknown_invalidation_source(&self) {
656        *self.inner.invalidation_sources.borrow_mut() = None;
657    }
658
659    fn record_state_invalidation_source(&self, state_id: StateId) {
660        let mut sources = self.inner.invalidation_sources.borrow_mut();
661        if let Some(source_set) = sources.as_mut() {
662            source_set.insert(state_id);
663        }
664    }
665
666    fn enqueue_invalidation(&self) {
667        self.inner.invalid.set(true);
668        if !self.is_effectively_active() {
669            return;
670        }
671        if !self.inner.enqueued.replace(true) {
672            self.inner
673                .runtime
674                .register_invalid_scope(self.id(), self.downgrade());
675        }
676    }
677
678    fn invalidate(&self) {
679        self.record_unknown_invalidation_source();
680        self.enqueue_invalidation();
681    }
682
683    pub(crate) fn invalidate_from_state(&self, state_id: StateId) {
684        self.record_state_invalidation_source(state_id);
685        self.enqueue_invalidation();
686    }
687
688    fn mark_recomposed(&self) {
689        self.inner.invalid.set(false);
690        self.inner.force_reuse.set(false);
691        self.inner.force_recompose.set(false);
692        self.inner
693            .invalidation_sources
694            .borrow_mut()
695            .replace(HashSet::default());
696        if self.inner.enqueued.replace(false) {
697            self.inner.runtime.mark_scope_recomposed(self.id());
698        }
699        let pending = self.inner.pending_recompose.replace(false);
700        if pending {
701            if self.inner.active.get() {
702                self.invalidate();
703            } else {
704                self.inner.invalid.set(true);
705            }
706        }
707    }
708
709    fn set_recompose(&self, callback: Box<dyn FnMut(&Composer) + 'static>) {
710        *self.inner.recompose.borrow_mut() = Some(RecomposeCallback::Dynamic(callback));
711    }
712
713    fn set_recompose_fn(&self, callback: fn(&Composer)) {
714        *self.inner.recompose.borrow_mut() = Some(RecomposeCallback::Static(callback));
715    }
716
717    fn run_recompose(&self, composer: &Composer) -> bool {
718        let callback = self.inner.recompose.borrow_mut().take();
719        if let Some(callback) = callback {
720            let callback = match callback {
721                RecomposeCallback::Static(callback) => {
722                    callback(composer);
723                    RecomposeCallback::Static(callback)
724                }
725                RecomposeCallback::Dynamic(mut callback) => {
726                    callback(composer);
727                    RecomposeCallback::Dynamic(callback)
728                }
729            };
730            let mut slot = self.inner.recompose.borrow_mut();
731            if slot.is_none() {
732                *slot = Some(callback);
733            }
734            true
735        } else {
736            false
737        }
738    }
739
740    fn has_recompose_callback(&self) -> bool {
741        self.inner.recompose.borrow().is_some()
742    }
743
744    fn snapshot_locals(&self, stack: LocalStackSnapshot) {
745        *self.inner.local_stack.borrow_mut() = stack;
746    }
747
748    fn local_stack(&self) -> LocalStackSnapshot {
749        self.inner.local_stack.borrow().clone()
750    }
751
752    fn set_parent_hint(&self, parent: Option<NodeId>) {
753        self.inner.parent_hint.set(parent);
754    }
755
756    fn set_parent_scope(&self, parent: Option<RecomposeScope>) {
757        *self.inner.parent_scope.borrow_mut() = parent.map(|scope| scope.downgrade());
758    }
759
760    fn parent_scope(&self) -> Option<RecomposeScope> {
761        self.inner
762            .parent_scope
763            .borrow()
764            .as_ref()
765            .and_then(Weak::upgrade)
766            .map(|inner| RecomposeScope { inner })
767    }
768
769    fn set_lifetime_owner_scope(&self, owner: Option<RecomposeScope>) {
770        *self.inner.lifetime_owner_scope.borrow_mut() = owner.map(|scope| scope.downgrade());
771    }
772
773    #[cfg(test)]
774    fn lifetime_owner_scope(&self) -> Option<RecomposeScope> {
775        self.inner
776            .lifetime_owner_scope
777            .borrow()
778            .as_ref()
779            .and_then(Weak::upgrade)
780            .map(|inner| RecomposeScope { inner })
781    }
782
783    fn callback_promotion_target(&self) -> Option<RecomposeScope> {
784        let mut current = self.parent_scope();
785        while let Some(scope) = current {
786            if scope.has_recompose_callback() {
787                return Some(scope);
788            }
789            current = scope.parent_scope();
790        }
791        None
792    }
793
794    fn parent_hint(&self) -> Option<NodeId> {
795        self.inner.parent_hint.get()
796    }
797
798    fn set_slots_host(&self, host: &Rc<SlotsHost>) {
799        self.inner.slots_storage_key.set(host.storage_key());
800        *self.inner.slots_runtime_state.borrow_mut() =
801            host.runtime_state().map(|state| Rc::downgrade(&state));
802    }
803
804    pub(crate) fn slots_storage_key(&self) -> Option<usize> {
805        let key = self.inner.slots_storage_key.get();
806        (key != 0).then_some(key)
807    }
808
809    pub(crate) fn slots_runtime_state(&self) -> Option<Rc<crate::composer::ComposerRuntimeState>> {
810        self.inner
811            .slots_runtime_state
812            .borrow()
813            .as_ref()
814            .and_then(std::rc::Weak::upgrade)
815    }
816
817    pub fn deactivate(&self) {
818        if !self.inner.active.replace(false) {
819            return;
820        }
821        self.inner
822            .deactivations
823            .set(self.inner.deactivations.get() + 1);
824        if self.inner.enqueued.replace(false) {
825            self.inner.runtime.mark_scope_recomposed(self.id());
826        }
827    }
828
829    pub(crate) fn defer_until_reactivated(&self) {
830        if self.inner.enqueued.replace(false) {
831            self.inner.runtime.mark_scope_recomposed(self.id());
832        }
833    }
834
835    pub fn reactivate(&self) {
836        self.inner.active.set(true);
837        if self.inner.invalid.get()
838            && self.is_effectively_active()
839            && !self.inner.enqueued.replace(true)
840        {
841            self.inner
842                .runtime
843                .register_invalid_scope(self.id(), self.downgrade());
844        }
845    }
846
847    pub fn force_reuse(&self) {
848        self.inner.force_reuse.set(true);
849        self.inner.force_recompose.set(false);
850        self.inner.pending_recompose.set(true);
851    }
852
853    pub(crate) fn request_pending_recompose(&self) {
854        self.inner.pending_recompose.set(true);
855    }
856
857    pub fn force_recompose(&self) {
858        self.inner.force_recompose.set(true);
859        self.inner.force_reuse.set(false);
860        self.inner.pending_recompose.set(false);
861    }
862
863    pub(crate) fn set_retention_mode(&self, mode: RetentionMode) {
864        self.inner.retention_mode.set(mode);
865    }
866
867    pub(crate) fn retention_mode(&self) -> RetentionMode {
868        self.inner.retention_mode.get()
869    }
870
871    pub fn should_recompose(&self) -> bool {
872        if self.inner.force_recompose.replace(false) {
873            self.inner.force_reuse.set(false);
874            return true;
875        }
876        if self.inner.force_reuse.replace(false) {
877            return false;
878        }
879        self.is_invalid()
880    }
881
882    pub fn has_composed_once(&self) -> bool {
883        self.inner.composed_once.get()
884    }
885
886    fn mark_composed_once(&self) {
887        self.inner.composed_once.set(true);
888    }
889
890    fn invalidated_only_by(&self, allowed_sources: &HashSet<StateId>) -> Option<bool> {
891        let sources = self.inner.invalidation_sources.borrow();
892        let sources = sources.as_ref()?;
893        if sources.is_empty() {
894            return None;
895        }
896        Some(
897            sources
898                .iter()
899                .all(|source| allowed_sources.contains(source)),
900        )
901    }
902
903    fn has_unknown_invalidation_source(&self) -> bool {
904        self.inner.invalidation_sources.borrow().is_none()
905    }
906}
907
908#[cfg(test)]
909impl RecomposeScope {
910    pub(crate) fn new_for_test(runtime: RuntimeHandle) -> Self {
911        Self::new(runtime)
912    }
913}
914
915#[derive(Debug, Clone, Copy, Default)]
916pub struct RecomposeOptions {
917    pub force_reuse: bool,
918    pub force_recompose: bool,
919    pub retention: RetentionMode,
920}
921
922#[derive(Debug, Clone, PartialEq, Eq)]
923pub enum NodeError {
924    Missing {
925        id: NodeId,
926    },
927    TypeMismatch {
928        id: NodeId,
929        expected: &'static str,
930    },
931    MissingContext {
932        id: NodeId,
933        reason: &'static str,
934    },
935    AlreadyExists {
936        id: NodeId,
937    },
938    MalformedCommandPayload {
939        tag: &'static str,
940    },
941    SlotHostUnavailable {
942        operation: &'static str,
943        reason: &'static str,
944    },
945    RecompositionLimitExceeded {
946        operation: &'static str,
947        limit: usize,
948    },
949}
950
951impl std::fmt::Display for NodeError {
952    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
953        match self {
954            NodeError::Missing { id } => write!(f, "node {id} missing"),
955            NodeError::TypeMismatch { id, expected } => {
956                write!(f, "node {id} type mismatch; expected {expected}")
957            }
958            NodeError::MissingContext { id, reason } => {
959                write!(f, "missing context for node {id}: {reason}")
960            }
961            NodeError::AlreadyExists { id } => {
962                write!(f, "node {id} already exists")
963            }
964            NodeError::MalformedCommandPayload { tag } => {
965                write!(f, "command queue missing or invalid {tag} payload")
966            }
967            NodeError::SlotHostUnavailable { operation, reason } => {
968                write!(f, "{operation} cannot access slot host: {reason}")
969            }
970            NodeError::RecompositionLimitExceeded { operation, limit } => {
971                write!(
972                    f,
973                    "{operation} exceeded {limit} iterations while reconciling composition"
974                )
975            }
976        }
977    }
978}
979
980impl std::error::Error for NodeError {}
981
982pub use subcompose::{
983    ContentTypeReusePolicy, DefaultSlotReusePolicy, SlotId, SlotReusePolicy, SubcomposeState,
984};
985
986#[derive(Copy, Clone, Debug, PartialEq, Eq)]
987pub enum Phase {
988    Compose,
989    Measure,
990    Layout,
991}
992
993pub use composer_context::{note_nested_slots_host, with_composer as with_current_composer};
994
995#[allow(non_snake_case)]
996pub fn withCurrentComposer<R>(f: impl FnOnce(&Composer) -> R) -> R {
997    composer_context::with_composer(f)
998}
999
1000fn with_current_composer_opt<R>(f: impl FnOnce(&Composer) -> R) -> Option<R> {
1001    composer_context::try_with_composer(f)
1002}
1003
1004#[doc(hidden)]
1005pub fn current_recompose_scope_invalidated_only_by(
1006    allowed_sources: impl IntoIterator<Item = StateId>,
1007) -> Option<bool> {
1008    with_current_composer_opt(|composer| {
1009        let allowed_sources = allowed_sources.into_iter().collect();
1010        let mut scope = composer.current_recompose_scope();
1011        let mut saw_unknown_source = false;
1012        while let Some(current) = scope {
1013            if current.has_unknown_invalidation_source() {
1014                saw_unknown_source = true;
1015                scope = current.parent_scope();
1016                continue;
1017            }
1018            if let Some(matches) = current.invalidated_only_by(&allowed_sources) {
1019                return Some(matches);
1020            }
1021            scope = current.parent_scope();
1022        }
1023        saw_unknown_source.then_some(false)
1024    })
1025    .flatten()
1026}
1027
1028#[track_caller]
1029pub fn with_key<K: Hash>(key: &K, content: impl FnOnce()) {
1030    let seed = explicit_group_key_seed(key, std::panic::Location::caller());
1031    with_current_composer(|composer| composer.with_group_seed(seed, |_| content()));
1032}
1033
1034#[derive(Default)]
1035struct DisposableEffectState {
1036    key: Option<effect_key::EffectKey>,
1037    cleanup: Option<Box<dyn FnOnce()>>,
1038}
1039
1040impl DisposableEffectState {
1041    fn should_run(&self, key: &effect_key::EffectKey) -> bool {
1042        match &self.key {
1043            Some(current) => key.differs_from(current),
1044            None => true,
1045        }
1046    }
1047
1048    fn set_key(&mut self, key: effect_key::EffectKey) {
1049        self.key = Some(key);
1050    }
1051
1052    fn set_cleanup(&mut self, cleanup: Option<Box<dyn FnOnce()>>) {
1053        self.cleanup = cleanup;
1054    }
1055
1056    fn run_cleanup(&mut self) {
1057        if let Some(cleanup) = self.cleanup.take() {
1058            cleanup();
1059        }
1060    }
1061}
1062
1063impl Drop for DisposableEffectState {
1064    fn drop(&mut self) {
1065        self.run_cleanup();
1066    }
1067}
1068
1069#[derive(Clone, Copy, Debug, Default)]
1070pub struct DisposableEffectScope;
1071
1072#[derive(Default)]
1073pub struct DisposableEffectResult {
1074    cleanup: Option<Box<dyn FnOnce()>>,
1075}
1076
1077impl DisposableEffectScope {
1078    pub fn on_dispose(&self, cleanup: impl FnOnce() + 'static) -> DisposableEffectResult {
1079        DisposableEffectResult::new(cleanup)
1080    }
1081}
1082
1083impl DisposableEffectResult {
1084    pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
1085        Self {
1086            cleanup: Some(Box::new(cleanup)),
1087        }
1088    }
1089
1090    fn into_cleanup(self) -> Option<Box<dyn FnOnce()>> {
1091        self.cleanup
1092    }
1093}
1094
1095#[allow(non_snake_case)]
1096pub fn SideEffect(effect: impl FnOnce() + 'static) {
1097    with_current_composer(|composer| composer.register_side_effect(effect));
1098}
1099
1100pub fn __disposable_effect_impl<K, F>(group_key: Key, keys: K, effect: F)
1101where
1102    K: PartialEq + 'static,
1103    F: FnOnce(DisposableEffectScope) -> DisposableEffectResult + 'static,
1104{
1105    with_current_composer(|composer| {
1106        composer.with_group(group_key, |composer| {
1107            let key = effect_key::EffectKey::new(keys);
1108            let state = composer.remember_effect::<DisposableEffectState>();
1109            if state.with(|state| state.should_run(&key)) {
1110                state.update(|state| {
1111                    state.run_cleanup();
1112                    state.set_key(key);
1113                });
1114                let state_for_effect = state.clone();
1115                let mut effect_opt = Some(effect);
1116                composer.register_side_effect(move || {
1117                    if let Some(effect) = effect_opt.take() {
1118                        let result = effect(DisposableEffectScope);
1119                        state_for_effect.update(|state| state.set_cleanup(result.into_cleanup()));
1120                    }
1121                });
1122            }
1123        });
1124    });
1125}
1126
1127#[macro_export]
1128macro_rules! DisposableEffect {
1129    ($keys:expr, $effect:expr) => {
1130        $crate::__disposable_effect_impl(
1131            $crate::location_key(file!(), line!(), column!()),
1132            $keys,
1133            $effect,
1134        )
1135    };
1136}
1137
1138#[macro_export]
1139macro_rules! clone_captures {
1140    ($($alias:ident $(= $value:expr)?),+ $(,)?; $body:expr) => {{
1141        $(let $alias = $crate::clone_captures!(@clone $alias $(= $value)?);)+
1142        $body
1143    }};
1144    (@clone $alias:ident = $value:expr) => {
1145        ($value).clone()
1146    };
1147    (@clone $alias:ident) => {
1148        $alias.clone()
1149    };
1150}
1151
1152pub fn with_node_mut<N: Node + 'static, R>(
1153    id: NodeId,
1154    f: impl FnOnce(&mut N) -> R,
1155) -> Result<R, NodeError> {
1156    with_current_composer(|composer| composer.with_node_mut(id, f))
1157}
1158
1159pub fn push_parent(id: NodeId) {
1160    with_current_composer(|composer| composer.push_parent(id));
1161}
1162
1163pub fn pop_parent() {
1164    with_current_composer(|composer| composer.pop_parent());
1165}
1166
1167pub trait Node: Any {
1168    fn mount(&mut self) {}
1169    fn update(&mut self) {}
1170    fn unmount(&mut self) {}
1171    /// Adds `child` to this node's child list, returning whether the list
1172    /// actually changed. A node that already holds the child returns `false`:
1173    /// callers record a structural change from this answer, and a structural
1174    /// change re-lowers the whole subtree, so claiming one for a no-op costs a
1175    /// full rebuild per call.
1176    fn insert_child(&mut self, _child: NodeId) -> bool {
1177        false
1178    }
1179    /// Removes `child` from this node's child list, returning whether the list
1180    /// actually changed. Only this node can answer that: a caller inspecting
1181    /// the child's parent pointer gets it wrong when the child was reparented
1182    /// while still listed here.
1183    fn remove_child(&mut self, _child: NodeId) -> bool {
1184        false
1185    }
1186    fn move_child(&mut self, _from: usize, _to: usize) {}
1187    fn update_children(&mut self, _children: &[NodeId]) {}
1188    fn children(&self) -> Vec<NodeId> {
1189        Vec::new()
1190    }
1191    /// Copies child IDs into the provided scratch buffer without allocating a
1192    /// fresh container for every traversal.
1193    fn collect_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
1194        out.clear();
1195        out.extend(self.children());
1196    }
1197    fn collect_owned_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
1198        self.collect_children_into(out);
1199    }
1200    /// Called after the node is created to record its own ID.
1201    /// Useful for nodes that need to store their ID for later operations.
1202    fn set_node_id(&mut self, _id: NodeId) {}
1203    /// Called when this node is attached to a parent.
1204    /// Nodes with parent tracking should set their parent reference here.
1205    fn on_attached_to_parent(&mut self, _parent: NodeId) {}
1206    /// Called when this node is removed from its parent.
1207    /// Nodes with parent tracking should clear their parent reference here.
1208    fn on_removed_from_parent(&mut self) {}
1209    /// Get this node's parent ID (for nodes that track parents).
1210    /// Returns None if node has no parent or doesn't track parents.
1211    fn parent(&self) -> Option<NodeId> {
1212        None
1213    }
1214    /// Mark this node as needing layout (for nodes with dirty flags).
1215    /// Called during bubbling to propagate dirtiness up the tree.
1216    fn mark_needs_layout(&self) {}
1217    /// Check if this node needs layout (for nodes with dirty flags).
1218    fn needs_layout(&self) -> bool {
1219        false
1220    }
1221    /// Mark this node as needing measure (size may have changed).
1222    /// Called during bubbling when children are added/removed.
1223    fn mark_needs_measure(&self) {}
1224    /// Check if this node needs measure (for nodes with dirty flags).
1225    fn needs_measure(&self) -> bool {
1226        false
1227    }
1228    /// Mark this node as needing semantics recomputation.
1229    fn mark_needs_semantics(&self) {}
1230    /// Check if this node needs semantics recomputation.
1231    fn needs_semantics(&self) -> bool {
1232        false
1233    }
1234    /// Set parent reference for dirty flag bubbling ONLY.
1235    /// This is a minimal version of on_attached_to_parent that doesn't trigger
1236    /// registry updates or other side effects. Used during measurement when we
1237    /// need to establish parent connections for bubble_measure_dirty without
1238    /// causing the full attachment lifecycle.
1239    ///
1240    /// Default implementation uses the normal parent-attachment hook.
1241    fn set_parent_for_bubbling(&mut self, parent: NodeId) {
1242        self.on_attached_to_parent(parent);
1243    }
1244
1245    /// Returns a recycle pool key when this node supports shell reuse.
1246    fn recycle_key(&self) -> Option<TypeId> {
1247        None
1248    }
1249
1250    /// Bounds how many recyclable shells of this node type should be retained.
1251    fn recycle_pool_limit(&self) -> Option<usize> {
1252        None
1253    }
1254
1255    /// Clears live attachments before the node shell enters a recycle pool.
1256    fn prepare_for_recycle(&mut self) {}
1257
1258    /// Optionally provides a compact replacement box for this recycled shell.
1259    ///
1260    /// Returning `Some` lets nodes move pooled survivors onto fresh compact
1261    /// storage so the recycle pool does not pin large spike-era allocations.
1262    fn rehouse_for_recycle(&self) -> Option<Box<dyn Node>> {
1263        None
1264    }
1265
1266    /// Optionally moves a live node onto a fresh box during applier compaction.
1267    ///
1268    /// This is used after large-majority teardowns where a small surviving live
1269    /// tree can otherwise pin allocator pages from a much larger spike-era node
1270    /// population. Implementations must preserve the node's live state.
1271    fn rehouse_for_live_compaction(&mut self) -> Option<Box<dyn Node>> {
1272        None
1273    }
1274
1275    /// Returns the node-owned heap retained beyond the node's own box allocation.
1276    fn debug_heap_bytes(&self) -> usize {
1277        0
1278    }
1279}
1280
1281/// Unified API for bubbling layout dirty flags from a node to the root (Applier context).
1282///
1283/// This is the canonical function for dirty bubbling during the apply phase (structural changes).
1284/// Call this after mutations like insert/remove/move that happen during apply.
1285///
1286/// # Behavior
1287/// 1. Marks the starting node as needing layout
1288/// 2. Walks up the parent chain, marking each ancestor
1289/// 3. Stops when it reaches a node that's already dirty (O(1) optimization)
1290/// 4. Stops at the root (node with no parent)
1291///
1292/// # Performance
1293/// This function is O(height) in the worst case, but typically O(1) due to early exit
1294/// when encountering an already-dirty ancestor.
1295///
1296/// # Usage
1297/// - Call from composer mutations (insert/remove/move) during apply phase
1298/// - Call from applier-level operations that modify the tree structure
1299pub fn bubble_layout_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1300    bubble_layout_dirty_applier(applier, node_id);
1301}
1302
1303/// Unified API for bubbling measure dirty flags from a node to the root (Applier context).
1304///
1305/// Call this when a node's size may have changed (children added/removed, modifier changed).
1306/// This ensures that measure_layout will increment the cache epoch and re-measure the subtree.
1307///
1308/// # Behavior
1309/// 1. Marks the starting node as needing measure
1310/// 2. Walks up the parent chain, marking each ancestor
1311/// 3. Stops when it reaches a node that's already dirty (O(1) optimization)
1312/// 4. Stops at the root (node with no parent)
1313pub fn bubble_measure_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1314    bubble_measure_dirty_applier(applier, node_id);
1315}
1316
1317/// Unified API for bubbling semantics dirty flags from a node to the root (Applier context).
1318///
1319/// This mirrors [`bubble_layout_dirty`] but toggles semantics-specific dirty
1320/// flags instead of layout ones, allowing semantics updates to propagate during
1321/// the apply phase without forcing layout work.
1322pub fn bubble_semantics_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1323    bubble_semantics_dirty_applier(applier, node_id);
1324}
1325
1326/// Schedules semantics bubbling for a node using the active composer if present.
1327///
1328/// This defers the work to the apply phase where we can safely mutate the
1329/// applier tree without re-entrantly borrowing the composer during composition.
1330pub fn queue_semantics_invalidation(node_id: NodeId) {
1331    let _ = composer_context::try_with_composer(|composer| {
1332        composer.enqueue_semantics_invalidation(node_id);
1333    });
1334}
1335
1336/// Unified API for bubbling layout dirty flags from a node to the root (Composer context).
1337///
1338/// This is the canonical function for dirty bubbling during composition (property changes).
1339/// Call this after property changes that happen during composition via with_node_mut.
1340///
1341/// # Behavior
1342/// 1. Marks the starting node as needing layout
1343/// 2. Walks up the parent chain, marking each ancestor
1344/// 3. Stops when it reaches a node that's already dirty (O(1) optimization)
1345/// 4. Stops at the root (node with no parent)
1346///
1347/// # Performance
1348/// This function is O(height) in the worst case, but typically O(1) due to early exit
1349/// when encountering an already-dirty ancestor.
1350///
1351/// # Type Requirements
1352/// The node type N must implement Node (which includes mark_needs_layout, parent, etc.).
1353/// Typically this will be LayoutNode or similar layout-aware node types.
1354///
1355/// # Usage
1356/// - Call from property setters during composition (e.g., set_modifier, set_measure_policy)
1357/// - Call from widget composition when layout-affecting state changes
1358pub fn bubble_layout_dirty_in_composer<N: Node + 'static>(node_id: NodeId) {
1359    bubble_layout_dirty_composer::<N>(node_id);
1360}
1361
1362/// Unified API for bubbling measure dirty flags from a node to the root during composition.
1363///
1364/// This queues a dirty-bubble command on the active composer so measure invalidation
1365/// runs during the apply phase, avoiding re-entrant applier borrows while widgets are
1366/// mutating nodes via `with_node_mut`.
1367pub fn bubble_measure_dirty_in_composer(node_id: NodeId) {
1368    with_current_composer(|composer| {
1369        composer.commands_mut().push(Command::BubbleDirty {
1370            node_id,
1371            bubble: DirtyBubble {
1372                layout: false,
1373                measure: true,
1374                semantics: false,
1375            },
1376        });
1377    });
1378}
1379
1380/// Unified API for bubbling semantics dirty flags from a node to the root (Composer context).
1381///
1382/// This mirrors [`bubble_layout_dirty_in_composer`] but routes through the semantics
1383/// dirty flag instead of the layout one. Modifier nodes can request semantics
1384/// invalidations without triggering measure/layout work, and the runtime can
1385/// query the root to determine whether the semantics tree needs rebuilding.
1386pub fn bubble_semantics_dirty_in_composer<N: Node + 'static>(node_id: NodeId) {
1387    bubble_semantics_dirty_composer::<N>(node_id);
1388}
1389
1390fn bubble_layout_dirty_applier(applier: &mut dyn Applier, mut node_id: NodeId) {
1391    if let Ok(node) = applier.get_mut(node_id) {
1392        node.mark_needs_layout();
1393    }
1394
1395    loop {
1396        let parent_id = match applier.get_mut(node_id) {
1397            Ok(node) => node.parent(),
1398            Err(_) => None,
1399        };
1400
1401        match parent_id {
1402            Some(pid) => {
1403                if let Ok(parent) = applier.get_mut(pid) {
1404                    let parent_already_dirty = parent.needs_layout();
1405                    if !parent_already_dirty {
1406                        parent.mark_needs_layout();
1407                    }
1408                    node_id = pid;
1409                } else {
1410                    break;
1411                }
1412            }
1413            None => break,
1414        }
1415    }
1416}
1417
1418fn bubble_measure_dirty_applier(applier: &mut dyn Applier, mut node_id: NodeId) {
1419    if let Ok(node) = applier.get_mut(node_id) {
1420        node.mark_needs_measure();
1421    }
1422
1423    loop {
1424        let parent_id = match applier.get_mut(node_id) {
1425            Ok(node) => node.parent(),
1426            Err(_) => None,
1427        };
1428
1429        match parent_id {
1430            Some(pid) => {
1431                if let Ok(parent) = applier.get_mut(pid) {
1432                    if !parent.needs_measure() {
1433                        parent.mark_needs_measure();
1434                    }
1435                    node_id = pid;
1436                } else {
1437                    break;
1438                }
1439            }
1440            None => {
1441                break;
1442            }
1443        }
1444    }
1445}
1446
1447fn bubble_semantics_dirty_applier(applier: &mut dyn Applier, mut node_id: NodeId) {
1448    if let Ok(node) = applier.get_mut(node_id) {
1449        node.mark_needs_semantics();
1450    }
1451
1452    loop {
1453        let parent_id = match applier.get_mut(node_id) {
1454            Ok(node) => node.parent(),
1455            Err(_) => None,
1456        };
1457
1458        match parent_id {
1459            Some(pid) => {
1460                if let Ok(parent) = applier.get_mut(pid) {
1461                    if !parent.needs_semantics() {
1462                        parent.mark_needs_semantics();
1463                    }
1464                    node_id = pid;
1465                } else {
1466                    break;
1467                }
1468            }
1469            None => break,
1470        }
1471    }
1472}
1473
1474fn bubble_layout_dirty_composer<N: Node + 'static>(mut node_id: NodeId) {
1475    let _ = with_node_mut(node_id, |node: &mut N| {
1476        node.mark_needs_layout();
1477    });
1478
1479    while let Ok(Some(pid)) = with_node_mut(node_id, |node: &mut N| node.parent()) {
1480        let parent_id = pid;
1481
1482        let advanced = with_node_mut(parent_id, |node: &mut N| {
1483            if !node.needs_layout() {
1484                node.mark_needs_layout();
1485            }
1486            true
1487        })
1488        .unwrap_or(false);
1489
1490        if advanced {
1491            node_id = parent_id;
1492        } else {
1493            break;
1494        }
1495    }
1496}
1497
1498fn bubble_semantics_dirty_composer<N: Node + 'static>(mut node_id: NodeId) {
1499    let _ = with_node_mut(node_id, |node: &mut N| {
1500        node.mark_needs_semantics();
1501    });
1502
1503    while let Ok(Some(pid)) = with_node_mut(node_id, |node: &mut N| node.parent()) {
1504        let parent_id = pid;
1505
1506        let advanced = with_node_mut(parent_id, |node: &mut N| {
1507            if !node.needs_semantics() {
1508                node.mark_needs_semantics();
1509            }
1510            true
1511        })
1512        .unwrap_or(false);
1513
1514        if advanced {
1515            node_id = parent_id;
1516        } else {
1517            break;
1518        }
1519    }
1520}
1521
1522impl dyn Node {
1523    pub fn as_any_mut(&mut self) -> &mut dyn Any {
1524        self
1525    }
1526}
1527
1528pub struct RecycledNode {
1529    stable_id: NodeId,
1530    node: Box<dyn Node>,
1531    warm_origin: bool,
1532}
1533
1534impl RecycledNode {
1535    fn new(stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) -> Self {
1536        let node = node.rehouse_for_recycle().unwrap_or(node);
1537        Self {
1538            stable_id,
1539            node,
1540            warm_origin,
1541        }
1542    }
1543
1544    fn from_shell(stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) -> Self {
1545        Self {
1546            stable_id,
1547            node,
1548            warm_origin,
1549        }
1550    }
1551
1552    pub fn stable_id(&self) -> NodeId {
1553        self.stable_id
1554    }
1555
1556    fn warm_origin(&self) -> bool {
1557        self.warm_origin
1558    }
1559
1560    fn set_warm_origin(&mut self, warm_origin: bool) {
1561        self.warm_origin = warm_origin;
1562    }
1563
1564    pub fn node_mut(&mut self) -> &mut dyn Node {
1565        self.node.as_mut()
1566    }
1567
1568    pub fn into_parts(self) -> (NodeId, Box<dyn Node>, bool) {
1569        (self.stable_id, self.node, self.warm_origin)
1570    }
1571}
1572
1573#[derive(Debug, Clone, PartialEq, Eq)]
1574pub struct RecycledNodeInsertion {
1575    pub id: NodeId,
1576    pub stable_id_reused: bool,
1577    pub fallback_error: Option<NodeError>,
1578}
1579
1580impl RecycledNodeInsertion {
1581    fn reused(stable_id: NodeId) -> Self {
1582        Self {
1583            id: stable_id,
1584            stable_id_reused: true,
1585            fallback_error: None,
1586        }
1587    }
1588
1589    fn fresh(id: NodeId, fallback_error: Option<NodeError>) -> Self {
1590        Self {
1591            id,
1592            stable_id_reused: false,
1593            fallback_error,
1594        }
1595    }
1596}
1597
1598pub trait Applier: Any {
1599    fn create(&mut self, node: Box<dyn Node>) -> NodeId;
1600    fn get_mut(&mut self, id: NodeId) -> Result<&mut dyn Node, NodeError>;
1601    fn remove(&mut self, id: NodeId) -> Result<(), NodeError>;
1602
1603    /// Records that `parent_id`'s child list changed structurally this frame
1604    /// (insert, remove, move, or reparent). Incremental scene consumers drain
1605    /// the recorded parents and re-patch those subtrees so removed nodes are
1606    /// evicted from a persistent render graph even when the frame's scene
1607    /// update is otherwise scoped to unrelated dirty nodes.
1608    fn record_structural_change(&mut self, _parent_id: NodeId) {}
1609
1610    /// Returns the current generation for a node index.
1611    /// Generation is incremented when an index is reused from the freelist,
1612    /// preventing stale slot entries from matching recycled nodes.
1613    fn node_generation(&self, id: NodeId) -> u32;
1614
1615    /// Inserts a node with a pre-assigned ID.
1616    ///
1617    /// This is used for virtual nodes whose IDs are allocated separately
1618    /// (e.g., via allocate_virtual_node_id()). Unlike `create()` which assigns
1619    /// a new ID, this method uses the provided ID.
1620    ///
1621    /// Returns Ok(()) if successful, or an error if the ID is already in use.
1622    fn insert_with_id(&mut self, id: NodeId, node: Box<dyn Node>) -> Result<(), NodeError>;
1623
1624    /// Reinserts a recycled node at its retained stable ID, or creates a fresh ID if that
1625    /// retained ID is no longer available.
1626    fn insert_recycled_node_or_create(
1627        &mut self,
1628        stable_id: NodeId,
1629        node: Box<dyn Node>,
1630    ) -> RecycledNodeInsertion {
1631        let id = self.create(node);
1632        RecycledNodeInsertion::fresh(id, Some(NodeError::AlreadyExists { id: stable_id }))
1633    }
1634
1635    fn as_any(&self) -> &dyn Any
1636    where
1637        Self: Sized,
1638    {
1639        self
1640    }
1641
1642    fn as_any_mut(&mut self) -> &mut dyn Any
1643    where
1644        Self: Sized,
1645    {
1646        self
1647    }
1648
1649    /// Trim trailing tombstones/unused capacity after structural changes.
1650    fn compact(&mut self) {}
1651
1652    /// Returns a previously recycled node shell and its stable ID for the requested concrete type.
1653    fn take_recycled_node(&mut self, _key: TypeId) -> Option<RecycledNode> {
1654        None
1655    }
1656
1657    /// Marks whether a reinserted recycled node originated from the warm recycle path.
1658    fn set_recycled_node_origin(&mut self, _id: NodeId, _warm_origin: bool) {}
1659
1660    /// Seeds a warm recyclable shell for future reuse without requiring a prior removal.
1661    fn seed_recycled_node_shell(
1662        &mut self,
1663        _key: TypeId,
1664        _recycle_pool_limit: Option<usize>,
1665        _shell: Box<dyn Node>,
1666    ) {
1667    }
1668
1669    /// Records that the current apply pass had to allocate a fresh recyclable shell for this type.
1670    fn record_fresh_recyclable_creation(&mut self, _key: TypeId) {}
1671
1672    /// Drops any recyclable shells that should not survive beyond the current apply pass.
1673    fn clear_recycled_nodes(&mut self) {}
1674}
1675
1676type TypedNodeUpdate = fn(&mut dyn Node, NodeId) -> Result<(), NodeError>;
1677type CommandCallback = Box<dyn FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static>;
1678
1679#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1680pub(crate) struct DirtyBubble {
1681    layout: bool,
1682    measure: bool,
1683    semantics: bool,
1684}
1685
1686impl DirtyBubble {
1687    pub(crate) const LAYOUT_AND_MEASURE: Self = Self {
1688        layout: true,
1689        measure: true,
1690        semantics: false,
1691    };
1692
1693    pub(crate) const SEMANTICS: Self = Self {
1694        layout: false,
1695        measure: false,
1696        semantics: true,
1697    };
1698
1699    fn apply(self, applier: &mut dyn Applier, node_id: NodeId) {
1700        if self.layout {
1701            bubble_layout_dirty(applier, node_id);
1702        }
1703        if self.measure {
1704            bubble_measure_dirty(applier, node_id);
1705        }
1706        if self.semantics {
1707            bubble_semantics_dirty(applier, node_id);
1708        }
1709    }
1710}
1711
1712pub(crate) enum Command {
1713    BubbleDirty {
1714        node_id: NodeId,
1715        bubble: DirtyBubble,
1716    },
1717    UpdateTypedNode {
1718        id: NodeId,
1719        updater: TypedNodeUpdate,
1720    },
1721    RemoveNode {
1722        id: NodeId,
1723    },
1724    MountNode {
1725        id: NodeId,
1726    },
1727    AttachChild {
1728        parent_id: NodeId,
1729        child_id: NodeId,
1730        bubble: DirtyBubble,
1731    },
1732    InsertChild {
1733        parent_id: NodeId,
1734        child_id: NodeId,
1735        appended_index: usize,
1736        insert_index: usize,
1737        bubble: DirtyBubble,
1738    },
1739    MoveChild {
1740        parent_id: NodeId,
1741        from_index: usize,
1742        to_index: usize,
1743        bubble: DirtyBubble,
1744    },
1745    RemoveChild {
1746        parent_id: NodeId,
1747        child_id: NodeId,
1748    },
1749    DetachChild {
1750        parent_id: NodeId,
1751        child_id: NodeId,
1752    },
1753    SyncChildren {
1754        parent_id: NodeId,
1755        expected_children: ChildList,
1756    },
1757    Callback(CommandCallback),
1758}
1759
1760#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1761struct DeferredChildCleanup {
1762    child_id: NodeId,
1763    generation: u32,
1764    removed_from_parent: bool,
1765}
1766
1767#[derive(Default)]
1768struct DeferredChildCleanupQueue {
1769    pending: Vec<DeferredChildCleanup>,
1770    preserved: Vec<(NodeId, u32)>,
1771}
1772
1773impl DeferredChildCleanupQueue {
1774    fn push(&mut self, child_id: NodeId, generation: u32, removed_from_parent: bool) {
1775        if self
1776            .preserved
1777            .iter()
1778            .any(|&(preserved_id, preserved_generation)| {
1779                preserved_id == child_id && preserved_generation == generation
1780            })
1781        {
1782            return;
1783        }
1784        self.pending.push(DeferredChildCleanup {
1785            child_id,
1786            generation,
1787            removed_from_parent,
1788        });
1789    }
1790
1791    fn preserve(&mut self, child_id: NodeId, generation: u32) {
1792        if !self
1793            .preserved
1794            .iter()
1795            .any(|&(preserved_id, preserved_generation)| {
1796                preserved_id == child_id && preserved_generation == generation
1797            })
1798        {
1799            self.preserved.push((child_id, generation));
1800        }
1801        self.pending
1802            .retain(|cleanup| cleanup.child_id != child_id || cleanup.generation != generation);
1803    }
1804
1805    fn flush(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
1806        for cleanup in self.pending {
1807            cleanup_detached_child(applier, cleanup)?;
1808        }
1809        Ok(())
1810    }
1811}
1812
1813impl Command {
1814    pub(crate) fn update_node<N: Node + 'static>(id: NodeId) -> Self {
1815        Self::UpdateTypedNode {
1816            id,
1817            updater: update_typed_node::<N>,
1818        }
1819    }
1820
1821    pub(crate) fn callback(
1822        callback: impl FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static,
1823    ) -> Self {
1824        Self::Callback(Box::new(callback))
1825    }
1826
1827    pub(crate) fn apply(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
1828        let mut deferred_cleanup = DeferredChildCleanupQueue::default();
1829        self.apply_with_cleanup(applier, &mut deferred_cleanup)?;
1830        deferred_cleanup.flush(applier)
1831    }
1832
1833    fn apply_with_cleanup(
1834        self,
1835        applier: &mut dyn Applier,
1836        deferred_cleanup: &mut DeferredChildCleanupQueue,
1837    ) -> Result<(), NodeError> {
1838        match self {
1839            Self::BubbleDirty { node_id, bubble } => {
1840                bubble.apply(applier, node_id);
1841                Ok(())
1842            }
1843            Self::UpdateTypedNode { id, updater } => {
1844                let node = match applier.get_mut(id) {
1845                    Ok(node) => node,
1846                    Err(NodeError::Missing { .. }) => return Ok(()),
1847                    Err(err) => return Err(err),
1848                };
1849                updater(node, id)
1850            }
1851            Self::RemoveNode { id } => {
1852                if let Ok(node) = applier.get_mut(id) {
1853                    node.unmount();
1854                }
1855                match applier.remove(id) {
1856                    Ok(()) | Err(NodeError::Missing { .. }) => Ok(()),
1857                    Err(err) => Err(err),
1858                }
1859            }
1860            Self::MountNode { id } => {
1861                let node = match applier.get_mut(id) {
1862                    Ok(node) => node,
1863                    Err(NodeError::Missing { .. }) => return Ok(()),
1864                    Err(err) => return Err(err),
1865                };
1866                node.set_node_id(id);
1867                node.mount();
1868                Ok(())
1869            }
1870            Self::AttachChild {
1871                parent_id,
1872                child_id,
1873                bubble,
1874            } => {
1875                if insert_child_with_reparenting(applier, parent_id, child_id) {
1876                    bubble.apply(applier, parent_id);
1877                } else if let Ok(child) = applier.get_mut(child_id) {
1878                    let dirty_bubble = DirtyBubble {
1879                        layout: child.needs_layout(),
1880                        measure: child.needs_measure(),
1881                        semantics: false,
1882                    };
1883                    dirty_bubble.apply(applier, parent_id);
1884                }
1885                Ok(())
1886            }
1887            Self::InsertChild {
1888                parent_id,
1889                child_id,
1890                appended_index,
1891                insert_index,
1892                bubble,
1893            } => {
1894                insert_child_with_reparenting(applier, parent_id, child_id);
1895                bubble.apply(applier, parent_id);
1896                if insert_index != appended_index
1897                    && let Ok(parent_node) = applier.get_mut(parent_id)
1898                {
1899                    parent_node.move_child(appended_index, insert_index);
1900                }
1901                Ok(())
1902            }
1903            Self::MoveChild {
1904                parent_id,
1905                from_index,
1906                to_index,
1907                bubble,
1908            } => {
1909                if let Ok(parent_node) = applier.get_mut(parent_id) {
1910                    parent_node.move_child(from_index, to_index);
1911                }
1912                bubble.apply(applier, parent_id);
1913                note_structural_move(parent_id, from_index, to_index);
1914                applier.record_structural_change(parent_id);
1915                Ok(())
1916            }
1917            Self::RemoveChild {
1918                parent_id,
1919                child_id,
1920            } => apply_remove_child(applier, parent_id, child_id, deferred_cleanup),
1921            Self::DetachChild {
1922                parent_id,
1923                child_id,
1924            } => {
1925                let generation = applier.node_generation(child_id);
1926                detach_child_from_parent(applier, parent_id, child_id)?;
1927                deferred_cleanup.preserve(child_id, generation);
1928                Ok(())
1929            }
1930            Self::SyncChildren {
1931                parent_id,
1932                expected_children,
1933            } => sync_children(applier, parent_id, &expected_children, deferred_cleanup),
1934            Self::Callback(callback) => callback(applier),
1935        }
1936    }
1937}
1938
1939const COMMAND_CHUNK_CAPACITY: usize = 1024;
1940const COMMAND_FLUSH_THRESHOLD: usize = COMMAND_CHUNK_CAPACITY * 4;
1941type ChildList = SmallVec<[NodeId; 4]>;
1942const SMALL_CHILD_SYNC_LINEAR_THRESHOLD: usize = 8;
1943
1944#[derive(Copy, Clone)]
1945enum CommandTag {
1946    BubbleDirty,
1947    UpdateTypedNode,
1948    RemoveNode,
1949    MountNode,
1950    AttachChild,
1951    InsertChild,
1952    MoveChild,
1953    RemoveChild,
1954    DetachChild,
1955    SyncChildren,
1956    Callback,
1957}
1958
1959impl CommandTag {
1960    fn label(self) -> &'static str {
1961        match self {
1962            Self::BubbleDirty => "BubbleDirty",
1963            Self::UpdateTypedNode => "UpdateTypedNode",
1964            Self::RemoveNode => "RemoveNode",
1965            Self::MountNode => "MountNode",
1966            Self::AttachChild => "AttachChild",
1967            Self::InsertChild => "InsertChild",
1968            Self::MoveChild => "MoveChild",
1969            Self::RemoveChild => "RemoveChild",
1970            Self::DetachChild => "DetachChild",
1971            Self::SyncChildren => "SyncChildren",
1972            Self::Callback => "Callback",
1973        }
1974    }
1975}
1976
1977#[derive(Copy, Clone)]
1978struct BubbleDirtyCommand {
1979    node_id: NodeId,
1980    bubble: DirtyBubble,
1981}
1982
1983#[derive(Copy, Clone)]
1984struct UpdateTypedNodeCommand {
1985    id: NodeId,
1986    updater: TypedNodeUpdate,
1987}
1988
1989#[derive(Copy, Clone)]
1990struct AttachChildCommand {
1991    parent_id: NodeId,
1992    child_id: NodeId,
1993    bubble: DirtyBubble,
1994}
1995
1996#[derive(Copy, Clone)]
1997struct InsertChildCommand {
1998    parent_id: NodeId,
1999    child_id: NodeId,
2000    appended_index: usize,
2001    insert_index: usize,
2002    bubble: DirtyBubble,
2003}
2004
2005#[derive(Copy, Clone)]
2006struct MoveChildCommand {
2007    parent_id: NodeId,
2008    from_index: usize,
2009    to_index: usize,
2010    bubble: DirtyBubble,
2011}
2012
2013#[derive(Copy, Clone)]
2014struct RemoveChildCommand {
2015    parent_id: NodeId,
2016    child_id: NodeId,
2017}
2018
2019#[derive(Copy, Clone)]
2020struct DetachChildCommand {
2021    parent_id: NodeId,
2022    child_id: NodeId,
2023}
2024
2025struct SyncChildrenCommand {
2026    parent_id: NodeId,
2027    child_start: usize,
2028    child_len: usize,
2029}
2030
2031#[derive(Default)]
2032struct CommandQueue {
2033    chunks: Vec<Vec<CommandTag>>,
2034    len: usize,
2035    bubble_dirty: Vec<BubbleDirtyCommand>,
2036    update_typed_nodes: Vec<UpdateTypedNodeCommand>,
2037    remove_nodes: Vec<NodeId>,
2038    mount_nodes: Vec<NodeId>,
2039    attach_children: Vec<AttachChildCommand>,
2040    insert_children: Vec<InsertChildCommand>,
2041    move_children: Vec<MoveChildCommand>,
2042    remove_children: Vec<RemoveChildCommand>,
2043    detach_children: Vec<DetachChildCommand>,
2044    sync_children: Vec<SyncChildrenCommand>,
2045    sync_child_ids: Vec<NodeId>,
2046    callbacks: Vec<CommandCallback>,
2047}
2048
2049impl CommandQueue {
2050    fn push_tag(&mut self, tag: CommandTag) {
2051        let needs_chunk = self
2052            .chunks
2053            .last()
2054            .map(|chunk| chunk.len() == chunk.capacity())
2055            .unwrap_or(true);
2056        if needs_chunk {
2057            self.chunks.push(Vec::with_capacity(COMMAND_CHUNK_CAPACITY));
2058        }
2059        if let Some(chunk) = self.chunks.last_mut() {
2060            chunk.push(tag);
2061            self.len += 1;
2062        }
2063    }
2064
2065    fn push(&mut self, command: Command) {
2066        match command {
2067            Command::BubbleDirty { node_id, bubble } => {
2068                self.bubble_dirty
2069                    .push(BubbleDirtyCommand { node_id, bubble });
2070                self.push_tag(CommandTag::BubbleDirty);
2071            }
2072            Command::UpdateTypedNode { id, updater } => {
2073                self.update_typed_nodes
2074                    .push(UpdateTypedNodeCommand { id, updater });
2075                self.push_tag(CommandTag::UpdateTypedNode);
2076            }
2077            Command::RemoveNode { id } => {
2078                self.remove_nodes.push(id);
2079                self.push_tag(CommandTag::RemoveNode);
2080            }
2081            Command::MountNode { id } => {
2082                self.mount_nodes.push(id);
2083                self.push_tag(CommandTag::MountNode);
2084            }
2085            Command::AttachChild {
2086                parent_id,
2087                child_id,
2088                bubble,
2089            } => {
2090                self.attach_children.push(AttachChildCommand {
2091                    parent_id,
2092                    child_id,
2093                    bubble,
2094                });
2095                self.push_tag(CommandTag::AttachChild);
2096            }
2097            Command::InsertChild {
2098                parent_id,
2099                child_id,
2100                appended_index,
2101                insert_index,
2102                bubble,
2103            } => {
2104                self.insert_children.push(InsertChildCommand {
2105                    parent_id,
2106                    child_id,
2107                    appended_index,
2108                    insert_index,
2109                    bubble,
2110                });
2111                self.push_tag(CommandTag::InsertChild);
2112            }
2113            Command::MoveChild {
2114                parent_id,
2115                from_index,
2116                to_index,
2117                bubble,
2118            } => {
2119                self.move_children.push(MoveChildCommand {
2120                    parent_id,
2121                    from_index,
2122                    to_index,
2123                    bubble,
2124                });
2125                self.push_tag(CommandTag::MoveChild);
2126            }
2127            Command::RemoveChild {
2128                parent_id,
2129                child_id,
2130            } => {
2131                self.remove_children.push(RemoveChildCommand {
2132                    parent_id,
2133                    child_id,
2134                });
2135                self.push_tag(CommandTag::RemoveChild);
2136            }
2137            Command::DetachChild {
2138                parent_id,
2139                child_id,
2140            } => {
2141                self.detach_children.push(DetachChildCommand {
2142                    parent_id,
2143                    child_id,
2144                });
2145                self.push_tag(CommandTag::DetachChild);
2146            }
2147            Command::SyncChildren {
2148                parent_id,
2149                expected_children,
2150            } => {
2151                let child_start = self.sync_child_ids.len();
2152                let child_len = expected_children.len();
2153                self.sync_child_ids.extend(expected_children);
2154                self.sync_children.push(SyncChildrenCommand {
2155                    parent_id,
2156                    child_start,
2157                    child_len,
2158                });
2159                self.push_tag(CommandTag::SyncChildren);
2160            }
2161            Command::Callback(callback) => {
2162                self.callbacks.push(callback);
2163                self.push_tag(CommandTag::Callback);
2164            }
2165        }
2166    }
2167
2168    fn len(&self) -> usize {
2169        self.len
2170    }
2171
2172    fn capacity(&self) -> usize {
2173        self.chunks.iter().map(Vec::capacity).sum()
2174    }
2175
2176    fn payload_len_bytes(&self) -> usize {
2177        self.bubble_dirty
2178            .len()
2179            .saturating_mul(std::mem::size_of::<BubbleDirtyCommand>())
2180            .saturating_add(
2181                self.update_typed_nodes
2182                    .len()
2183                    .saturating_mul(std::mem::size_of::<UpdateTypedNodeCommand>()),
2184            )
2185            .saturating_add(
2186                self.remove_nodes
2187                    .len()
2188                    .saturating_mul(std::mem::size_of::<NodeId>()),
2189            )
2190            .saturating_add(
2191                self.mount_nodes
2192                    .len()
2193                    .saturating_mul(std::mem::size_of::<NodeId>()),
2194            )
2195            .saturating_add(
2196                self.attach_children
2197                    .len()
2198                    .saturating_mul(std::mem::size_of::<AttachChildCommand>()),
2199            )
2200            .saturating_add(
2201                self.insert_children
2202                    .len()
2203                    .saturating_mul(std::mem::size_of::<InsertChildCommand>()),
2204            )
2205            .saturating_add(
2206                self.move_children
2207                    .len()
2208                    .saturating_mul(std::mem::size_of::<MoveChildCommand>()),
2209            )
2210            .saturating_add(
2211                self.remove_children
2212                    .len()
2213                    .saturating_mul(std::mem::size_of::<RemoveChildCommand>()),
2214            )
2215            .saturating_add(
2216                self.detach_children
2217                    .len()
2218                    .saturating_mul(std::mem::size_of::<DetachChildCommand>()),
2219            )
2220            .saturating_add(
2221                self.sync_children
2222                    .len()
2223                    .saturating_mul(std::mem::size_of::<SyncChildrenCommand>()),
2224            )
2225            .saturating_add(
2226                self.sync_child_ids
2227                    .len()
2228                    .saturating_mul(std::mem::size_of::<NodeId>()),
2229            )
2230            .saturating_add(
2231                self.callbacks
2232                    .len()
2233                    .saturating_mul(std::mem::size_of::<CommandCallback>()),
2234            )
2235    }
2236
2237    fn payload_capacity_bytes(&self) -> usize {
2238        self.bubble_dirty
2239            .capacity()
2240            .saturating_mul(std::mem::size_of::<BubbleDirtyCommand>())
2241            .saturating_add(
2242                self.update_typed_nodes
2243                    .capacity()
2244                    .saturating_mul(std::mem::size_of::<UpdateTypedNodeCommand>()),
2245            )
2246            .saturating_add(
2247                self.remove_nodes
2248                    .capacity()
2249                    .saturating_mul(std::mem::size_of::<NodeId>()),
2250            )
2251            .saturating_add(
2252                self.mount_nodes
2253                    .capacity()
2254                    .saturating_mul(std::mem::size_of::<NodeId>()),
2255            )
2256            .saturating_add(
2257                self.attach_children
2258                    .capacity()
2259                    .saturating_mul(std::mem::size_of::<AttachChildCommand>()),
2260            )
2261            .saturating_add(
2262                self.insert_children
2263                    .capacity()
2264                    .saturating_mul(std::mem::size_of::<InsertChildCommand>()),
2265            )
2266            .saturating_add(
2267                self.move_children
2268                    .capacity()
2269                    .saturating_mul(std::mem::size_of::<MoveChildCommand>()),
2270            )
2271            .saturating_add(
2272                self.remove_children
2273                    .capacity()
2274                    .saturating_mul(std::mem::size_of::<RemoveChildCommand>()),
2275            )
2276            .saturating_add(
2277                self.detach_children
2278                    .capacity()
2279                    .saturating_mul(std::mem::size_of::<DetachChildCommand>()),
2280            )
2281            .saturating_add(
2282                self.sync_children
2283                    .capacity()
2284                    .saturating_mul(std::mem::size_of::<SyncChildrenCommand>()),
2285            )
2286            .saturating_add(
2287                self.sync_child_ids
2288                    .capacity()
2289                    .saturating_mul(std::mem::size_of::<NodeId>()),
2290            )
2291            .saturating_add(
2292                self.callbacks
2293                    .capacity()
2294                    .saturating_mul(std::mem::size_of::<CommandCallback>()),
2295            )
2296    }
2297
2298    fn apply(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
2299        let mut bubble_dirty = self.bubble_dirty.into_iter();
2300        let mut update_typed_nodes = self.update_typed_nodes.into_iter();
2301        let mut remove_nodes = self.remove_nodes.into_iter();
2302        let mut mount_nodes = self.mount_nodes.into_iter();
2303        let mut attach_children = self.attach_children.into_iter();
2304        let mut insert_children = self.insert_children.into_iter();
2305        let mut move_children = self.move_children.into_iter();
2306        let mut remove_children = self.remove_children.into_iter();
2307        let mut detach_children = self.detach_children.into_iter();
2308        let mut sync_children_commands = self.sync_children.into_iter();
2309        let sync_child_ids = self.sync_child_ids;
2310        let mut callbacks = self.callbacks.into_iter();
2311        let mut deferred_cleanup = DeferredChildCleanupQueue::default();
2312
2313        for chunk in self.chunks {
2314            for tag in chunk {
2315                match tag {
2316                    CommandTag::BubbleDirty => {
2317                        let BubbleDirtyCommand { node_id, bubble } =
2318                            next_command_payload(&mut bubble_dirty, tag)?;
2319                        Command::BubbleDirty { node_id, bubble }
2320                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2321                    }
2322                    CommandTag::UpdateTypedNode => {
2323                        let UpdateTypedNodeCommand { id, updater } =
2324                            next_command_payload(&mut update_typed_nodes, tag)?;
2325                        Command::UpdateTypedNode { id, updater }
2326                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2327                    }
2328                    CommandTag::RemoveNode => {
2329                        let id = next_command_payload(&mut remove_nodes, tag)?;
2330                        Command::RemoveNode { id }
2331                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2332                    }
2333                    CommandTag::MountNode => {
2334                        let id = next_command_payload(&mut mount_nodes, tag)?;
2335                        Command::MountNode { id }
2336                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2337                    }
2338                    CommandTag::AttachChild => {
2339                        let AttachChildCommand {
2340                            parent_id,
2341                            child_id,
2342                            bubble,
2343                        } = next_command_payload(&mut attach_children, tag)?;
2344                        Command::AttachChild {
2345                            parent_id,
2346                            child_id,
2347                            bubble,
2348                        }
2349                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2350                    }
2351                    CommandTag::InsertChild => {
2352                        let InsertChildCommand {
2353                            parent_id,
2354                            child_id,
2355                            appended_index,
2356                            insert_index,
2357                            bubble,
2358                        } = next_command_payload(&mut insert_children, tag)?;
2359                        Command::InsertChild {
2360                            parent_id,
2361                            child_id,
2362                            appended_index,
2363                            insert_index,
2364                            bubble,
2365                        }
2366                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2367                    }
2368                    CommandTag::MoveChild => {
2369                        let MoveChildCommand {
2370                            parent_id,
2371                            from_index,
2372                            to_index,
2373                            bubble,
2374                        } = next_command_payload(&mut move_children, tag)?;
2375                        Command::MoveChild {
2376                            parent_id,
2377                            from_index,
2378                            to_index,
2379                            bubble,
2380                        }
2381                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2382                    }
2383                    CommandTag::RemoveChild => {
2384                        let RemoveChildCommand {
2385                            parent_id,
2386                            child_id,
2387                        } = next_command_payload(&mut remove_children, tag)?;
2388                        Command::RemoveChild {
2389                            parent_id,
2390                            child_id,
2391                        }
2392                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2393                    }
2394                    CommandTag::DetachChild => {
2395                        let DetachChildCommand {
2396                            parent_id,
2397                            child_id,
2398                        } = next_command_payload(&mut detach_children, tag)?;
2399                        Command::DetachChild {
2400                            parent_id,
2401                            child_id,
2402                        }
2403                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2404                    }
2405                    CommandTag::SyncChildren => {
2406                        let SyncChildrenCommand {
2407                            parent_id,
2408                            child_start,
2409                            child_len,
2410                        } = next_command_payload(&mut sync_children_commands, tag)?;
2411                        let child_end = child_start
2412                            .checked_add(child_len)
2413                            .ok_or_else(|| command_payload_error(tag))?;
2414                        let expected_children = sync_child_ids
2415                            .get(child_start..child_end)
2416                            .ok_or_else(|| command_payload_error(tag))?;
2417                        sync_children(
2418                            applier,
2419                            parent_id,
2420                            expected_children,
2421                            &mut deferred_cleanup,
2422                        )?;
2423                    }
2424                    CommandTag::Callback => {
2425                        let callback = next_command_payload(&mut callbacks, tag)?;
2426                        Command::Callback(callback)
2427                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2428                    }
2429                }
2430            }
2431        }
2432
2433        debug_assert!(bubble_dirty.next().is_none());
2434        debug_assert!(update_typed_nodes.next().is_none());
2435        debug_assert!(remove_nodes.next().is_none());
2436        debug_assert!(mount_nodes.next().is_none());
2437        debug_assert!(attach_children.next().is_none());
2438        debug_assert!(insert_children.next().is_none());
2439        debug_assert!(move_children.next().is_none());
2440        debug_assert!(remove_children.next().is_none());
2441        debug_assert!(detach_children.next().is_none());
2442        debug_assert!(sync_children_commands.next().is_none());
2443        debug_assert!(callbacks.next().is_none());
2444        deferred_cleanup.flush(applier)
2445    }
2446}
2447
2448fn command_payload_error(tag: CommandTag) -> NodeError {
2449    NodeError::MalformedCommandPayload { tag: tag.label() }
2450}
2451
2452fn next_command_payload<T>(
2453    payloads: &mut impl Iterator<Item = T>,
2454    tag: CommandTag,
2455) -> Result<T, NodeError> {
2456    payloads.next().ok_or_else(|| command_payload_error(tag))
2457}
2458
2459fn update_typed_node<N: Node + 'static>(node: &mut dyn Node, id: NodeId) -> Result<(), NodeError> {
2460    let typed = node
2461        .as_any_mut()
2462        .downcast_mut::<N>()
2463        .ok_or(NodeError::TypeMismatch {
2464            id,
2465            expected: std::any::type_name::<N>(),
2466        })?;
2467    typed.update();
2468    Ok(())
2469}
2470
2471fn insert_child_with_reparenting(
2472    applier: &mut dyn Applier,
2473    parent_id: NodeId,
2474    child_id: NodeId,
2475) -> bool {
2476    if parent_id == child_id {
2477        debug_assert_ne!(
2478            parent_id, child_id,
2479            "a node cannot be attached as its own child"
2480        );
2481        return false;
2482    }
2483
2484    let old_parent = applier
2485        .get_mut(child_id)
2486        .ok()
2487        .and_then(|node| node.parent());
2488    if let Some(old_parent_id) = old_parent
2489        && old_parent_id != parent_id
2490    {
2491        let removed = applier
2492            .get_mut(old_parent_id)
2493            .is_ok_and(|old_parent_node| old_parent_node.remove_child(child_id));
2494        if let Ok(child_node) = applier.get_mut(child_id) {
2495            child_node.on_removed_from_parent();
2496        }
2497        if removed {
2498            bubble_layout_dirty(applier, old_parent_id);
2499            bubble_measure_dirty(applier, old_parent_id);
2500            note_structural("reparent-detach", old_parent_id, child_id);
2501            applier.record_structural_change(old_parent_id);
2502        }
2503    }
2504
2505    let inserted = applier
2506        .get_mut(parent_id)
2507        .is_ok_and(|parent_node| parent_node.insert_child(child_id));
2508    if inserted {
2509        note_structural("attach", parent_id, child_id);
2510        applier.record_structural_change(parent_id);
2511    }
2512    if let Ok(child_node) = applier.get_mut(child_id) {
2513        child_node.on_attached_to_parent(parent_id);
2514    }
2515    inserted
2516}
2517
2518fn apply_remove_child(
2519    applier: &mut dyn Applier,
2520    parent_id: NodeId,
2521    child_id: NodeId,
2522    deferred_cleanup: &mut DeferredChildCleanupQueue,
2523) -> Result<(), NodeError> {
2524    detach_child_from_parent(applier, parent_id, child_id)?;
2525
2526    let generation = applier.node_generation(child_id);
2527    let removed_from_parent = if let Ok(node) = applier.get_mut(child_id) {
2528        node.parent().is_none()
2529    } else {
2530        return Ok(());
2531    };
2532    deferred_cleanup.push(child_id, generation, removed_from_parent);
2533    Ok(())
2534}
2535
2536fn detach_child_from_parent(
2537    applier: &mut dyn Applier,
2538    parent_id: NodeId,
2539    child_id: NodeId,
2540) -> Result<(), NodeError> {
2541    let removed = applier
2542        .get_mut(parent_id)
2543        .is_ok_and(|parent_node| parent_node.remove_child(child_id));
2544    if removed {
2545        bubble_layout_dirty(applier, parent_id);
2546        bubble_measure_dirty(applier, parent_id);
2547        note_structural("detach", parent_id, child_id);
2548        applier.record_structural_change(parent_id);
2549    }
2550
2551    if let Ok(node) = applier.get_mut(child_id) {
2552        match node.parent() {
2553            Some(existing_parent_id) if existing_parent_id == parent_id => {
2554                node.on_removed_from_parent();
2555            }
2556            None => {}
2557            Some(_) => return Ok(()),
2558        }
2559    } else {
2560        return Ok(());
2561    }
2562
2563    Ok(())
2564}
2565
2566fn cleanup_detached_child(
2567    applier: &mut dyn Applier,
2568    cleanup: DeferredChildCleanup,
2569) -> Result<(), NodeError> {
2570    if applier.node_generation(cleanup.child_id) != cleanup.generation {
2571        return Ok(());
2572    }
2573
2574    let parent_id = match applier.get_mut(cleanup.child_id) {
2575        Ok(node) => node.parent(),
2576        Err(NodeError::Missing { .. }) => return Ok(()),
2577        Err(err) => return Err(err),
2578    };
2579    if parent_id.is_some() {
2580        return Ok(());
2581    }
2582
2583    if let Ok(node) = applier.get_mut(cleanup.child_id) {
2584        if !cleanup.removed_from_parent {
2585            node.on_removed_from_parent();
2586        }
2587        node.unmount();
2588    }
2589    match applier.remove(cleanup.child_id) {
2590        Ok(()) | Err(NodeError::Missing { .. }) => Ok(()),
2591        Err(err) => Err(err),
2592    }
2593}
2594
2595fn remove_child_and_cleanup_now(
2596    applier: &mut dyn Applier,
2597    parent_id: NodeId,
2598    child_id: NodeId,
2599) -> Result<(), NodeError> {
2600    let mut deferred_cleanup = DeferredChildCleanupQueue::default();
2601    apply_remove_child(applier, parent_id, child_id, &mut deferred_cleanup)?;
2602    deferred_cleanup.flush(applier)
2603}
2604
2605fn collect_current_children(applier: &mut dyn Applier, parent_id: NodeId) -> ChildList {
2606    let mut scratch = SmallVec::<[NodeId; 8]>::new();
2607    if let Ok(node) = applier.get_mut(parent_id) {
2608        node.collect_children_into(&mut scratch);
2609    }
2610    let mut current = ChildList::new();
2611    current.extend(scratch);
2612    current
2613}
2614
2615fn sync_children(
2616    applier: &mut dyn Applier,
2617    parent_id: NodeId,
2618    expected_children: &[NodeId],
2619    deferred_cleanup: &mut DeferredChildCleanupQueue,
2620) -> Result<(), NodeError> {
2621    let mut current = collect_current_children(applier, parent_id);
2622    let children_changed = current.as_slice() != expected_children;
2623
2624    if children_changed {
2625        if current.len().max(expected_children.len()) <= SMALL_CHILD_SYNC_LINEAR_THRESHOLD {
2626            sync_children_small(
2627                applier,
2628                parent_id,
2629                &mut current,
2630                expected_children,
2631                deferred_cleanup,
2632            )?;
2633        } else {
2634            let mut target_positions: HashMap<NodeId, usize> = HashMap::default();
2635            target_positions.reserve(expected_children.len());
2636            for (index, &child) in expected_children.iter().enumerate() {
2637                target_positions.insert(child, index);
2638            }
2639
2640            for index in (0..current.len()).rev() {
2641                let child = current[index];
2642                if !target_positions.contains_key(&child) {
2643                    current.remove(index);
2644                    apply_remove_child(applier, parent_id, child, deferred_cleanup)?;
2645                }
2646            }
2647
2648            let mut current_positions = build_child_positions(&current);
2649            for (target_index, &child) in expected_children.iter().enumerate() {
2650                if let Some(current_index) = current_positions.get(&child).copied() {
2651                    if current_index != target_index {
2652                        let from_index = current_index;
2653                        let to_index = move_child_in_diff_state(
2654                            &mut current,
2655                            &mut current_positions,
2656                            from_index,
2657                            target_index,
2658                        );
2659                        Command::MoveChild {
2660                            parent_id,
2661                            from_index,
2662                            to_index,
2663                            bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2664                        }
2665                        .apply(applier)?;
2666                    }
2667                } else {
2668                    let insert_index = target_index.min(current.len());
2669                    let appended_index = current.len();
2670                    insert_child_into_diff_state(
2671                        &mut current,
2672                        &mut current_positions,
2673                        insert_index,
2674                        child,
2675                    );
2676                    Command::InsertChild {
2677                        parent_id,
2678                        child_id: child,
2679                        appended_index,
2680                        insert_index,
2681                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2682                    }
2683                    .apply(applier)?;
2684                }
2685            }
2686        }
2687    }
2688
2689    reconcile_children(applier, parent_id, expected_children, !children_changed)
2690}
2691
2692fn sync_children_small(
2693    applier: &mut dyn Applier,
2694    parent_id: NodeId,
2695    current: &mut ChildList,
2696    expected_children: &[NodeId],
2697    deferred_cleanup: &mut DeferredChildCleanupQueue,
2698) -> Result<(), NodeError> {
2699    for index in (0..current.len()).rev() {
2700        let child = current[index];
2701        if !expected_children.contains(&child) {
2702            current.remove(index);
2703            apply_remove_child(applier, parent_id, child, deferred_cleanup)?;
2704        }
2705    }
2706
2707    for (target_index, &child) in expected_children.iter().enumerate() {
2708        if let Some(current_index) = current
2709            .iter()
2710            .position(|&current_child| current_child == child)
2711        {
2712            if current_index != target_index {
2713                let child = current.remove(current_index);
2714                let to_index = target_index.min(current.len());
2715                current.insert(to_index, child);
2716                Command::MoveChild {
2717                    parent_id,
2718                    from_index: current_index,
2719                    to_index,
2720                    bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2721                }
2722                .apply(applier)?;
2723            }
2724        } else {
2725            let insert_index = target_index.min(current.len());
2726            let appended_index = current.len();
2727            current.insert(insert_index, child);
2728            Command::InsertChild {
2729                parent_id,
2730                child_id: child,
2731                appended_index,
2732                insert_index,
2733                bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2734            }
2735            .apply(applier)?;
2736        }
2737    }
2738
2739    Ok(())
2740}
2741
2742fn reconcile_children(
2743    applier: &mut dyn Applier,
2744    parent_id: NodeId,
2745    expected_children: &[NodeId],
2746    needs_dirty_check: bool,
2747) -> Result<(), NodeError> {
2748    let mut repaired = false;
2749    for &child_id in expected_children {
2750        let needs_attach = if let Ok(node) = applier.get_mut(child_id) {
2751            node.parent() != Some(parent_id)
2752        } else {
2753            false
2754        };
2755
2756        if needs_attach {
2757            insert_child_with_reparenting(applier, parent_id, child_id);
2758            repaired = true;
2759        }
2760    }
2761
2762    let is_dirty = if needs_dirty_check {
2763        if let Ok(node) = applier.get_mut(parent_id) {
2764            node.needs_layout()
2765        } else {
2766            false
2767        }
2768    } else {
2769        false
2770    };
2771
2772    if repaired {
2773        bubble_layout_dirty(applier, parent_id);
2774        bubble_measure_dirty(applier, parent_id);
2775    } else if is_dirty {
2776        bubble_layout_dirty(applier, parent_id);
2777    }
2778
2779    Ok(())
2780}
2781
2782#[derive(Default)]
2783pub struct MemoryApplier {
2784    nodes: Vec<Option<Box<dyn Node>>>,
2785    physical_stable_ids: Vec<u32>,
2786    physical_warm_recycled_origins: Vec<bool>,
2787    stable_to_physical: HashMap<NodeId, usize>,
2788    stable_generations: HashMap<NodeId, u32>,
2789    free_ids: BinaryHeap<Reverse<usize>>,
2790    high_id_nodes: HashMap<NodeId, Box<dyn Node>>,
2791    high_id_warm_recycled_origins: HashMap<NodeId, bool>,
2792    high_id_generations: HashMap<NodeId, u32>,
2793    next_stable_id: NodeId,
2794    layout_runtime: Option<RuntimeHandle>,
2795    slots: SlotTable,
2796    recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
2797    returning_recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
2798    cold_recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
2799    recycled_node_limits: HashMap<TypeId, usize>,
2800    warm_recycled_node_targets: HashMap<TypeId, usize>,
2801    fresh_recyclable_creations: HashMap<TypeId, usize>,
2802    recycled_node_prototypes: HashMap<TypeId, Box<dyn Node>>,
2803    structural_change_parents: Vec<NodeId>,
2804    virtual_node_ids: HashSet<NodeId>,
2805}
2806
2807struct RemovalFrame {
2808    node_id: NodeId,
2809    children: SmallVec<[NodeId; 8]>,
2810    next_child: usize,
2811}
2812
2813#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2814pub struct MemoryApplierDebugStats {
2815    pub next_stable_id: NodeId,
2816    pub nodes_len: usize,
2817    pub nodes_cap: usize,
2818    pub physical_stable_ids_len: usize,
2819    pub physical_stable_ids_cap: usize,
2820    pub stable_to_physical_len: usize,
2821    pub stable_to_physical_cap: usize,
2822    pub stable_generations_len: usize,
2823    pub stable_generations_cap: usize,
2824    pub free_ids_len: usize,
2825    pub free_ids_cap: usize,
2826    pub high_id_nodes_len: usize,
2827    pub high_id_nodes_cap: usize,
2828    pub high_id_generations_len: usize,
2829    pub high_id_generations_cap: usize,
2830    pub recycled_type_count: usize,
2831    pub recycled_type_cap: usize,
2832    pub recycled_node_count: usize,
2833    pub recycled_node_capacity: usize,
2834    pub warm_recycled_node_id_count: usize,
2835    pub warm_recycled_node_id_capacity: usize,
2836}
2837
2838impl MemoryApplier {
2839    const EAGER_COMPACT_NODE_LEN: usize = 1_024;
2840    const HIGH_ID_THRESHOLD: NodeId = 1_000_000_000;
2841    const INVALID_STABLE_ID: u32 = u32::MAX;
2842    const INITIAL_DENSE_NODE_CAP: usize = 32;
2843    const LARGE_DENSE_NODE_GROWTH_THRESHOLD: usize = 32 * 1024;
2844    const LARGE_DENSE_NODE_GROWTH_DIVISOR: usize = 4;
2845
2846    fn pack_stable_id(stable_id: NodeId) -> u32 {
2847        u32::try_from(stable_id).expect("stable id overflow")
2848    }
2849
2850    fn unpack_stable_id(stable_id: u32) -> NodeId {
2851        stable_id as NodeId
2852    }
2853
2854    fn next_dense_node_target_len(old_len: usize) -> usize {
2855        if old_len < Self::INITIAL_DENSE_NODE_CAP {
2856            return Self::INITIAL_DENSE_NODE_CAP;
2857        }
2858        if old_len < Self::LARGE_DENSE_NODE_GROWTH_THRESHOLD {
2859            return old_len.saturating_mul(2);
2860        }
2861
2862        let incremental_growth =
2863            (old_len / Self::LARGE_DENSE_NODE_GROWTH_DIVISOR).max(Self::INITIAL_DENSE_NODE_CAP);
2864        old_len.saturating_add(incremental_growth)
2865    }
2866
2867    fn ensure_dense_node_storage_capacity(&mut self) {
2868        let len = self
2869            .nodes
2870            .len()
2871            .max(self.physical_stable_ids.len())
2872            .max(self.physical_warm_recycled_origins.len());
2873        if len < self.nodes.capacity()
2874            && len < self.physical_stable_ids.capacity()
2875            && len < self.physical_warm_recycled_origins.capacity()
2876        {
2877            return;
2878        }
2879
2880        let target = Self::next_dense_node_target_len(len);
2881        if self.nodes.capacity() < target {
2882            self.nodes
2883                .reserve_exact(target.saturating_sub(self.nodes.len()));
2884        }
2885        if self.physical_stable_ids.capacity() < target {
2886            self.physical_stable_ids
2887                .reserve_exact(target.saturating_sub(self.physical_stable_ids.len()));
2888        }
2889        if self.physical_warm_recycled_origins.capacity() < target {
2890            self.physical_warm_recycled_origins
2891                .reserve_exact(target.saturating_sub(self.physical_warm_recycled_origins.len()));
2892        }
2893    }
2894
2895    fn ensure_stable_index_capacity(&mut self) {
2896        let len = self
2897            .stable_to_physical
2898            .len()
2899            .max(self.stable_generations.len());
2900        if len < self.stable_to_physical.capacity() && len < self.stable_generations.capacity() {
2901            return;
2902        }
2903
2904        let target = Self::next_dense_node_target_len(len);
2905        let additional = target.saturating_sub(len);
2906        if self.stable_to_physical.capacity() < target {
2907            self.stable_to_physical.reserve(additional);
2908        }
2909        if self.stable_generations.capacity() < target {
2910            self.stable_generations.reserve(additional);
2911        }
2912    }
2913
2914    pub fn new() -> Self {
2915        Self {
2916            nodes: Vec::new(),
2917            physical_stable_ids: Vec::new(),
2918            physical_warm_recycled_origins: Vec::new(),
2919            stable_to_physical: HashMap::default(),
2920            stable_generations: HashMap::default(),
2921            free_ids: BinaryHeap::new(),
2922            high_id_nodes: HashMap::default(),
2923            high_id_warm_recycled_origins: HashMap::default(),
2924            high_id_generations: HashMap::default(),
2925            next_stable_id: 0,
2926            layout_runtime: None,
2927            slots: SlotTable::default(),
2928            recycled_nodes: HashMap::default(),
2929            returning_recycled_nodes: HashMap::default(),
2930            cold_recycled_nodes: HashMap::default(),
2931            recycled_node_limits: HashMap::default(),
2932            warm_recycled_node_targets: HashMap::default(),
2933            fresh_recyclable_creations: HashMap::default(),
2934            recycled_node_prototypes: HashMap::default(),
2935            structural_change_parents: Vec::new(),
2936            virtual_node_ids: HashSet::default(),
2937        }
2938    }
2939
2940    pub fn slots(&mut self) -> &mut SlotTable {
2941        &mut self.slots
2942    }
2943
2944    /// Drains the parents recorded via [`Applier::record_structural_change`],
2945    /// keeping only nodes still attached to `root` (a parent that was itself
2946    /// removed is covered by its own surviving ancestor's record). A virtual
2947    /// parent — a subcompose slot wrapper the render graph never contains —
2948    /// is reported as its nearest non-virtual ancestor: that is the node
2949    /// whose graph child set the change altered, and an id the graph cannot
2950    /// resolve would force the scoped scene update to give up and rebuild.
2951    /// Resolves a scene-scope candidate the way structural records are
2952    /// resolved: to its nearest non-virtual ancestor, and only while still
2953    /// attached to `root`. A node detached after recording must not reach the
2954    /// scoped scene update — an id the graph cannot resolve forces it to give
2955    /// up and rebuild the whole scene.
2956    pub fn scene_node_attached_to(&mut self, node_id: NodeId, root: NodeId) -> Option<NodeId> {
2957        let resolved = self.first_non_virtual_ancestor(node_id)?;
2958        self.is_attached_to(resolved, root).then_some(resolved)
2959    }
2960
2961    pub fn take_structural_change_parents_attached_to(&mut self, root: NodeId) -> Vec<NodeId> {
2962        let recorded = std::mem::take(&mut self.structural_change_parents);
2963        let mut attached = Vec::with_capacity(recorded.len());
2964        for parent_id in recorded {
2965            let Some(parent_id) = self.first_non_virtual_ancestor(parent_id) else {
2966                continue;
2967            };
2968            if self.is_attached_to(parent_id, root) && !attached.contains(&parent_id) {
2969                attached.push(parent_id);
2970            }
2971        }
2972        attached
2973    }
2974
2975    fn first_non_virtual_ancestor(&mut self, node_id: NodeId) -> Option<NodeId> {
2976        let mut current = node_id;
2977        for _ in 0..100_000 {
2978            if !self.virtual_node_ids.contains(&current) {
2979                return Some(current);
2980            }
2981            match self.get_mut(current) {
2982                Ok(node) => current = node.parent()?,
2983                Err(_) => return None,
2984            }
2985        }
2986        None
2987    }
2988
2989    fn is_attached_to(&mut self, node_id: NodeId, root: NodeId) -> bool {
2990        let mut current = node_id;
2991        for _ in 0..100_000 {
2992            if current == root {
2993                return true;
2994            }
2995            match self.get_mut(current) {
2996                Ok(node) => match node.parent() {
2997                    Some(parent) => current = parent,
2998                    None => return false,
2999                },
3000                Err(_) => return false,
3001            }
3002        }
3003        false
3004    }
3005
3006    pub fn with_node<N: Node + 'static, R>(
3007        &mut self,
3008        id: NodeId,
3009        f: impl FnOnce(&mut N) -> R,
3010    ) -> Result<R, NodeError> {
3011        let physical_id = self
3012            .resolve_node_index(id)
3013            .ok_or(NodeError::Missing { id })?;
3014        let slot = self
3015            .nodes
3016            .get_mut(physical_id)
3017            .ok_or(NodeError::Missing { id })?
3018            .as_deref_mut()
3019            .ok_or(NodeError::Missing { id })?;
3020        let typed = slot
3021            .as_any_mut()
3022            .downcast_mut::<N>()
3023            .ok_or(NodeError::TypeMismatch {
3024                id,
3025                expected: std::any::type_name::<N>(),
3026            })?;
3027        Ok(f(typed))
3028    }
3029
3030    pub fn len(&self) -> usize {
3031        self.nodes.iter().filter(|n| n.is_some()).count()
3032    }
3033
3034    pub fn capacity(&self) -> usize {
3035        self.nodes.len()
3036    }
3037
3038    pub fn tombstone_count(&self) -> usize {
3039        self.nodes.iter().filter(|n| n.is_none()).count()
3040    }
3041
3042    pub fn freelist_len(&self) -> usize {
3043        self.free_ids.len()
3044    }
3045
3046    pub fn debug_recycled_node_count(&self) -> usize {
3047        self.total_recycled_node_count()
3048    }
3049
3050    pub fn debug_recycled_node_count_for<N: Node + 'static>(&self) -> usize {
3051        let key = TypeId::of::<N>();
3052        self.recycled_nodes.get(&key).map(Vec::len).unwrap_or(0)
3053            + self
3054                .returning_recycled_nodes
3055                .get(&key)
3056                .map(Vec::len)
3057                .unwrap_or(0)
3058            + self
3059                .cold_recycled_nodes
3060                .get(&key)
3061                .map(Vec::len)
3062                .unwrap_or(0)
3063    }
3064
3065    pub fn debug_stats(&self) -> MemoryApplierDebugStats {
3066        let mut recycled_keys: HashSet<TypeId> = HashSet::default();
3067        recycled_keys.extend(self.recycled_nodes.keys().copied());
3068        recycled_keys.extend(self.returning_recycled_nodes.keys().copied());
3069        recycled_keys.extend(self.cold_recycled_nodes.keys().copied());
3070
3071        MemoryApplierDebugStats {
3072            next_stable_id: self.next_stable_id,
3073            nodes_len: self.len(),
3074            nodes_cap: self.nodes.len(),
3075            physical_stable_ids_len: self.physical_stable_ids.len(),
3076            physical_stable_ids_cap: self.physical_stable_ids.capacity(),
3077            stable_to_physical_len: self.stable_to_physical.len(),
3078            stable_to_physical_cap: self.stable_to_physical.capacity(),
3079            stable_generations_len: self.stable_generations.len(),
3080            stable_generations_cap: self.stable_generations.capacity(),
3081            free_ids_len: self.free_ids.len(),
3082            free_ids_cap: self.free_ids.capacity(),
3083            high_id_nodes_len: self.high_id_nodes.len(),
3084            high_id_nodes_cap: self.high_id_nodes.capacity(),
3085            high_id_generations_len: self.high_id_generations.len(),
3086            high_id_generations_cap: self.high_id_generations.capacity(),
3087            recycled_type_count: recycled_keys.len(),
3088            recycled_type_cap: self.recycled_nodes.capacity()
3089                + self.returning_recycled_nodes.capacity()
3090                + self.cold_recycled_nodes.capacity(),
3091            recycled_node_count: self.total_recycled_node_count(),
3092            recycled_node_capacity: self.total_recycled_node_capacity(),
3093            warm_recycled_node_id_count: self.total_warm_recycled_node_id_count(),
3094            warm_recycled_node_id_capacity: self.total_warm_recycled_node_id_capacity(),
3095        }
3096    }
3097
3098    pub fn is_empty(&self) -> bool {
3099        self.len() == 0
3100    }
3101
3102    pub fn debug_live_node_heap_bytes(&self) -> usize {
3103        let dense_nodes = self
3104            .nodes
3105            .iter()
3106            .flatten()
3107            .map(|node| std::mem::size_of_val(&**node) + node.debug_heap_bytes())
3108            .sum::<usize>();
3109        let high_id_nodes = self
3110            .high_id_nodes
3111            .values()
3112            .map(|node| std::mem::size_of_val(&**node) + node.debug_heap_bytes())
3113            .sum::<usize>();
3114        dense_nodes + high_id_nodes
3115    }
3116
3117    pub fn debug_recycled_node_heap_bytes(&self) -> usize {
3118        let pool_bytes = |pools: &HashMap<TypeId, Vec<RecycledNode>>| {
3119            pools
3120                .values()
3121                .flat_map(|nodes| nodes.iter())
3122                .map(|node| std::mem::size_of_val(&*node.node) + node.node.debug_heap_bytes())
3123                .sum::<usize>()
3124        };
3125
3126        pool_bytes(&self.recycled_nodes)
3127            + pool_bytes(&self.returning_recycled_nodes)
3128            + pool_bytes(&self.cold_recycled_nodes)
3129    }
3130
3131    pub fn set_runtime_handle(&mut self, handle: RuntimeHandle) {
3132        self.layout_runtime = Some(handle);
3133    }
3134
3135    pub fn clear_runtime_handle(&mut self) {
3136        self.layout_runtime = None;
3137    }
3138
3139    pub fn runtime_handle(&self) -> Option<RuntimeHandle> {
3140        self.layout_runtime.clone()
3141    }
3142
3143    fn pool_node_count(pools: &HashMap<TypeId, Vec<RecycledNode>>) -> usize {
3144        pools.values().map(Vec::len).sum()
3145    }
3146
3147    fn pool_node_capacity(pools: &HashMap<TypeId, Vec<RecycledNode>>) -> usize {
3148        pools.values().map(Vec::capacity).sum()
3149    }
3150
3151    fn total_recycled_node_count(&self) -> usize {
3152        Self::pool_node_count(&self.recycled_nodes)
3153            + Self::pool_node_count(&self.returning_recycled_nodes)
3154            + Self::pool_node_count(&self.cold_recycled_nodes)
3155    }
3156
3157    fn total_recycled_node_capacity(&self) -> usize {
3158        Self::pool_node_capacity(&self.recycled_nodes)
3159            + Self::pool_node_capacity(&self.returning_recycled_nodes)
3160            + Self::pool_node_capacity(&self.cold_recycled_nodes)
3161    }
3162
3163    fn total_warm_recycled_node_id_count(&self) -> usize {
3164        self.live_warm_recycled_origin_count()
3165            + Self::pool_node_count(&self.recycled_nodes)
3166            + Self::pool_node_count(&self.returning_recycled_nodes)
3167    }
3168
3169    fn total_warm_recycled_node_id_capacity(&self) -> usize {
3170        self.live_warm_recycled_origin_capacity()
3171            + Self::pool_node_capacity(&self.recycled_nodes)
3172            + Self::pool_node_capacity(&self.returning_recycled_nodes)
3173    }
3174
3175    fn remember_recycle_pool_limit(&mut self, key: TypeId, recycle_pool_limit: Option<usize>) {
3176        if let Some(limit) = recycle_pool_limit {
3177            self.recycled_node_limits.insert(key, limit);
3178        } else {
3179            self.recycled_node_limits.remove(&key);
3180        }
3181    }
3182
3183    fn recycle_pool_limit_for(&self, key: TypeId) -> Option<usize> {
3184        self.recycled_node_limits.get(&key).copied()
3185    }
3186
3187    fn warm_recycled_pool_len(&self, key: TypeId) -> usize {
3188        self.recycled_nodes.get(&key).map(Vec::len).unwrap_or(0)
3189    }
3190
3191    fn warm_recycled_node_target(&self, key: TypeId) -> usize {
3192        self.warm_recycled_node_targets
3193            .get(&key)
3194            .copied()
3195            .unwrap_or(0)
3196    }
3197
3198    fn warm_recycled_node_target_limit(&self, key: TypeId) -> usize {
3199        let Some(limit) = self.recycle_pool_limit_for(key) else {
3200            return usize::MAX;
3201        };
3202        if limit <= 8 { limit } else { limit / 4 }
3203    }
3204
3205    fn update_warm_recycled_node_target(&mut self, key: TypeId, observed_demand: usize) -> usize {
3206        let target_limit = self.warm_recycled_node_target_limit(key);
3207        let existing = self.warm_recycled_node_target(key).min(target_limit);
3208        if observed_demand == 0 {
3209            return existing;
3210        }
3211
3212        let target = match self.recycle_pool_limit_for(key) {
3213            Some(limit) if limit > 8 => target_limit,
3214            Some(_) => observed_demand.min(target_limit),
3215            None => observed_demand,
3216        };
3217        self.warm_recycled_node_targets.insert(key, target);
3218        target
3219    }
3220
3221    fn remember_recycled_node_prototype(&mut self, key: TypeId, shell: &dyn Node) {
3222        if self.recycled_node_prototypes.contains_key(&key) {
3223            return;
3224        }
3225        if let Some(prototype) = shell.rehouse_for_recycle() {
3226            self.recycled_node_prototypes.insert(key, prototype);
3227        }
3228    }
3229
3230    fn live_warm_recycled_origin_count(&self) -> usize {
3231        self.physical_warm_recycled_origins
3232            .iter()
3233            .zip(self.nodes.iter())
3234            .filter(|(warm_origin, node)| **warm_origin && node.is_some())
3235            .count()
3236            + self
3237                .high_id_warm_recycled_origins
3238                .values()
3239                .filter(|warm_origin| **warm_origin)
3240                .count()
3241    }
3242
3243    fn live_warm_recycled_origin_capacity(&self) -> usize {
3244        self.physical_warm_recycled_origins.capacity()
3245            + self.high_id_warm_recycled_origins.capacity()
3246    }
3247
3248    fn push_recycled_node(
3249        &mut self,
3250        key: TypeId,
3251        recycle_pool_limit: Option<usize>,
3252        recycled: RecycledNode,
3253    ) {
3254        self.remember_recycle_pool_limit(key, recycle_pool_limit);
3255        self.remember_recycled_node_prototype(key, recycled.node.as_ref());
3256
3257        let warm_origin = recycled.warm_origin();
3258        let pool = if warm_origin {
3259            self.returning_recycled_nodes.entry(key).or_default()
3260        } else {
3261            self.cold_recycled_nodes.entry(key).or_default()
3262        };
3263        pool.push(recycled);
3264        if let Some(limit) = recycle_pool_limit
3265            && pool.len() > limit
3266        {
3267            let excess = pool.len() - limit;
3268            let dropped: Vec<_> = pool.drain(0..excess).collect();
3269            drop(dropped);
3270        }
3271    }
3272
3273    fn push_warm_recycled_node(
3274        &mut self,
3275        key: TypeId,
3276        recycle_pool_limit: Option<usize>,
3277        mut recycled: RecycledNode,
3278    ) {
3279        self.remember_recycle_pool_limit(key, recycle_pool_limit);
3280
3281        recycled.set_warm_origin(true);
3282        let mut dropped = Vec::new();
3283        let mut remove_pool_entry = false;
3284        {
3285            let pool = self.recycled_nodes.entry(key).or_default();
3286            pool.push(recycled);
3287            if let Some(limit) = recycle_pool_limit
3288                && pool.len() > limit
3289            {
3290                let excess = pool.len() - limit;
3291                dropped = pool.drain(0..excess).collect();
3292                remove_pool_entry = pool.is_empty();
3293            }
3294        }
3295        if remove_pool_entry {
3296            self.recycled_nodes.remove(&key);
3297        }
3298        drop(dropped);
3299    }
3300
3301    fn seed_recycled_node_shell_impl(
3302        &mut self,
3303        key: TypeId,
3304        recycle_pool_limit: Option<usize>,
3305        shell: Box<dyn Node>,
3306    ) {
3307        let limit = recycle_pool_limit.unwrap_or(usize::MAX);
3308        if self.warm_recycled_pool_len(key) >= limit {
3309            return;
3310        }
3311
3312        self.remember_recycled_node_prototype(key, shell.as_ref());
3313        let stable_id = self.next_stable_id;
3314        self.next_stable_id = self.next_stable_id.saturating_add(1);
3315        self.push_warm_recycled_node(
3316            key,
3317            recycle_pool_limit,
3318            RecycledNode::from_shell(stable_id, shell, true),
3319        );
3320    }
3321
3322    fn take_recycled_node_from_pool(
3323        pools: &mut HashMap<TypeId, Vec<RecycledNode>>,
3324        key: TypeId,
3325    ) -> Option<RecycledNode> {
3326        let pool = pools.get_mut(&key)?;
3327        let node = pool.pop();
3328        if pool.is_empty() {
3329            pools.remove(&key);
3330        }
3331        node
3332    }
3333
3334    fn compact_idle_warm_pool(&mut self, key: TypeId) {
3335        let Some(pool) = self.recycled_nodes.get_mut(&key) else {
3336            return;
3337        };
3338        if pool.capacity() <= pool.len().saturating_mul(4).max(64) {
3339            return;
3340        }
3341
3342        let retained = pool.len();
3343        let mut compacted = Vec::with_capacity(retained);
3344        compacted.append(pool);
3345        let remove_pool_entry = compacted.is_empty();
3346        *pool = compacted;
3347        let _ = pool;
3348
3349        if remove_pool_entry {
3350            self.recycled_nodes.remove(&key);
3351        }
3352    }
3353
3354    fn trim_idle_warm_pool_to_target(&mut self, key: TypeId, target: usize) {
3355        let pool_len = self.warm_recycled_pool_len(key);
3356        if pool_len <= target {
3357            return;
3358        }
3359
3360        let Some(pool) = self.recycled_nodes.get_mut(&key) else {
3361            return;
3362        };
3363        let removable = (pool_len - target).min(pool.len());
3364        let dropped: Vec<_> = pool.drain(0..removable).collect();
3365        let remove_pool_entry = pool.is_empty();
3366        let _ = pool;
3367
3368        if remove_pool_entry {
3369            self.recycled_nodes.remove(&key);
3370        }
3371        drop(dropped);
3372    }
3373
3374    fn replenish_warm_pool_to_target(&mut self, key: TypeId, target: usize) {
3375        let missing = target.saturating_sub(self.warm_recycled_pool_len(key));
3376        if missing == 0 {
3377            return;
3378        }
3379
3380        let recycle_pool_limit = self.recycle_pool_limit_for(key);
3381        let mut shells = Vec::with_capacity(missing);
3382        if let Some(prototype) = self.recycled_node_prototypes.get(&key) {
3383            for _ in 0..missing {
3384                let Some(shell) = prototype.rehouse_for_recycle() else {
3385                    break;
3386                };
3387                shells.push(shell);
3388            }
3389        }
3390
3391        for shell in shells {
3392            self.seed_recycled_node_shell_impl(key, recycle_pool_limit, shell);
3393        }
3394    }
3395
3396    fn prune_stable_generations(&mut self) {
3397        let retained_len = self.stable_to_physical.len() + self.total_recycled_node_count();
3398        if retained_len == self.stable_generations.len() {
3399            return;
3400        }
3401
3402        let mut retained = HashMap::default();
3403        retained.reserve(retained_len);
3404        for stable_id in self.stable_to_physical.keys().copied() {
3405            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3406                retained.insert(stable_id, generation);
3407            }
3408        }
3409        for stable_id in self
3410            .recycled_nodes
3411            .values()
3412            .flat_map(|nodes| nodes.iter().map(RecycledNode::stable_id))
3413        {
3414            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3415                retained.insert(stable_id, generation);
3416            }
3417        }
3418        for stable_id in self
3419            .returning_recycled_nodes
3420            .values()
3421            .flat_map(|nodes| nodes.iter().map(RecycledNode::stable_id))
3422        {
3423            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3424                retained.insert(stable_id, generation);
3425            }
3426        }
3427        for stable_id in self
3428            .cold_recycled_nodes
3429            .values()
3430            .flat_map(|nodes| nodes.iter().map(RecycledNode::stable_id))
3431        {
3432            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3433                retained.insert(stable_id, generation);
3434            }
3435        }
3436        self.stable_generations = retained;
3437    }
3438
3439    pub fn dump_tree(&self, root: Option<NodeId>) -> String {
3440        let mut output = String::new();
3441        if let Some(root_id) = root {
3442            self.dump_node(&mut output, root_id, 0);
3443        } else {
3444            output.push_str("(no root)\n");
3445        }
3446        output
3447    }
3448
3449    fn dump_node(&self, output: &mut String, id: NodeId, depth: usize) {
3450        let indent = "  ".repeat(depth);
3451        if let Some(physical_id) = self.resolve_node_index(id) {
3452            if let Some(node) = self.nodes.get(physical_id).and_then(Option::as_ref) {
3453                let type_name = std::any::type_name_of_val(&**node);
3454                output.push_str(&format!("{}[{}] {}\n", indent, id, type_name));
3455
3456                let children = node.children();
3457                for child_id in children {
3458                    self.dump_node(output, child_id, depth + 1);
3459                }
3460            } else {
3461                output.push_str(&format!(
3462                    "{}[{}] (missing physical node {})\n",
3463                    indent, id, physical_id
3464                ));
3465            }
3466        } else {
3467            output.push_str(&format!("{}[{}] (missing)\n", indent, id));
3468        }
3469    }
3470
3471    fn resolve_node_index(&self, id: NodeId) -> Option<usize> {
3472        self.stable_to_physical.get(&id).copied()
3473    }
3474
3475    fn contains_node_id(&self, id: NodeId) -> bool {
3476        self.resolve_node_index(id).is_some() || self.high_id_nodes.contains_key(&id)
3477    }
3478
3479    fn insert_high_id_node(&mut self, stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) {
3480        self.high_id_nodes.insert(stable_id, node);
3481        self.high_id_warm_recycled_origins
3482            .insert(stable_id, warm_origin);
3483        self.high_id_generations.entry(stable_id).or_insert(0);
3484    }
3485
3486    fn insert_available_with_id(&mut self, stable_id: NodeId, node: Box<dyn Node>) {
3487        if stable_id >= Self::HIGH_ID_THRESHOLD {
3488            self.insert_high_id_node(stable_id, node, false);
3489            return;
3490        }
3491
3492        let physical_id = if let Some(Reverse(free_physical_id)) = self.free_ids.pop() {
3493            self.nodes[free_physical_id] = Some(node);
3494            self.physical_stable_ids[free_physical_id] = Self::pack_stable_id(stable_id);
3495            self.physical_warm_recycled_origins[free_physical_id] = false;
3496            free_physical_id
3497        } else {
3498            self.ensure_dense_node_storage_capacity();
3499            let physical_id = self.nodes.len();
3500            self.nodes.push(Some(node));
3501            self.physical_stable_ids
3502                .push(Self::pack_stable_id(stable_id));
3503            self.physical_warm_recycled_origins.push(false);
3504            physical_id
3505        };
3506
3507        self.next_stable_id = self.next_stable_id.max(stable_id.saturating_add(1));
3508        self.ensure_stable_index_capacity();
3509        self.stable_generations.entry(stable_id).or_insert(0);
3510        self.physical_stable_ids[physical_id] = Self::pack_stable_id(stable_id);
3511        self.stable_to_physical.insert(stable_id, physical_id);
3512    }
3513
3514    fn get_ref(&self, id: NodeId) -> Result<&dyn Node, NodeError> {
3515        if let Some(physical_id) = self.resolve_node_index(id) {
3516            let slot = self
3517                .nodes
3518                .get(physical_id)
3519                .ok_or(NodeError::Missing { id })?
3520                .as_deref()
3521                .ok_or(NodeError::Missing { id })?;
3522            return Ok(slot);
3523        }
3524
3525        self.high_id_nodes
3526            .get(&id)
3527            .map(|node| node.as_ref())
3528            .ok_or(NodeError::Missing { id })
3529    }
3530
3531    fn node_parent(&self, id: NodeId) -> Result<Option<NodeId>, NodeError> {
3532        Ok(self.get_ref(id)?.parent())
3533    }
3534
3535    fn collect_owned_children(
3536        &self,
3537        node_id: NodeId,
3538        out: &mut SmallVec<[NodeId; 8]>,
3539    ) -> Result<(), NodeError> {
3540        self.get_ref(node_id)?.collect_owned_children_into(out);
3541        out.retain(|child_id| {
3542            self.node_parent(*child_id)
3543                .map(|parent| parent == Some(node_id))
3544                .unwrap_or(false)
3545        });
3546        Ok(())
3547    }
3548
3549    fn remove_node_storage(&mut self, node_id: NodeId) -> Result<(), NodeError> {
3550        self.virtual_node_ids.remove(&node_id);
3551        if self.high_id_nodes.contains_key(&node_id) {
3552            if let Some(mut node) = self.high_id_nodes.remove(&node_id)
3553                && let Some(key) = node.recycle_key()
3554            {
3555                let recycle_pool_limit = node.recycle_pool_limit();
3556                let warm_origin = self
3557                    .high_id_warm_recycled_origins
3558                    .remove(&node_id)
3559                    .unwrap_or(false);
3560                node.prepare_for_recycle();
3561                self.push_recycled_node(
3562                    key,
3563                    recycle_pool_limit,
3564                    RecycledNode::new(node_id, node, warm_origin),
3565                );
3566            }
3567            let generation = self.high_id_generations.entry(node_id).or_insert(0);
3568            *generation = generation.wrapping_add(1);
3569            return Ok(());
3570        }
3571
3572        let physical_id = self
3573            .resolve_node_index(node_id)
3574            .ok_or(NodeError::Missing { id: node_id })?;
3575        if let Some(mut node) = self.nodes[physical_id].take()
3576            && let Some(key) = node.recycle_key()
3577        {
3578            let recycle_pool_limit = node.recycle_pool_limit();
3579            let warm_origin = self
3580                .physical_warm_recycled_origins
3581                .get_mut(physical_id)
3582                .map(std::mem::take)
3583                .unwrap_or(false);
3584            node.prepare_for_recycle();
3585            self.push_recycled_node(
3586                key,
3587                recycle_pool_limit,
3588                RecycledNode::new(node_id, node, warm_origin),
3589            );
3590        }
3591        self.physical_stable_ids[physical_id] = Self::INVALID_STABLE_ID;
3592        self.stable_to_physical.remove(&node_id);
3593        if let Some(generation) = self.stable_generations.get_mut(&node_id) {
3594            *generation = generation.wrapping_add(1);
3595        } else {
3596            self.stable_generations.insert(node_id, 1);
3597        }
3598        self.free_ids.push(Reverse(physical_id));
3599        Ok(())
3600    }
3601
3602    fn remove_subtree_postorder(&mut self, id: NodeId) -> Result<usize, NodeError> {
3603        self.get_ref(id)?;
3604
3605        let mut root_children = SmallVec::<[NodeId; 8]>::new();
3606        self.collect_owned_children(id, &mut root_children)?;
3607
3608        let mut stack = Vec::new();
3609        stack.push(RemovalFrame {
3610            node_id: id,
3611            children: root_children,
3612            next_child: 0,
3613        });
3614        let mut max_depth = stack.len();
3615
3616        while let Some(frame) = stack.last_mut() {
3617            if frame.next_child < frame.children.len() {
3618                let child_id = frame.children[frame.next_child];
3619                frame.next_child += 1;
3620
3621                if let Ok(child) = self.get_mut(child_id) {
3622                    child.on_removed_from_parent();
3623                    child.unmount();
3624                }
3625
3626                let mut child_children = SmallVec::<[NodeId; 8]>::new();
3627                self.collect_owned_children(child_id, &mut child_children)?;
3628                stack.push(RemovalFrame {
3629                    node_id: child_id,
3630                    children: child_children,
3631                    next_child: 0,
3632                });
3633                max_depth = max_depth.max(stack.len());
3634                continue;
3635            }
3636
3637            let node_id = frame.node_id;
3638            stack.pop();
3639            self.remove_node_storage(node_id)?;
3640        }
3641
3642        Ok(max_depth)
3643    }
3644
3645    #[cfg(test)]
3646    fn debug_remove_max_traversal_depth(&mut self, id: NodeId) -> Result<usize, NodeError> {
3647        self.remove_subtree_postorder(id)
3648    }
3649}
3650
3651impl Applier for MemoryApplier {
3652    fn record_structural_change(&mut self, parent_id: NodeId) {
3653        if self.structural_change_parents.last() != Some(&parent_id) {
3654            self.structural_change_parents.push(parent_id);
3655        }
3656    }
3657
3658    fn create(&mut self, node: Box<dyn Node>) -> NodeId {
3659        let stable_id = self.next_stable_id;
3660        self.next_stable_id = self.next_stable_id.saturating_add(1);
3661        if stable_id >= Self::HIGH_ID_THRESHOLD {
3662            self.insert_high_id_node(stable_id, node, false);
3663            return stable_id;
3664        }
3665
3666        self.ensure_stable_index_capacity();
3667        self.stable_generations.insert(stable_id, 0);
3668
3669        let physical_id = if let Some(Reverse(id)) = self.free_ids.pop() {
3670            debug_assert!(self.nodes[id].is_none(), "freelist entry {id} is not None");
3671            self.nodes[id] = Some(node);
3672            self.physical_stable_ids[id] = Self::pack_stable_id(stable_id);
3673            self.physical_warm_recycled_origins[id] = false;
3674            id
3675        } else {
3676            self.ensure_dense_node_storage_capacity();
3677            let id = self.nodes.len();
3678            self.nodes.push(Some(node));
3679            self.physical_stable_ids
3680                .push(Self::pack_stable_id(stable_id));
3681            self.physical_warm_recycled_origins.push(false);
3682            id
3683        };
3684        self.stable_to_physical.insert(stable_id, physical_id);
3685        stable_id
3686    }
3687
3688    fn node_generation(&self, id: NodeId) -> u32 {
3689        self.high_id_generations
3690            .get(&id)
3691            .copied()
3692            .or_else(|| self.stable_generations.get(&id).copied())
3693            .unwrap_or(0)
3694    }
3695
3696    fn get_mut(&mut self, id: NodeId) -> Result<&mut dyn Node, NodeError> {
3697        if let Some(physical_id) = self.resolve_node_index(id) {
3698            let slot = self.nodes[physical_id]
3699                .as_deref_mut()
3700                .ok_or(NodeError::Missing { id })?;
3701            return Ok(slot);
3702        }
3703        self.high_id_nodes
3704            .get_mut(&id)
3705            .map(|n| n.as_mut())
3706            .ok_or(NodeError::Missing { id })
3707    }
3708
3709    fn remove(&mut self, id: NodeId) -> Result<(), NodeError> {
3710        self.remove_subtree_postorder(id).map(|_| ())
3711    }
3712
3713    fn insert_with_id(&mut self, id: NodeId, node: Box<dyn Node>) -> Result<(), NodeError> {
3714        if self.contains_node_id(id) {
3715            return Err(NodeError::AlreadyExists { id });
3716        }
3717        self.insert_available_with_id(id, node);
3718        self.virtual_node_ids.insert(id);
3719        Ok(())
3720    }
3721
3722    fn insert_recycled_node_or_create(
3723        &mut self,
3724        stable_id: NodeId,
3725        node: Box<dyn Node>,
3726    ) -> RecycledNodeInsertion {
3727        if self.contains_node_id(stable_id) {
3728            let id = self.create(node);
3729            return RecycledNodeInsertion::fresh(
3730                id,
3731                Some(NodeError::AlreadyExists { id: stable_id }),
3732            );
3733        }
3734
3735        self.insert_available_with_id(stable_id, node);
3736        RecycledNodeInsertion::reused(stable_id)
3737    }
3738
3739    fn compact(&mut self) {
3740        let live_count = self.nodes.iter().filter(|slot| slot.is_some()).count();
3741        let tombstone_count = self.nodes.len().saturating_sub(live_count);
3742        if tombstone_count == 0 {
3743            return;
3744        }
3745        if self.nodes.len() > Self::EAGER_COMPACT_NODE_LEN && tombstone_count < live_count {
3746            return;
3747        }
3748        let rehouse_live_nodes = tombstone_count >= live_count;
3749        let mut packed_nodes = Vec::with_capacity(live_count);
3750        let mut packed_physical_stable_ids = Vec::with_capacity(live_count);
3751        let mut packed_warm_recycled_origins = Vec::with_capacity(live_count);
3752        let mut stable_to_physical = HashMap::default();
3753        stable_to_physical.reserve(live_count);
3754
3755        for physical_id in 0..self.nodes.len() {
3756            let Some(mut node) = self.nodes[physical_id].take() else {
3757                continue;
3758            };
3759            if rehouse_live_nodes && let Some(rehoused) = node.rehouse_for_live_compaction() {
3760                node = rehoused;
3761            }
3762            let stable_id = std::mem::replace(
3763                &mut self.physical_stable_ids[physical_id],
3764                Self::INVALID_STABLE_ID,
3765            );
3766            debug_assert_ne!(
3767                stable_id,
3768                Self::INVALID_STABLE_ID,
3769                "live physical slot must have a stable id",
3770            );
3771            let stable_id = Self::unpack_stable_id(stable_id);
3772            packed_nodes.push(Some(node));
3773            packed_physical_stable_ids.push(Self::pack_stable_id(stable_id));
3774            packed_warm_recycled_origins.push(self.physical_warm_recycled_origins[physical_id]);
3775            stable_to_physical.insert(stable_id, packed_nodes.len() - 1);
3776        }
3777
3778        self.nodes = packed_nodes;
3779        self.physical_stable_ids = packed_physical_stable_ids;
3780        self.physical_warm_recycled_origins = packed_warm_recycled_origins;
3781        self.free_ids = BinaryHeap::new();
3782        self.stable_to_physical = stable_to_physical;
3783        self.prune_stable_generations();
3784    }
3785
3786    fn take_recycled_node(&mut self, key: TypeId) -> Option<RecycledNode> {
3787        Self::take_recycled_node_from_pool(&mut self.returning_recycled_nodes, key)
3788            .or_else(|| Self::take_recycled_node_from_pool(&mut self.recycled_nodes, key))
3789    }
3790
3791    fn set_recycled_node_origin(&mut self, id: NodeId, warm_origin: bool) {
3792        if let Some(physical_id) = self.resolve_node_index(id) {
3793            self.physical_warm_recycled_origins[physical_id] = warm_origin;
3794        } else if self.high_id_nodes.contains_key(&id) {
3795            self.high_id_warm_recycled_origins.insert(id, warm_origin);
3796        }
3797    }
3798
3799    fn seed_recycled_node_shell(
3800        &mut self,
3801        key: TypeId,
3802        recycle_pool_limit: Option<usize>,
3803        shell: Box<dyn Node>,
3804    ) {
3805        self.seed_recycled_node_shell_impl(key, recycle_pool_limit, shell);
3806    }
3807
3808    fn record_fresh_recyclable_creation(&mut self, key: TypeId) {
3809        *self.fresh_recyclable_creations.entry(key).or_insert(0) += 1;
3810    }
3811
3812    fn clear_recycled_nodes(&mut self) {
3813        let returning = std::mem::take(&mut self.returning_recycled_nodes);
3814        for (key, mut nodes) in returning {
3815            let pool = self.recycled_nodes.entry(key).or_default();
3816            pool.append(&mut nodes);
3817        }
3818
3819        let fresh_recyclable_creations = std::mem::take(&mut self.fresh_recyclable_creations);
3820        let cold = std::mem::take(&mut self.cold_recycled_nodes);
3821        for (key, mut nodes) in cold {
3822            let needed = fresh_recyclable_creations.get(&key).copied().unwrap_or(0);
3823            if needed > 0 {
3824                let remaining_limit = self
3825                    .recycle_pool_limit_for(key)
3826                    .unwrap_or(usize::MAX)
3827                    .saturating_sub(self.warm_recycled_pool_len(key));
3828                let promote = nodes.len().min(needed).min(remaining_limit);
3829                let split_at = nodes.len().saturating_sub(promote);
3830                let promoted = nodes.split_off(split_at);
3831                for mut recycled in promoted {
3832                    recycled.set_warm_origin(true);
3833                    self.recycled_nodes.entry(key).or_default().push(recycled);
3834                }
3835            }
3836        }
3837
3838        let mut keys: HashSet<TypeId> = HashSet::default();
3839        keys.extend(self.recycled_nodes.keys().copied());
3840        keys.extend(self.recycled_node_limits.keys().copied());
3841        keys.extend(self.warm_recycled_node_targets.keys().copied());
3842        keys.extend(self.recycled_node_prototypes.keys().copied());
3843        for key in keys {
3844            let observed_demand = fresh_recyclable_creations.get(&key).copied().unwrap_or(0);
3845            let target = self.update_warm_recycled_node_target(key, observed_demand);
3846            self.replenish_warm_pool_to_target(key, target);
3847            self.trim_idle_warm_pool_to_target(key, target);
3848            self.compact_idle_warm_pool(key);
3849        }
3850        self.prune_stable_generations();
3851        self.compact();
3852    }
3853}
3854
3855pub trait ApplierHost {
3856    fn borrow_dyn(&self) -> RefMut<'_, dyn Applier>;
3857    /// Compact internal storage after commands have been applied.
3858    fn compact(&self) {}
3859}
3860
3861pub struct ConcreteApplierHost<A: Applier + 'static> {
3862    inner: RefCell<A>,
3863}
3864
3865impl<A: Applier + 'static> ConcreteApplierHost<A> {
3866    pub fn new(applier: A) -> Self {
3867        Self {
3868            inner: RefCell::new(applier),
3869        }
3870    }
3871
3872    pub fn borrow_typed(&self) -> RefMut<'_, A> {
3873        self.inner.borrow_mut()
3874    }
3875
3876    pub fn try_borrow_typed(&self) -> Result<RefMut<'_, A>, std::cell::BorrowMutError> {
3877        self.inner.try_borrow_mut()
3878    }
3879
3880    pub fn into_inner(self) -> A {
3881        self.inner.into_inner()
3882    }
3883}
3884
3885impl<A: Applier + 'static> ApplierHost for ConcreteApplierHost<A> {
3886    fn borrow_dyn(&self) -> RefMut<'_, dyn Applier> {
3887        RefMut::map(self.inner.borrow_mut(), |applier| {
3888            applier as &mut dyn Applier
3889        })
3890    }
3891
3892    fn compact(&self) {
3893        self.inner.borrow_mut().compact();
3894    }
3895}
3896
3897pub struct ApplierGuard<'a, A: Applier + 'static> {
3898    inner: RefMut<'a, A>,
3899}
3900
3901impl<'a, A: Applier + 'static> ApplierGuard<'a, A> {
3902    fn new(inner: RefMut<'a, A>) -> Self {
3903        Self { inner }
3904    }
3905}
3906
3907impl<'a, A: Applier + 'static> Deref for ApplierGuard<'a, A> {
3908    type Target = A;
3909
3910    fn deref(&self) -> &Self::Target {
3911        &self.inner
3912    }
3913}
3914
3915impl<'a, A: Applier + 'static> DerefMut for ApplierGuard<'a, A> {
3916    fn deref_mut(&mut self) -> &mut Self::Target {
3917        &mut self.inner
3918    }
3919}
3920
3921pub struct SlotsHost {
3922    storage_key: Cell<usize>,
3923    inner: RefCell<SlotsHostInner>,
3924}
3925
3926#[derive(Debug, Default)]
3927pub(crate) struct SlotPassOutcome {
3928    pub(crate) compacted: bool,
3929    pub(crate) compact_anchor_registry_storage: bool,
3930    pub(crate) compact_payload_storage: bool,
3931}
3932
3933#[derive(Default)]
3934pub(crate) struct FinishedSlotPass {
3935    pub(crate) outcome: SlotPassOutcome,
3936    pub(crate) detached_root_children: Vec<slot::DetachedSubtree>,
3937}
3938
3939struct ActivePassState {
3940    state: slot::SlotWriteSessionState,
3941}
3942
3943struct SlotsHostInner {
3944    table: SlotTable,
3945    nested_hosts: Vec<std::rc::Weak<SlotsHost>>,
3946    lifecycle: slot::SlotLifecycleCoordinator,
3947    runtime_state: Option<Rc<crate::composer::ComposerRuntimeState>>,
3948    active_pass: Option<ActivePassState>,
3949}
3950
3951impl Drop for SlotsHost {
3952    fn drop(&mut self) {
3953        let storage_key = self.storage_key.get();
3954        let inner = self.inner.get_mut();
3955        if let Some(state) = inner.runtime_state.clone() {
3956            if let Err(err) = state.dispose_retained_subtrees_for_host(
3957                storage_key,
3958                &mut inner.table,
3959                &mut inner.lifecycle,
3960            ) {
3961                log::error!(
3962                    "retained subtree disposal failed while dropping SlotsHost {storage_key}: {err}"
3963                );
3964                state.abandon_retained_subtrees_for_host(
3965                    storage_key,
3966                    &mut inner.table,
3967                    &mut inner.lifecycle,
3968                );
3969            } else {
3970                state.clear_host_storage_key(storage_key);
3971            }
3972        }
3973        inner.lifecycle.dispose_slot_table(&mut inner.table);
3974    }
3975}
3976
3977impl SlotsHost {
3978    pub fn storage_key(&self) -> usize {
3979        self.storage_key.get()
3980    }
3981
3982    pub fn new(storage: SlotTable) -> Self {
3983        let storage_key = storage.storage_id();
3984        Self {
3985            storage_key: Cell::new(storage_key),
3986            inner: RefCell::new(SlotsHostInner {
3987                table: storage,
3988                nested_hosts: Vec::new(),
3989                lifecycle: slot::SlotLifecycleCoordinator::default(),
3990                runtime_state: None,
3991                active_pass: None,
3992            }),
3993        }
3994    }
3995
3996    pub fn note_nested_host(&self, nested: &Rc<SlotsHost>) {
3997        let Ok(mut inner) = self.inner.try_borrow_mut() else {
3998            return;
3999        };
4000        inner.nested_hosts.retain(|held| held.upgrade().is_some());
4001        if inner
4002            .nested_hosts
4003            .iter()
4004            .any(|held| held.upgrade().is_some_and(|host| Rc::ptr_eq(&host, nested)))
4005        {
4006            return;
4007        }
4008        inner.nested_hosts.push(Rc::downgrade(nested));
4009    }
4010
4011    pub(crate) fn forget_effects(&self) -> bool {
4012        let (forgotten, nested, runtime_state) = {
4013            let Ok(mut inner) = self.inner.try_borrow_mut() else {
4014                return false;
4015            };
4016            if inner.active_pass.is_some() {
4017                return false;
4018            }
4019            let drops = inner.table.take_effect_drops();
4020            inner.nested_hosts.retain(|held| held.upgrade().is_some());
4021            let nested: Vec<Rc<SlotsHost>> = inner
4022                .nested_hosts
4023                .iter()
4024                .filter_map(std::rc::Weak::upgrade)
4025                .collect();
4026            (drops, nested, inner.runtime_state.clone())
4027        };
4028        let mut any = !forgotten.is_empty();
4029        drop(forgotten);
4030        for host in nested {
4031            any |= host.forget_effects();
4032        }
4033        if any && let Some(runtime_state) = runtime_state {
4034            runtime_state.force_recompose_host_scopes(self.storage_key());
4035        }
4036        any
4037    }
4038
4039    pub(crate) fn bind_runtime_state(&self, state: &Rc<crate::composer::ComposerRuntimeState>) {
4040        let mut inner = self.inner.borrow_mut();
4041        inner.runtime_state = Some(Rc::clone(state));
4042    }
4043
4044    pub(crate) fn rebind_orphaned_runtime_state(
4045        &self,
4046        state: &Rc<crate::composer::ComposerRuntimeState>,
4047    ) -> bool {
4048        let inner = self.inner.borrow();
4049        if inner.active_pass.is_some() {
4050            log::error!("cannot rebind SlotsHost during an active pass");
4051            return false;
4052        }
4053        let Some(bound_state) = inner.runtime_state.as_ref() else {
4054            drop(inner);
4055            self.bind_runtime_state(state);
4056            return true;
4057        };
4058        if Rc::ptr_eq(bound_state, state) {
4059            return true;
4060        }
4061        if bound_state.has_live_applier_host() {
4062            return false;
4063        }
4064        drop(inner);
4065
4066        let mut inner = self.inner.borrow_mut();
4067        let Some(bound_state) = inner.runtime_state.as_ref() else {
4068            inner.runtime_state = Some(Rc::clone(state));
4069            return true;
4070        };
4071        if Rc::ptr_eq(bound_state, state) {
4072            return true;
4073        }
4074        if bound_state.has_live_applier_host() {
4075            return false;
4076        }
4077
4078        let previous_state = Rc::clone(bound_state);
4079        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4080        lifecycle.flush_pending_drops();
4081        let host_key = self.storage_key();
4082        if previous_state
4083            .dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)
4084            .is_err()
4085        {
4086            inner.lifecycle = lifecycle;
4087            return false;
4088        }
4089        previous_state.clear_host(self);
4090        lifecycle.flush_pending_drops();
4091        inner.runtime_state = Some(Rc::clone(state));
4092        inner.lifecycle = lifecycle;
4093        true
4094    }
4095
4096    pub(crate) fn runtime_state(&self) -> Option<Rc<crate::composer::ComposerRuntimeState>> {
4097        self.inner.borrow().runtime_state.clone()
4098    }
4099
4100    pub(crate) fn borrow(&self) -> Ref<'_, SlotTable> {
4101        Ref::map(self.inner.borrow(), |inner| &inner.table)
4102    }
4103
4104    pub(crate) fn borrow_mut(&self) -> RefMut<'_, SlotTable> {
4105        RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.table)
4106    }
4107
4108    pub fn into_table(self: Rc<Self>) -> Result<SlotTable, NodeError> {
4109        if Rc::strong_count(&self) != 1 {
4110            return Err(NodeError::SlotHostUnavailable {
4111                operation: "SlotsHost::into_table",
4112                reason: "other host references are alive",
4113            });
4114        }
4115        self.take_table_for_transfer()
4116    }
4117
4118    fn take_table_for_transfer(&self) -> Result<SlotTable, NodeError> {
4119        let inner = self.inner.borrow();
4120        if inner.active_pass.is_some() {
4121            return Err(NodeError::SlotHostUnavailable {
4122                operation: "SlotsHost::into_table",
4123                reason: "slot pass is active",
4124            });
4125        }
4126        drop(inner);
4127        let mut inner = self.inner.borrow_mut();
4128        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4129        lifecycle.flush_pending_drops();
4130        if let Some(state) = inner.runtime_state.clone() {
4131            let host_key = self.storage_key();
4132            state.dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)?;
4133            state.clear_host(self);
4134            lifecycle.flush_pending_drops();
4135        }
4136        let taken = std::mem::take(&mut inner.table);
4137        self.storage_key.set(inner.table.storage_id());
4138        inner.runtime_state = None;
4139        inner.lifecycle = lifecycle;
4140        Ok(taken)
4141    }
4142
4143    pub fn reset(&self) -> Result<(), NodeError> {
4144        let inner = self.inner.borrow();
4145        if inner.active_pass.is_some() {
4146            return Err(NodeError::SlotHostUnavailable {
4147                operation: "SlotsHost::reset",
4148                reason: "slot pass is active",
4149            });
4150        }
4151        let runtime_state = inner.runtime_state.clone();
4152        drop(inner);
4153        let mut inner = self.inner.borrow_mut();
4154        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4155        if let Some(state) = runtime_state {
4156            let host_key = self.storage_key();
4157            state.dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)?;
4158            state.clear_host(self);
4159        }
4160        lifecycle.dispose_slot_table(&mut inner.table);
4161        inner.table = SlotTable::default();
4162        self.storage_key.set(inner.table.storage_id());
4163        inner.runtime_state = None;
4164        inner.lifecycle = slot::SlotLifecycleCoordinator::default();
4165        Ok(())
4166    }
4167
4168    pub(crate) fn abandon_after_apply_failure(&self) {
4169        let inner = self.inner.borrow();
4170        if inner.active_pass.is_some() {
4171            log::error!("cannot abandon SlotsHost during an active pass");
4172            return;
4173        }
4174        let runtime_state = inner.runtime_state.clone();
4175        drop(inner);
4176        let mut inner = self.inner.borrow_mut();
4177        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4178        if let Some(state) = runtime_state {
4179            let host_key = self.storage_key();
4180            state.abandon_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle);
4181        }
4182        lifecycle.dispose_slot_table(&mut inner.table);
4183        inner.table = SlotTable::default();
4184        self.storage_key.set(inner.table.storage_id());
4185        inner.runtime_state = None;
4186        inner.lifecycle = slot::SlotLifecycleCoordinator::default();
4187    }
4188
4189    pub(crate) fn debug_stats(&self) -> SlotTableDebugStats {
4190        let inner = self.inner.borrow();
4191        let local = inner.table.debug_stats();
4192        let lifecycle = inner.lifecycle.debug_stats();
4193        let retention = inner
4194            .runtime_state
4195            .clone()
4196            .map(|state| state.slot_retention_debug_stats(self))
4197            .unwrap_or_default();
4198        SlotTableDebugStats::from_parts(local, lifecycle, retention)
4199    }
4200
4201    pub(crate) fn debug_snapshot(&self) -> slot::SlotDebugSnapshot {
4202        let inner = self.inner.borrow();
4203        let mut snapshot = inner.table.debug_snapshot();
4204        if let Some(state) = inner.runtime_state.clone() {
4205            state.fill_slot_debug_snapshot(self, &mut snapshot);
4206        }
4207        snapshot
4208    }
4209
4210    pub(crate) fn begin_pass(&self, mode: slot::SlotPassMode) {
4211        let mut inner = self.inner.borrow_mut();
4212        if inner.active_pass.is_some() {
4213            log::error!("slot pass already active for host");
4214            return;
4215        }
4216        let mut state = slot::SlotWriteSessionState::default();
4217        state.reset_for_pass(mode);
4218        inner.active_pass = Some(ActivePassState { state });
4219    }
4220
4221    pub(crate) fn has_active_pass(&self) -> bool {
4222        self.inner.borrow().active_pass.is_some()
4223    }
4224
4225    pub(crate) fn try_push_branch_fold(&self, key: Key) -> Option<usize> {
4226        let mut inner = self.inner.try_borrow_mut().ok()?;
4227        let pass = inner.active_pass.as_mut()?;
4228        Some(pass.state.push_branch_fold(key))
4229    }
4230
4231    pub(crate) fn try_close_branch_fold(&self, token: usize) -> bool {
4232        let Ok(mut inner) = self.inner.try_borrow_mut() else {
4233            return false;
4234        };
4235        let Some(pass) = inner.active_pass.as_mut() else {
4236            return false;
4237        };
4238        pass.state.close_branch_fold(token);
4239        true
4240    }
4241
4242    pub(crate) fn abandon_active_pass(&self) {
4243        self.inner.borrow_mut().active_pass = None;
4244    }
4245
4246    pub(crate) fn with_write_session<R>(
4247        &self,
4248        f: impl FnOnce(&mut slot::SlotWriteSession<'_>) -> R,
4249    ) -> R {
4250        let mut inner = self.inner.borrow_mut();
4251        let SlotsHostInner {
4252            table,
4253            lifecycle,
4254            active_pass,
4255            ..
4256        } = &mut *inner;
4257        let active_pass = active_pass
4258            .as_mut()
4259            .expect("slot write session requires an active pass");
4260        let mut session = table.write_session(lifecycle, &mut active_pass.state);
4261        f(&mut session)
4262    }
4263
4264    pub(crate) fn with_table_and_lifecycle_mut<R>(
4265        &self,
4266        f: impl FnOnce(&mut SlotTable, &mut slot::SlotLifecycleCoordinator) -> R,
4267    ) -> R {
4268        let mut inner = self.inner.borrow_mut();
4269        let SlotsHostInner {
4270            table, lifecycle, ..
4271        } = &mut *inner;
4272        f(table, lifecycle)
4273    }
4274
4275    pub(crate) fn finish_pass(
4276        &self,
4277        applier: &mut dyn Applier,
4278    ) -> Result<FinishedSlotPass, NodeError> {
4279        let mut inner = self.inner.borrow_mut();
4280        let SlotsHostInner {
4281            table,
4282            lifecycle,
4283            active_pass: active_pass_slot,
4284            ..
4285        } = &mut *inner;
4286        let Some(mut active_pass) = active_pass_slot.take() else {
4287            return Ok(FinishedSlotPass::default());
4288        };
4289
4290        active_pass.state.flush_payload_location_refreshes(table);
4291
4292        #[cfg(debug_assertions)]
4293        if let Err(err) = active_pass.state.validate(table) {
4294            log::error!("slot writer invariant violation before finalize_pass: {err:?}");
4295            return Err(NodeError::SlotHostUnavailable {
4296                operation: "SlotsHost::finish_pass",
4297                reason: "slot writer invariant violation",
4298            });
4299        }
4300
4301        let detached_root_children = {
4302            let mut session = table.write_session(lifecycle, &mut active_pass.state);
4303            session.finalize_pass(applier)?
4304        };
4305
4306        Ok(FinishedSlotPass {
4307            outcome: SlotPassOutcome {
4308                compacted: active_pass.state.request_compaction,
4309                compact_anchor_registry_storage: active_pass
4310                    .state
4311                    .request_anchor_storage_compaction,
4312                compact_payload_storage: active_pass.state.request_payload_storage_compaction,
4313            },
4314            detached_root_children,
4315        })
4316    }
4317
4318    pub(crate) fn complete_pass_cleanup(&self, outcome: &SlotPassOutcome) {
4319        let mut inner = self.inner.borrow_mut();
4320        let SlotsHostInner {
4321            table,
4322            lifecycle,
4323            runtime_state,
4324            ..
4325        } = &mut *inner;
4326        lifecycle.flush_pending_drops();
4327        if outcome.compacted {
4328            table.compact_storage();
4329            lifecycle.compact_storage();
4330        }
4331        if let Some(state) = runtime_state.clone() {
4332            state.compact_table_identity_storage_for_host(
4333                self,
4334                table,
4335                outcome.compact_anchor_registry_storage,
4336                outcome.compact_payload_storage,
4337            );
4338        } else {
4339            if outcome.compact_anchor_registry_storage {
4340                table.compact_anchor_registry_storage(None);
4341            }
4342            if outcome.compact_payload_storage {
4343                table.compact_payload_anchor_registry_storage(None);
4344            }
4345        }
4346        table.assert_fast_integrity("slot pass cleanup");
4347        #[cfg(any(test, debug_assertions))]
4348        {
4349            table.debug_verify();
4350            if let Some(state) = runtime_state.clone() {
4351                state.debug_verify_host(self, table);
4352            }
4353        }
4354    }
4355}
4356
4357fn build_child_positions(children: &[NodeId]) -> HashMap<NodeId, usize> {
4358    let mut positions = HashMap::default();
4359    positions.reserve(children.len());
4360    for (index, &child) in children.iter().enumerate() {
4361        positions.insert(child, index);
4362    }
4363    positions
4364}
4365
4366fn refresh_child_positions(
4367    current: &[NodeId],
4368    positions: &mut HashMap<NodeId, usize>,
4369    start: usize,
4370    end: usize,
4371) {
4372    if current.is_empty() || start >= current.len() {
4373        return;
4374    }
4375    let end = end.min(current.len() - 1);
4376    for (offset, &child) in current[start..=end].iter().enumerate() {
4377        positions.insert(child, start + offset);
4378    }
4379}
4380
4381fn insert_child_into_diff_state(
4382    current: &mut ChildList,
4383    positions: &mut HashMap<NodeId, usize>,
4384    index: usize,
4385    child: NodeId,
4386) {
4387    let index = index.min(current.len());
4388    current.insert(index, child);
4389    refresh_child_positions(current, positions, index, current.len() - 1);
4390}
4391
4392fn move_child_in_diff_state(
4393    current: &mut ChildList,
4394    positions: &mut HashMap<NodeId, usize>,
4395    from_index: usize,
4396    target_index: usize,
4397) -> usize {
4398    let child = current.remove(from_index);
4399    let to_index = target_index.min(current.len());
4400    current.insert(to_index, child);
4401    refresh_child_positions(
4402        current,
4403        positions,
4404        from_index.min(to_index),
4405        from_index.max(to_index),
4406    );
4407    to_index
4408}
4409
4410pub(crate) use state::MutableStateInner;
4411pub use state::{
4412    MutableState, OwnedMutableState, SnapshotStateList, SnapshotStateMap, State,
4413    StateSubscriptionHold,
4414};
4415
4416fn hash_key<K: Hash>(key: &K) -> Key {
4417    let mut hasher = hash::default::new();
4418    key.hash(&mut hasher);
4419    hasher.finish()
4420}
4421
4422pub(crate) fn explicit_group_key_seed<K: Hash>(
4423    key: &K,
4424    caller: &'static std::panic::Location<'static>,
4425) -> slot::GroupKeySeed {
4426    let source_key = location_key(caller.file(), caller.line(), caller.column());
4427    let explicit_key = hash_key(key);
4428    slot::GroupKeySeed::keyed(source_key, explicit_key)
4429}
4430
4431#[cfg(test)]
4432#[path = "tests/mod.rs"]
4433mod tests;
4434
4435#[cfg(test)]
4436#[path = "tests/recursive_decrease_increase_test.rs"]
4437mod recursive_decrease_increase_test;
4438
4439pub mod collections;
4440pub mod hash;
4441
4442/// Where a test writes real files. Behind `test-helpers` so only a test build
4443/// of the workspace carries it.
4444#[cfg(any(test, feature = "test-helpers"))]
4445pub mod test_scratch;
4446#[cfg(any(test, feature = "test-helpers"))]
4447pub use test_scratch::test_scratch_dir;
4448
4449pub(crate) fn note_structural(reason: &str, parent_id: NodeId, child_id: NodeId) {
4450    if env_flag!("CRANPOSE_STRUCTURAL_DIAG") {
4451        eprintln!("[structural] {reason} parent={parent_id} child={child_id}");
4452    }
4453}
4454
4455pub(crate) fn note_structural_move(parent_id: NodeId, from_index: usize, to_index: usize) {
4456    if env_flag!("CRANPOSE_STRUCTURAL_DIAG") {
4457        eprintln!("[structural] move parent={parent_id} from={from_index} to={to_index}");
4458    }
4459}