Skip to main content

soil_rt/
error.rs

1use std::fmt;
2
3/// The classification of a runtime failure.
4///
5/// This is the `panic` effect made concrete (design §3.3): anything that
6/// can go wrong at runtime is one of these kinds, and hosts receive it
7/// through the C ABI as a structured error, never as an unwind.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum PanicKind {
10    /// Integer overflow in an arithmetic primitive.
11    Overflow,
12    /// Zero divisor in integer `/` or `%`.
13    DivideByZero,
14    /// Input JSON rejected by type-directed decode.
15    DecodeError,
16    /// A value of the wrong shape reached an operation (defensive; the
17    /// checker prevents this in checked code).
18    TypeError,
19    /// A derived operation (`eq`/`compare`/`hash`/`show`) was invoked on
20    /// a type marked non-derivable, or reached a closure.
21    DerivationError,
22    /// The C ABI was used against its documented contract (undefined
23    /// `TypeId`, wrong arity, use after teardown).
24    CapiMisuse,
25    /// A bug in the runtime itself, caught at the C ABI boundary.
26    Internal,
27}
28
29impl PanicKind {
30    pub fn name(self) -> &'static str {
31        match self {
32            PanicKind::Overflow => "overflow",
33            PanicKind::DivideByZero => "divide-by-zero",
34            PanicKind::DecodeError => "decode-error",
35            PanicKind::TypeError => "type-error",
36            PanicKind::DerivationError => "derivation-error",
37            PanicKind::CapiMisuse => "capi-misuse",
38            PanicKind::Internal => "internal",
39        }
40    }
41}
42
43/// One frame of a definition-level trace.
44///
45/// The runtime only carries these; filling them in is the interpreter's
46/// and daemon's job (debug-mode instrumentation, design §4.9).
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct TraceFrame {
49    /// The Trellis definition name, e.g. `"csvstats::median"`.
50    pub definition: String,
51}
52
53/// A structured runtime error — the debug-mode payload of the host-stub
54/// rule (design §3.11): kind, message, definition-level trace, and the
55/// offending inputs as canonical JSON when available.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct SoilError {
58    pub kind: PanicKind,
59    pub message: String,
60    pub trace: Vec<TraceFrame>,
61    /// Canonical-JSON rendering of the inputs that provoked the error,
62    /// when the failing operation had them at hand.
63    pub inputs: Option<String>,
64}
65
66impl SoilError {
67    pub fn new(kind: PanicKind, message: impl Into<String>) -> Self {
68        SoilError {
69            kind,
70            message: message.into(),
71            trace: Vec::new(),
72            inputs: None,
73        }
74    }
75
76    pub fn with_inputs(mut self, inputs: impl Into<String>) -> Self {
77        self.inputs = Some(inputs.into());
78        self
79    }
80}
81
82impl fmt::Display for SoilError {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "{}: {}", self.kind.name(), self.message)?;
85        if let Some(inputs) = &self.inputs {
86            write!(f, " (inputs: {inputs})")?;
87        }
88        for frame in &self.trace {
89            write!(f, "\n  in {}", frame.definition)?;
90        }
91        Ok(())
92    }
93}
94
95impl std::error::Error for SoilError {}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn display_includes_kind_message_and_trace() {
103        let mut err =
104            SoilError::new(PanicKind::DivideByZero, "divisor is zero").with_inputs("[10,0]");
105        err.trace.push(TraceFrame {
106            definition: "csvstats::mean".to_string(),
107        });
108        let rendered = err.to_string();
109        assert_eq!(
110            rendered,
111            "divide-by-zero: divisor is zero (inputs: [10,0])\n  in csvstats::mean"
112        );
113    }
114
115    #[test]
116    fn kind_names_are_stable() {
117        assert_eq!(PanicKind::Overflow.name(), "overflow");
118        assert_eq!(PanicKind::Internal.name(), "internal");
119    }
120}