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