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