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