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