Skip to main content

cranpose_core/
lib.rs

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