Skip to main content

cranpose_core/
lib.rs

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