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