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    /// Called after the node is created to record its own ID.
1131    /// Useful for nodes that need to store their ID for later operations.
1132    fn set_node_id(&mut self, _id: NodeId) {}
1133    /// Called when this node is attached to a parent.
1134    /// Nodes with parent tracking should set their parent reference here.
1135    fn on_attached_to_parent(&mut self, _parent: NodeId) {}
1136    /// Called when this node is removed from its parent.
1137    /// Nodes with parent tracking should clear their parent reference here.
1138    fn on_removed_from_parent(&mut self) {}
1139    /// Get this node's parent ID (for nodes that track parents).
1140    /// Returns None if node has no parent or doesn't track parents.
1141    fn parent(&self) -> Option<NodeId> {
1142        None
1143    }
1144    /// Mark this node as needing layout (for nodes with dirty flags).
1145    /// Called during bubbling to propagate dirtiness up the tree.
1146    fn mark_needs_layout(&self) {}
1147    /// Check if this node needs layout (for nodes with dirty flags).
1148    fn needs_layout(&self) -> bool {
1149        false
1150    }
1151    /// Mark this node as needing measure (size may have changed).
1152    /// Called during bubbling when children are added/removed.
1153    fn mark_needs_measure(&self) {}
1154    /// Check if this node needs measure (for nodes with dirty flags).
1155    fn needs_measure(&self) -> bool {
1156        false
1157    }
1158    /// Mark this node as needing semantics recomputation.
1159    fn mark_needs_semantics(&self) {}
1160    /// Check if this node needs semantics recomputation.
1161    fn needs_semantics(&self) -> bool {
1162        false
1163    }
1164    /// Set parent reference for dirty flag bubbling ONLY.
1165    /// This is a minimal version of on_attached_to_parent that doesn't trigger
1166    /// registry updates or other side effects. Used during measurement when we
1167    /// need to establish parent connections for bubble_measure_dirty without
1168    /// causing the full attachment lifecycle.
1169    ///
1170    /// Default implementation uses the normal parent-attachment hook.
1171    fn set_parent_for_bubbling(&mut self, parent: NodeId) {
1172        self.on_attached_to_parent(parent);
1173    }
1174
1175    /// Returns a recycle pool key when this node supports shell reuse.
1176    fn recycle_key(&self) -> Option<TypeId> {
1177        None
1178    }
1179
1180    /// Bounds how many recyclable shells of this node type should be retained.
1181    fn recycle_pool_limit(&self) -> Option<usize> {
1182        None
1183    }
1184
1185    /// Clears live attachments before the node shell enters a recycle pool.
1186    fn prepare_for_recycle(&mut self) {}
1187
1188    /// Optionally provides a compact replacement box for this recycled shell.
1189    ///
1190    /// Returning `Some` lets nodes move pooled survivors onto fresh compact
1191    /// storage so the recycle pool does not pin large spike-era allocations.
1192    fn rehouse_for_recycle(&self) -> Option<Box<dyn Node>> {
1193        None
1194    }
1195
1196    /// Optionally moves a live node onto a fresh box during applier compaction.
1197    ///
1198    /// This is used after large-majority teardowns where a small surviving live
1199    /// tree can otherwise pin allocator pages from a much larger spike-era node
1200    /// population. Implementations must preserve the node's live state.
1201    fn rehouse_for_live_compaction(&mut self) -> Option<Box<dyn Node>> {
1202        None
1203    }
1204
1205    /// Returns the node-owned heap retained beyond the node's own box allocation.
1206    fn debug_heap_bytes(&self) -> usize {
1207        0
1208    }
1209}
1210
1211/// Unified API for bubbling layout dirty flags from a node to the root (Applier context).
1212///
1213/// This is the canonical function for dirty bubbling during the apply phase (structural changes).
1214/// Call this after mutations like insert/remove/move that happen during apply.
1215///
1216/// # Behavior
1217/// 1. Marks the starting node as needing layout
1218/// 2. Walks up the parent chain, marking each ancestor
1219/// 3. Stops when it reaches a node that's already dirty (O(1) optimization)
1220/// 4. Stops at the root (node with no parent)
1221///
1222/// # Performance
1223/// This function is O(height) in the worst case, but typically O(1) due to early exit
1224/// when encountering an already-dirty ancestor.
1225///
1226/// # Usage
1227/// - Call from composer mutations (insert/remove/move) during apply phase
1228/// - Call from applier-level operations that modify the tree structure
1229pub fn bubble_layout_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1230    bubble_layout_dirty_applier(applier, node_id);
1231}
1232
1233/// Unified API for bubbling measure dirty flags from a node to the root (Applier context).
1234///
1235/// Call this when a node's size may have changed (children added/removed, modifier changed).
1236/// This ensures that measure_layout will increment the cache epoch and re-measure the subtree.
1237///
1238/// # Behavior
1239/// 1. Marks the starting node as needing measure
1240/// 2. Walks up the parent chain, marking each ancestor
1241/// 3. Stops when it reaches a node that's already dirty (O(1) optimization)
1242/// 4. Stops at the root (node with no parent)
1243pub fn bubble_measure_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1244    bubble_measure_dirty_applier(applier, node_id);
1245}
1246
1247/// Unified API for bubbling semantics dirty flags from a node to the root (Applier context).
1248///
1249/// This mirrors [`bubble_layout_dirty`] but toggles semantics-specific dirty
1250/// flags instead of layout ones, allowing semantics updates to propagate during
1251/// the apply phase without forcing layout work.
1252pub fn bubble_semantics_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1253    bubble_semantics_dirty_applier(applier, node_id);
1254}
1255
1256/// Schedules semantics bubbling for a node using the active composer if present.
1257///
1258/// This defers the work to the apply phase where we can safely mutate the
1259/// applier tree without re-entrantly borrowing the composer during composition.
1260pub fn queue_semantics_invalidation(node_id: NodeId) {
1261    let _ = composer_context::try_with_composer(|composer| {
1262        composer.enqueue_semantics_invalidation(node_id);
1263    });
1264}
1265
1266/// Unified API for bubbling layout dirty flags from a node to the root (Composer context).
1267///
1268/// This is the canonical function for dirty bubbling during composition (property changes).
1269/// Call this after property changes that happen during composition via with_node_mut.
1270///
1271/// # Behavior
1272/// 1. Marks the starting node as needing layout
1273/// 2. Walks up the parent chain, marking each ancestor
1274/// 3. Stops when it reaches a node that's already dirty (O(1) optimization)
1275/// 4. Stops at the root (node with no parent)
1276///
1277/// # Performance
1278/// This function is O(height) in the worst case, but typically O(1) due to early exit
1279/// when encountering an already-dirty ancestor.
1280///
1281/// # Type Requirements
1282/// The node type N must implement Node (which includes mark_needs_layout, parent, etc.).
1283/// Typically this will be LayoutNode or similar layout-aware node types.
1284///
1285/// # Usage
1286/// - Call from property setters during composition (e.g., set_modifier, set_measure_policy)
1287/// - Call from widget composition when layout-affecting state changes
1288pub fn bubble_layout_dirty_in_composer<N: Node + 'static>(node_id: NodeId) {
1289    bubble_layout_dirty_composer::<N>(node_id);
1290}
1291
1292/// Unified API for bubbling measure dirty flags from a node to the root during composition.
1293///
1294/// This queues a dirty-bubble command on the active composer so measure invalidation
1295/// runs during the apply phase, avoiding re-entrant applier borrows while widgets are
1296/// mutating nodes via `with_node_mut`.
1297pub fn bubble_measure_dirty_in_composer(node_id: NodeId) {
1298    with_current_composer(|composer| {
1299        composer.commands_mut().push(Command::BubbleDirty {
1300            node_id,
1301            bubble: DirtyBubble {
1302                layout: false,
1303                measure: true,
1304                semantics: false,
1305            },
1306        });
1307    });
1308}
1309
1310/// Unified API for bubbling semantics dirty flags from a node to the root (Composer context).
1311///
1312/// This mirrors [`bubble_layout_dirty_in_composer`] but routes through the semantics
1313/// dirty flag instead of the layout one. Modifier nodes can request semantics
1314/// invalidations without triggering measure/layout work, and the runtime can
1315/// query the root to determine whether the semantics tree needs rebuilding.
1316pub fn bubble_semantics_dirty_in_composer<N: Node + 'static>(node_id: NodeId) {
1317    bubble_semantics_dirty_composer::<N>(node_id);
1318}
1319
1320/// Internal implementation for applier-based bubbling.
1321fn bubble_layout_dirty_applier(applier: &mut dyn Applier, mut node_id: NodeId) {
1322    // First, mark the starting node dirty (critical!)
1323    // This ensures root gets marked even if it has no parent
1324    if let Ok(node) = applier.get_mut(node_id) {
1325        node.mark_needs_layout();
1326    }
1327
1328    // Then bubble up to ancestors
1329    loop {
1330        // Get parent of current node
1331        let parent_id = match applier.get_mut(node_id) {
1332            Ok(node) => node.parent(),
1333            Err(_) => None,
1334        };
1335
1336        match parent_id {
1337            Some(pid) => {
1338                // Mark parent as needing layout
1339                if let Ok(parent) = applier.get_mut(pid) {
1340                    let parent_already_dirty = parent.needs_layout();
1341                    if !parent_already_dirty {
1342                        parent.mark_needs_layout();
1343                    }
1344                    node_id = pid;
1345                } else {
1346                    break;
1347                }
1348            }
1349            None => break, // No parent, stop
1350        }
1351    }
1352}
1353
1354/// Internal implementation for applier-based bubbling of measure dirtiness.
1355fn bubble_measure_dirty_applier(applier: &mut dyn Applier, mut node_id: NodeId) {
1356    // First, mark the starting node as needing measure
1357    if let Ok(node) = applier.get_mut(node_id) {
1358        node.mark_needs_measure();
1359    }
1360
1361    // Then bubble up to ancestors
1362    loop {
1363        // Get parent of current node
1364        let parent_id = match applier.get_mut(node_id) {
1365            Ok(node) => node.parent(),
1366            Err(_) => None,
1367        };
1368
1369        match parent_id {
1370            Some(pid) => {
1371                // Mark parent as needing measure
1372                if let Ok(parent) = applier.get_mut(pid) {
1373                    if !parent.needs_measure() {
1374                        parent.mark_needs_measure();
1375                    }
1376                    node_id = pid;
1377                } else {
1378                    break;
1379                }
1380            }
1381            None => {
1382                break; // No parent, stop
1383            }
1384        }
1385    }
1386}
1387
1388/// Internal implementation for applier-based bubbling of semantics dirtiness.
1389fn bubble_semantics_dirty_applier(applier: &mut dyn Applier, mut node_id: NodeId) {
1390    if let Ok(node) = applier.get_mut(node_id) {
1391        node.mark_needs_semantics();
1392    }
1393
1394    loop {
1395        let parent_id = match applier.get_mut(node_id) {
1396            Ok(node) => node.parent(),
1397            Err(_) => None,
1398        };
1399
1400        match parent_id {
1401            Some(pid) => {
1402                if let Ok(parent) = applier.get_mut(pid) {
1403                    if !parent.needs_semantics() {
1404                        parent.mark_needs_semantics();
1405                    }
1406                    node_id = pid;
1407                } else {
1408                    break;
1409                }
1410            }
1411            None => break,
1412        }
1413    }
1414}
1415
1416/// Internal implementation for composer-based bubbling.
1417/// This uses with_node_mut and works during composition with a concrete node type.
1418/// The node type N must implement Node (which includes mark_needs_layout, parent, etc.).
1419fn bubble_layout_dirty_composer<N: Node + 'static>(mut node_id: NodeId) {
1420    // Mark the starting node dirty
1421    let _ = with_node_mut(node_id, |node: &mut N| {
1422        node.mark_needs_layout();
1423    });
1424
1425    // Then bubble up to ancestors
1426    while let Ok(Some(pid)) = with_node_mut(node_id, |node: &mut N| node.parent()) {
1427        let parent_id = pid;
1428
1429        // Mark parent as needing layout
1430        let advanced = with_node_mut(parent_id, |node: &mut N| {
1431            if !node.needs_layout() {
1432                node.mark_needs_layout();
1433            }
1434            true
1435        })
1436        .unwrap_or(false);
1437
1438        if advanced {
1439            node_id = parent_id;
1440        } else {
1441            break;
1442        }
1443    }
1444}
1445
1446/// Internal implementation for composer-based bubbling of semantics dirtiness.
1447fn bubble_semantics_dirty_composer<N: Node + 'static>(mut node_id: NodeId) {
1448    // Mark the starting node semantics-dirty.
1449    let _ = with_node_mut(node_id, |node: &mut N| {
1450        node.mark_needs_semantics();
1451    });
1452
1453    while let Ok(Some(pid)) = with_node_mut(node_id, |node: &mut N| node.parent()) {
1454        let parent_id = pid;
1455
1456        let advanced = with_node_mut(parent_id, |node: &mut N| {
1457            if !node.needs_semantics() {
1458                node.mark_needs_semantics();
1459            }
1460            true
1461        })
1462        .unwrap_or(false);
1463
1464        if advanced {
1465            node_id = parent_id;
1466        } else {
1467            break;
1468        }
1469    }
1470}
1471
1472impl dyn Node {
1473    pub fn as_any_mut(&mut self) -> &mut dyn Any {
1474        self
1475    }
1476}
1477
1478pub struct RecycledNode {
1479    stable_id: NodeId,
1480    node: Box<dyn Node>,
1481    warm_origin: bool,
1482}
1483
1484impl RecycledNode {
1485    fn new(stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) -> Self {
1486        let node = node.rehouse_for_recycle().unwrap_or(node);
1487        Self {
1488            stable_id,
1489            node,
1490            warm_origin,
1491        }
1492    }
1493
1494    fn from_shell(stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) -> Self {
1495        Self {
1496            stable_id,
1497            node,
1498            warm_origin,
1499        }
1500    }
1501
1502    pub fn stable_id(&self) -> NodeId {
1503        self.stable_id
1504    }
1505
1506    fn warm_origin(&self) -> bool {
1507        self.warm_origin
1508    }
1509
1510    fn set_warm_origin(&mut self, warm_origin: bool) {
1511        self.warm_origin = warm_origin;
1512    }
1513
1514    pub fn node_mut(&mut self) -> &mut dyn Node {
1515        self.node.as_mut()
1516    }
1517
1518    pub fn into_parts(self) -> (NodeId, Box<dyn Node>, bool) {
1519        (self.stable_id, self.node, self.warm_origin)
1520    }
1521}
1522
1523#[derive(Debug, Clone, PartialEq, Eq)]
1524pub struct RecycledNodeInsertion {
1525    pub id: NodeId,
1526    pub stable_id_reused: bool,
1527    pub fallback_error: Option<NodeError>,
1528}
1529
1530impl RecycledNodeInsertion {
1531    fn reused(stable_id: NodeId) -> Self {
1532        Self {
1533            id: stable_id,
1534            stable_id_reused: true,
1535            fallback_error: None,
1536        }
1537    }
1538
1539    fn fresh(id: NodeId, fallback_error: Option<NodeError>) -> Self {
1540        Self {
1541            id,
1542            stable_id_reused: false,
1543            fallback_error,
1544        }
1545    }
1546}
1547
1548pub trait Applier: Any {
1549    fn create(&mut self, node: Box<dyn Node>) -> NodeId;
1550    fn get_mut(&mut self, id: NodeId) -> Result<&mut dyn Node, NodeError>;
1551    fn remove(&mut self, id: NodeId) -> Result<(), NodeError>;
1552
1553    /// Records that `parent_id`'s child list changed structurally this frame
1554    /// (insert, remove, move, or reparent). Incremental scene consumers drain
1555    /// the recorded parents and re-patch those subtrees so removed nodes are
1556    /// evicted from a persistent render graph even when the frame's scene
1557    /// update is otherwise scoped to unrelated dirty nodes.
1558    fn record_structural_change(&mut self, _parent_id: NodeId) {}
1559
1560    /// Returns the current generation for a node index.
1561    /// Generation is incremented when an index is reused from the freelist,
1562    /// preventing stale slot entries from matching recycled nodes.
1563    fn node_generation(&self, id: NodeId) -> u32;
1564
1565    /// Inserts a node with a pre-assigned ID.
1566    ///
1567    /// This is used for virtual nodes whose IDs are allocated separately
1568    /// (e.g., via allocate_virtual_node_id()). Unlike `create()` which assigns
1569    /// a new ID, this method uses the provided ID.
1570    ///
1571    /// Returns Ok(()) if successful, or an error if the ID is already in use.
1572    fn insert_with_id(&mut self, id: NodeId, node: Box<dyn Node>) -> Result<(), NodeError>;
1573
1574    /// Reinserts a recycled node at its retained stable ID, or creates a fresh ID if that
1575    /// retained ID is no longer available.
1576    fn insert_recycled_node_or_create(
1577        &mut self,
1578        stable_id: NodeId,
1579        node: Box<dyn Node>,
1580    ) -> RecycledNodeInsertion {
1581        let id = self.create(node);
1582        RecycledNodeInsertion::fresh(id, Some(NodeError::AlreadyExists { id: stable_id }))
1583    }
1584
1585    fn as_any(&self) -> &dyn Any
1586    where
1587        Self: Sized,
1588    {
1589        self
1590    }
1591
1592    fn as_any_mut(&mut self) -> &mut dyn Any
1593    where
1594        Self: Sized,
1595    {
1596        self
1597    }
1598
1599    /// Trim trailing tombstones/unused capacity after structural changes.
1600    fn compact(&mut self) {}
1601
1602    /// Returns a previously recycled node shell and its stable ID for the requested concrete type.
1603    fn take_recycled_node(&mut self, _key: TypeId) -> Option<RecycledNode> {
1604        None
1605    }
1606
1607    /// Marks whether a reinserted recycled node originated from the warm recycle path.
1608    fn set_recycled_node_origin(&mut self, _id: NodeId, _warm_origin: bool) {}
1609
1610    /// Seeds a warm recyclable shell for future reuse without requiring a prior removal.
1611    fn seed_recycled_node_shell(
1612        &mut self,
1613        _key: TypeId,
1614        _recycle_pool_limit: Option<usize>,
1615        _shell: Box<dyn Node>,
1616    ) {
1617    }
1618
1619    /// Records that the current apply pass had to allocate a fresh recyclable shell for this type.
1620    fn record_fresh_recyclable_creation(&mut self, _key: TypeId) {}
1621
1622    /// Drops any recyclable shells that should not survive beyond the current apply pass.
1623    fn clear_recycled_nodes(&mut self) {}
1624}
1625
1626type TypedNodeUpdate = fn(&mut dyn Node, NodeId) -> Result<(), NodeError>;
1627type CommandCallback = Box<dyn FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static>;
1628
1629#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1630pub(crate) struct DirtyBubble {
1631    layout: bool,
1632    measure: bool,
1633    semantics: bool,
1634}
1635
1636impl DirtyBubble {
1637    pub(crate) const LAYOUT_AND_MEASURE: Self = Self {
1638        layout: true,
1639        measure: true,
1640        semantics: false,
1641    };
1642
1643    pub(crate) const SEMANTICS: Self = Self {
1644        layout: false,
1645        measure: false,
1646        semantics: true,
1647    };
1648
1649    fn apply(self, applier: &mut dyn Applier, node_id: NodeId) {
1650        if self.layout {
1651            bubble_layout_dirty(applier, node_id);
1652        }
1653        if self.measure {
1654            bubble_measure_dirty(applier, node_id);
1655        }
1656        if self.semantics {
1657            bubble_semantics_dirty(applier, node_id);
1658        }
1659    }
1660}
1661
1662pub(crate) enum Command {
1663    BubbleDirty {
1664        node_id: NodeId,
1665        bubble: DirtyBubble,
1666    },
1667    UpdateTypedNode {
1668        id: NodeId,
1669        updater: TypedNodeUpdate,
1670    },
1671    RemoveNode {
1672        id: NodeId,
1673    },
1674    MountNode {
1675        id: NodeId,
1676    },
1677    AttachChild {
1678        parent_id: NodeId,
1679        child_id: NodeId,
1680        bubble: DirtyBubble,
1681    },
1682    InsertChild {
1683        parent_id: NodeId,
1684        child_id: NodeId,
1685        appended_index: usize,
1686        insert_index: usize,
1687        bubble: DirtyBubble,
1688    },
1689    MoveChild {
1690        parent_id: NodeId,
1691        from_index: usize,
1692        to_index: usize,
1693        bubble: DirtyBubble,
1694    },
1695    RemoveChild {
1696        parent_id: NodeId,
1697        child_id: NodeId,
1698    },
1699    DetachChild {
1700        parent_id: NodeId,
1701        child_id: NodeId,
1702    },
1703    SyncChildren {
1704        parent_id: NodeId,
1705        expected_children: ChildList,
1706    },
1707    Callback(CommandCallback),
1708}
1709
1710#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1711struct DeferredChildCleanup {
1712    child_id: NodeId,
1713    generation: u32,
1714    removed_from_parent: bool,
1715}
1716
1717#[derive(Default)]
1718struct DeferredChildCleanupQueue {
1719    pending: Vec<DeferredChildCleanup>,
1720    preserved: Vec<(NodeId, u32)>,
1721}
1722
1723impl DeferredChildCleanupQueue {
1724    fn push(&mut self, child_id: NodeId, generation: u32, removed_from_parent: bool) {
1725        if self
1726            .preserved
1727            .iter()
1728            .any(|&(preserved_id, preserved_generation)| {
1729                preserved_id == child_id && preserved_generation == generation
1730            })
1731        {
1732            return;
1733        }
1734        self.pending.push(DeferredChildCleanup {
1735            child_id,
1736            generation,
1737            removed_from_parent,
1738        });
1739    }
1740
1741    fn preserve(&mut self, child_id: NodeId, generation: u32) {
1742        if !self
1743            .preserved
1744            .iter()
1745            .any(|&(preserved_id, preserved_generation)| {
1746                preserved_id == child_id && preserved_generation == generation
1747            })
1748        {
1749            self.preserved.push((child_id, generation));
1750        }
1751        self.pending
1752            .retain(|cleanup| cleanup.child_id != child_id || cleanup.generation != generation);
1753    }
1754
1755    fn flush(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
1756        for cleanup in self.pending {
1757            cleanup_detached_child(applier, cleanup)?;
1758        }
1759        Ok(())
1760    }
1761}
1762
1763impl Command {
1764    pub(crate) fn update_node<N: Node + 'static>(id: NodeId) -> Self {
1765        Self::UpdateTypedNode {
1766            id,
1767            updater: update_typed_node::<N>,
1768        }
1769    }
1770
1771    pub(crate) fn callback(
1772        callback: impl FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static,
1773    ) -> Self {
1774        Self::Callback(Box::new(callback))
1775    }
1776
1777    pub(crate) fn apply(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
1778        let mut deferred_cleanup = DeferredChildCleanupQueue::default();
1779        self.apply_with_cleanup(applier, &mut deferred_cleanup)?;
1780        deferred_cleanup.flush(applier)
1781    }
1782
1783    fn apply_with_cleanup(
1784        self,
1785        applier: &mut dyn Applier,
1786        deferred_cleanup: &mut DeferredChildCleanupQueue,
1787    ) -> Result<(), NodeError> {
1788        match self {
1789            Self::BubbleDirty { node_id, bubble } => {
1790                bubble.apply(applier, node_id);
1791                Ok(())
1792            }
1793            Self::UpdateTypedNode { id, updater } => {
1794                let node = match applier.get_mut(id) {
1795                    Ok(node) => node,
1796                    Err(NodeError::Missing { .. }) => return Ok(()),
1797                    Err(err) => return Err(err),
1798                };
1799                updater(node, id)
1800            }
1801            Self::RemoveNode { id } => {
1802                if let Ok(node) = applier.get_mut(id) {
1803                    node.unmount();
1804                }
1805                match applier.remove(id) {
1806                    Ok(()) | Err(NodeError::Missing { .. }) => Ok(()),
1807                    Err(err) => Err(err),
1808                }
1809            }
1810            Self::MountNode { id } => {
1811                let node = match applier.get_mut(id) {
1812                    Ok(node) => node,
1813                    Err(NodeError::Missing { .. }) => return Ok(()),
1814                    Err(err) => return Err(err),
1815                };
1816                node.set_node_id(id);
1817                node.mount();
1818                Ok(())
1819            }
1820            Self::AttachChild {
1821                parent_id,
1822                child_id,
1823                bubble,
1824            } => {
1825                insert_child_with_reparenting(applier, parent_id, child_id);
1826                bubble.apply(applier, parent_id);
1827                Ok(())
1828            }
1829            Self::InsertChild {
1830                parent_id,
1831                child_id,
1832                appended_index,
1833                insert_index,
1834                bubble,
1835            } => {
1836                insert_child_with_reparenting(applier, parent_id, child_id);
1837                bubble.apply(applier, parent_id);
1838                if insert_index != appended_index {
1839                    if let Ok(parent_node) = applier.get_mut(parent_id) {
1840                        parent_node.move_child(appended_index, insert_index);
1841                    }
1842                }
1843                Ok(())
1844            }
1845            Self::MoveChild {
1846                parent_id,
1847                from_index,
1848                to_index,
1849                bubble,
1850            } => {
1851                if let Ok(parent_node) = applier.get_mut(parent_id) {
1852                    parent_node.move_child(from_index, to_index);
1853                }
1854                bubble.apply(applier, parent_id);
1855                applier.record_structural_change(parent_id);
1856                Ok(())
1857            }
1858            Self::RemoveChild {
1859                parent_id,
1860                child_id,
1861            } => apply_remove_child(applier, parent_id, child_id, deferred_cleanup),
1862            Self::DetachChild {
1863                parent_id,
1864                child_id,
1865            } => {
1866                let generation = applier.node_generation(child_id);
1867                detach_child_from_parent(applier, parent_id, child_id)?;
1868                deferred_cleanup.preserve(child_id, generation);
1869                Ok(())
1870            }
1871            Self::SyncChildren {
1872                parent_id,
1873                expected_children,
1874            } => sync_children(applier, parent_id, &expected_children, deferred_cleanup),
1875            Self::Callback(callback) => callback(applier),
1876        }
1877    }
1878}
1879
1880const COMMAND_CHUNK_CAPACITY: usize = 1024;
1881const COMMAND_FLUSH_THRESHOLD: usize = COMMAND_CHUNK_CAPACITY * 4;
1882type ChildList = SmallVec<[NodeId; 4]>;
1883const SMALL_CHILD_SYNC_LINEAR_THRESHOLD: usize = 8;
1884
1885#[derive(Copy, Clone)]
1886enum CommandTag {
1887    BubbleDirty,
1888    UpdateTypedNode,
1889    RemoveNode,
1890    MountNode,
1891    AttachChild,
1892    InsertChild,
1893    MoveChild,
1894    RemoveChild,
1895    DetachChild,
1896    SyncChildren,
1897    Callback,
1898}
1899
1900impl CommandTag {
1901    fn label(self) -> &'static str {
1902        match self {
1903            Self::BubbleDirty => "BubbleDirty",
1904            Self::UpdateTypedNode => "UpdateTypedNode",
1905            Self::RemoveNode => "RemoveNode",
1906            Self::MountNode => "MountNode",
1907            Self::AttachChild => "AttachChild",
1908            Self::InsertChild => "InsertChild",
1909            Self::MoveChild => "MoveChild",
1910            Self::RemoveChild => "RemoveChild",
1911            Self::DetachChild => "DetachChild",
1912            Self::SyncChildren => "SyncChildren",
1913            Self::Callback => "Callback",
1914        }
1915    }
1916}
1917
1918#[derive(Copy, Clone)]
1919struct BubbleDirtyCommand {
1920    node_id: NodeId,
1921    bubble: DirtyBubble,
1922}
1923
1924#[derive(Copy, Clone)]
1925struct UpdateTypedNodeCommand {
1926    id: NodeId,
1927    updater: TypedNodeUpdate,
1928}
1929
1930#[derive(Copy, Clone)]
1931struct AttachChildCommand {
1932    parent_id: NodeId,
1933    child_id: NodeId,
1934    bubble: DirtyBubble,
1935}
1936
1937#[derive(Copy, Clone)]
1938struct InsertChildCommand {
1939    parent_id: NodeId,
1940    child_id: NodeId,
1941    appended_index: usize,
1942    insert_index: usize,
1943    bubble: DirtyBubble,
1944}
1945
1946#[derive(Copy, Clone)]
1947struct MoveChildCommand {
1948    parent_id: NodeId,
1949    from_index: usize,
1950    to_index: usize,
1951    bubble: DirtyBubble,
1952}
1953
1954#[derive(Copy, Clone)]
1955struct RemoveChildCommand {
1956    parent_id: NodeId,
1957    child_id: NodeId,
1958}
1959
1960#[derive(Copy, Clone)]
1961struct DetachChildCommand {
1962    parent_id: NodeId,
1963    child_id: NodeId,
1964}
1965
1966struct SyncChildrenCommand {
1967    parent_id: NodeId,
1968    child_start: usize,
1969    child_len: usize,
1970}
1971
1972#[derive(Default)]
1973struct CommandQueue {
1974    chunks: Vec<Vec<CommandTag>>,
1975    len: usize,
1976    bubble_dirty: Vec<BubbleDirtyCommand>,
1977    update_typed_nodes: Vec<UpdateTypedNodeCommand>,
1978    remove_nodes: Vec<NodeId>,
1979    mount_nodes: Vec<NodeId>,
1980    attach_children: Vec<AttachChildCommand>,
1981    insert_children: Vec<InsertChildCommand>,
1982    move_children: Vec<MoveChildCommand>,
1983    remove_children: Vec<RemoveChildCommand>,
1984    detach_children: Vec<DetachChildCommand>,
1985    sync_children: Vec<SyncChildrenCommand>,
1986    sync_child_ids: Vec<NodeId>,
1987    callbacks: Vec<CommandCallback>,
1988}
1989
1990impl CommandQueue {
1991    fn push_tag(&mut self, tag: CommandTag) {
1992        let needs_chunk = self
1993            .chunks
1994            .last()
1995            .map(|chunk| chunk.len() == chunk.capacity())
1996            .unwrap_or(true);
1997        if needs_chunk {
1998            self.chunks.push(Vec::with_capacity(COMMAND_CHUNK_CAPACITY));
1999        }
2000        if let Some(chunk) = self.chunks.last_mut() {
2001            chunk.push(tag);
2002            self.len += 1;
2003        }
2004    }
2005
2006    fn push(&mut self, command: Command) {
2007        match command {
2008            Command::BubbleDirty { node_id, bubble } => {
2009                self.bubble_dirty
2010                    .push(BubbleDirtyCommand { node_id, bubble });
2011                self.push_tag(CommandTag::BubbleDirty);
2012            }
2013            Command::UpdateTypedNode { id, updater } => {
2014                self.update_typed_nodes
2015                    .push(UpdateTypedNodeCommand { id, updater });
2016                self.push_tag(CommandTag::UpdateTypedNode);
2017            }
2018            Command::RemoveNode { id } => {
2019                self.remove_nodes.push(id);
2020                self.push_tag(CommandTag::RemoveNode);
2021            }
2022            Command::MountNode { id } => {
2023                self.mount_nodes.push(id);
2024                self.push_tag(CommandTag::MountNode);
2025            }
2026            Command::AttachChild {
2027                parent_id,
2028                child_id,
2029                bubble,
2030            } => {
2031                self.attach_children.push(AttachChildCommand {
2032                    parent_id,
2033                    child_id,
2034                    bubble,
2035                });
2036                self.push_tag(CommandTag::AttachChild);
2037            }
2038            Command::InsertChild {
2039                parent_id,
2040                child_id,
2041                appended_index,
2042                insert_index,
2043                bubble,
2044            } => {
2045                self.insert_children.push(InsertChildCommand {
2046                    parent_id,
2047                    child_id,
2048                    appended_index,
2049                    insert_index,
2050                    bubble,
2051                });
2052                self.push_tag(CommandTag::InsertChild);
2053            }
2054            Command::MoveChild {
2055                parent_id,
2056                from_index,
2057                to_index,
2058                bubble,
2059            } => {
2060                self.move_children.push(MoveChildCommand {
2061                    parent_id,
2062                    from_index,
2063                    to_index,
2064                    bubble,
2065                });
2066                self.push_tag(CommandTag::MoveChild);
2067            }
2068            Command::RemoveChild {
2069                parent_id,
2070                child_id,
2071            } => {
2072                self.remove_children.push(RemoveChildCommand {
2073                    parent_id,
2074                    child_id,
2075                });
2076                self.push_tag(CommandTag::RemoveChild);
2077            }
2078            Command::DetachChild {
2079                parent_id,
2080                child_id,
2081            } => {
2082                self.detach_children.push(DetachChildCommand {
2083                    parent_id,
2084                    child_id,
2085                });
2086                self.push_tag(CommandTag::DetachChild);
2087            }
2088            Command::SyncChildren {
2089                parent_id,
2090                expected_children,
2091            } => {
2092                let child_start = self.sync_child_ids.len();
2093                let child_len = expected_children.len();
2094                self.sync_child_ids.extend(expected_children);
2095                self.sync_children.push(SyncChildrenCommand {
2096                    parent_id,
2097                    child_start,
2098                    child_len,
2099                });
2100                self.push_tag(CommandTag::SyncChildren);
2101            }
2102            Command::Callback(callback) => {
2103                self.callbacks.push(callback);
2104                self.push_tag(CommandTag::Callback);
2105            }
2106        }
2107    }
2108
2109    fn len(&self) -> usize {
2110        self.len
2111    }
2112
2113    fn capacity(&self) -> usize {
2114        self.chunks.iter().map(Vec::capacity).sum()
2115    }
2116
2117    fn payload_len_bytes(&self) -> usize {
2118        self.bubble_dirty
2119            .len()
2120            .saturating_mul(std::mem::size_of::<BubbleDirtyCommand>())
2121            .saturating_add(
2122                self.update_typed_nodes
2123                    .len()
2124                    .saturating_mul(std::mem::size_of::<UpdateTypedNodeCommand>()),
2125            )
2126            .saturating_add(
2127                self.remove_nodes
2128                    .len()
2129                    .saturating_mul(std::mem::size_of::<NodeId>()),
2130            )
2131            .saturating_add(
2132                self.mount_nodes
2133                    .len()
2134                    .saturating_mul(std::mem::size_of::<NodeId>()),
2135            )
2136            .saturating_add(
2137                self.attach_children
2138                    .len()
2139                    .saturating_mul(std::mem::size_of::<AttachChildCommand>()),
2140            )
2141            .saturating_add(
2142                self.insert_children
2143                    .len()
2144                    .saturating_mul(std::mem::size_of::<InsertChildCommand>()),
2145            )
2146            .saturating_add(
2147                self.move_children
2148                    .len()
2149                    .saturating_mul(std::mem::size_of::<MoveChildCommand>()),
2150            )
2151            .saturating_add(
2152                self.remove_children
2153                    .len()
2154                    .saturating_mul(std::mem::size_of::<RemoveChildCommand>()),
2155            )
2156            .saturating_add(
2157                self.detach_children
2158                    .len()
2159                    .saturating_mul(std::mem::size_of::<DetachChildCommand>()),
2160            )
2161            .saturating_add(
2162                self.sync_children
2163                    .len()
2164                    .saturating_mul(std::mem::size_of::<SyncChildrenCommand>()),
2165            )
2166            .saturating_add(
2167                self.sync_child_ids
2168                    .len()
2169                    .saturating_mul(std::mem::size_of::<NodeId>()),
2170            )
2171            .saturating_add(
2172                self.callbacks
2173                    .len()
2174                    .saturating_mul(std::mem::size_of::<CommandCallback>()),
2175            )
2176    }
2177
2178    fn payload_capacity_bytes(&self) -> usize {
2179        self.bubble_dirty
2180            .capacity()
2181            .saturating_mul(std::mem::size_of::<BubbleDirtyCommand>())
2182            .saturating_add(
2183                self.update_typed_nodes
2184                    .capacity()
2185                    .saturating_mul(std::mem::size_of::<UpdateTypedNodeCommand>()),
2186            )
2187            .saturating_add(
2188                self.remove_nodes
2189                    .capacity()
2190                    .saturating_mul(std::mem::size_of::<NodeId>()),
2191            )
2192            .saturating_add(
2193                self.mount_nodes
2194                    .capacity()
2195                    .saturating_mul(std::mem::size_of::<NodeId>()),
2196            )
2197            .saturating_add(
2198                self.attach_children
2199                    .capacity()
2200                    .saturating_mul(std::mem::size_of::<AttachChildCommand>()),
2201            )
2202            .saturating_add(
2203                self.insert_children
2204                    .capacity()
2205                    .saturating_mul(std::mem::size_of::<InsertChildCommand>()),
2206            )
2207            .saturating_add(
2208                self.move_children
2209                    .capacity()
2210                    .saturating_mul(std::mem::size_of::<MoveChildCommand>()),
2211            )
2212            .saturating_add(
2213                self.remove_children
2214                    .capacity()
2215                    .saturating_mul(std::mem::size_of::<RemoveChildCommand>()),
2216            )
2217            .saturating_add(
2218                self.detach_children
2219                    .capacity()
2220                    .saturating_mul(std::mem::size_of::<DetachChildCommand>()),
2221            )
2222            .saturating_add(
2223                self.sync_children
2224                    .capacity()
2225                    .saturating_mul(std::mem::size_of::<SyncChildrenCommand>()),
2226            )
2227            .saturating_add(
2228                self.sync_child_ids
2229                    .capacity()
2230                    .saturating_mul(std::mem::size_of::<NodeId>()),
2231            )
2232            .saturating_add(
2233                self.callbacks
2234                    .capacity()
2235                    .saturating_mul(std::mem::size_of::<CommandCallback>()),
2236            )
2237    }
2238
2239    fn apply(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
2240        let mut bubble_dirty = self.bubble_dirty.into_iter();
2241        let mut update_typed_nodes = self.update_typed_nodes.into_iter();
2242        let mut remove_nodes = self.remove_nodes.into_iter();
2243        let mut mount_nodes = self.mount_nodes.into_iter();
2244        let mut attach_children = self.attach_children.into_iter();
2245        let mut insert_children = self.insert_children.into_iter();
2246        let mut move_children = self.move_children.into_iter();
2247        let mut remove_children = self.remove_children.into_iter();
2248        let mut detach_children = self.detach_children.into_iter();
2249        let mut sync_children_commands = self.sync_children.into_iter();
2250        let sync_child_ids = self.sync_child_ids;
2251        let mut callbacks = self.callbacks.into_iter();
2252        let mut deferred_cleanup = DeferredChildCleanupQueue::default();
2253
2254        for chunk in self.chunks {
2255            for tag in chunk {
2256                match tag {
2257                    CommandTag::BubbleDirty => {
2258                        let BubbleDirtyCommand { node_id, bubble } =
2259                            next_command_payload(&mut bubble_dirty, tag)?;
2260                        Command::BubbleDirty { node_id, bubble }
2261                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2262                    }
2263                    CommandTag::UpdateTypedNode => {
2264                        let UpdateTypedNodeCommand { id, updater } =
2265                            next_command_payload(&mut update_typed_nodes, tag)?;
2266                        Command::UpdateTypedNode { id, updater }
2267                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2268                    }
2269                    CommandTag::RemoveNode => {
2270                        let id = next_command_payload(&mut remove_nodes, tag)?;
2271                        Command::RemoveNode { id }
2272                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2273                    }
2274                    CommandTag::MountNode => {
2275                        let id = next_command_payload(&mut mount_nodes, tag)?;
2276                        Command::MountNode { id }
2277                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2278                    }
2279                    CommandTag::AttachChild => {
2280                        let AttachChildCommand {
2281                            parent_id,
2282                            child_id,
2283                            bubble,
2284                        } = next_command_payload(&mut attach_children, tag)?;
2285                        Command::AttachChild {
2286                            parent_id,
2287                            child_id,
2288                            bubble,
2289                        }
2290                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2291                    }
2292                    CommandTag::InsertChild => {
2293                        let InsertChildCommand {
2294                            parent_id,
2295                            child_id,
2296                            appended_index,
2297                            insert_index,
2298                            bubble,
2299                        } = next_command_payload(&mut insert_children, tag)?;
2300                        Command::InsertChild {
2301                            parent_id,
2302                            child_id,
2303                            appended_index,
2304                            insert_index,
2305                            bubble,
2306                        }
2307                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2308                    }
2309                    CommandTag::MoveChild => {
2310                        let MoveChildCommand {
2311                            parent_id,
2312                            from_index,
2313                            to_index,
2314                            bubble,
2315                        } = next_command_payload(&mut move_children, tag)?;
2316                        Command::MoveChild {
2317                            parent_id,
2318                            from_index,
2319                            to_index,
2320                            bubble,
2321                        }
2322                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2323                    }
2324                    CommandTag::RemoveChild => {
2325                        let RemoveChildCommand {
2326                            parent_id,
2327                            child_id,
2328                        } = next_command_payload(&mut remove_children, tag)?;
2329                        Command::RemoveChild {
2330                            parent_id,
2331                            child_id,
2332                        }
2333                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2334                    }
2335                    CommandTag::DetachChild => {
2336                        let DetachChildCommand {
2337                            parent_id,
2338                            child_id,
2339                        } = next_command_payload(&mut detach_children, tag)?;
2340                        Command::DetachChild {
2341                            parent_id,
2342                            child_id,
2343                        }
2344                        .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2345                    }
2346                    CommandTag::SyncChildren => {
2347                        let SyncChildrenCommand {
2348                            parent_id,
2349                            child_start,
2350                            child_len,
2351                        } = next_command_payload(&mut sync_children_commands, tag)?;
2352                        let child_end = child_start
2353                            .checked_add(child_len)
2354                            .ok_or_else(|| command_payload_error(tag))?;
2355                        let expected_children = sync_child_ids
2356                            .get(child_start..child_end)
2357                            .ok_or_else(|| command_payload_error(tag))?;
2358                        sync_children(
2359                            applier,
2360                            parent_id,
2361                            expected_children,
2362                            &mut deferred_cleanup,
2363                        )?;
2364                    }
2365                    CommandTag::Callback => {
2366                        let callback = next_command_payload(&mut callbacks, tag)?;
2367                        Command::Callback(callback)
2368                            .apply_with_cleanup(applier, &mut deferred_cleanup)?;
2369                    }
2370                }
2371            }
2372        }
2373
2374        debug_assert!(bubble_dirty.next().is_none());
2375        debug_assert!(update_typed_nodes.next().is_none());
2376        debug_assert!(remove_nodes.next().is_none());
2377        debug_assert!(mount_nodes.next().is_none());
2378        debug_assert!(attach_children.next().is_none());
2379        debug_assert!(insert_children.next().is_none());
2380        debug_assert!(move_children.next().is_none());
2381        debug_assert!(remove_children.next().is_none());
2382        debug_assert!(detach_children.next().is_none());
2383        debug_assert!(sync_children_commands.next().is_none());
2384        debug_assert!(callbacks.next().is_none());
2385        deferred_cleanup.flush(applier)
2386    }
2387}
2388
2389fn command_payload_error(tag: CommandTag) -> NodeError {
2390    NodeError::MalformedCommandPayload { tag: tag.label() }
2391}
2392
2393fn next_command_payload<T>(
2394    payloads: &mut impl Iterator<Item = T>,
2395    tag: CommandTag,
2396) -> Result<T, NodeError> {
2397    payloads.next().ok_or_else(|| command_payload_error(tag))
2398}
2399
2400fn update_typed_node<N: Node + 'static>(node: &mut dyn Node, id: NodeId) -> Result<(), NodeError> {
2401    let typed = node
2402        .as_any_mut()
2403        .downcast_mut::<N>()
2404        .ok_or(NodeError::TypeMismatch {
2405            id,
2406            expected: std::any::type_name::<N>(),
2407        })?;
2408    typed.update();
2409    Ok(())
2410}
2411
2412fn insert_child_with_reparenting(applier: &mut dyn Applier, parent_id: NodeId, child_id: NodeId) {
2413    if parent_id == child_id {
2414        debug_assert_ne!(
2415            parent_id, child_id,
2416            "a node cannot be attached as its own child"
2417        );
2418        return;
2419    }
2420
2421    let old_parent = applier
2422        .get_mut(child_id)
2423        .ok()
2424        .and_then(|node| node.parent());
2425    if let Some(old_parent_id) = old_parent {
2426        if old_parent_id != parent_id {
2427            if let Ok(old_parent_node) = applier.get_mut(old_parent_id) {
2428                old_parent_node.remove_child(child_id);
2429            }
2430            if let Ok(child_node) = applier.get_mut(child_id) {
2431                child_node.on_removed_from_parent();
2432            }
2433            bubble_layout_dirty(applier, old_parent_id);
2434            bubble_measure_dirty(applier, old_parent_id);
2435            applier.record_structural_change(old_parent_id);
2436        }
2437    }
2438
2439    if let Ok(parent_node) = applier.get_mut(parent_id) {
2440        parent_node.insert_child(child_id);
2441    }
2442    applier.record_structural_change(parent_id);
2443    if let Ok(child_node) = applier.get_mut(child_id) {
2444        child_node.on_attached_to_parent(parent_id);
2445    }
2446}
2447
2448fn apply_remove_child(
2449    applier: &mut dyn Applier,
2450    parent_id: NodeId,
2451    child_id: NodeId,
2452    deferred_cleanup: &mut DeferredChildCleanupQueue,
2453) -> Result<(), NodeError> {
2454    detach_child_from_parent(applier, parent_id, child_id)?;
2455
2456    let generation = applier.node_generation(child_id);
2457    let removed_from_parent = if let Ok(node) = applier.get_mut(child_id) {
2458        node.parent().is_none()
2459    } else {
2460        return Ok(());
2461    };
2462    deferred_cleanup.push(child_id, generation, removed_from_parent);
2463    Ok(())
2464}
2465
2466fn detach_child_from_parent(
2467    applier: &mut dyn Applier,
2468    parent_id: NodeId,
2469    child_id: NodeId,
2470) -> Result<(), NodeError> {
2471    if let Ok(parent_node) = applier.get_mut(parent_id) {
2472        parent_node.remove_child(child_id);
2473    }
2474    bubble_layout_dirty(applier, parent_id);
2475    bubble_measure_dirty(applier, parent_id);
2476    applier.record_structural_change(parent_id);
2477
2478    if let Ok(node) = applier.get_mut(child_id) {
2479        match node.parent() {
2480            Some(existing_parent_id) if existing_parent_id == parent_id => {
2481                node.on_removed_from_parent();
2482            }
2483            None => {}
2484            Some(_) => return Ok(()),
2485        }
2486    } else {
2487        return Ok(());
2488    }
2489
2490    Ok(())
2491}
2492
2493fn cleanup_detached_child(
2494    applier: &mut dyn Applier,
2495    cleanup: DeferredChildCleanup,
2496) -> Result<(), NodeError> {
2497    if applier.node_generation(cleanup.child_id) != cleanup.generation {
2498        return Ok(());
2499    }
2500
2501    let parent_id = match applier.get_mut(cleanup.child_id) {
2502        Ok(node) => node.parent(),
2503        Err(NodeError::Missing { .. }) => return Ok(()),
2504        Err(err) => return Err(err),
2505    };
2506    if parent_id.is_some() {
2507        return Ok(());
2508    }
2509
2510    if let Ok(node) = applier.get_mut(cleanup.child_id) {
2511        if !cleanup.removed_from_parent {
2512            node.on_removed_from_parent();
2513        }
2514        node.unmount();
2515    }
2516    match applier.remove(cleanup.child_id) {
2517        Ok(()) | Err(NodeError::Missing { .. }) => Ok(()),
2518        Err(err) => Err(err),
2519    }
2520}
2521
2522fn remove_child_and_cleanup_now(
2523    applier: &mut dyn Applier,
2524    parent_id: NodeId,
2525    child_id: NodeId,
2526) -> Result<(), NodeError> {
2527    let mut deferred_cleanup = DeferredChildCleanupQueue::default();
2528    apply_remove_child(applier, parent_id, child_id, &mut deferred_cleanup)?;
2529    deferred_cleanup.flush(applier)
2530}
2531
2532fn collect_current_children(applier: &mut dyn Applier, parent_id: NodeId) -> ChildList {
2533    let mut scratch = SmallVec::<[NodeId; 8]>::new();
2534    if let Ok(node) = applier.get_mut(parent_id) {
2535        node.collect_children_into(&mut scratch);
2536    }
2537    let mut current = ChildList::new();
2538    current.extend(scratch);
2539    current
2540}
2541
2542fn sync_children(
2543    applier: &mut dyn Applier,
2544    parent_id: NodeId,
2545    expected_children: &[NodeId],
2546    deferred_cleanup: &mut DeferredChildCleanupQueue,
2547) -> Result<(), NodeError> {
2548    let mut current = collect_current_children(applier, parent_id);
2549    let children_changed = current.as_slice() != expected_children;
2550
2551    if children_changed {
2552        if current.len().max(expected_children.len()) <= SMALL_CHILD_SYNC_LINEAR_THRESHOLD {
2553            sync_children_small(
2554                applier,
2555                parent_id,
2556                &mut current,
2557                expected_children,
2558                deferred_cleanup,
2559            )?;
2560        } else {
2561            let mut target_positions: HashMap<NodeId, usize> = HashMap::default();
2562            target_positions.reserve(expected_children.len());
2563            for (index, &child) in expected_children.iter().enumerate() {
2564                target_positions.insert(child, index);
2565            }
2566
2567            for index in (0..current.len()).rev() {
2568                let child = current[index];
2569                if !target_positions.contains_key(&child) {
2570                    current.remove(index);
2571                    apply_remove_child(applier, parent_id, child, deferred_cleanup)?;
2572                }
2573            }
2574
2575            let mut current_positions = build_child_positions(&current);
2576            for (target_index, &child) in expected_children.iter().enumerate() {
2577                if let Some(current_index) = current_positions.get(&child).copied() {
2578                    if current_index != target_index {
2579                        let from_index = current_index;
2580                        let to_index = move_child_in_diff_state(
2581                            &mut current,
2582                            &mut current_positions,
2583                            from_index,
2584                            target_index,
2585                        );
2586                        Command::MoveChild {
2587                            parent_id,
2588                            from_index,
2589                            to_index,
2590                            bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2591                        }
2592                        .apply(applier)?;
2593                    }
2594                } else {
2595                    let insert_index = target_index.min(current.len());
2596                    let appended_index = current.len();
2597                    insert_child_into_diff_state(
2598                        &mut current,
2599                        &mut current_positions,
2600                        insert_index,
2601                        child,
2602                    );
2603                    Command::InsertChild {
2604                        parent_id,
2605                        child_id: child,
2606                        appended_index,
2607                        insert_index,
2608                        bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2609                    }
2610                    .apply(applier)?;
2611                }
2612            }
2613        }
2614    }
2615
2616    reconcile_children(applier, parent_id, expected_children, !children_changed)
2617}
2618
2619fn sync_children_small(
2620    applier: &mut dyn Applier,
2621    parent_id: NodeId,
2622    current: &mut ChildList,
2623    expected_children: &[NodeId],
2624    deferred_cleanup: &mut DeferredChildCleanupQueue,
2625) -> Result<(), NodeError> {
2626    for index in (0..current.len()).rev() {
2627        let child = current[index];
2628        if !expected_children.contains(&child) {
2629            current.remove(index);
2630            apply_remove_child(applier, parent_id, child, deferred_cleanup)?;
2631        }
2632    }
2633
2634    for (target_index, &child) in expected_children.iter().enumerate() {
2635        if let Some(current_index) = current
2636            .iter()
2637            .position(|&current_child| current_child == child)
2638        {
2639            if current_index != target_index {
2640                let child = current.remove(current_index);
2641                let to_index = target_index.min(current.len());
2642                current.insert(to_index, child);
2643                Command::MoveChild {
2644                    parent_id,
2645                    from_index: current_index,
2646                    to_index,
2647                    bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2648                }
2649                .apply(applier)?;
2650            }
2651        } else {
2652            let insert_index = target_index.min(current.len());
2653            let appended_index = current.len();
2654            current.insert(insert_index, child);
2655            Command::InsertChild {
2656                parent_id,
2657                child_id: child,
2658                appended_index,
2659                insert_index,
2660                bubble: DirtyBubble::LAYOUT_AND_MEASURE,
2661            }
2662            .apply(applier)?;
2663        }
2664    }
2665
2666    Ok(())
2667}
2668
2669fn reconcile_children(
2670    applier: &mut dyn Applier,
2671    parent_id: NodeId,
2672    expected_children: &[NodeId],
2673    needs_dirty_check: bool,
2674) -> Result<(), NodeError> {
2675    let mut repaired = false;
2676    for &child_id in expected_children {
2677        let needs_attach = if let Ok(node) = applier.get_mut(child_id) {
2678            node.parent() != Some(parent_id)
2679        } else {
2680            false
2681        };
2682
2683        if needs_attach {
2684            insert_child_with_reparenting(applier, parent_id, child_id);
2685            repaired = true;
2686        }
2687    }
2688
2689    let is_dirty = if needs_dirty_check {
2690        if let Ok(node) = applier.get_mut(parent_id) {
2691            node.needs_layout()
2692        } else {
2693            false
2694        }
2695    } else {
2696        false
2697    };
2698
2699    if repaired {
2700        bubble_layout_dirty(applier, parent_id);
2701        bubble_measure_dirty(applier, parent_id);
2702    } else if is_dirty {
2703        bubble_layout_dirty(applier, parent_id);
2704    }
2705
2706    Ok(())
2707}
2708
2709#[derive(Default)]
2710pub struct MemoryApplier {
2711    nodes: Vec<Option<Box<dyn Node>>>,
2712    /// Stable node ID occupying each physical slot.
2713    physical_stable_ids: Vec<u32>,
2714    physical_warm_recycled_origins: Vec<bool>,
2715    /// Physical storage location for each live stable node ID.
2716    stable_to_physical: HashMap<NodeId, usize>,
2717    /// Generation counter per stable node ID.
2718    stable_generations: HashMap<NodeId, u32>,
2719    /// Lowest free physical slots are reused first so the dense storage stays compact.
2720    free_ids: BinaryHeap<Reverse<usize>>,
2721    /// Storage for high-ID nodes (like virtual nodes with IDs starting at 0xFFFFFFFF00000000)
2722    /// that can't be stored in the Vec without causing capacity overflow.
2723    high_id_nodes: HashMap<NodeId, Box<dyn Node>>,
2724    high_id_warm_recycled_origins: HashMap<NodeId, bool>,
2725    high_id_generations: HashMap<NodeId, u32>,
2726    next_stable_id: NodeId,
2727    layout_runtime: Option<RuntimeHandle>,
2728    slots: SlotTable,
2729    recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
2730    returning_recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
2731    cold_recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
2732    recycled_node_limits: HashMap<TypeId, usize>,
2733    warm_recycled_node_targets: HashMap<TypeId, usize>,
2734    fresh_recyclable_creations: HashMap<TypeId, usize>,
2735    recycled_node_prototypes: HashMap<TypeId, Box<dyn Node>>,
2736    /// Parents whose child lists changed structurally since the last drain
2737    /// (see [`Applier::record_structural_change`]).
2738    structural_change_parents: Vec<NodeId>,
2739}
2740
2741struct RemovalFrame {
2742    node_id: NodeId,
2743    children: SmallVec<[NodeId; 8]>,
2744    next_child: usize,
2745}
2746
2747#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2748pub struct MemoryApplierDebugStats {
2749    pub next_stable_id: NodeId,
2750    pub nodes_len: usize,
2751    pub nodes_cap: usize,
2752    pub physical_stable_ids_len: usize,
2753    pub physical_stable_ids_cap: usize,
2754    pub stable_to_physical_len: usize,
2755    pub stable_to_physical_cap: usize,
2756    pub stable_generations_len: usize,
2757    pub stable_generations_cap: usize,
2758    pub free_ids_len: usize,
2759    pub free_ids_cap: usize,
2760    pub high_id_nodes_len: usize,
2761    pub high_id_nodes_cap: usize,
2762    pub high_id_generations_len: usize,
2763    pub high_id_generations_cap: usize,
2764    pub recycled_type_count: usize,
2765    pub recycled_type_cap: usize,
2766    pub recycled_node_count: usize,
2767    pub recycled_node_capacity: usize,
2768    pub warm_recycled_node_id_count: usize,
2769    pub warm_recycled_node_id_capacity: usize,
2770}
2771
2772impl MemoryApplier {
2773    const EAGER_COMPACT_NODE_LEN: usize = 1_024;
2774    const HIGH_ID_THRESHOLD: NodeId = 1_000_000_000;
2775    const INVALID_STABLE_ID: u32 = u32::MAX;
2776    const INITIAL_DENSE_NODE_CAP: usize = 32;
2777    const LARGE_DENSE_NODE_GROWTH_THRESHOLD: usize = 32 * 1024;
2778    const LARGE_DENSE_NODE_GROWTH_DIVISOR: usize = 4;
2779
2780    fn pack_stable_id(stable_id: NodeId) -> u32 {
2781        u32::try_from(stable_id).expect("stable id overflow")
2782    }
2783
2784    fn unpack_stable_id(stable_id: u32) -> NodeId {
2785        stable_id as NodeId
2786    }
2787
2788    fn next_dense_node_target_len(old_len: usize) -> usize {
2789        if old_len < Self::INITIAL_DENSE_NODE_CAP {
2790            return Self::INITIAL_DENSE_NODE_CAP;
2791        }
2792        if old_len < Self::LARGE_DENSE_NODE_GROWTH_THRESHOLD {
2793            return old_len.saturating_mul(2);
2794        }
2795
2796        let incremental_growth =
2797            (old_len / Self::LARGE_DENSE_NODE_GROWTH_DIVISOR).max(Self::INITIAL_DENSE_NODE_CAP);
2798        old_len.saturating_add(incremental_growth)
2799    }
2800
2801    fn ensure_dense_node_storage_capacity(&mut self) {
2802        let len = self
2803            .nodes
2804            .len()
2805            .max(self.physical_stable_ids.len())
2806            .max(self.physical_warm_recycled_origins.len());
2807        if len < self.nodes.capacity()
2808            && len < self.physical_stable_ids.capacity()
2809            && len < self.physical_warm_recycled_origins.capacity()
2810        {
2811            return;
2812        }
2813
2814        let target = Self::next_dense_node_target_len(len);
2815        if self.nodes.capacity() < target {
2816            self.nodes
2817                .reserve_exact(target.saturating_sub(self.nodes.len()));
2818        }
2819        if self.physical_stable_ids.capacity() < target {
2820            self.physical_stable_ids
2821                .reserve_exact(target.saturating_sub(self.physical_stable_ids.len()));
2822        }
2823        if self.physical_warm_recycled_origins.capacity() < target {
2824            self.physical_warm_recycled_origins
2825                .reserve_exact(target.saturating_sub(self.physical_warm_recycled_origins.len()));
2826        }
2827    }
2828
2829    fn ensure_stable_index_capacity(&mut self) {
2830        let len = self
2831            .stable_to_physical
2832            .len()
2833            .max(self.stable_generations.len());
2834        if len < self.stable_to_physical.capacity() && len < self.stable_generations.capacity() {
2835            return;
2836        }
2837
2838        let target = Self::next_dense_node_target_len(len);
2839        let additional = target.saturating_sub(len);
2840        if self.stable_to_physical.capacity() < target {
2841            self.stable_to_physical.reserve(additional);
2842        }
2843        if self.stable_generations.capacity() < target {
2844            self.stable_generations.reserve(additional);
2845        }
2846    }
2847
2848    pub fn new() -> Self {
2849        Self {
2850            nodes: Vec::new(),
2851            physical_stable_ids: Vec::new(),
2852            physical_warm_recycled_origins: Vec::new(),
2853            stable_to_physical: HashMap::default(),
2854            stable_generations: HashMap::default(),
2855            free_ids: BinaryHeap::new(),
2856            high_id_nodes: HashMap::default(),
2857            high_id_warm_recycled_origins: HashMap::default(),
2858            high_id_generations: HashMap::default(),
2859            next_stable_id: 0,
2860            layout_runtime: None,
2861            slots: SlotTable::default(),
2862            recycled_nodes: HashMap::default(),
2863            returning_recycled_nodes: HashMap::default(),
2864            cold_recycled_nodes: HashMap::default(),
2865            recycled_node_limits: HashMap::default(),
2866            warm_recycled_node_targets: HashMap::default(),
2867            fresh_recyclable_creations: HashMap::default(),
2868            recycled_node_prototypes: HashMap::default(),
2869            structural_change_parents: Vec::new(),
2870        }
2871    }
2872
2873    pub fn slots(&mut self) -> &mut SlotTable {
2874        &mut self.slots
2875    }
2876
2877    /// Drains the parents recorded via [`Applier::record_structural_change`],
2878    /// keeping only nodes still attached to `root` (a parent that was itself
2879    /// removed is covered by its own surviving ancestor's record).
2880    pub fn take_structural_change_parents_attached_to(&mut self, root: NodeId) -> Vec<NodeId> {
2881        let recorded = std::mem::take(&mut self.structural_change_parents);
2882        let mut attached = Vec::with_capacity(recorded.len());
2883        for parent_id in recorded {
2884            if self.is_attached_to(parent_id, root) && !attached.contains(&parent_id) {
2885                attached.push(parent_id);
2886            }
2887        }
2888        attached
2889    }
2890
2891    fn is_attached_to(&mut self, node_id: NodeId, root: NodeId) -> bool {
2892        let mut current = node_id;
2893        // Parent chains are tree-shaped; the guard only bounds a corrupted
2894        // cyclic chain so the walk cannot spin forever.
2895        for _ in 0..100_000 {
2896            if current == root {
2897                return true;
2898            }
2899            match self.get_mut(current) {
2900                Ok(node) => match node.parent() {
2901                    Some(parent) => current = parent,
2902                    None => return false,
2903                },
2904                Err(_) => return false,
2905            }
2906        }
2907        false
2908    }
2909
2910    pub fn with_node<N: Node + 'static, R>(
2911        &mut self,
2912        id: NodeId,
2913        f: impl FnOnce(&mut N) -> R,
2914    ) -> Result<R, NodeError> {
2915        let physical_id = self
2916            .resolve_node_index(id)
2917            .ok_or(NodeError::Missing { id })?;
2918        let slot = self
2919            .nodes
2920            .get_mut(physical_id)
2921            .ok_or(NodeError::Missing { id })?
2922            .as_deref_mut()
2923            .ok_or(NodeError::Missing { id })?;
2924        let typed = slot
2925            .as_any_mut()
2926            .downcast_mut::<N>()
2927            .ok_or(NodeError::TypeMismatch {
2928                id,
2929                expected: std::any::type_name::<N>(),
2930            })?;
2931        Ok(f(typed))
2932    }
2933
2934    pub fn len(&self) -> usize {
2935        self.nodes.iter().filter(|n| n.is_some()).count()
2936    }
2937
2938    pub fn capacity(&self) -> usize {
2939        self.nodes.len()
2940    }
2941
2942    pub fn tombstone_count(&self) -> usize {
2943        self.nodes.iter().filter(|n| n.is_none()).count()
2944    }
2945
2946    pub fn freelist_len(&self) -> usize {
2947        self.free_ids.len()
2948    }
2949
2950    pub fn debug_recycled_node_count(&self) -> usize {
2951        self.total_recycled_node_count()
2952    }
2953
2954    pub fn debug_recycled_node_count_for<N: Node + 'static>(&self) -> usize {
2955        let key = TypeId::of::<N>();
2956        self.recycled_nodes.get(&key).map(Vec::len).unwrap_or(0)
2957            + self
2958                .returning_recycled_nodes
2959                .get(&key)
2960                .map(Vec::len)
2961                .unwrap_or(0)
2962            + self
2963                .cold_recycled_nodes
2964                .get(&key)
2965                .map(Vec::len)
2966                .unwrap_or(0)
2967    }
2968
2969    pub fn debug_stats(&self) -> MemoryApplierDebugStats {
2970        let mut recycled_keys: HashSet<TypeId> = HashSet::default();
2971        recycled_keys.extend(self.recycled_nodes.keys().copied());
2972        recycled_keys.extend(self.returning_recycled_nodes.keys().copied());
2973        recycled_keys.extend(self.cold_recycled_nodes.keys().copied());
2974
2975        MemoryApplierDebugStats {
2976            next_stable_id: self.next_stable_id,
2977            nodes_len: self.len(),
2978            nodes_cap: self.nodes.len(),
2979            physical_stable_ids_len: self.physical_stable_ids.len(),
2980            physical_stable_ids_cap: self.physical_stable_ids.capacity(),
2981            stable_to_physical_len: self.stable_to_physical.len(),
2982            stable_to_physical_cap: self.stable_to_physical.capacity(),
2983            stable_generations_len: self.stable_generations.len(),
2984            stable_generations_cap: self.stable_generations.capacity(),
2985            free_ids_len: self.free_ids.len(),
2986            free_ids_cap: self.free_ids.capacity(),
2987            high_id_nodes_len: self.high_id_nodes.len(),
2988            high_id_nodes_cap: self.high_id_nodes.capacity(),
2989            high_id_generations_len: self.high_id_generations.len(),
2990            high_id_generations_cap: self.high_id_generations.capacity(),
2991            recycled_type_count: recycled_keys.len(),
2992            recycled_type_cap: self.recycled_nodes.capacity()
2993                + self.returning_recycled_nodes.capacity()
2994                + self.cold_recycled_nodes.capacity(),
2995            recycled_node_count: self.total_recycled_node_count(),
2996            recycled_node_capacity: self.total_recycled_node_capacity(),
2997            warm_recycled_node_id_count: self.total_warm_recycled_node_id_count(),
2998            warm_recycled_node_id_capacity: self.total_warm_recycled_node_id_capacity(),
2999        }
3000    }
3001
3002    pub fn is_empty(&self) -> bool {
3003        self.len() == 0
3004    }
3005
3006    pub fn debug_live_node_heap_bytes(&self) -> usize {
3007        let dense_nodes = self
3008            .nodes
3009            .iter()
3010            .flatten()
3011            .map(|node| std::mem::size_of_val(&**node) + node.debug_heap_bytes())
3012            .sum::<usize>();
3013        let high_id_nodes = self
3014            .high_id_nodes
3015            .values()
3016            .map(|node| std::mem::size_of_val(&**node) + node.debug_heap_bytes())
3017            .sum::<usize>();
3018        dense_nodes + high_id_nodes
3019    }
3020
3021    pub fn debug_recycled_node_heap_bytes(&self) -> usize {
3022        let pool_bytes = |pools: &HashMap<TypeId, Vec<RecycledNode>>| {
3023            pools
3024                .values()
3025                .flat_map(|nodes| nodes.iter())
3026                .map(|node| std::mem::size_of_val(&*node.node) + node.node.debug_heap_bytes())
3027                .sum::<usize>()
3028        };
3029
3030        pool_bytes(&self.recycled_nodes)
3031            + pool_bytes(&self.returning_recycled_nodes)
3032            + pool_bytes(&self.cold_recycled_nodes)
3033    }
3034
3035    pub fn set_runtime_handle(&mut self, handle: RuntimeHandle) {
3036        self.layout_runtime = Some(handle);
3037    }
3038
3039    pub fn clear_runtime_handle(&mut self) {
3040        self.layout_runtime = None;
3041    }
3042
3043    pub fn runtime_handle(&self) -> Option<RuntimeHandle> {
3044        self.layout_runtime.clone()
3045    }
3046
3047    fn pool_node_count(pools: &HashMap<TypeId, Vec<RecycledNode>>) -> usize {
3048        pools.values().map(Vec::len).sum()
3049    }
3050
3051    fn pool_node_capacity(pools: &HashMap<TypeId, Vec<RecycledNode>>) -> usize {
3052        pools.values().map(Vec::capacity).sum()
3053    }
3054
3055    fn total_recycled_node_count(&self) -> usize {
3056        Self::pool_node_count(&self.recycled_nodes)
3057            + Self::pool_node_count(&self.returning_recycled_nodes)
3058            + Self::pool_node_count(&self.cold_recycled_nodes)
3059    }
3060
3061    fn total_recycled_node_capacity(&self) -> usize {
3062        Self::pool_node_capacity(&self.recycled_nodes)
3063            + Self::pool_node_capacity(&self.returning_recycled_nodes)
3064            + Self::pool_node_capacity(&self.cold_recycled_nodes)
3065    }
3066
3067    fn total_warm_recycled_node_id_count(&self) -> usize {
3068        self.live_warm_recycled_origin_count()
3069            + Self::pool_node_count(&self.recycled_nodes)
3070            + Self::pool_node_count(&self.returning_recycled_nodes)
3071    }
3072
3073    fn total_warm_recycled_node_id_capacity(&self) -> usize {
3074        self.live_warm_recycled_origin_capacity()
3075            + Self::pool_node_capacity(&self.recycled_nodes)
3076            + Self::pool_node_capacity(&self.returning_recycled_nodes)
3077    }
3078
3079    fn remember_recycle_pool_limit(&mut self, key: TypeId, recycle_pool_limit: Option<usize>) {
3080        if let Some(limit) = recycle_pool_limit {
3081            self.recycled_node_limits.insert(key, limit);
3082        } else {
3083            self.recycled_node_limits.remove(&key);
3084        }
3085    }
3086
3087    fn recycle_pool_limit_for(&self, key: TypeId) -> Option<usize> {
3088        self.recycled_node_limits.get(&key).copied()
3089    }
3090
3091    fn warm_recycled_pool_len(&self, key: TypeId) -> usize {
3092        self.recycled_nodes.get(&key).map(Vec::len).unwrap_or(0)
3093    }
3094
3095    fn warm_recycled_node_target(&self, key: TypeId) -> usize {
3096        self.warm_recycled_node_targets
3097            .get(&key)
3098            .copied()
3099            .unwrap_or(0)
3100    }
3101
3102    fn warm_recycled_node_target_limit(&self, key: TypeId) -> usize {
3103        let Some(limit) = self.recycle_pool_limit_for(key) else {
3104            return usize::MAX;
3105        };
3106        if limit <= 8 {
3107            limit
3108        } else {
3109            limit / 4
3110        }
3111    }
3112
3113    fn update_warm_recycled_node_target(&mut self, key: TypeId, observed_demand: usize) -> usize {
3114        let target_limit = self.warm_recycled_node_target_limit(key);
3115        let existing = self.warm_recycled_node_target(key).min(target_limit);
3116        if observed_demand == 0 {
3117            return existing;
3118        }
3119
3120        let target = match self.recycle_pool_limit_for(key) {
3121            Some(limit) if limit > 8 => target_limit,
3122            Some(_) => observed_demand.min(target_limit),
3123            None => observed_demand,
3124        };
3125        self.warm_recycled_node_targets.insert(key, target);
3126        target
3127    }
3128
3129    fn remember_recycled_node_prototype(&mut self, key: TypeId, shell: &dyn Node) {
3130        if self.recycled_node_prototypes.contains_key(&key) {
3131            return;
3132        }
3133        if let Some(prototype) = shell.rehouse_for_recycle() {
3134            self.recycled_node_prototypes.insert(key, prototype);
3135        }
3136    }
3137
3138    fn live_warm_recycled_origin_count(&self) -> usize {
3139        self.physical_warm_recycled_origins
3140            .iter()
3141            .zip(self.nodes.iter())
3142            .filter(|(warm_origin, node)| **warm_origin && node.is_some())
3143            .count()
3144            + self
3145                .high_id_warm_recycled_origins
3146                .values()
3147                .filter(|warm_origin| **warm_origin)
3148                .count()
3149    }
3150
3151    fn live_warm_recycled_origin_capacity(&self) -> usize {
3152        self.physical_warm_recycled_origins.capacity()
3153            + self.high_id_warm_recycled_origins.capacity()
3154    }
3155
3156    fn push_recycled_node(
3157        &mut self,
3158        key: TypeId,
3159        recycle_pool_limit: Option<usize>,
3160        recycled: RecycledNode,
3161    ) {
3162        self.remember_recycle_pool_limit(key, recycle_pool_limit);
3163        self.remember_recycled_node_prototype(key, recycled.node.as_ref());
3164
3165        let warm_origin = recycled.warm_origin();
3166        let pool = if warm_origin {
3167            self.returning_recycled_nodes.entry(key).or_default()
3168        } else {
3169            self.cold_recycled_nodes.entry(key).or_default()
3170        };
3171        pool.push(recycled);
3172        if let Some(limit) = recycle_pool_limit {
3173            if pool.len() > limit {
3174                let excess = pool.len() - limit;
3175                let dropped: Vec<_> = pool.drain(0..excess).collect();
3176                drop(dropped);
3177            }
3178        }
3179    }
3180
3181    fn push_warm_recycled_node(
3182        &mut self,
3183        key: TypeId,
3184        recycle_pool_limit: Option<usize>,
3185        mut recycled: RecycledNode,
3186    ) {
3187        self.remember_recycle_pool_limit(key, recycle_pool_limit);
3188
3189        recycled.set_warm_origin(true);
3190        let mut dropped = Vec::new();
3191        let mut remove_pool_entry = false;
3192        {
3193            let pool = self.recycled_nodes.entry(key).or_default();
3194            pool.push(recycled);
3195            if let Some(limit) = recycle_pool_limit {
3196                if pool.len() > limit {
3197                    let excess = pool.len() - limit;
3198                    dropped = pool.drain(0..excess).collect();
3199                    remove_pool_entry = pool.is_empty();
3200                }
3201            }
3202        }
3203        if remove_pool_entry {
3204            self.recycled_nodes.remove(&key);
3205        }
3206        drop(dropped);
3207    }
3208
3209    fn seed_recycled_node_shell_impl(
3210        &mut self,
3211        key: TypeId,
3212        recycle_pool_limit: Option<usize>,
3213        shell: Box<dyn Node>,
3214    ) {
3215        let limit = recycle_pool_limit.unwrap_or(usize::MAX);
3216        if self.warm_recycled_pool_len(key) >= limit {
3217            return;
3218        }
3219
3220        self.remember_recycled_node_prototype(key, shell.as_ref());
3221        let stable_id = self.next_stable_id;
3222        self.next_stable_id = self.next_stable_id.saturating_add(1);
3223        self.push_warm_recycled_node(
3224            key,
3225            recycle_pool_limit,
3226            RecycledNode::from_shell(stable_id, shell, true),
3227        );
3228    }
3229
3230    fn take_recycled_node_from_pool(
3231        pools: &mut HashMap<TypeId, Vec<RecycledNode>>,
3232        key: TypeId,
3233    ) -> Option<RecycledNode> {
3234        let pool = pools.get_mut(&key)?;
3235        let node = pool.pop();
3236        if pool.is_empty() {
3237            pools.remove(&key);
3238        }
3239        node
3240    }
3241
3242    fn compact_idle_warm_pool(&mut self, key: TypeId) {
3243        let Some(pool) = self.recycled_nodes.get_mut(&key) else {
3244            return;
3245        };
3246        if pool.capacity() <= pool.len().saturating_mul(4).max(64) {
3247            return;
3248        }
3249
3250        let retained = pool.len();
3251        let mut compacted = Vec::with_capacity(retained);
3252        compacted.append(pool);
3253        let remove_pool_entry = compacted.is_empty();
3254        *pool = compacted;
3255        let _ = pool;
3256
3257        if remove_pool_entry {
3258            self.recycled_nodes.remove(&key);
3259        }
3260    }
3261
3262    fn trim_idle_warm_pool_to_target(&mut self, key: TypeId, target: usize) {
3263        let pool_len = self.warm_recycled_pool_len(key);
3264        if pool_len <= target {
3265            return;
3266        }
3267
3268        let Some(pool) = self.recycled_nodes.get_mut(&key) else {
3269            return;
3270        };
3271        let removable = (pool_len - target).min(pool.len());
3272        let dropped: Vec<_> = pool.drain(0..removable).collect();
3273        let remove_pool_entry = pool.is_empty();
3274        let _ = pool;
3275
3276        if remove_pool_entry {
3277            self.recycled_nodes.remove(&key);
3278        }
3279        drop(dropped);
3280    }
3281
3282    fn replenish_warm_pool_to_target(&mut self, key: TypeId, target: usize) {
3283        let missing = target.saturating_sub(self.warm_recycled_pool_len(key));
3284        if missing == 0 {
3285            return;
3286        }
3287
3288        let recycle_pool_limit = self.recycle_pool_limit_for(key);
3289        let mut shells = Vec::with_capacity(missing);
3290        if let Some(prototype) = self.recycled_node_prototypes.get(&key) {
3291            for _ in 0..missing {
3292                let Some(shell) = prototype.rehouse_for_recycle() else {
3293                    break;
3294                };
3295                shells.push(shell);
3296            }
3297        }
3298
3299        for shell in shells {
3300            self.seed_recycled_node_shell_impl(key, recycle_pool_limit, shell);
3301        }
3302    }
3303
3304    fn prune_stable_generations(&mut self) {
3305        let retained_len = self.stable_to_physical.len() + self.total_recycled_node_count();
3306        if retained_len == self.stable_generations.len() {
3307            return;
3308        }
3309
3310        let mut retained = HashMap::default();
3311        retained.reserve(retained_len);
3312        for stable_id in self.stable_to_physical.keys().copied() {
3313            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3314                retained.insert(stable_id, generation);
3315            }
3316        }
3317        for stable_id in self
3318            .recycled_nodes
3319            .values()
3320            .flat_map(|nodes| nodes.iter().map(RecycledNode::stable_id))
3321        {
3322            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3323                retained.insert(stable_id, generation);
3324            }
3325        }
3326        for stable_id in self
3327            .returning_recycled_nodes
3328            .values()
3329            .flat_map(|nodes| nodes.iter().map(RecycledNode::stable_id))
3330        {
3331            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3332                retained.insert(stable_id, generation);
3333            }
3334        }
3335        for stable_id in self
3336            .cold_recycled_nodes
3337            .values()
3338            .flat_map(|nodes| nodes.iter().map(RecycledNode::stable_id))
3339        {
3340            if let Some(generation) = self.stable_generations.get(&stable_id).copied() {
3341                retained.insert(stable_id, generation);
3342            }
3343        }
3344        self.stable_generations = retained;
3345    }
3346
3347    pub fn dump_tree(&self, root: Option<NodeId>) -> String {
3348        let mut output = String::new();
3349        if let Some(root_id) = root {
3350            self.dump_node(&mut output, root_id, 0);
3351        } else {
3352            output.push_str("(no root)\n");
3353        }
3354        output
3355    }
3356
3357    fn dump_node(&self, output: &mut String, id: NodeId, depth: usize) {
3358        let indent = "  ".repeat(depth);
3359        if let Some(physical_id) = self.resolve_node_index(id) {
3360            if let Some(node) = self.nodes.get(physical_id).and_then(Option::as_ref) {
3361                let type_name = std::any::type_name_of_val(&**node);
3362                output.push_str(&format!("{}[{}] {}\n", indent, id, type_name));
3363
3364                let children = node.children();
3365                for child_id in children {
3366                    self.dump_node(output, child_id, depth + 1);
3367                }
3368            } else {
3369                output.push_str(&format!(
3370                    "{}[{}] (missing physical node {})\n",
3371                    indent, id, physical_id
3372                ));
3373            }
3374        } else {
3375            output.push_str(&format!("{}[{}] (missing)\n", indent, id));
3376        }
3377    }
3378
3379    fn resolve_node_index(&self, id: NodeId) -> Option<usize> {
3380        self.stable_to_physical.get(&id).copied()
3381    }
3382
3383    fn contains_node_id(&self, id: NodeId) -> bool {
3384        self.resolve_node_index(id).is_some() || self.high_id_nodes.contains_key(&id)
3385    }
3386
3387    fn insert_high_id_node(&mut self, stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) {
3388        self.high_id_nodes.insert(stable_id, node);
3389        self.high_id_warm_recycled_origins
3390            .insert(stable_id, warm_origin);
3391        self.high_id_generations.entry(stable_id).or_insert(0);
3392    }
3393
3394    fn insert_available_with_id(&mut self, stable_id: NodeId, node: Box<dyn Node>) {
3395        if stable_id >= Self::HIGH_ID_THRESHOLD {
3396            self.insert_high_id_node(stable_id, node, false);
3397            return;
3398        }
3399
3400        let physical_id = if let Some(Reverse(free_physical_id)) = self.free_ids.pop() {
3401            self.nodes[free_physical_id] = Some(node);
3402            self.physical_stable_ids[free_physical_id] = Self::pack_stable_id(stable_id);
3403            self.physical_warm_recycled_origins[free_physical_id] = false;
3404            free_physical_id
3405        } else {
3406            self.ensure_dense_node_storage_capacity();
3407            let physical_id = self.nodes.len();
3408            self.nodes.push(Some(node));
3409            self.physical_stable_ids
3410                .push(Self::pack_stable_id(stable_id));
3411            self.physical_warm_recycled_origins.push(false);
3412            physical_id
3413        };
3414
3415        self.next_stable_id = self.next_stable_id.max(stable_id.saturating_add(1));
3416        self.ensure_stable_index_capacity();
3417        self.stable_generations.entry(stable_id).or_insert(0);
3418        self.physical_stable_ids[physical_id] = Self::pack_stable_id(stable_id);
3419        self.stable_to_physical.insert(stable_id, physical_id);
3420    }
3421
3422    fn get_ref(&self, id: NodeId) -> Result<&dyn Node, NodeError> {
3423        if let Some(physical_id) = self.resolve_node_index(id) {
3424            let slot = self
3425                .nodes
3426                .get(physical_id)
3427                .ok_or(NodeError::Missing { id })?
3428                .as_deref()
3429                .ok_or(NodeError::Missing { id })?;
3430            return Ok(slot);
3431        }
3432
3433        self.high_id_nodes
3434            .get(&id)
3435            .map(|node| node.as_ref())
3436            .ok_or(NodeError::Missing { id })
3437    }
3438
3439    fn collect_node_children_into(
3440        &self,
3441        id: NodeId,
3442        out: &mut SmallVec<[NodeId; 8]>,
3443    ) -> Result<(), NodeError> {
3444        self.get_ref(id)?.collect_children_into(out);
3445        Ok(())
3446    }
3447
3448    fn node_parent(&self, id: NodeId) -> Result<Option<NodeId>, NodeError> {
3449        Ok(self.get_ref(id)?.parent())
3450    }
3451
3452    fn collect_owned_children(
3453        &self,
3454        node_id: NodeId,
3455        out: &mut SmallVec<[NodeId; 8]>,
3456    ) -> Result<(), NodeError> {
3457        self.collect_node_children_into(node_id, out)?;
3458        out.retain(|child_id| {
3459            self.node_parent(*child_id)
3460                .map(|parent| parent == Some(node_id))
3461                .unwrap_or(false)
3462        });
3463        Ok(())
3464    }
3465
3466    fn remove_node_storage(&mut self, node_id: NodeId) -> Result<(), NodeError> {
3467        if self.high_id_nodes.contains_key(&node_id) {
3468            if let Some(mut node) = self.high_id_nodes.remove(&node_id) {
3469                if let Some(key) = node.recycle_key() {
3470                    let recycle_pool_limit = node.recycle_pool_limit();
3471                    let warm_origin = self
3472                        .high_id_warm_recycled_origins
3473                        .remove(&node_id)
3474                        .unwrap_or(false);
3475                    node.prepare_for_recycle();
3476                    self.push_recycled_node(
3477                        key,
3478                        recycle_pool_limit,
3479                        RecycledNode::new(node_id, node, warm_origin),
3480                    );
3481                }
3482            }
3483            let generation = self.high_id_generations.entry(node_id).or_insert(0);
3484            *generation = generation.wrapping_add(1);
3485            return Ok(());
3486        }
3487
3488        let physical_id = self
3489            .resolve_node_index(node_id)
3490            .ok_or(NodeError::Missing { id: node_id })?;
3491        if let Some(mut node) = self.nodes[physical_id].take() {
3492            if let Some(key) = node.recycle_key() {
3493                let recycle_pool_limit = node.recycle_pool_limit();
3494                let warm_origin = self
3495                    .physical_warm_recycled_origins
3496                    .get_mut(physical_id)
3497                    .map(std::mem::take)
3498                    .unwrap_or(false);
3499                node.prepare_for_recycle();
3500                self.push_recycled_node(
3501                    key,
3502                    recycle_pool_limit,
3503                    RecycledNode::new(node_id, node, warm_origin),
3504                );
3505            }
3506        }
3507        self.physical_stable_ids[physical_id] = Self::INVALID_STABLE_ID;
3508        self.stable_to_physical.remove(&node_id);
3509        if let Some(generation) = self.stable_generations.get_mut(&node_id) {
3510            *generation = generation.wrapping_add(1);
3511        } else {
3512            self.stable_generations.insert(node_id, 1);
3513        }
3514        self.free_ids.push(Reverse(physical_id));
3515        Ok(())
3516    }
3517
3518    fn remove_subtree_postorder(&mut self, id: NodeId) -> Result<usize, NodeError> {
3519        self.get_ref(id)?;
3520
3521        let mut root_children = SmallVec::<[NodeId; 8]>::new();
3522        self.collect_owned_children(id, &mut root_children)?;
3523
3524        let mut stack = Vec::new();
3525        stack.push(RemovalFrame {
3526            node_id: id,
3527            children: root_children,
3528            next_child: 0,
3529        });
3530        let mut max_depth = stack.len();
3531
3532        while let Some(frame) = stack.last_mut() {
3533            if frame.next_child < frame.children.len() {
3534                let child_id = frame.children[frame.next_child];
3535                frame.next_child += 1;
3536
3537                if let Ok(child) = self.get_mut(child_id) {
3538                    child.on_removed_from_parent();
3539                    child.unmount();
3540                }
3541
3542                let mut child_children = SmallVec::<[NodeId; 8]>::new();
3543                self.collect_owned_children(child_id, &mut child_children)?;
3544                stack.push(RemovalFrame {
3545                    node_id: child_id,
3546                    children: child_children,
3547                    next_child: 0,
3548                });
3549                max_depth = max_depth.max(stack.len());
3550                continue;
3551            }
3552
3553            let node_id = frame.node_id;
3554            stack.pop();
3555            self.remove_node_storage(node_id)?;
3556        }
3557
3558        Ok(max_depth)
3559    }
3560
3561    #[cfg(test)]
3562    fn debug_remove_max_traversal_depth(&mut self, id: NodeId) -> Result<usize, NodeError> {
3563        self.remove_subtree_postorder(id)
3564    }
3565}
3566
3567impl Applier for MemoryApplier {
3568    fn record_structural_change(&mut self, parent_id: NodeId) {
3569        if self.structural_change_parents.last() != Some(&parent_id) {
3570            self.structural_change_parents.push(parent_id);
3571        }
3572    }
3573
3574    fn create(&mut self, node: Box<dyn Node>) -> NodeId {
3575        let stable_id = self.next_stable_id;
3576        self.next_stable_id = self.next_stable_id.saturating_add(1);
3577        if stable_id >= Self::HIGH_ID_THRESHOLD {
3578            self.insert_high_id_node(stable_id, node, false);
3579            return stable_id;
3580        }
3581
3582        self.ensure_stable_index_capacity();
3583        self.stable_generations.insert(stable_id, 0);
3584
3585        let physical_id = if let Some(Reverse(id)) = self.free_ids.pop() {
3586            debug_assert!(self.nodes[id].is_none(), "freelist entry {id} is not None");
3587            self.nodes[id] = Some(node);
3588            self.physical_stable_ids[id] = Self::pack_stable_id(stable_id);
3589            self.physical_warm_recycled_origins[id] = false;
3590            id
3591        } else {
3592            self.ensure_dense_node_storage_capacity();
3593            let id = self.nodes.len();
3594            self.nodes.push(Some(node));
3595            self.physical_stable_ids
3596                .push(Self::pack_stable_id(stable_id));
3597            self.physical_warm_recycled_origins.push(false);
3598            id
3599        };
3600        self.stable_to_physical.insert(stable_id, physical_id);
3601        stable_id
3602    }
3603
3604    fn node_generation(&self, id: NodeId) -> u32 {
3605        self.high_id_generations
3606            .get(&id)
3607            .copied()
3608            .or_else(|| self.stable_generations.get(&id).copied())
3609            .unwrap_or(0)
3610    }
3611
3612    fn get_mut(&mut self, id: NodeId) -> Result<&mut dyn Node, NodeError> {
3613        // Try Vec first (common case for normal nodes), then HashMap for virtual nodes.
3614        if let Some(physical_id) = self.resolve_node_index(id) {
3615            let slot = self.nodes[physical_id]
3616                .as_deref_mut()
3617                .ok_or(NodeError::Missing { id })?;
3618            return Ok(slot);
3619        }
3620        self.high_id_nodes
3621            .get_mut(&id)
3622            .map(|n| n.as_mut())
3623            .ok_or(NodeError::Missing { id })
3624    }
3625
3626    fn remove(&mut self, id: NodeId) -> Result<(), NodeError> {
3627        self.remove_subtree_postorder(id).map(|_| ())
3628    }
3629
3630    fn insert_with_id(&mut self, id: NodeId, node: Box<dyn Node>) -> Result<(), NodeError> {
3631        if self.contains_node_id(id) {
3632            return Err(NodeError::AlreadyExists { id });
3633        }
3634        self.insert_available_with_id(id, node);
3635        Ok(())
3636    }
3637
3638    fn insert_recycled_node_or_create(
3639        &mut self,
3640        stable_id: NodeId,
3641        node: Box<dyn Node>,
3642    ) -> RecycledNodeInsertion {
3643        if self.contains_node_id(stable_id) {
3644            let id = self.create(node);
3645            return RecycledNodeInsertion::fresh(
3646                id,
3647                Some(NodeError::AlreadyExists { id: stable_id }),
3648            );
3649        }
3650
3651        self.insert_available_with_id(stable_id, node);
3652        RecycledNodeInsertion::reused(stable_id)
3653    }
3654
3655    fn compact(&mut self) {
3656        let live_count = self.nodes.iter().filter(|slot| slot.is_some()).count();
3657        let tombstone_count = self.nodes.len().saturating_sub(live_count);
3658        if tombstone_count == 0 {
3659            return;
3660        }
3661        if self.nodes.len() > Self::EAGER_COMPACT_NODE_LEN && tombstone_count < live_count {
3662            return;
3663        }
3664        let rehouse_live_nodes = tombstone_count >= live_count;
3665        let mut packed_nodes = Vec::with_capacity(live_count);
3666        let mut packed_physical_stable_ids = Vec::with_capacity(live_count);
3667        let mut packed_warm_recycled_origins = Vec::with_capacity(live_count);
3668        let mut stable_to_physical = HashMap::default();
3669        stable_to_physical.reserve(live_count);
3670
3671        for physical_id in 0..self.nodes.len() {
3672            let Some(mut node) = self.nodes[physical_id].take() else {
3673                continue;
3674            };
3675            if rehouse_live_nodes {
3676                if let Some(rehoused) = node.rehouse_for_live_compaction() {
3677                    node = rehoused;
3678                }
3679            }
3680            let stable_id = std::mem::replace(
3681                &mut self.physical_stable_ids[physical_id],
3682                Self::INVALID_STABLE_ID,
3683            );
3684            debug_assert_ne!(
3685                stable_id,
3686                Self::INVALID_STABLE_ID,
3687                "live physical slot must have a stable id",
3688            );
3689            let stable_id = Self::unpack_stable_id(stable_id);
3690            packed_nodes.push(Some(node));
3691            packed_physical_stable_ids.push(Self::pack_stable_id(stable_id));
3692            packed_warm_recycled_origins.push(self.physical_warm_recycled_origins[physical_id]);
3693            stable_to_physical.insert(stable_id, packed_nodes.len() - 1);
3694        }
3695
3696        self.nodes = packed_nodes;
3697        self.physical_stable_ids = packed_physical_stable_ids;
3698        self.physical_warm_recycled_origins = packed_warm_recycled_origins;
3699        self.free_ids = BinaryHeap::new();
3700        self.stable_to_physical = stable_to_physical;
3701        self.prune_stable_generations();
3702    }
3703
3704    fn take_recycled_node(&mut self, key: TypeId) -> Option<RecycledNode> {
3705        Self::take_recycled_node_from_pool(&mut self.returning_recycled_nodes, key)
3706            .or_else(|| Self::take_recycled_node_from_pool(&mut self.recycled_nodes, key))
3707    }
3708
3709    fn set_recycled_node_origin(&mut self, id: NodeId, warm_origin: bool) {
3710        if let Some(physical_id) = self.resolve_node_index(id) {
3711            self.physical_warm_recycled_origins[physical_id] = warm_origin;
3712        } else if self.high_id_nodes.contains_key(&id) {
3713            self.high_id_warm_recycled_origins.insert(id, warm_origin);
3714        }
3715    }
3716
3717    fn seed_recycled_node_shell(
3718        &mut self,
3719        key: TypeId,
3720        recycle_pool_limit: Option<usize>,
3721        shell: Box<dyn Node>,
3722    ) {
3723        self.seed_recycled_node_shell_impl(key, recycle_pool_limit, shell);
3724    }
3725
3726    fn record_fresh_recyclable_creation(&mut self, key: TypeId) {
3727        *self.fresh_recyclable_creations.entry(key).or_insert(0) += 1;
3728    }
3729
3730    fn clear_recycled_nodes(&mut self) {
3731        let returning = std::mem::take(&mut self.returning_recycled_nodes);
3732        for (key, mut nodes) in returning {
3733            let pool = self.recycled_nodes.entry(key).or_default();
3734            pool.append(&mut nodes);
3735        }
3736
3737        let fresh_recyclable_creations = std::mem::take(&mut self.fresh_recyclable_creations);
3738        let cold = std::mem::take(&mut self.cold_recycled_nodes);
3739        for (key, mut nodes) in cold {
3740            let needed = fresh_recyclable_creations.get(&key).copied().unwrap_or(0);
3741            if needed > 0 {
3742                let remaining_limit = self
3743                    .recycle_pool_limit_for(key)
3744                    .unwrap_or(usize::MAX)
3745                    .saturating_sub(self.warm_recycled_pool_len(key));
3746                let promote = nodes.len().min(needed).min(remaining_limit);
3747                let split_at = nodes.len().saturating_sub(promote);
3748                let promoted = nodes.split_off(split_at);
3749                for mut recycled in promoted {
3750                    recycled.set_warm_origin(true);
3751                    self.recycled_nodes.entry(key).or_default().push(recycled);
3752                }
3753            }
3754        }
3755
3756        let mut keys: HashSet<TypeId> = HashSet::default();
3757        keys.extend(self.recycled_nodes.keys().copied());
3758        keys.extend(self.recycled_node_limits.keys().copied());
3759        keys.extend(self.warm_recycled_node_targets.keys().copied());
3760        keys.extend(self.recycled_node_prototypes.keys().copied());
3761        for key in keys {
3762            let observed_demand = fresh_recyclable_creations.get(&key).copied().unwrap_or(0);
3763            let target = self.update_warm_recycled_node_target(key, observed_demand);
3764            self.replenish_warm_pool_to_target(key, target);
3765            self.trim_idle_warm_pool_to_target(key, target);
3766            self.compact_idle_warm_pool(key);
3767        }
3768        self.prune_stable_generations();
3769        // Compact the dense physical storage if it has grown very large relative
3770        // to the live node count (e.g. after a spike of recyclable nodes that
3771        // were all removed). This keeps warm_recycled_node_id_capacity bounded.
3772        self.compact();
3773    }
3774}
3775
3776pub trait ApplierHost {
3777    fn borrow_dyn(&self) -> RefMut<'_, dyn Applier>;
3778    /// Compact internal storage after commands have been applied.
3779    fn compact(&self) {}
3780}
3781
3782pub struct ConcreteApplierHost<A: Applier + 'static> {
3783    inner: RefCell<A>,
3784}
3785
3786impl<A: Applier + 'static> ConcreteApplierHost<A> {
3787    pub fn new(applier: A) -> Self {
3788        Self {
3789            inner: RefCell::new(applier),
3790        }
3791    }
3792
3793    pub fn borrow_typed(&self) -> RefMut<'_, A> {
3794        self.inner.borrow_mut()
3795    }
3796
3797    pub fn try_borrow_typed(&self) -> Result<RefMut<'_, A>, std::cell::BorrowMutError> {
3798        self.inner.try_borrow_mut()
3799    }
3800
3801    pub fn into_inner(self) -> A {
3802        self.inner.into_inner()
3803    }
3804}
3805
3806impl<A: Applier + 'static> ApplierHost for ConcreteApplierHost<A> {
3807    fn borrow_dyn(&self) -> RefMut<'_, dyn Applier> {
3808        RefMut::map(self.inner.borrow_mut(), |applier| {
3809            applier as &mut dyn Applier
3810        })
3811    }
3812
3813    fn compact(&self) {
3814        self.inner.borrow_mut().compact();
3815    }
3816}
3817
3818pub struct ApplierGuard<'a, A: Applier + 'static> {
3819    inner: RefMut<'a, A>,
3820}
3821
3822impl<'a, A: Applier + 'static> ApplierGuard<'a, A> {
3823    fn new(inner: RefMut<'a, A>) -> Self {
3824        Self { inner }
3825    }
3826}
3827
3828impl<'a, A: Applier + 'static> Deref for ApplierGuard<'a, A> {
3829    type Target = A;
3830
3831    fn deref(&self) -> &Self::Target {
3832        &self.inner
3833    }
3834}
3835
3836impl<'a, A: Applier + 'static> DerefMut for ApplierGuard<'a, A> {
3837    fn deref_mut(&mut self) -> &mut Self::Target {
3838        &mut self.inner
3839    }
3840}
3841
3842pub struct SlotsHost {
3843    storage_key: Cell<usize>,
3844    inner: RefCell<SlotsHostInner>,
3845}
3846
3847#[derive(Debug, Default)]
3848pub(crate) struct SlotPassOutcome {
3849    pub(crate) compacted: bool,
3850    pub(crate) compact_anchor_registry_storage: bool,
3851    pub(crate) compact_payload_storage: bool,
3852}
3853
3854#[derive(Default)]
3855pub(crate) struct FinishedSlotPass {
3856    pub(crate) outcome: SlotPassOutcome,
3857    pub(crate) detached_root_children: Vec<slot::DetachedSubtree>,
3858}
3859
3860struct ActivePassState {
3861    state: slot::SlotWriteSessionState,
3862}
3863
3864struct SlotsHostInner {
3865    table: SlotTable,
3866    nested_hosts: Vec<std::rc::Weak<SlotsHost>>,
3867    lifecycle: slot::SlotLifecycleCoordinator,
3868    runtime_state: Option<Rc<crate::composer::ComposerRuntimeState>>,
3869    active_pass: Option<ActivePassState>,
3870}
3871
3872impl Drop for SlotsHost {
3873    fn drop(&mut self) {
3874        let storage_key = self.storage_key.get();
3875        let inner = self.inner.get_mut();
3876        if let Some(state) = inner.runtime_state.clone() {
3877            if let Err(err) = state.dispose_retained_subtrees_for_host(
3878                storage_key,
3879                &mut inner.table,
3880                &mut inner.lifecycle,
3881            ) {
3882                log::error!(
3883                    "retained subtree disposal failed while dropping SlotsHost {storage_key}: {err}"
3884                );
3885                state.abandon_retained_subtrees_for_host(
3886                    storage_key,
3887                    &mut inner.table,
3888                    &mut inner.lifecycle,
3889                );
3890            } else {
3891                state.clear_host_storage_key(storage_key);
3892            }
3893        }
3894        inner.lifecycle.dispose_slot_table(&mut inner.table);
3895    }
3896}
3897
3898impl SlotsHost {
3899    pub fn storage_key(&self) -> usize {
3900        self.storage_key.get()
3901    }
3902
3903    pub fn new(storage: SlotTable) -> Self {
3904        let storage_key = storage.storage_id();
3905        Self {
3906            storage_key: Cell::new(storage_key),
3907            inner: RefCell::new(SlotsHostInner {
3908                table: storage,
3909                nested_hosts: Vec::new(),
3910                lifecycle: slot::SlotLifecycleCoordinator::default(),
3911                runtime_state: None,
3912                active_pass: None,
3913            }),
3914        }
3915    }
3916
3917    pub fn note_nested_host(&self, nested: &Rc<SlotsHost>) {
3918        let Ok(mut inner) = self.inner.try_borrow_mut() else {
3919            return;
3920        };
3921        inner.nested_hosts.retain(|held| held.upgrade().is_some());
3922        if inner
3923            .nested_hosts
3924            .iter()
3925            .any(|held| held.upgrade().is_some_and(|host| Rc::ptr_eq(&host, nested)))
3926        {
3927            return;
3928        }
3929        inner.nested_hosts.push(Rc::downgrade(nested));
3930    }
3931
3932    pub(crate) fn forget_effects(&self) -> bool {
3933        let (forgotten, nested, runtime_state) = {
3934            let Ok(mut inner) = self.inner.try_borrow_mut() else {
3935                return false;
3936            };
3937            if inner.active_pass.is_some() {
3938                return false;
3939            }
3940            let drops = inner.table.take_effect_drops();
3941            inner.nested_hosts.retain(|held| held.upgrade().is_some());
3942            let nested: Vec<Rc<SlotsHost>> = inner
3943                .nested_hosts
3944                .iter()
3945                .filter_map(std::rc::Weak::upgrade)
3946                .collect();
3947            (drops, nested, inner.runtime_state.clone())
3948        };
3949        let mut any = !forgotten.is_empty();
3950        drop(forgotten);
3951        for host in nested {
3952            any |= host.forget_effects();
3953        }
3954        if any {
3955            if let Some(runtime_state) = runtime_state {
3956                runtime_state.force_recompose_host_scopes(self.storage_key());
3957            }
3958        }
3959        any
3960    }
3961
3962    pub(crate) fn bind_runtime_state(&self, state: &Rc<crate::composer::ComposerRuntimeState>) {
3963        let mut inner = self.inner.borrow_mut();
3964        inner.runtime_state = Some(Rc::clone(state));
3965    }
3966
3967    pub(crate) fn rebind_orphaned_runtime_state(
3968        &self,
3969        state: &Rc<crate::composer::ComposerRuntimeState>,
3970    ) -> bool {
3971        let inner = self.inner.borrow();
3972        if inner.active_pass.is_some() {
3973            log::error!("cannot rebind SlotsHost during an active pass");
3974            return false;
3975        }
3976        let Some(bound_state) = inner.runtime_state.as_ref() else {
3977            drop(inner);
3978            self.bind_runtime_state(state);
3979            return true;
3980        };
3981        if Rc::ptr_eq(bound_state, state) {
3982            return true;
3983        }
3984        if bound_state.has_live_applier_host() {
3985            return false;
3986        }
3987        drop(inner);
3988
3989        let mut inner = self.inner.borrow_mut();
3990        let Some(bound_state) = inner.runtime_state.as_ref() else {
3991            inner.runtime_state = Some(Rc::clone(state));
3992            return true;
3993        };
3994        if Rc::ptr_eq(bound_state, state) {
3995            return true;
3996        }
3997        if bound_state.has_live_applier_host() {
3998            return false;
3999        }
4000
4001        let previous_state = Rc::clone(bound_state);
4002        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4003        lifecycle.flush_pending_drops();
4004        let host_key = self.storage_key();
4005        if previous_state
4006            .dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)
4007            .is_err()
4008        {
4009            inner.lifecycle = lifecycle;
4010            return false;
4011        }
4012        previous_state.clear_host(self);
4013        lifecycle.flush_pending_drops();
4014        inner.runtime_state = Some(Rc::clone(state));
4015        inner.lifecycle = lifecycle;
4016        true
4017    }
4018
4019    pub(crate) fn runtime_state(&self) -> Option<Rc<crate::composer::ComposerRuntimeState>> {
4020        self.inner.borrow().runtime_state.clone()
4021    }
4022
4023    pub(crate) fn borrow(&self) -> Ref<'_, SlotTable> {
4024        Ref::map(self.inner.borrow(), |inner| &inner.table)
4025    }
4026
4027    pub(crate) fn borrow_mut(&self) -> RefMut<'_, SlotTable> {
4028        RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.table)
4029    }
4030
4031    pub fn into_table(self: Rc<Self>) -> Result<SlotTable, NodeError> {
4032        if Rc::strong_count(&self) != 1 {
4033            return Err(NodeError::SlotHostUnavailable {
4034                operation: "SlotsHost::into_table",
4035                reason: "other host references are alive",
4036            });
4037        }
4038        self.take_table_for_transfer()
4039    }
4040
4041    fn take_table_for_transfer(&self) -> Result<SlotTable, NodeError> {
4042        let inner = self.inner.borrow();
4043        if inner.active_pass.is_some() {
4044            return Err(NodeError::SlotHostUnavailable {
4045                operation: "SlotsHost::into_table",
4046                reason: "slot pass is active",
4047            });
4048        }
4049        drop(inner);
4050        let mut inner = self.inner.borrow_mut();
4051        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4052        lifecycle.flush_pending_drops();
4053        if let Some(state) = inner.runtime_state.clone() {
4054            let host_key = self.storage_key();
4055            state.dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)?;
4056            state.clear_host(self);
4057            lifecycle.flush_pending_drops();
4058        }
4059        let taken = std::mem::take(&mut inner.table);
4060        self.storage_key.set(inner.table.storage_id());
4061        inner.runtime_state = None;
4062        inner.lifecycle = lifecycle;
4063        Ok(taken)
4064    }
4065
4066    pub fn reset(&self) -> Result<(), NodeError> {
4067        let inner = self.inner.borrow();
4068        if inner.active_pass.is_some() {
4069            return Err(NodeError::SlotHostUnavailable {
4070                operation: "SlotsHost::reset",
4071                reason: "slot pass is active",
4072            });
4073        }
4074        let runtime_state = inner.runtime_state.clone();
4075        drop(inner);
4076        let mut inner = self.inner.borrow_mut();
4077        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4078        if let Some(state) = runtime_state {
4079            let host_key = self.storage_key();
4080            state.dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)?;
4081            state.clear_host(self);
4082        }
4083        lifecycle.dispose_slot_table(&mut inner.table);
4084        inner.table = SlotTable::default();
4085        self.storage_key.set(inner.table.storage_id());
4086        inner.runtime_state = None;
4087        inner.lifecycle = slot::SlotLifecycleCoordinator::default();
4088        Ok(())
4089    }
4090
4091    pub(crate) fn abandon_after_apply_failure(&self) {
4092        let inner = self.inner.borrow();
4093        if inner.active_pass.is_some() {
4094            log::error!("cannot abandon SlotsHost during an active pass");
4095            return;
4096        }
4097        let runtime_state = inner.runtime_state.clone();
4098        drop(inner);
4099        let mut inner = self.inner.borrow_mut();
4100        let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4101        if let Some(state) = runtime_state {
4102            let host_key = self.storage_key();
4103            state.abandon_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle);
4104        }
4105        lifecycle.dispose_slot_table(&mut inner.table);
4106        inner.table = SlotTable::default();
4107        self.storage_key.set(inner.table.storage_id());
4108        inner.runtime_state = None;
4109        inner.lifecycle = slot::SlotLifecycleCoordinator::default();
4110    }
4111
4112    pub(crate) fn debug_stats(&self) -> SlotTableDebugStats {
4113        let inner = self.inner.borrow();
4114        let local = inner.table.debug_stats();
4115        let lifecycle = inner.lifecycle.debug_stats();
4116        let retention = inner
4117            .runtime_state
4118            .clone()
4119            .map(|state| state.slot_retention_debug_stats(self))
4120            .unwrap_or_default();
4121        SlotTableDebugStats::from_parts(local, lifecycle, retention)
4122    }
4123
4124    pub(crate) fn debug_snapshot(&self) -> slot::SlotDebugSnapshot {
4125        let inner = self.inner.borrow();
4126        let mut snapshot = inner.table.debug_snapshot();
4127        if let Some(state) = inner.runtime_state.clone() {
4128            state.fill_slot_debug_snapshot(self, &mut snapshot);
4129        }
4130        snapshot
4131    }
4132
4133    pub(crate) fn begin_pass(&self, mode: slot::SlotPassMode) {
4134        let mut inner = self.inner.borrow_mut();
4135        if inner.active_pass.is_some() {
4136            log::error!("slot pass already active for host");
4137            return;
4138        }
4139        let mut state = slot::SlotWriteSessionState::default();
4140        state.reset_for_pass(mode);
4141        inner.active_pass = Some(ActivePassState { state });
4142    }
4143
4144    pub(crate) fn has_active_pass(&self) -> bool {
4145        self.inner.borrow().active_pass.is_some()
4146    }
4147
4148    pub(crate) fn abandon_active_pass(&self) {
4149        self.inner.borrow_mut().active_pass = None;
4150    }
4151
4152    pub(crate) fn with_write_session<R>(
4153        &self,
4154        f: impl FnOnce(&mut slot::SlotWriteSession<'_>) -> R,
4155    ) -> R {
4156        let mut inner = self.inner.borrow_mut();
4157        let SlotsHostInner {
4158            table,
4159            lifecycle,
4160            active_pass,
4161            ..
4162        } = &mut *inner;
4163        let active_pass = active_pass
4164            .as_mut()
4165            .expect("slot write session requires an active pass");
4166        let mut session = table.write_session(lifecycle, &mut active_pass.state);
4167        f(&mut session)
4168    }
4169
4170    pub(crate) fn with_table_and_lifecycle_mut<R>(
4171        &self,
4172        f: impl FnOnce(&mut SlotTable, &mut slot::SlotLifecycleCoordinator) -> R,
4173    ) -> R {
4174        let mut inner = self.inner.borrow_mut();
4175        let SlotsHostInner {
4176            table, lifecycle, ..
4177        } = &mut *inner;
4178        f(table, lifecycle)
4179    }
4180
4181    pub(crate) fn finish_pass(
4182        &self,
4183        applier: &mut dyn Applier,
4184    ) -> Result<FinishedSlotPass, NodeError> {
4185        let mut inner = self.inner.borrow_mut();
4186        let SlotsHostInner {
4187            table,
4188            lifecycle,
4189            active_pass: active_pass_slot,
4190            ..
4191        } = &mut *inner;
4192        let Some(mut active_pass) = active_pass_slot.take() else {
4193            return Ok(FinishedSlotPass::default());
4194        };
4195
4196        active_pass.state.flush_payload_location_refreshes(table);
4197
4198        #[cfg(debug_assertions)]
4199        if let Err(err) = active_pass.state.validate(table) {
4200            log::error!("slot writer invariant violation before finalize_pass: {err:?}");
4201            return Err(NodeError::SlotHostUnavailable {
4202                operation: "SlotsHost::finish_pass",
4203                reason: "slot writer invariant violation",
4204            });
4205        }
4206
4207        let detached_root_children = {
4208            let mut session = table.write_session(lifecycle, &mut active_pass.state);
4209            session.finalize_pass(applier)?
4210        };
4211
4212        Ok(FinishedSlotPass {
4213            outcome: SlotPassOutcome {
4214                compacted: active_pass.state.request_compaction,
4215                compact_anchor_registry_storage: active_pass
4216                    .state
4217                    .request_anchor_storage_compaction,
4218                compact_payload_storage: active_pass.state.request_payload_storage_compaction,
4219            },
4220            detached_root_children,
4221        })
4222    }
4223
4224    pub(crate) fn complete_pass_cleanup(&self, outcome: &SlotPassOutcome) {
4225        let mut inner = self.inner.borrow_mut();
4226        let SlotsHostInner {
4227            table,
4228            lifecycle,
4229            runtime_state,
4230            ..
4231        } = &mut *inner;
4232        lifecycle.flush_pending_drops();
4233        if outcome.compacted {
4234            table.compact_storage();
4235            lifecycle.compact_storage();
4236        }
4237        if let Some(state) = runtime_state.clone() {
4238            state.compact_table_identity_storage_for_host(
4239                self,
4240                table,
4241                outcome.compact_anchor_registry_storage,
4242                outcome.compact_payload_storage,
4243            );
4244        } else {
4245            if outcome.compact_anchor_registry_storage {
4246                table.compact_anchor_registry_storage(None);
4247            }
4248            if outcome.compact_payload_storage {
4249                table.compact_payload_anchor_registry_storage(None);
4250            }
4251        }
4252        table.assert_fast_integrity("slot pass cleanup");
4253        #[cfg(any(test, debug_assertions))]
4254        {
4255            table.debug_verify();
4256            if let Some(state) = runtime_state.clone() {
4257                state.debug_verify_host(self, table);
4258            }
4259        }
4260    }
4261}
4262
4263fn build_child_positions(children: &[NodeId]) -> HashMap<NodeId, usize> {
4264    let mut positions = HashMap::default();
4265    positions.reserve(children.len());
4266    for (index, &child) in children.iter().enumerate() {
4267        positions.insert(child, index);
4268    }
4269    positions
4270}
4271
4272fn refresh_child_positions(
4273    current: &[NodeId],
4274    positions: &mut HashMap<NodeId, usize>,
4275    start: usize,
4276    end: usize,
4277) {
4278    if current.is_empty() || start >= current.len() {
4279        return;
4280    }
4281    let end = end.min(current.len() - 1);
4282    for (offset, &child) in current[start..=end].iter().enumerate() {
4283        positions.insert(child, start + offset);
4284    }
4285}
4286
4287fn insert_child_into_diff_state(
4288    current: &mut ChildList,
4289    positions: &mut HashMap<NodeId, usize>,
4290    index: usize,
4291    child: NodeId,
4292) {
4293    let index = index.min(current.len());
4294    current.insert(index, child);
4295    refresh_child_positions(current, positions, index, current.len() - 1);
4296}
4297
4298fn move_child_in_diff_state(
4299    current: &mut ChildList,
4300    positions: &mut HashMap<NodeId, usize>,
4301    from_index: usize,
4302    target_index: usize,
4303) -> usize {
4304    let child = current.remove(from_index);
4305    let to_index = target_index.min(current.len());
4306    current.insert(to_index, child);
4307    refresh_child_positions(
4308        current,
4309        positions,
4310        from_index.min(to_index),
4311        from_index.max(to_index),
4312    );
4313    to_index
4314}
4315
4316pub(crate) use state::MutableStateInner;
4317pub use state::{MutableState, OwnedMutableState, SnapshotStateList, SnapshotStateMap, State};
4318
4319fn hash_key<K: Hash>(key: &K) -> Key {
4320    let mut hasher = hash::default::new();
4321    key.hash(&mut hasher);
4322    hasher.finish()
4323}
4324
4325pub(crate) fn explicit_group_key_seed<K: Hash>(
4326    key: &K,
4327    caller: &'static std::panic::Location<'static>,
4328) -> slot::GroupKeySeed {
4329    let source_key = location_key(caller.file(), caller.line(), caller.column());
4330    let explicit_key = hash_key(key);
4331    slot::GroupKeySeed::keyed(source_key, explicit_key)
4332}
4333
4334#[cfg(test)]
4335#[path = "tests/mod.rs"]
4336mod tests;
4337
4338#[cfg(test)]
4339#[path = "tests/recursive_decrease_increase_test.rs"]
4340mod recursive_decrease_increase_test;
4341
4342pub mod collections;
4343pub mod hash;