Skip to main content

cranpose_core/
lib.rs

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