Skip to main content

cranpose_core/
lib.rs

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