zombie_rs/
record.rs

1//! Record nodes for tracking computation state during bind operations.
2//!
3//! The record system implements a stack-based state machine following C++ semantics:
4//! - `Record::Root`: Top-level record, creates RootContext when suspended
5//! - `Record::Head`: Active record for a bind computation
6//! - `Record::Value`: Completed record holding a result tock
7//!
8//! # Time Measurement
9//!
10//! Currently uses PLANK_TIME (constant) for time cost estimation, matching the
11//! C++ reference implementation's `use_measured_time = false` setting. This provides:
12//! - Deterministic behavior across runs
13//! - No timing overhead
14//! - Simpler implementation
15//!
16//! For measured time support, see `ZombieConfig::use_measured_time` (currently
17//! the config option exists but measured time is not yet implemented in the
18//! record completion path).
19
20use std::any::Any;
21use std::rc::Rc;
22
23use smallvec::SmallVec;
24
25use crate::context::{EZombieNode, FullContext, RootContext, ContextNode, TockVec, ValueVec};
26use crate::meter::{Space, Time, PLANK_TIME_NS};
27use crate::tock::Tock;
28
29/// Type-erased replay function.
30///
31/// Takes a vector of pointers to input values and executes the computation.
32/// The function should call `finish_record` when complete.
33pub type ReplayFn = Box<dyn Fn(&[*const dyn Any])>;
34
35/// Inputs for replay: each entry is (tock, optional cached node).
36/// Uses SmallVec for inline storage when input count is small (typical case).
37pub type ReplayInputs = SmallVec<[(Tock, Option<Rc<dyn EZombieNode>>); 4]>;
38
39/// Dependencies stored as SmallVec - most binds have only 1-4 dependencies.
40/// This avoids HashMap/HashSet overhead which is significant for small collections.
41/// Duplicates are allowed since we only need to iterate, not lookup.
42pub type DependencyVec = SmallVec<[Tock; 4]>;
43
44/// Stores information needed to replay a computation.
45#[derive(Clone)]
46pub struct Replayer {
47    pub f: Rc<ReplayFn>,
48    pub inputs: TockVec,
49}
50
51impl Replayer {
52    pub fn new(f: ReplayFn, inputs: TockVec) -> Rc<Self> {
53        Rc::new(Self {
54            f: Rc::new(f),
55            inputs,
56        })
57    }
58}
59
60impl std::fmt::Debug for Replayer {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("Replayer")
63            .field("inputs", &self.inputs)
64            .finish()
65    }
66}
67
68/// Common record state shared by Root and Head records.
69pub struct RecordState {
70    pub start_tock: Tock,
71    pub values: ValueVec,
72    pub space_taken: Space,
73    /// Dependencies stored as SmallVec for efficiency.
74    /// Most binds have only 1-4 dependencies, so inline storage avoids allocation.
75    /// We use a simple append-only list since we only need iteration, not lookup.
76    pub dependencies: DependencyVec,
77}
78
79impl std::fmt::Debug for RecordState {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        f.debug_struct("RecordState")
82            .field("start_tock", &self.start_tock)
83            .field("values_count", &self.values.len())
84            .field("space_taken", &self.space_taken)
85            .field("dependencies_count", &self.dependencies.len())
86            .finish()
87    }
88}
89
90impl RecordState {
91    pub fn new(start_tock: Tock) -> Self {
92        Self {
93            start_tock,
94            values: ValueVec::new(),
95            space_taken: Space::ZERO,
96            dependencies: DependencyVec::new(),
97        }
98    }
99
100    pub fn push_value(&mut self, node: Rc<dyn EZombieNode>) {
101        self.space_taken += Space::from_bytes(node.size());
102        self.values.push(Some(node));
103    }
104
105    /// Register a dependency on a tock.
106    /// Only registers if tock is before this record's start (external dependency).
107    /// Duplicates are allowed - we only iterate, never lookup.
108    #[inline]
109    pub fn register_dependency(&mut self, tock: Tock) {
110        if tock < self.start_tock {
111            self.dependencies.push(tock);
112        }
113    }
114}
115
116/// Record enum - stack entries for tracking computation state.
117///
118/// This enum replaces the previous `RecordNode` trait, providing:
119/// - No dynamic dispatch overhead
120/// - Exhaustive pattern matching
121/// - Better memory layout (single allocation)
122#[derive(Debug)]
123pub enum Record {
124    /// Root record - top level, not evictable.
125    Root(RootRecord),
126    /// Head record - active bind computation.
127    Head(HeadRecord),
128    /// Value record - completed computation.
129    Value(ValueRecord),
130}
131
132impl Record {
133    /// Create a new root record.
134    pub fn new_root(start_tock: Tock) -> Self {
135        Record::Root(RootRecord::new(start_tock))
136    }
137
138    /// Create a new head record.
139    pub fn new_head(replayer: Rc<Replayer>, start_tock: Tock, start_time: Time) -> Self {
140        Record::Head(HeadRecord::new(replayer, start_tock, start_time))
141    }
142
143    /// Create a new value record.
144    pub fn new_value(result_tock: Tock, context_start_tock: Tock) -> Self {
145        Record::Value(ValueRecord::new(result_tock, context_start_tock))
146    }
147
148    /// Get start tock.
149    pub fn start_tock(&self) -> Tock {
150        match self {
151            Record::Root(r) => r.state.start_tock,
152            Record::Head(r) => r.state.start_tock,
153            Record::Value(r) => r.result_tock,
154        }
155    }
156
157    /// Get values in this record.
158    pub fn values(&self) -> &[Option<Rc<dyn EZombieNode>>] {
159        match self {
160            Record::Root(r) => &r.state.values,
161            Record::Head(r) => &r.state.values,
162            Record::Value(_) => &[],
163        }
164    }
165
166    /// Push a value to this record.
167    pub fn push_value(&mut self, node: Rc<dyn EZombieNode>) {
168        match self {
169            Record::Root(r) => r.state.push_value(node),
170            Record::Head(r) => r.state.push_value(node),
171            Record::Value(_) => panic!("ValueRecord cannot push values"),
172        }
173    }
174
175    /// Register a dependency on the current record.
176    pub fn register_dependency(&mut self, tock: Tock) {
177        match self {
178            Record::Root(r) => r.state.register_dependency(tock),
179            Record::Head(r) => r.state.register_dependency(tock),
180            Record::Value(_) => {} // No-op
181        }
182    }
183
184    /// Get dependencies as a slice.
185    pub fn dependencies(&self) -> &[Tock] {
186        match self {
187            Record::Root(r) => &r.state.dependencies,
188            Record::Head(r) => &r.state.dependencies,
189            Record::Value(_) => &[],
190        }
191    }
192
193    /// Get space taken.
194    pub fn space_taken(&self) -> Space {
195        match self {
196            Record::Root(r) => r.state.space_taken,
197            Record::Head(r) => r.state.space_taken,
198            Record::Value(_) => Space::ZERO,
199        }
200    }
201
202    /// Check if this is a head record.
203    pub fn is_head(&self) -> bool {
204        matches!(self, Record::Head(_))
205    }
206
207    /// Check if this is a value record.
208    pub fn is_value(&self) -> bool {
209        matches!(self, Record::Value(_))
210    }
211
212    /// Check if this is a tailcall record (HeadRecord).
213    pub fn is_tailcall(&self) -> bool {
214        matches!(self, Record::Head(_))
215    }
216
217    /// Get result tock (for ValueRecord).
218    pub fn get_result_tock(&self) -> Option<Tock> {
219        match self {
220            Record::Value(r) => Some(r.result_tock),
221            _ => None,
222        }
223    }
224
225    /// Get the start_tock of the context containing this record's result (for ValueRecord).
226    pub fn get_context_start_tock(&self) -> Option<Tock> {
227        match self {
228            Record::Value(r) => Some(r.context_start_tock),
229            _ => None,
230        }
231    }
232
233    /// Get replayer (for HeadRecord).
234    pub fn get_replayer(&self) -> Option<Rc<Replayer>> {
235        match self {
236            Record::Head(r) => Some(r.replayer.clone()),
237            _ => None,
238        }
239    }
240
241    /// Handle suspension (creates context).
242    ///
243    /// For Root: creates RootContext
244    /// For Head: creates FullContext (tailcall semantics)
245    ///
246    /// # Returns
247    /// For HeadRecord: returns (dependencies, forward_uf) for backedge registration.
248    /// For RootRecord: returns None (RootContext doesn't need backedges).
249    pub fn suspend(
250        &mut self,
251        replayer: Rc<Replayer>,
252        current_tock: Tock,
253        akasha_insert: &mut dyn FnMut(Tock, Rc<dyn ContextNode>),
254        heap_push: Option<&mut dyn FnMut(Rc<FullContext>)>,
255    ) -> Option<(TockVec, crate::union_find::UF)> {
256        match self {
257            Record::Root(r) => {
258                // Create RootContext with accumulated values
259                let ctx = RootContext::new_with_values(
260                    r.state.start_tock,
261                    std::mem::take(&mut r.state.values),
262                    r.state.space_taken,
263                    Some(replayer),
264                );
265                akasha_insert(r.state.start_tock, ctx);
266                None
267            }
268            Record::Head(r) => {
269                // HeadRecord suspend uses tailcall semantics:
270                // Complete the current computation and save state to FullContext,
271                // then the caller will push a new HeadRecord for the inner bind.
272                let deps: TockVec = std::mem::take(&mut r.state.dependencies);
273                let ctx = FullContext::new(
274                    r.state.start_tock,
275                    current_tock,
276                    std::mem::take(&mut r.state.values),
277                    r.state.space_taken,
278                    Time::from_duration(std::time::Duration::from_nanos(PLANK_TIME_NS)),
279                    r.replayer.clone(),
280                    deps.clone(),
281                );
282                // Clone forward_uf before moving ctx
283                let forward_uf = *ctx.forward_uf();
284                akasha_insert(r.state.start_tock, ctx.clone());
285                if let Some(push_fn) = heap_push {
286                    push_fn(ctx);
287                }
288                Some((deps, forward_uf))
289            }
290            Record::Value(_) => panic!("ValueRecord cannot be suspended"),
291        }
292    }
293
294    /// Called when child record returns.
295    pub fn resume(&mut self, new_tock: Tock) {
296        match self {
297            Record::Root(r) => {
298                r.state = RecordState::new(new_tock);
299            }
300            Record::Head(r) => {
301                r.state = RecordState::new(new_tock);
302            }
303            Record::Value(_) => panic!("ValueRecord cannot be resumed"),
304        }
305    }
306
307    /// Handle completion (creates FullContext).
308    ///
309    /// # Returns
310    /// Returns (dependencies, forward_uf) for backedge registration, or None if not a HeadRecord.
311    pub fn complete(
312        &mut self,
313        current_tock: Tock,
314        replayer: Option<Rc<Replayer>>,
315        akasha_insert: &mut dyn FnMut(Tock, Rc<dyn ContextNode>),
316        heap_push: &mut dyn FnMut(Rc<FullContext>),
317    ) -> Option<(TockVec, crate::union_find::UF)> {
318        match self {
319            Record::Root(_) => panic!("RootRecord cannot be completed"),
320            Record::Head(r) => {
321                let deps: TockVec = std::mem::take(&mut r.state.dependencies);
322                let ctx = FullContext::new(
323                    r.state.start_tock,
324                    current_tock,
325                    std::mem::take(&mut r.state.values),
326                    r.state.space_taken,
327                    Time::from_duration(std::time::Duration::from_nanos(PLANK_TIME_NS)),
328                    replayer.unwrap_or_else(|| r.replayer.clone()),
329                    deps.clone(),
330                );
331                // Clone forward_uf before moving ctx
332                let forward_uf = *ctx.forward_uf();
333                akasha_insert(r.state.start_tock, ctx.clone());
334                heap_push(ctx);
335                Some((deps, forward_uf))
336            }
337            Record::Value(_) => {
338                // ValueRecord is already complete - no-op
339                None
340            }
341        }
342    }
343
344    /// Prepare replay by collecting inputs.
345    /// Returns the replayer and inputs (some may be missing).
346    pub fn prepare_replay(
347        &mut self,
348        get_input: &mut dyn FnMut(Tock) -> Option<Rc<dyn EZombieNode>>,
349    ) -> (Rc<Replayer>, ReplayInputs) {
350        match self {
351            Record::Root(_) => panic!("RootRecord cannot replay"),
352            Record::Head(r) => {
353                if r.played {
354                    panic!("HeadRecord already played");
355                }
356                let mut inputs = ReplayInputs::new();
357                for &input_tock in r.replayer.inputs.iter() {
358                    let node = get_input(input_tock);
359                    inputs.push((input_tock, node));
360                }
361                r.played = true;
362                (r.replayer.clone(), inputs)
363            }
364            Record::Value(_) => panic!("ValueRecord cannot replay"),
365        }
366    }
367}
368
369// Keep the struct definitions for internal use
370
371/// Root record - top level, not evictable.
372#[derive(Debug)]
373pub struct RootRecord {
374    state: RecordState,
375}
376
377impl RootRecord {
378    pub fn new(start_tock: Tock) -> Self {
379        Self {
380            state: RecordState::new(start_tock),
381        }
382    }
383}
384
385/// Head record - active bind computation.
386#[derive(Debug)]
387pub struct HeadRecord {
388    state: RecordState,
389    replayer: Rc<Replayer>,
390    #[allow(dead_code)]
391    start_time: Time,
392    played: bool,
393}
394
395impl HeadRecord {
396    /// Create a new HeadRecord.
397    /// Dependencies are registered from replayer inputs.
398    pub fn new(replayer: Rc<Replayer>, start_tock: Tock, start_time: Time) -> Self {
399        let mut state = RecordState::new(start_tock);
400
401        // Register dependencies from replayer inputs
402        for &input_tock in &replayer.inputs {
403            state.register_dependency(input_tock);
404        }
405
406        Self {
407            state,
408            replayer,
409            start_time,
410            played: false,
411        }
412    }
413}
414
415/// Value record - completed computation.
416#[derive(Debug)]
417pub struct ValueRecord {
418    result_tock: Tock,
419    context_start_tock: Tock,
420}
421
422impl ValueRecord {
423    pub fn new(result_tock: Tock, context_start_tock: Tock) -> Self {
424        Self { result_tock, context_start_tock }
425    }
426
427    pub fn context_start_tock(&self) -> Tock {
428        self.context_start_tock
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use crate::context::ZombieNode;
436    use crate::meter::Time;
437
438    #[test]
439    fn test_root_record() {
440        let record = Record::new_root(Tock(0));
441        assert_eq!(record.start_tock(), Tock(0));
442        assert!(record.values().is_empty());
443        assert!(!record.is_head());
444        assert!(!record.is_value());
445    }
446
447    #[test]
448    fn test_value_record() {
449        let record = Record::new_value(Tock(42), Tock(40));
450        assert!(record.is_value());
451        assert_eq!(record.get_result_tock(), Some(Tock(42)));
452        assert_eq!(record.get_context_start_tock(), Some(Tock(40)));
453    }
454
455    // =========================================================================
456    // Edge Case Tests
457    // =========================================================================
458
459    /// Test that only external dependencies are registered
460    #[test]
461    fn test_record_state_dependency_filtering() {
462        let mut state = RecordState::new(Tock(10));
463
464        // Dependencies before start_tock should be registered
465        state.register_dependency(Tock(5));
466        state.register_dependency(Tock(0));
467
468        // Dependencies at or after start_tock should NOT be registered
469        state.register_dependency(Tock(10)); // Same as start
470        state.register_dependency(Tock(15)); // After start
471
472        // Only 2 dependencies should be registered
473        assert_eq!(state.dependencies.len(), 2);
474        assert!(state.dependencies.contains(&Tock(5)));
475        assert!(state.dependencies.contains(&Tock(0)));
476    }
477
478    /// Test space tracking accumulates correctly
479    #[test]
480    fn test_record_space_tracking() {
481        let mut record = Record::new_root(Tock(0));
482
483        assert_eq!(record.space_taken().as_bytes(), 0);
484
485        // Push nodes with known sizes
486        let node1 = ZombieNode::new(Tock(1), vec![0u8; 100]);
487        let node2 = ZombieNode::new(Tock(2), vec![0u8; 200]);
488
489        record.push_value(node1 as Rc<dyn crate::context::EZombieNode>);
490        assert!(record.space_taken().as_bytes() >= 100);
491
492        let space_after_first = record.space_taken().as_bytes();
493        record.push_value(node2 as Rc<dyn crate::context::EZombieNode>);
494        assert!(record.space_taken().as_bytes() >= space_after_first + 200);
495    }
496
497    /// Test HeadRecord initialization and replayer access
498    #[test]
499    fn test_head_record_replayer() {
500        let inputs = {
501            let mut v = TockVec::new();
502            v.push(Tock(1));
503            v.push(Tock(5));
504            v
505        };
506
507        let replayer = Replayer::new(
508            Box::new(|_| {}),
509            inputs.clone(),
510        );
511
512        let record = Record::new_head(
513            replayer.clone(),
514            Tock(10),
515            Time::ZERO,
516        );
517
518        assert!(record.is_head());
519        assert!(!record.is_value());
520        assert_eq!(record.start_tock(), Tock(10));
521
522        // Replayer should be accessible
523        let got_replayer = record.get_replayer();
524        assert!(got_replayer.is_some());
525        assert_eq!(got_replayer.unwrap().inputs.len(), 2);
526
527        // Dependencies from replayer inputs should be registered
528        let deps = record.dependencies();
529        assert_eq!(deps.len(), 2);
530        assert!(deps.contains(&Tock(1)));
531        assert!(deps.contains(&Tock(5)));
532    }
533
534    /// Test ValueRecord has no values or dependencies
535    #[test]
536    fn test_value_record_is_terminal() {
537        let record = Record::new_value(Tock(42), Tock(40));
538
539        // ValueRecord should have no values
540        assert!(record.values().is_empty());
541
542        // ValueRecord should have no dependencies
543        assert!(record.dependencies().is_empty());
544
545        // ValueRecord space is zero
546        assert_eq!(record.space_taken().as_bytes(), 0);
547
548        // Replayer should be None
549        assert!(record.get_replayer().is_none());
550    }
551}
552