zombie_rs/
context.rs

1//! Context nodes for storing computation state and enabling replay.
2//!
3//! Contexts are the fundamental building blocks of the tock tree (akasha).
4//! They store zombie values and the metadata needed to recompute them.
5//!
6//! # Context Types
7//!
8//! - [`RootContext`]: Non-evictable values at the top level. Created when
9//!   `Zombie::new` is called outside of any `bind` operation.
10//!
11//! - [`FullContext`]: Evictable values from `bind` operations. Contains a
12//!   [`Replayer`] that can recreate all values in this context.
13//!
14//! # Paper Reference
15//!
16//! Section 3 (PVZ): "A context at tock t contain the following:
17//! - A function pointer
18//! - A list of tock representing the input of said function pointer
19//! - A Timespan recording how long the execution take
20//! - A vector of shared ptr, storing ZombieNode"
21//!
22//! # Memory Layout
23//!
24//! Each context owns an array of `Option<Rc<dyn EZombieNode>>`. When a value
25//! is evicted, its slot becomes `None` but the context remains to preserve
26//! the replayer for future recomputation.
27
28use std::any::Any;
29use std::cell::{Cell, RefCell};
30use std::num::NonZeroUsize;
31use std::rc::{Rc, Weak};
32
33use smallvec::SmallVec;
34
35use crate::config::Cost;
36use crate::meter::{Space, Time, ZombieOps};
37use crate::record::Replayer;
38use crate::tock::Tock;
39use crate::union_find::UF;
40
41/// SmallVec type alias for storing Tock dependencies.
42/// Most bind operations have 1-4 dependencies, so inline storage for 4 elements
43/// avoids heap allocation in the common case. sizeof(Tock) = 8, so 4*8 = 32 bytes inline.
44pub type TockVec = SmallVec<[Tock; 4]>;
45
46/// SmallVec type alias for storing zombie node values.
47/// Most contexts contain 1-2 values, so inline storage for 2 elements
48/// avoids heap allocation. sizeof(Option<Rc<...>>) = 16, so 2*16 = 32 bytes inline.
49pub type ValueVec = SmallVec<[Option<Rc<dyn EZombieNode>>; 2]>;
50
51/// Reference-counted context node (type-erased).
52pub type ContextRef = Rc<dyn ContextNode>;
53
54/// Weak reference to context node (type-erased).
55pub type ContextWeakRef = Weak<dyn ContextNode>;
56
57/// Type-erased zombie node trait.
58pub trait EZombieNode: Any {
59    fn created_time(&self) -> Tock;
60    fn size(&self) -> usize;
61    fn as_any(&self) -> &dyn Any;
62    fn get_value_ptr(&self) -> *const dyn Any;
63    fn set_context(&self, ctx: ContextWeakRef);
64    fn get_context(&self) -> Option<ContextRef>;
65    /// Get a clone of the weak context reference without upgrading.
66    /// This is more efficient than get_context() when you only need the weak reference.
67    fn clone_context_weak(&self) -> Option<ContextWeakRef>;
68    fn notify_accessed(&self);
69}
70
71/// Concrete zombie node holding a value of type T.
72///
73/// Memory layout optimized to match C++ implementation:
74/// - created_time: 8 bytes (Tock = u64)
75/// - value: sizeof(T)
76/// - context: 16 bytes (Weak pointer, but dyn makes it fat pointer)
77///
78/// Note: Unlike C++ which stores context as a simple weak_ptr (8 bytes),
79/// Rust requires a fat pointer for dyn ContextNode (16 bytes).
80/// This is an unavoidable overhead in Rust's type system.
81pub struct ZombieNode<T: ZombieOps> {
82    pub created_time: Tock,
83    pub value: T,
84    // Use Cell for smaller size (RefCell adds 8 bytes for borrow flag)
85    context: std::cell::Cell<Option<ContextWeakRef>>,
86}
87
88impl<T: 'static + Clone + ZombieOps> ZombieNode<T> {
89    #[inline]
90    pub fn new(created_time: Tock, value: T) -> Rc<Self> {
91        Rc::new(Self {
92            created_time,
93            value,
94            context: std::cell::Cell::new(None),
95        })
96    }
97
98    #[inline]
99    pub fn get(&self) -> &T {
100        self.notify_accessed();
101        &self.value
102    }
103}
104
105impl<T: 'static + ZombieOps> EZombieNode for ZombieNode<T> {
106    fn created_time(&self) -> Tock {
107        self.created_time
108    }
109
110    fn size(&self) -> usize {
111        // Compute size dynamically like C++ get_size() virtual function
112        self.value.get_size()
113    }
114
115    fn as_any(&self) -> &dyn Any {
116        self
117    }
118
119    fn get_value_ptr(&self) -> *const dyn Any {
120        &self.value as &dyn Any as *const dyn Any
121    }
122
123    fn set_context(&self, ctx: ContextWeakRef) {
124        self.context.set(Some(ctx));
125    }
126
127    fn get_context(&self) -> Option<ContextRef> {
128        // Take the weak pointer out, try to upgrade, then put it back
129        let weak = self.context.take();
130        let result = weak.as_ref().and_then(|w| w.upgrade());
131        self.context.set(weak);
132        result
133    }
134
135    fn clone_context_weak(&self) -> Option<ContextWeakRef> {
136        // Take the weak pointer out, clone it, then put it back
137        let weak = self.context.take();
138        let result = weak.clone();
139        self.context.set(weak);
140        result
141    }
142
143    fn notify_accessed(&self) {
144        if let Some(ctx) = self.get_context() {
145            ctx.accessed();
146        }
147    }
148}
149
150/// Trait for context nodes (tock tree entries).
151///
152/// Note: This trait must be dyn-compatible, so no generic methods allowed.
153/// Type-specific operations are done via downcasting through as_any().
154pub trait ContextNode {
155    fn start_tock(&self) -> Tock;
156    fn end_tock(&self) -> Tock;
157    fn evictable(&self) -> bool;
158    fn evict(&self);
159    fn evict_at(&self, idx: usize);
160    fn accessed(&self);
161    fn get_replayer(&self) -> Option<Rc<Replayer>>;
162    fn get_value_at(&self, idx: usize) -> Option<Rc<dyn EZombieNode>>;
163    /// For downcasting to concrete context types.
164    fn as_any(&self) -> &dyn Any;
165    /// Get forward_uf for backedge registration. Returns None for RootContext.
166    fn get_forward_uf(&self) -> Option<UF> {
167        None
168    }
169}
170
171impl std::fmt::Debug for dyn ContextNode {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        write!(f, "ContextNode(start={}, end={})", self.start_tock(), self.end_tock())
174    }
175}
176
177/// Root context - top level, not evictable.
178pub struct RootContext {
179    start: Tock,
180    end: RefCell<Tock>,
181    values: RefCell<ValueVec>,
182    // Using Cell<Space> instead of RefCell<Space> saves 8 bytes (Space is Copy)
183    space_taken: Cell<Space>,
184    replayer: Option<Rc<Replayer>>,
185}
186
187impl std::fmt::Debug for RootContext {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        f.debug_struct("RootContext")
190            .field("start", &self.start)
191            .field("end", &*self.end.borrow())
192            .field("values_count", &self.values.borrow().len())
193            .finish()
194    }
195}
196
197impl RootContext {
198    pub fn new(start: Tock) -> Rc<Self> {
199        Rc::new(Self {
200            start,
201            end: RefCell::new(start),
202            values: RefCell::new(ValueVec::new()),
203            space_taken: Cell::new(Space::ZERO),
204            replayer: None,
205        })
206    }
207
208    /// Create with pre-recorded values.
209    pub fn new_with_values(
210        start: Tock,
211        values: ValueVec,
212        space: Space,
213        replayer: Option<Rc<Replayer>>,
214    ) -> Rc<Self> {
215        let end = Tock(start.0 + values.len() as i64 + 1);
216        let ctx = Rc::new(Self {
217            start,
218            end: RefCell::new(end),
219            values: RefCell::new(values),
220            space_taken: Cell::new(space),
221            replayer,
222        });
223
224        // Set context reference for all values
225        let weak_ctx: ContextWeakRef = Rc::downgrade(&ctx) as ContextWeakRef;
226        for value in ctx.values.borrow().iter().flatten() {
227            value.set_context(weak_ctx.clone());
228        }
229
230        ctx
231    }
232
233    /// Push a new value to this context.
234    pub fn push(&self, node: Rc<dyn EZombieNode>) {
235        self.space_taken.set(self.space_taken.get() + Space::from_bytes(node.size()));
236        self.values.borrow_mut().push(Some(node));
237        *self.end.borrow_mut() = self.start + self.values.borrow().len() + 1;
238    }
239}
240
241impl ContextNode for RootContext {
242    fn start_tock(&self) -> Tock {
243        self.start
244    }
245
246    fn end_tock(&self) -> Tock {
247        *self.end.borrow()
248    }
249
250    fn evictable(&self) -> bool {
251        false
252    }
253
254    fn evict(&self) {
255        // Root context cannot be evicted
256    }
257
258    fn evict_at(&self, _idx: usize) {
259        // Root context values cannot be evicted
260    }
261
262    fn accessed(&self) {
263        // No-op for root context
264    }
265
266    fn get_replayer(&self) -> Option<Rc<Replayer>> {
267        self.replayer.clone()
268    }
269
270    fn get_value_at(&self, idx: usize) -> Option<Rc<dyn EZombieNode>> {
271        self.values.borrow().get(idx).cloned().flatten()
272    }
273
274    fn as_any(&self) -> &dyn Any {
275        self
276    }
277}
278
279/// Full context - evictable, created by bind operations.
280pub struct FullContext {
281    start: Tock,
282    end: Tock,
283    values: RefCell<ValueVec>,
284    // Using Cell<Space> instead of RefCell<Space> saves 8 bytes (Space is Copy)
285    space_taken: Cell<Space>,
286    time_taken: Time,
287    /// Last accessed time in nanoseconds since UNIX epoch.
288    /// Using Cell<u64> instead of RefCell<Duration> saves 16 bytes.
289    last_accessed_ns: Cell<u64>,
290    replayer: Rc<Replayer>,
291    dependencies: TockVec,
292
293    // For eviction cost calculation (Union-Find based)
294    // Using Cell<Option<NonZeroUsize>> instead of RefCell<Option<usize>> saves 16 bytes:
295    // - RefCell<Option<usize>> = 24 bytes (borrow flag + padding + option + value)
296    // - Cell<Option<NonZeroUsize>> = 8 bytes (niche optimization: None = 0)
297    // The index is stored as (actual_index + 1) to allow 0 to represent None.
298    pool_index: Cell<Option<NonZeroUsize>>,
299    forward_uf: UF,
300    backward_uf: UF,
301    // NOTE: Backedges are stored in Trailokya's backedges HashMap, not here.
302    // This achieves zero memory overhead when eviction is disabled.
303}
304
305impl std::fmt::Debug for FullContext {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        f.debug_struct("FullContext")
308            .field("start", &self.start)
309            .field("end", &self.end)
310            .field("values_count", &self.values.borrow().len())
311            .finish()
312    }
313}
314
315impl FullContext {
316    /// Create a new FullContext.
317    ///
318    /// Note: Backedge registration should be done by the caller after creation
319    /// but before inserting into akasha. This matches the C++ design where
320    /// backedge registration happens in the constructor (before the context
321    /// is visible in akasha).
322    pub fn new(
323        start: Tock,
324        end: Tock,
325        values: ValueVec,
326        space: Space,
327        time_taken: Time,
328        replayer: Rc<Replayer>,
329        dependencies: TockVec,
330    ) -> Rc<Self> {
331        let ctx = Rc::new(Self {
332            start,
333            end,
334            values: RefCell::new(values),
335            space_taken: Cell::new(space),
336            time_taken,
337            last_accessed_ns: Cell::new(0),
338            replayer,
339            dependencies,
340            pool_index: Cell::new(None),
341            forward_uf: UF::new(Time::ZERO),
342            backward_uf: UF::new(Time::ZERO),
343        });
344
345        // Set context reference for all values
346        let weak_ctx: ContextWeakRef = Rc::downgrade(&ctx) as ContextWeakRef;
347        for value in ctx.values.borrow().iter().flatten() {
348            value.set_context(weak_ctx.clone());
349        }
350
351        ctx
352    }
353
354    pub fn set_pool_index(&self, idx: usize) {
355        // Store as idx + 1 to use NonZeroUsize (allows niche optimization)
356        self.pool_index.set(NonZeroUsize::new(idx + 1));
357    }
358
359    pub fn clear_pool_index(&self) {
360        self.pool_index.set(None);
361    }
362
363    pub fn pool_index(&self) -> Option<usize> {
364        // Convert back: stored value - 1 = actual index
365        self.pool_index.get().map(|n| n.get() - 1)
366    }
367
368    /// Compute time cost for eviction decision.
369    pub fn time_cost(&self) -> Time {
370        self.time_taken + self.forward_uf.value() + self.backward_uf.value()
371    }
372
373    /// Compute eviction cost using metric function.
374    pub fn cost(&self, metric: fn(Time, Time, Space) -> Cost) -> Cost {
375        metric(self.time_taken, self.time_cost(), self.space_taken.get())
376    }
377
378    pub fn space(&self) -> Space {
379        self.space_taken.get()
380    }
381
382    pub fn dependencies(&self) -> &[Tock] {
383        &self.dependencies
384    }
385
386    /// Get the time taken for this computation (used for cost calculation).
387    pub fn time_taken(&self) -> Time {
388        self.time_taken
389    }
390
391    /// Get the backward UF for this context.
392    pub fn backward_uf(&self) -> &UF {
393        &self.backward_uf
394    }
395
396    /// Get the forward UF for this context.
397    pub fn forward_uf(&self) -> &UF {
398        &self.forward_uf
399    }
400}
401
402impl ContextNode for FullContext {
403    fn start_tock(&self) -> Tock {
404        self.start
405    }
406
407    fn end_tock(&self) -> Tock {
408        self.end
409    }
410
411    fn evictable(&self) -> bool {
412        true
413    }
414
415    fn evict(&self) {
416        // Clear all values to free memory
417        for v in self.values.borrow_mut().iter_mut() {
418            *v = None;
419        }
420        self.space_taken.set(Space::ZERO);
421
422        // Merge time cost into this context's own UF structures
423        // This is the basic cost propagation within the context itself.
424        //
425        // NOTE: Full cost propagation to other contexts (dependencies, parent,
426        // dependent contexts) is handled in Trailokya::evict_one() since it
427        // requires access to the akasha (tock tree) to find related contexts.
428        let cost = UF::new(self.time_taken);
429        self.forward_uf.merge(&cost);
430        self.backward_uf.merge(&cost);
431    }
432
433    fn evict_at(&self, idx: usize) {
434        let mut values = self.values.borrow_mut();
435        if let Some(node) = values.get_mut(idx).and_then(|slot| slot.take()) {
436            self.space_taken.set(self.space_taken.get() - Space::from_bytes(node.size()));
437        }
438    }
439
440    fn accessed(&self) {
441        // Update last accessed time for recency tracking.
442        // The GD heap uses this during eviction to prioritize recently accessed items.
443        use std::time::{SystemTime, UNIX_EPOCH};
444        let now_ns = SystemTime::now()
445            .duration_since(UNIX_EPOCH)
446            .map(|d| d.as_nanos() as u64)
447            .unwrap_or(0);
448        self.last_accessed_ns.set(now_ns);
449    }
450
451    fn get_replayer(&self) -> Option<Rc<Replayer>> {
452        Some(self.replayer.clone())
453    }
454
455    fn get_value_at(&self, idx: usize) -> Option<Rc<dyn EZombieNode>> {
456        self.values.borrow().get(idx).cloned().flatten()
457    }
458
459    fn as_any(&self) -> &dyn Any {
460        self
461    }
462
463    fn get_forward_uf(&self) -> Option<UF> {
464        Some(self.forward_uf)
465    }
466}
467
468/// Downcast an `Rc<dyn EZombieNode>` to `Rc<ZombieNode<T>>`.
469///
470/// This is a critical hot-path function used during `get()` and `bind()` operations.
471///
472/// # Type Safety
473///
474/// Uses Rust's `TypeId` mechanism (via `Any::is::<T>()`) to verify the concrete type
475/// before downcasting. This is the standard, safe way to perform runtime type checking.
476///
477/// # Memory Safety (unsafe block justification)
478///
479/// The unsafe block is necessary because Rust doesn't provide a safe way to convert
480/// `Rc<dyn Trait>` to `Rc<ConcreteType>` even after verifying the type. Here's why
481/// the conversion is safe:
482///
483/// 1. **Type Verification**: We've confirmed via `is::<ZombieNode<T>>()` that the
484///    concrete type is exactly `ZombieNode<T>`.
485///
486/// 2. **Rc Layout**: `Rc<dyn Trait>` stores a fat pointer (data + vtable) to an `RcBox`.
487///    `Rc<ConcreteType>` stores a thin pointer to the same `RcBox`. Both point to the
488///    same allocation; only the pointer representation differs.
489///
490/// 3. **Clone Before Convert**: We clone the `Rc` first, incrementing the strong count.
491///    `Rc::into_raw` gives us the data pointer without decrementing count. `Rc::from_raw`
492///    reconstructs the `Rc` from that data pointer. The net effect: count stays correct.
493///
494/// 4. **Fat-to-Thin Conversion**: `raw as *const ZombieNode<T>` extracts just the data
495///    pointer from the fat pointer. This is well-defined and portable.
496///
497/// This pattern is widely used in the Rust ecosystem for trait object downcasting.
498/// See also: `downcast-rs`, `intertrait`, and similar crates.
499///
500/// # Example
501///
502/// ```ignore
503/// let node: Rc<dyn EZombieNode> = ZombieNode::new(Tock(1), 42i32);
504/// let typed: Option<Rc<ZombieNode<i32>>> = downcast_zombie(&node);
505/// assert!(typed.is_some());
506/// ```
507#[inline]
508pub fn downcast_zombie<T: 'static + ZombieOps>(node: &Rc<dyn EZombieNode>) -> Option<Rc<ZombieNode<T>>> {
509    // Type check via Any - this is the standard Rust way to do runtime type checking
510    let any_ref = node.as_any();
511    if !any_ref.is::<ZombieNode<T>>() {
512        return None;
513    }
514
515    // SAFETY: See function documentation above for detailed justification.
516    //
517    // Summary:
518    // - Type verified via TypeId comparison
519    // - Clone ensures reference count is incremented before into_raw
520    // - Fat-to-thin pointer cast preserves the data pointer
521    // - from_raw reconstructs Rc with correct RcBox address
522    let cloned = node.clone();
523    let raw: *const dyn EZombieNode = Rc::into_raw(cloned);
524    let data_ptr = raw as *const ZombieNode<T>;
525    Some(unsafe { Rc::from_raw(data_ptr) })
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531
532    #[test]
533    fn test_root_context() {
534        let ctx = RootContext::new(Tock(0));
535        assert!(!ctx.evictable());
536        assert_eq!(ctx.start_tock(), Tock(0));
537    }
538
539    #[test]
540    fn test_zombie_node_context_link() {
541        let ctx = RootContext::new(Tock(0));
542        let node = ZombieNode::new(Tock(1), 42);
543
544        // Set context
545        let weak_ctx: ContextWeakRef = Rc::downgrade(&ctx) as ContextWeakRef;
546        node.set_context(weak_ctx);
547
548        // Verify link
549        assert!(node.get_context().is_some());
550    }
551
552    #[test]
553    fn test_downcast_zombie() {
554        let node: Rc<dyn EZombieNode> = ZombieNode::new(Tock(1), 42i32);
555
556        // Correct type
557        let typed = downcast_zombie::<i32>(&node);
558        assert!(typed.is_some());
559        assert_eq!(typed.unwrap().value, 42);
560
561        // Wrong type
562        let wrong = downcast_zombie::<String>(&node);
563        assert!(wrong.is_none());
564    }
565}