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