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