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