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