Skip to main content

cranpose_core/
lib.rs

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