Skip to main content

wolfram_library_link/
errors.rs

1//! Typed errors surfaced across the LibraryLink boundary.
2//!
3//! [`LibraryError`] enumerates every failure the LibraryLink bridges can hit and
4//! renders to a structured `Failure["Variant", <|…|>]` via `From<&LibraryError>
5//! for Expr` (the `#[derive(Failure)]`) — what the kernel sees when the failure
6//! can be communicated over the link / WXF. When it can't (the library never
7//! initialized, or writing the Failure to the link failed), the bridge returns a
8//! C-ABI code directly: [`FAILED_TO_INIT`], [`FAILED_WITH_PANIC`], or
9//! [`LIBRARY_FUNCTION_ERROR`][crate::sys::LIBRARY_FUNCTION_ERROR].
10//!
11//! Link communication trades in [`Expr`], so the Failure is built directly — no
12//! detour through WXF bytes.
13
14use std::os::raw::c_int;
15
16use crate::expr::Expr;
17use wolfram_serialize::Failure;
18
19// C-ABI return codes for macro-generated wrapper code. `OFFSET` avoids clashing
20// with `sys::LIBRARY_FUNCTION_ERROR` and related kernel codes.
21const OFFSET: c_int = 1000;
22#[doc(hidden)]
23pub const FAILED_TO_INIT: c_int = OFFSET + 1;
24#[doc(hidden)]
25pub const FAILED_WITH_PANIC: c_int = OFFSET + 2;
26
27/// An error raised at the LibraryLink boundary.
28///
29/// `#[derive(Failure)]` renders each variant to its `Failure["VariantName",
30/// <|CamelCase fields|>]` expression (e.g. `RustPanic { message, .. }` →
31/// `Failure["RustPanic", <|"Message" -> …, "SourceLocation" -> …, "Backtrace" -> …|>]`).
32#[derive(Debug, Clone, Failure)]
33pub enum LibraryError {
34    /// A Rust panic caught while running an exported function. The `backtrace`
35    /// is a renderable [`Expr`] (a clickable `Column` of frames when the
36    /// `panic-failure-backtraces` feature is on *and* the backtrace env var is
37    /// set, else `Missing[…]`).
38    RustPanic {
39        /// The panic message (substituted into the `MessageTemplate`).
40        message: String,
41        /// `file:line` where the panic originated.
42        source_location: String,
43        /// The backtrace as a renderable expression.
44        backtrace: Expr,
45    },
46    /// The generated `generate_loader!` entry point was called incorrectly
47    /// (wrong head / argument count / argument type).
48    Loader {
49        /// What went wrong.
50        message: String,
51        /// What the loader expected (e.g. `"List"`, `"String"`, `"1 argument"`).
52        expected: String,
53        /// What it got — an arbitrary [`Expr`].
54        got: Expr,
55    },
56    /// A WSTP error with an error code.
57    #[cfg(feature = "wstp")]
58    WstpError {
59        /// The WSTP error code.
60        code: i32,
61        /// The WSTP error message.
62        message: String,
63    },
64    /// A WSTP error without an error code.
65    #[cfg(feature = "wstp")]
66    WstpErrorMessage {
67        /// The WSTP error message.
68        message: String,
69    },
70}
71
72#[cfg(feature = "wstp")]
73impl From<wstp::Error> for LibraryError {
74    fn from(e: wstp::Error) -> Self {
75        match e.code() {
76            Some(code) => LibraryError::WstpError {
77                code,
78                message: e.to_string(),
79            },
80            None => LibraryError::WstpErrorMessage {
81                message: e.to_string(),
82            },
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::expr::{expr, ExprKind};
91
92    fn failure_tag(e: &Expr) -> &str {
93        let ExprKind::Normal(n) = e.kind() else {
94            panic!("expected Normal, got {:?}", e);
95        };
96        let ExprKind::String(s) = n.elements()[0].kind() else {
97            panic!("expected String tag, got {:?}", n.elements()[0]);
98        };
99        s.as_str()
100    }
101
102    #[test]
103    fn rust_panic_is_failure_with_backtrace_expr() {
104        let backtrace = expr!(System::Missing["NotEnabled"]);
105        let err = LibraryError::RustPanic {
106            message: "boom".into(),
107            source_location: "src/x.rs:1".into(),
108            backtrace: backtrace.clone(),
109        };
110        let e = Expr::from(&err);
111        let ExprKind::Normal(normal) = e.kind() else {
112            panic!("expected Failure[...], got {:?}", e);
113        };
114        let ExprKind::Symbol(head) = normal.head().kind() else {
115            panic!("expected Symbol head");
116        };
117        assert_eq!(head.as_str(), "System`Failure");
118        let ExprKind::String(tag) = normal.elements()[0].kind() else {
119            panic!("expected String tag");
120        };
121        assert_eq!(tag.as_str(), "RustPanic");
122        let ExprKind::Association(assoc) = normal.elements()[1].kind() else {
123            panic!("expected Association");
124        };
125        let find = |k: &str| {
126            assoc
127                .iter()
128                .find(|e| e.key == Expr::from(k))
129                .map(|e| e.value.clone())
130        };
131        // Derived shape: snake_case fields → CamelCase association keys.
132        assert_eq!(find("Message"), Some(Expr::from("boom")));
133        assert_eq!(find("SourceLocation"), Some(Expr::from("src/x.rs:1")));
134        // The backtrace Expr is carried through verbatim — no serialization detour.
135        assert_eq!(find("Backtrace"), Some(backtrace));
136    }
137
138    #[test]
139    fn every_variant_renders_a_failure() {
140        let backtrace = Expr::string("bt");
141        let variants = [
142            LibraryError::RustPanic {
143                message: "m".into(),
144                source_location: "l".into(),
145                backtrace,
146            },
147            LibraryError::Loader {
148                message: "m".into(),
149                expected: "e".into(),
150                got: Expr::from(1i64),
151            },
152        ];
153        for v in &variants {
154            // The conversion is always a Failure[tag, <|…|>] — never field-less.
155            let e = Expr::from(v);
156            let ExprKind::Normal(normal) = e.kind() else {
157                panic!("expected Failure[...], got {:?}", e);
158            };
159            let ExprKind::Symbol(head) = normal.head().kind() else {
160                panic!("expected Symbol head");
161            };
162            assert_eq!(head.as_str(), "System`Failure");
163            assert!(!failure_tag(&e).is_empty());
164            assert_eq!(normal.elements().len(), 2, "must carry an association");
165        }
166    }
167}