Skip to main content

parallax_core/
error.rs

1//! First-class error taxonomy for Parallax.
2
3use crate::runtime::RuntimeKind;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use thiserror::Error;
7
8/// Stable machine-readable error codes.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
11pub enum ErrorCode {
12    /// Host runtime binary/engine is missing or not discoverable.
13    RuntimeUnavailable,
14    /// Adapter failed during initialization.
15    RuntimeInitializationFailure,
16    /// Guest program failed during execution.
17    ExecutionFailure,
18    /// Execution exceeded its deadline.
19    ExecutionTimeout,
20    /// Execution was cancelled by the caller.
21    ExecutionCancelled,
22    /// State capture failed.
23    CaptureFailure,
24    /// State restore failed.
25    RestoreFailure,
26    /// Serialization of PIR/snapshot/protocol failed.
27    SerializationFailure,
28    /// Snapshot failed integrity or schema validation.
29    InvalidSnapshot,
30    /// Worker protocol violation or version mismatch.
31    ProtocolViolation,
32    /// Requested capability is not available.
33    CapabilityViolation,
34    /// Migration was rejected by policy or analysis.
35    MigrationRejected,
36    /// Semantic loss detected (often with MigrationRejected).
37    SemanticLoss,
38    /// Value cannot be represented in PIR or target runtime.
39    UnsupportedValue,
40    /// Runtime worker process crashed.
41    AdapterCrashed,
42    /// Resource limit exceeded (memory, output, message size).
43    ResourceLimitExceeded,
44    /// Generic internal error — should be rare.
45    Internal,
46    /// Invalid user input / CLI arguments.
47    InvalidArgument,
48    /// I/O failure.
49    Io,
50}
51
52impl fmt::Display for ErrorCode {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        let s = serde_json::to_value(self)
55            .ok()
56            .and_then(|v| v.as_str().map(str::to_string))
57            .unwrap_or_else(|| format!("{:?}", self));
58        write!(f, "{}", s)
59    }
60}
61
62/// Suggested remediation for an error.
63#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
64pub struct Remediation {
65    /// Short actionable suggestion.
66    pub action: String,
67    /// Optional detail.
68    pub detail: Option<String>,
69}
70
71impl Remediation {
72    /// Create a simple remediation.
73    pub fn new(action: impl Into<String>) -> Self {
74        Self {
75            action: action.into(),
76            detail: None,
77        }
78    }
79
80    /// Create a remediation with detail.
81    pub fn with_detail(action: impl Into<String>, detail: impl Into<String>) -> Self {
82        Self {
83            action: action.into(),
84            detail: Some(detail.into()),
85        }
86    }
87}
88
89/// Structured Parallax error.
90#[derive(Clone, Debug, Error, Serialize, Deserialize)]
91#[error("{code}: {message}")]
92pub struct ParallaxError {
93    /// Stable error code.
94    pub code: ErrorCode,
95    /// Human-readable message.
96    pub message: String,
97    /// Component that produced the error.
98    pub source_component: String,
99    /// Operation being attempted.
100    pub operation: String,
101    /// Optional runtime involved.
102    pub runtime: Option<RuntimeKind>,
103    /// Structured context key/value pairs.
104    pub context: Vec<(String, String)>,
105    /// Suggested remediations.
106    pub remediations: Vec<Remediation>,
107    /// Optional verbose diagnostic (stack, stderr, etc.).
108    pub diagnostic: Option<String>,
109}
110
111impl ParallaxError {
112    /// Start building an error.
113    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
114        Self {
115            code,
116            message: message.into(),
117            source_component: "parallax".into(),
118            operation: "unknown".into(),
119            runtime: None,
120            context: Vec::new(),
121            remediations: Vec::new(),
122            diagnostic: None,
123        }
124    }
125
126    /// Set source component.
127    pub fn with_source(mut self, source: impl Into<String>) -> Self {
128        self.source_component = source.into();
129        self
130    }
131
132    /// Set operation name.
133    pub fn with_operation(mut self, op: impl Into<String>) -> Self {
134        self.operation = op.into();
135        self
136    }
137
138    /// Attach runtime.
139    pub fn with_runtime(mut self, runtime: RuntimeKind) -> Self {
140        self.runtime = Some(runtime);
141        self
142    }
143
144    /// Add context pair.
145    pub fn context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
146        self.context.push((key.into(), value.into()));
147        self
148    }
149
150    /// Add remediation.
151    pub fn remediate(mut self, remediation: Remediation) -> Self {
152        self.remediations.push(remediation);
153        self
154    }
155
156    /// Attach verbose diagnostic.
157    pub fn with_diagnostic(mut self, diagnostic: impl Into<String>) -> Self {
158        self.diagnostic = Some(diagnostic.into());
159        self
160    }
161
162    /// Format a concise user-facing report (non-JSON).
163    pub fn format_report(&self, verbose: bool) -> String {
164        let mut out = String::new();
165        out.push_str(&format!("{}: {}\n", self.code, self.message));
166        if let Some(rt) = &self.runtime {
167            out.push_str(&format!("Runtime: {}\n", rt));
168        }
169        out.push_str(&format!("Operation: {}\n", self.operation));
170        for (k, v) in &self.context {
171            out.push_str(&format!("{}:\n  {}\n", k, v));
172        }
173        if !self.remediations.is_empty() {
174            out.push_str("Possible actions:\n");
175            for r in &self.remediations {
176                out.push_str(&format!("  • {}\n", r.action));
177                if let Some(d) = &r.detail {
178                    out.push_str(&format!("    {}\n", d));
179                }
180            }
181        }
182        if verbose {
183            if let Some(diag) = &self.diagnostic {
184                out.push_str("\nDiagnostic:\n");
185                out.push_str(diag);
186                out.push('\n');
187            }
188        }
189        out
190    }
191}
192
193/// Convenient result alias.
194pub type Result<T> = std::result::Result<T, ParallaxError>;
195
196impl From<std::io::Error> for ParallaxError {
197    fn from(err: std::io::Error) -> Self {
198        ParallaxError::new(ErrorCode::Io, err.to_string())
199            .with_source("std::io")
200            .with_operation("io")
201    }
202}
203
204impl From<serde_json::Error> for ParallaxError {
205    fn from(err: serde_json::Error) -> Self {
206        ParallaxError::new(ErrorCode::SerializationFailure, err.to_string())
207            .with_source("serde_json")
208            .with_operation("serialize")
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn report_includes_remediation() {
218        let err = ParallaxError::new(ErrorCode::MigrationRejected, "integer precision")
219            .context("Value", "9007199254740993")
220            .remediate(Remediation::new("migrate using BigInt"));
221        let report = err.format_report(false);
222        assert!(report.contains("PYTHON_INT_PRECISION") || report.contains("MIGRATION_REJECTED"));
223        assert!(report.contains("BigInt"));
224    }
225}