zombie_rs/
error.rs

1//! Error types for zombie runtime operations.
2//!
3//! This module defines a comprehensive error hierarchy following Rust best practices.
4//! The design prioritizes:
5//! - Zero-cost when not used (no allocation until error occurs)
6//! - Clear error messages for debugging
7//! - Type safety via exhaustive matching
8//!
9//! # Error Handling Philosophy
10//!
11//! Internal invariant violations (logic bugs) should use `debug_assert!` or
12//! `unreachable!()` - these are programming errors, not runtime errors.
13//!
14//! External failures (eviction during replay, type mismatches from user code)
15//! should use `ZombieResult<T>` for graceful handling.
16
17use std::fmt;
18use crate::tock::Tock;
19
20/// Main error type for zombie operations.
21///
22/// Each variant includes enough context to diagnose the issue.
23/// Uses `Box<str>` instead of `String` for reasons to reduce size of error variants.
24#[derive(Debug)]
25pub enum ZombieError {
26    /// No context found for a given tock during replay.
27    /// This can happen if a context was incorrectly evicted while still needed.
28    ContextNotFound {
29        tock: Tock,
30        /// Additional context about where this happened
31        location: &'static str,
32    },
33
34    /// Context exists but has no replayer (should not happen in normal operation).
35    NoReplayer {
36        context_start: Tock,
37        context_end: Tock,
38    },
39
40    /// Replay did not produce the expected result.
41    ReplayFailed {
42        target: Tock,
43        reason: Box<str>,
44    },
45
46    /// Type mismatch during downcast (user stored wrong type or corruption).
47    TypeMismatch {
48        expected: &'static str,
49        tock: Tock,
50    },
51
52    /// Runtime not initialized - must call `Runtime::init()` first.
53    NotInitialized,
54
55    /// Invalid record state transition (internal logic error).
56    InvalidRecordState {
57        from: &'static str,
58        to: &'static str,
59    },
60
61    /// Eviction operation failed.
62    EvictionFailed {
63        reason: Box<str>,
64    },
65
66    /// Index out of bounds in context value array.
67    IndexOutOfBounds {
68        tock: Tock,
69        context_start: Tock,
70        context_end: Tock,
71        index: usize,
72        len: usize,
73    },
74
75    /// Replay loop exceeded maximum iterations (possible infinite loop).
76    ReplayLoopExceeded {
77        target: Tock,
78        max_iterations: usize,
79    },
80}
81
82impl fmt::Display for ZombieError {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            ZombieError::ContextNotFound { tock, location } => {
86                write!(f, "no context found for tock {} at {}", tock, location)
87            }
88            ZombieError::NoReplayer { context_start, context_end } => {
89                write!(f, "context [{}, {}) has no replayer", context_start, context_end)
90            }
91            ZombieError::ReplayFailed { target, reason } => {
92                write!(f, "replay to tock {} failed: {}", target, reason)
93            }
94            ZombieError::TypeMismatch { expected, tock } => {
95                write!(f, "type mismatch at tock {}, expected {}", tock, expected)
96            }
97            ZombieError::NotInitialized => {
98                write!(f, "Runtime not initialized, call Runtime::init() first")
99            }
100            ZombieError::InvalidRecordState { from, to } => {
101                write!(f, "invalid record state transition from {} to {}", from, to)
102            }
103            ZombieError::EvictionFailed { reason } => {
104                write!(f, "eviction failed: {}", reason)
105            }
106            ZombieError::IndexOutOfBounds { tock, context_start, context_end, index, len } => {
107                write!(f, "index {} out of bounds (len {}) for tock {} in context [{}, {})",
108                       index, len, tock, context_start, context_end)
109            }
110            ZombieError::ReplayLoopExceeded { target, max_iterations } => {
111                write!(f, "replay loop exceeded {} iterations for tock {}", max_iterations, target)
112            }
113        }
114    }
115}
116
117impl std::error::Error for ZombieError {}
118
119/// Result type alias for zombie operations.
120pub type ZombieResult<T> = Result<T, ZombieError>;
121
122// ============================================================================
123// Error Construction Helpers
124// ============================================================================
125
126impl ZombieError {
127    /// Create a ContextNotFound error with location context.
128    #[cold]
129    #[inline(never)]
130    pub fn context_not_found(tock: Tock, location: &'static str) -> Self {
131        ZombieError::ContextNotFound { tock, location }
132    }
133
134    /// Create a TypeMismatch error.
135    #[cold]
136    #[inline(never)]
137    pub fn type_mismatch<T>(tock: Tock) -> Self {
138        ZombieError::TypeMismatch {
139            expected: std::any::type_name::<T>(),
140            tock,
141        }
142    }
143
144    /// Create a ReplayFailed error.
145    #[cold]
146    #[inline(never)]
147    pub fn replay_failed(target: Tock, reason: impl Into<Box<str>>) -> Self {
148        ZombieError::ReplayFailed { target, reason: reason.into() }
149    }
150}
151
152// ============================================================================
153// Extension Traits for Ergonomic Error Handling
154// ============================================================================
155
156/// Extension trait for Option that converts to ZombieError.
157pub trait OptionExt<T> {
158    /// Convert None to ContextNotFound error.
159    fn context_not_found(self, tock: Tock, location: &'static str) -> ZombieResult<T>;
160
161    /// Convert None to NoReplayer error.
162    fn no_replayer(self, context_start: Tock, context_end: Tock) -> ZombieResult<T>;
163
164    /// Convert None to TypeMismatch error.
165    fn type_mismatch<U>(self, tock: Tock) -> ZombieResult<T>;
166}
167
168impl<T> OptionExt<T> for Option<T> {
169    fn context_not_found(self, tock: Tock, location: &'static str) -> ZombieResult<T> {
170        self.ok_or_else(|| ZombieError::context_not_found(tock, location))
171    }
172
173    fn no_replayer(self, context_start: Tock, context_end: Tock) -> ZombieResult<T> {
174        self.ok_or(ZombieError::NoReplayer { context_start, context_end })
175    }
176
177    fn type_mismatch<U>(self, tock: Tock) -> ZombieResult<T> {
178        self.ok_or_else(|| ZombieError::type_mismatch::<U>(tock))
179    }
180}
181
182/// Extension trait for Result to add context to errors.
183pub trait ResultExt<T> {
184    /// Add location context to errors.
185    fn with_location(self, location: &'static str) -> ZombieResult<T>;
186}
187
188impl<T> ResultExt<T> for ZombieResult<T> {
189    fn with_location(self, _location: &'static str) -> ZombieResult<T> {
190        // For now, just pass through. Could enhance error with location later.
191        self
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_error_display() {
201        let err = ZombieError::context_not_found(Tock(42), "test_location");
202        assert!(err.to_string().contains("42"));
203        assert!(err.to_string().contains("test_location"));
204    }
205
206    #[test]
207    fn test_type_mismatch_includes_type_name() {
208        let err = ZombieError::type_mismatch::<Vec<i32>>(Tock(10));
209        let msg = err.to_string();
210        assert!(msg.contains("Vec"));
211        assert!(msg.contains("10"));
212    }
213
214    #[test]
215    fn test_option_ext() {
216        let none: Option<i32> = None;
217        let result = none.context_not_found(Tock(5), "test");
218        assert!(result.is_err());
219    }
220}