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