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