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