Skip to main content

cranpose_core/
lib.rs

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