Skip to main content

tessera_ui/
context.rs

1//! # Context provider
2//!
3//! ## Usage
4//!
5//! Provide themes and configuration objects to descendant components.
6
7use std::{
8    any::{Any, TypeId},
9    cell::RefCell,
10    marker::PhantomData,
11    sync::Arc,
12};
13
14use im::HashMap as ImHashMap;
15use parking_lot::RwLock;
16use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
17
18use crate::{
19    execution_context::{with_execution_context, with_execution_context_mut},
20    read_dependencies::ReadDependencies,
21    runtime::{
22        RuntimePhase, compute_context_slot_key, current_phase,
23        current_replay_boundary_instance_key_from_scope, ensure_build_phase,
24        record_replay_boundary_invalidation_for_instance_key,
25    },
26    session::with_runtime_globals,
27};
28
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30struct ContextSnapshotEntry {
31    slot: u32,
32    generation: u64,
33    key: SlotKey,
34}
35
36type ContextBindings = ImHashMap<TypeId, ContextSnapshotEntry>;
37
38#[derive(Clone, Debug, Default, Eq, PartialEq)]
39pub(crate) struct ContextMap(Option<Arc<ContextBindings>>);
40
41impl ContextMap {
42    fn new() -> Self {
43        Self(None)
44    }
45
46    fn from_bindings(bindings: ContextBindings) -> Self {
47        Self((!bindings.is_empty()).then(|| Arc::new(bindings)))
48    }
49
50    fn get(&self, type_id: &TypeId) -> Option<&ContextSnapshotEntry> {
51        self.0.as_deref()?.get(type_id)
52    }
53
54    fn update(&self, type_id: TypeId, entry: ContextSnapshotEntry) -> Self {
55        let mut bindings = self.0.as_deref().cloned().unwrap_or_default();
56        bindings.insert(type_id, entry);
57        Self::from_bindings(bindings)
58    }
59
60    #[cfg(test)]
61    fn is_empty(&self) -> bool {
62        self.0.is_none()
63    }
64
65    pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
66        match (&self.0, &other.0) {
67            (None, None) => true,
68            (Some(a), Some(b)) => Arc::ptr_eq(a, b),
69            _ => false,
70        }
71    }
72}
73
74#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)]
75struct SlotKey {
76    instance_logic_id: u64,
77    slot_hash: u64,
78    type_id: TypeId,
79}
80
81struct SlotEntry {
82    key: SlotKey,
83    generation: u64,
84    value: Option<Arc<dyn Any + Send + Sync>>,
85    last_alive_epoch: u64,
86}
87
88#[derive(Default)]
89struct SlotTable {
90    entries: Vec<SlotEntry>,
91    free_list: Vec<u32>,
92    key_to_slot: HashMap<SlotKey, u32>,
93    slots_by_owner: HashMap<u64, HashSet<u32>>,
94    epoch: u64,
95}
96
97impl SlotTable {
98    fn index_slot(&mut self, key: SlotKey, slot: u32) {
99        self.key_to_slot.insert(key, slot);
100        self.slots_by_owner
101            .entry(key.instance_logic_id)
102            .or_default()
103            .insert(slot);
104    }
105
106    fn unindex_slot(&mut self, key: SlotKey, slot: u32) {
107        self.key_to_slot.remove(&key);
108        if let Some(slots) = self.slots_by_owner.get_mut(&key.instance_logic_id) {
109            slots.remove(&slot);
110            if slots.is_empty() {
111                self.slots_by_owner.remove(&key.instance_logic_id);
112            }
113        }
114    }
115
116    fn begin_epoch(&mut self) {
117        self.epoch = self.epoch.wrapping_add(1);
118    }
119}
120
121fn with_slot_table<R>(f: impl FnOnce(&SlotTable) -> R) -> R {
122    with_runtime_globals(|globals| f(&globals.context.slot_table.borrow()))
123}
124
125fn with_slot_table_mut<R>(f: impl FnOnce(&mut SlotTable) -> R) -> R {
126    with_runtime_globals(|globals| f(&mut globals.context.slot_table.borrow_mut()))
127}
128
129pub(crate) fn reset_context_slots() {
130    let removed = with_slot_table_mut(|table| {
131        let mut removed = Vec::new();
132        for entry in &mut table.entries {
133            if let Some(value) = entry.value.take() {
134                removed.push(value);
135            }
136            entry.generation = entry.generation.wrapping_add(1);
137        }
138        table.key_to_slot.clear();
139        table.slots_by_owner.clear();
140        table.free_list = (0..table.entries.len() as u32).collect();
141        removed
142    });
143    drop(removed);
144}
145
146#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
147struct ContextReadDependencyKey {
148    slot: u32,
149    generation: u64,
150}
151
152type ContextReadDependencyTracker = ReadDependencies<ContextReadDependencyKey, u64>;
153
154fn with_context_read_dependency_tracker<R>(
155    f: impl FnOnce(&ContextReadDependencyTracker) -> R,
156) -> R {
157    with_runtime_globals(|globals| f(&globals.context.read_dependency_tracker.borrow()))
158}
159
160fn with_context_read_dependency_tracker_mut<R>(
161    f: impl FnOnce(&mut ContextReadDependencyTracker) -> R,
162) -> R {
163    with_runtime_globals(|globals| f(&mut globals.context.read_dependency_tracker.borrow_mut()))
164}
165
166#[derive(Default)]
167pub(crate) struct ContextGlobals {
168    slot_table: RefCell<SlotTable>,
169    read_dependency_tracker: RefCell<ContextReadDependencyTracker>,
170}
171
172pub(crate) fn current_context_map() -> ContextMap {
173    with_execution_context(|context| context.context_map.clone())
174}
175
176fn resolve_snapshot_entry(
177    table: &SlotTable,
178    entry: ContextSnapshotEntry,
179) -> Option<ContextSnapshotEntry> {
180    let live_entry = table.entries.get(entry.slot as usize);
181    if let Some(live_entry) = live_entry
182        && live_entry.value.is_some()
183        && live_entry.key == entry.key
184    {
185        return Some(ContextSnapshotEntry {
186            slot: entry.slot,
187            generation: live_entry.generation,
188            key: entry.key,
189        });
190    }
191
192    let slot = table.key_to_slot.get(&entry.key).copied()?;
193    let live_entry = table.entries.get(slot as usize)?;
194    live_entry.value.as_ref()?;
195    Some(ContextSnapshotEntry {
196        slot,
197        generation: live_entry.generation,
198        key: entry.key,
199    })
200}
201
202pub(crate) fn normalize_context_snapshot(snapshot: &ContextMap) -> ContextMap {
203    let Some(bindings) = snapshot.0.as_deref() else {
204        return ContextMap::new();
205    };
206    let normalized = with_slot_table(|table| {
207        let mut updated = None;
208        for (&type_id, &entry) in bindings {
209            let resolved = resolve_snapshot_entry(table, entry);
210            if resolved == Some(entry) {
211                continue;
212            }
213            let next = updated.get_or_insert_with(|| bindings.clone());
214            if let Some(resolved) = resolved {
215                next.insert(type_id, resolved);
216            } else {
217                next.remove(&type_id);
218            }
219        }
220        updated
221    });
222    normalized.map_or_else(|| snapshot.clone(), ContextMap::from_bindings)
223}
224
225pub(crate) fn context_from_previous_snapshot_for_instance<T>(
226    instance_key: u64,
227) -> Option<Context<T>>
228where
229    T: Send + Sync + 'static,
230{
231    let map = crate::runtime::previous_component_context(instance_key)?;
232    map.get(&TypeId::of::<T>())
233        .copied()
234        .and_then(|entry| with_slot_table(|table| resolve_snapshot_entry(table, entry)))
235        .map(|entry| Context::new(entry.slot, entry.generation))
236}
237
238pub(crate) fn remove_context_read_dependencies(instance_keys: &HashSet<u64>) {
239    if instance_keys.is_empty() {
240        return;
241    }
242    with_context_read_dependency_tracker_mut(|tracker| tracker.remove_readers(instance_keys));
243}
244
245pub(crate) fn reset_context_read_dependencies() {
246    with_context_read_dependency_tracker_mut(|tracker| {
247        *tracker = ContextReadDependencyTracker::default();
248    });
249}
250
251pub(crate) fn with_context_map<R>(map: ContextMap, f: impl FnOnce() -> R) -> R {
252    struct RestoreContext(ContextMap);
253
254    impl Drop for RestoreContext {
255        fn drop(&mut self) {
256            with_execution_context_mut(|context| {
257                std::mem::swap(&mut context.context_map, &mut self.0);
258            });
259        }
260    }
261
262    let previous =
263        with_execution_context_mut(|context| std::mem::replace(&mut context.context_map, map));
264    let _restore = RestoreContext(previous);
265    f()
266}
267
268fn track_context_read_dependency(slot: u32, generation: u64) {
269    if !matches!(current_phase(), Some(RuntimePhase::Build)) {
270        return;
271    }
272    let Some(reader_instance_key) = current_replay_boundary_instance_key_from_scope() else {
273        return;
274    };
275
276    let key = ContextReadDependencyKey { slot, generation };
277    with_context_read_dependency_tracker_mut(|tracker| tracker.record(key, reader_instance_key));
278}
279
280fn context_read_subscribers(slot: u32, generation: u64) -> Vec<u64> {
281    let key = ContextReadDependencyKey { slot, generation };
282    with_context_read_dependency_tracker(|tracker| {
283        tracker
284            .readers(&key)
285            .map(Iterator::collect)
286            .unwrap_or_default()
287    })
288}
289
290pub(crate) fn begin_recompose_context_slot_epoch() {
291    // Start a new context-slot epoch for the current recomposition pass.
292    with_slot_table_mut(SlotTable::begin_epoch);
293    with_execution_context_mut(|context| context.context_map = ContextMap::new());
294}
295
296pub(crate) fn recycle_recomposed_context_slots_for_instance_logic_ids(
297    instance_logic_ids: &HashSet<u64>,
298) {
299    if instance_logic_ids.is_empty() {
300        return;
301    }
302
303    // Recycle untouched context slots for logic ids recomposed in this pass.
304    let removed = with_slot_table_mut(|table| {
305        let epoch = table.epoch;
306        let mut freed: Vec<(u32, SlotKey)> = Vec::new();
307        let mut removed = Vec::new();
308        let slots: Vec<_> = instance_logic_ids
309            .iter()
310            .flat_map(|id| table.slots_by_owner.get(id).into_iter().flatten())
311            .copied()
312            .collect();
313        for slot in slots {
314            let entry = &mut table.entries[slot as usize];
315            if !instance_logic_ids.contains(&entry.key.instance_logic_id) {
316                continue;
317            }
318            if entry.value.is_none() {
319                continue;
320            }
321            if entry.last_alive_epoch == epoch {
322                continue;
323            }
324
325            freed.push((slot, entry.key));
326            removed.push(entry.value.take());
327            entry.generation = entry.generation.wrapping_add(1);
328            entry.last_alive_epoch = 0;
329        }
330
331        for (slot, key) in freed {
332            table.unindex_slot(key, slot);
333            table.free_list.push(slot);
334        }
335        removed
336    });
337    // User destructors may read context after all slot metadata is consistent.
338    drop(removed);
339}
340
341pub(crate) fn live_context_slot_instance_logic_ids() -> HashSet<u64> {
342    with_slot_table(|table| table.slots_by_owner.keys().copied().collect())
343}
344
345pub(crate) fn drop_context_slots_for_instance_logic_ids(instance_logic_ids: &HashSet<u64>) {
346    if instance_logic_ids.is_empty() {
347        return;
348    }
349
350    let removed = with_slot_table_mut(|table| {
351        let mut freed: Vec<(u32, SlotKey)> = Vec::new();
352        let mut removed = Vec::new();
353        let slots: Vec<_> = instance_logic_ids
354            .iter()
355            .flat_map(|id| table.slots_by_owner.get(id).into_iter().flatten())
356            .copied()
357            .collect();
358        for slot in slots {
359            let entry = &mut table.entries[slot as usize];
360            if entry.value.is_none() {
361                continue;
362            }
363            if !instance_logic_ids.contains(&entry.key.instance_logic_id) {
364                continue;
365            }
366            freed.push((slot, entry.key));
367            removed.push(entry.value.take());
368            entry.generation = entry.generation.wrapping_add(1);
369            entry.last_alive_epoch = 0;
370        }
371
372        for (slot, key) in freed {
373            table.unindex_slot(key, slot);
374            table.free_list.push(slot);
375        }
376        removed
377    });
378    drop(removed);
379}
380
381/// Handle to inherited visual defaults created by [`provide_context`].
382///
383/// Accessing a stale handle fails rather than reading a replacement value.
384pub struct Context<T> {
385    session: u128,
386    slot: u32,
387    generation: u64,
388    _marker: PhantomData<T>,
389}
390
391impl<T> Copy for Context<T> {}
392
393impl<T> Clone for Context<T> {
394    fn clone(&self) -> Self {
395        *self
396    }
397}
398
399impl<T> Context<T> {
400    fn new(slot: u32, generation: u64) -> Self {
401        Self {
402            session: crate::session::current_session_identity().expect("active context session"),
403            slot,
404            generation,
405            _marker: PhantomData,
406        }
407    }
408}
409
410impl<T> Context<T>
411where
412    T: Send + Sync + 'static,
413{
414    fn load_entry(&self) -> Arc<dyn Any + Send + Sync> {
415        assert_eq!(
416            crate::session::current_session_identity(),
417            Some(self.session),
418            "context belongs to a different runtime session"
419        );
420        with_slot_table(|table| {
421            let entry = table
422                .entries
423                .get(self.slot as usize)
424                .unwrap_or_else(|| panic!("Context points to freed slot: {}", self.slot));
425
426            if entry.generation != self.generation {
427                panic!(
428                    "Context is stale (slot {}, generation {}, current generation {})",
429                    self.slot, self.generation, entry.generation
430                );
431            }
432
433            if entry.key.type_id != TypeId::of::<T>() {
434                panic!(
435                    "Context type mismatch for slot {}: expected {}, stored {:?}",
436                    self.slot,
437                    std::any::type_name::<T>(),
438                    entry.key.type_id
439                );
440            }
441
442            entry
443                .value
444                .as_ref()
445                .unwrap_or_else(|| panic!("Context slot {} has been recycled", self.slot))
446                .clone()
447        })
448    }
449
450    fn load_lock(&self) -> Arc<RwLock<T>> {
451        self.load_entry()
452            .downcast::<RwLock<T>>()
453            .unwrap_or_else(|_| panic!("Context slot {} downcast failed", self.slot))
454    }
455
456    /// Execute a closure with a shared reference to the context value.
457    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
458        assert_eq!(
459            crate::session::current_session_identity(),
460            Some(self.session),
461            "context belongs to a different runtime session"
462        );
463        track_context_read_dependency(self.slot, self.generation);
464        let lock = self.load_lock();
465        let guard = lock.read();
466        f(&guard)
467    }
468
469    /// Execute a closure with a mutable reference to the context value.
470    pub fn with_mut<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
471        let lock = self.load_lock();
472        let result = {
473            let mut guard = lock.write();
474            f(&mut guard)
475        };
476        let subscribers = context_read_subscribers(self.slot, self.generation);
477        for instance_key in subscribers {
478            record_replay_boundary_invalidation_for_instance_key(instance_key);
479        }
480        result
481    }
482
483    /// Get a cloned value. Requires `T: Clone`.
484    pub fn get(&self) -> T
485    where
486        T: Clone,
487    {
488        self.with(Clone::clone)
489    }
490
491    /// Set the context value.
492    pub fn set(&self, value: T) {
493        self.with_mut(|v| *v = value);
494    }
495
496    /// Replace the context value and return the old value.
497    pub fn replace(&self, value: T) -> T {
498        self.with_mut(|v| std::mem::replace(v, value))
499    }
500}
501
502/// Provides a typed context value for the duration of the given closure.
503///
504/// The `init` closure is evaluated only when the context slot is created (or
505/// re-created after being recycled).
506///
507/// This is intended for use inside component build functions only.
508///
509/// # Examples
510///
511/// ```
512/// use tessera_ui::{Color, provide_context, tessera, use_context};
513///
514/// #[derive(Default)]
515/// struct Theme {
516///     primary: Color,
517/// }
518///
519/// #[tessera]
520/// fn root() {
521///     provide_context(
522///         || Theme {
523///             primary: Color::RED,
524///         },
525///         || {
526///             leaf();
527///         },
528///     );
529/// }
530///
531/// #[tessera]
532/// fn leaf() {
533///     let theme = use_context::<Theme>().expect("Theme must be provided");
534///     theme.with(|t| assert_eq!(t.primary, Color::RED));
535/// }
536/// ```
537pub fn provide_context<T, I, F, R>(init: I, f: F) -> R
538where
539    T: Send + Sync + 'static,
540    I: FnOnce() -> T,
541    F: FnOnce() -> R,
542{
543    ensure_build_phase();
544
545    let (instance_logic_id, slot_hash) = compute_context_slot_key();
546    let type_id = TypeId::of::<T>();
547    let slot_key = SlotKey {
548        instance_logic_id,
549        slot_hash,
550        type_id,
551    };
552
553    let (slot, generation) = {
554        with_slot_table_mut(|table| {
555            let epoch = table.epoch;
556
557            if let Some(slot) = table.key_to_slot.get(&slot_key).copied() {
558                let entry = table
559                    .entries
560                    .get_mut(slot as usize)
561                    .expect("context slot entry should exist");
562                entry.last_alive_epoch = epoch;
563
564                if entry.value.is_none() {
565                    entry.value = Some(Arc::new(RwLock::new(init())));
566                    entry.generation = entry.generation.wrapping_add(1);
567                }
568
569                let generation = entry.generation;
570                (slot, generation)
571            } else if let Some(slot) = table.free_list.pop() {
572                let entry = table
573                    .entries
574                    .get_mut(slot as usize)
575                    .expect("context slot entry should exist");
576
577                entry.key = slot_key;
578                entry.value = Some(Arc::new(RwLock::new(init())));
579                entry.last_alive_epoch = epoch;
580
581                let generation = entry.generation;
582                table.index_slot(slot_key, slot);
583                (slot, generation)
584            } else {
585                let generation = 0;
586                let slot = table.entries.len() as u32;
587                table.entries.push(SlotEntry {
588                    key: slot_key,
589                    generation,
590                    value: Some(Arc::new(RwLock::new(init()))),
591                    last_alive_epoch: epoch,
592                });
593                table.index_slot(slot_key, slot);
594                (slot, generation)
595            }
596        })
597    };
598
599    let map = current_context_map().update(
600        type_id,
601        ContextSnapshotEntry {
602            slot,
603            generation,
604            key: slot_key,
605        },
606    );
607    with_context_map(map, f)
608}
609
610/// Reads inherited visual defaults from the current build scope.
611///
612/// Returns `None` when no provider of this type is present.
613pub fn use_context<T>() -> Option<Context<T>>
614where
615    T: Send + Sync + 'static,
616{
617    ensure_build_phase();
618
619    with_execution_context(|context| {
620        context
621            .context_map
622            .get(&TypeId::of::<T>())
623            .copied()
624            .map(|entry| Context::new(entry.slot, entry.generation))
625    })
626}
627// (legacy comment removed)
628
629#[cfg(test)]
630mod tests {
631    use std::{any::TypeId, sync::Arc};
632
633    use parking_lot::RwLock;
634
635    use super::{
636        ContextMap, ContextSnapshotEntry, SlotEntry, SlotKey, SlotTable,
637        normalize_context_snapshot, with_slot_table_mut,
638    };
639    use crate::execution_context::{
640        reset_execution_context, with_execution_context, with_execution_context_mut,
641    };
642    use crate::runtime::{GroupPath, RuntimePhase, push_phase, with_replay_scope};
643
644    fn with_context_snapshot<R>(snapshot: &ContextMap, body: impl FnOnce() -> R) -> R {
645        with_replay_scope(
646            0,
647            GroupPath::default(),
648            None,
649            normalize_context_snapshot(snapshot),
650            body,
651        )
652    }
653
654    fn reset_test_state() {
655        with_slot_table_mut(|table| *table = SlotTable::default());
656        reset_execution_context();
657    }
658
659    #[test]
660    fn context_cleanup_drops_values_outside_table_borrow() {
661        let _runtime_session = crate::RuntimeSession::new();
662        let _runtime_scope = _runtime_session.enter();
663        struct ReadOnDrop;
664        impl Drop for ReadOnDrop {
665            fn drop(&mut self) {
666                super::with_slot_table(|table| {
667                    assert!(table.entries.iter().all(|entry| entry.value.is_none()));
668                    assert!(table.key_to_slot.is_empty());
669                });
670            }
671        }
672
673        for cleanup in 0..3 {
674            reset_test_state();
675            let key = SlotKey {
676                instance_logic_id: 42,
677                slot_hash: 7,
678                type_id: TypeId::of::<ReadOnDrop>(),
679            };
680            with_slot_table_mut(|table| {
681                table.entries.push(SlotEntry {
682                    key,
683                    generation: 0,
684                    value: Some(Arc::new(ReadOnDrop)),
685                    last_alive_epoch: 0,
686                });
687                table.index_slot(key, 0);
688                table.begin_epoch();
689            });
690            let instances = super::HashSet::from_iter([42]);
691            if cleanup == 2 {
692                crate::runtime::reset_slots();
693            } else if cleanup == 1 {
694                super::recycle_recomposed_context_slots_for_instance_logic_ids(&instances);
695            } else {
696                super::drop_context_slots_for_instance_logic_ids(&instances);
697            }
698            super::with_slot_table(|table| {
699                assert_eq!(table.free_list, [0]);
700                assert_eq!(table.entries[0].generation, 1);
701            });
702        }
703    }
704
705    #[test]
706    fn with_context_snapshot_restores_bindings_after_panic() {
707        let _runtime_session = crate::RuntimeSession::new();
708        let _runtime_scope = _runtime_session.enter();
709        reset_test_state();
710
711        let base_layer = ContextMap::new();
712        let parent_layer = base_layer.update(
713            TypeId::of::<u8>(),
714            ContextSnapshotEntry {
715                slot: 1,
716                generation: 2,
717                key: SlotKey {
718                    instance_logic_id: 7,
719                    slot_hash: 11,
720                    type_id: TypeId::of::<u8>(),
721                },
722            },
723        );
724        with_execution_context_mut(|context| {
725            context.context_map = parent_layer.clone();
726        });
727
728        let snapshot = ContextMap::new().update(
729            TypeId::of::<u32>(),
730            ContextSnapshotEntry {
731                slot: 3,
732                generation: 4,
733                key: SlotKey {
734                    instance_logic_id: 17,
735                    slot_hash: 19,
736                    type_id: TypeId::of::<u32>(),
737                },
738            },
739        );
740        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
741            with_context_snapshot(&snapshot, || {
742                with_execution_context(|context| {
743                    assert!(context.context_map.is_empty());
744                });
745                panic!("expected panic in context snapshot test");
746            });
747        }));
748        assert!(result.is_err());
749
750        with_execution_context(|context| {
751            assert_eq!(context.context_map, parent_layer);
752        });
753    }
754
755    #[test]
756    fn with_context_snapshot_shares_live_bindings() {
757        #[crate::tessera]
758        fn context_provider() {
759            super::provide_context(
760                || 42_u8,
761                || {
762                    let snapshot = super::current_context_map();
763                    with_context_snapshot(&snapshot, || {
764                        assert!(snapshot.ptr_eq(&super::current_context_map()));
765                    });
766                },
767            );
768        }
769
770        crate::testing::layout_test(|| context_provider()).run();
771    }
772
773    #[test]
774    fn with_context_snapshot_remaps_stale_generation() {
775        let _runtime_session = crate::RuntimeSession::new();
776        let _runtime_scope = _runtime_session.enter();
777        reset_test_state();
778
779        let key = SlotKey {
780            instance_logic_id: 31,
781            slot_hash: 37,
782            type_id: TypeId::of::<u8>(),
783        };
784        {
785            with_slot_table_mut(|table| {
786                *table = SlotTable::default();
787                table.entries.push(SlotEntry {
788                    key,
789                    generation: 2,
790                    value: Some(Arc::new(RwLock::new(42_u8))),
791                    last_alive_epoch: 1,
792                });
793                table.index_slot(key, 0);
794            });
795        }
796
797        let snapshot = ContextMap::new().update(
798            TypeId::of::<u8>(),
799            ContextSnapshotEntry {
800                slot: 0,
801                generation: 1,
802                key,
803            },
804        );
805
806        let _phase_guard = push_phase(RuntimePhase::Build);
807        with_context_snapshot(&snapshot, || {
808            let context = super::use_context::<u8>().expect("context should be remapped");
809            assert_eq!(context.get(), 42);
810        });
811    }
812
813    #[test]
814    fn with_context_snapshot_remaps_stale_slot_by_key() {
815        let _runtime_session = crate::RuntimeSession::new();
816        let _runtime_scope = _runtime_session.enter();
817        reset_test_state();
818
819        let key = SlotKey {
820            instance_logic_id: 41,
821            slot_hash: 43,
822            type_id: TypeId::of::<u16>(),
823        };
824        let old_key = SlotKey {
825            instance_logic_id: 47,
826            slot_hash: 53,
827            type_id: TypeId::of::<u16>(),
828        };
829        {
830            with_slot_table_mut(|table| {
831                *table = SlotTable::default();
832                table.entries.push(SlotEntry {
833                    key: old_key,
834                    generation: 9,
835                    value: None,
836                    last_alive_epoch: 0,
837                });
838                table.entries.push(SlotEntry {
839                    key,
840                    generation: 4,
841                    value: Some(Arc::new(RwLock::new(7_u16))),
842                    last_alive_epoch: 2,
843                });
844                table.index_slot(key, 1);
845            });
846        }
847
848        let snapshot = ContextMap::new().update(
849            TypeId::of::<u16>(),
850            ContextSnapshotEntry {
851                slot: 0,
852                generation: 3,
853                key,
854            },
855        );
856
857        let _phase_guard = push_phase(RuntimePhase::Build);
858        with_context_snapshot(&snapshot, || {
859            let context = super::use_context::<u16>().expect("context should be remapped by key");
860            assert_eq!(context.get(), 7);
861        });
862    }
863}