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