Preserve error chains through #[source] on FennelError variants
Infrastructure failures and eval errors were formatting the underlying
mlua::Error into a string, losing the cause chain. Now InternalError
(grouped as its own enum) and FennelError::Eval both carry the
mlua::Error via #[source]. TypeMismatch too. Enabled the error-send
feature on mlua so Error is Send+Sync. Boxed large variants to satisfy
clippy result_large_err lint.

Assisted-by: GLM-5.1 via pi
change rttymywtzwlsslmxymyxtmsxzvkzosqr
commit b4dc130901fe296c751b654f091245926348de7a
author Alpha Chen <alpha@kejadlen.dev>
date
parent svxylwpq
diff --git a/Cargo.toml b/Cargo.toml
index 643b239..668adee 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,7 +15,7 @@ clap_complete = "*"
 fs-err = "*"
 jiff = "*"
 miette = { version = "*", features = ["fancy"] }
-mlua = { version = "*", features = ["lua54", "serde", "vendored"] }
+mlua = { version = "*", features = ["lua54", "serde", "vendored", "error-send"] }
 regex = "*"
 sentry = { version = "*", features = ["backtrace", "contexts", "debug-images", "panic", "release-health", "reqwest", "rustls", "tokio"], default-features = false }
 sentry-tracing = "*"
diff --git a/src/ci.rs b/src/ci.rs
index cf6d753..2a0b9f0 100644
--- a/src/ci.rs
+++ b/src/ci.rs
@@ -334,7 +334,7 @@ pub fn eval_ci(
     })?;
 
     // Extract the registration table.
-    let lua_err = |e: mlua::Error| crate::fennel::FennelError::from_lua(source, name, &e);
+    let lua_err = |e: mlua::Error| crate::fennel::FennelError::from_lua(source, name, e);
     let registry: mlua::Table = fennel.lua().globals().get("_quire_jobs").map_err(lua_err)?;
     let mut jobs = Vec::new();
     for entry in registry.sequence_values::<mlua::Table>() {
diff --git a/src/error.rs b/src/error.rs
index 0e16cde..b0c220e 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -19,7 +19,7 @@ pub enum Error {
     ConfigNotFound(String),
 
     #[error(transparent)]
-    Fennel(#[from] crate::fennel::FennelError),
+    Fennel(#[from] Box<crate::fennel::FennelError>),
 
     #[error("CI validation failed")]
     #[related]
@@ -41,6 +41,12 @@ pub enum Error {
 
 pub type Result<T> = std::result::Result<T, Error>;
 
+impl From<crate::fennel::FennelError> for Error {
+    fn from(err: crate::fennel::FennelError) -> Self {
+        Error::Fennel(Box::new(err))
+    }
+}
+
 impl From<Vec<ValidationError>> for Error {
     fn from(errs: Vec<ValidationError>) -> Self {
         Error::Validation(errs)
diff --git a/src/fennel.rs b/src/fennel.rs
index 85e2162..206cfe0 100644
--- a/src/fennel.rs
+++ b/src/fennel.rs
@@ -5,6 +5,28 @@ use mlua::{Lua, LuaSerdeExt};
 
 const FENNEL_LUA: &str = include_str!("../vendor/fennel.lua");
 
+/// Infrastructure failures that should never occur during normal operation.
+#[derive(Debug, thiserror::Error, miette::Diagnostic)]
+pub enum InternalError {
+    #[error("failed to load fennel compiler")]
+    CompilerLoad(#[source] mlua::Error),
+
+    #[error("failed to register fennel module")]
+    ModuleRegister(#[source] mlua::Error),
+
+    #[error("fennel module not found")]
+    ModuleNotFound(#[source] mlua::Error),
+
+    #[error("fennel.eval not found")]
+    EvalNotFound(#[source] mlua::Error),
+
+    #[error("failed to create options table")]
+    OptionsTable(#[source] mlua::Error),
+
+    #[error("failed to set filename option")]
+    FilenameOption(#[source] mlua::Error),
+}
+
 /// Error kinds from the Fennel loader.
 #[derive(Debug, thiserror::Error, Diagnostic)]
 pub enum FennelError {
@@ -15,9 +37,9 @@ pub enum FennelError {
     #[diagnostic(code(fennel::io))]
     Io(#[from] std::io::Error),
 
-    #[error("{message}")]
+    #[error(transparent)]
     #[diagnostic(code(fennel::internal))]
-    Internal { message: String },
+    Internal(#[from] Box<InternalError>),
 
     #[error("{message}")]
     #[diagnostic(code(fennel::eval))]
@@ -27,15 +49,21 @@ pub enum FennelError {
         source_code: String,
         #[label("here")]
         label: SourceOffset,
+        #[source]
+        source: Box<mlua::Error>,
     },
 
     #[error("empty config: {name}")]
     #[diagnostic(code(fennel::empty))]
     Empty { name: String },
 
-    #[error("{0}")]
+    #[error("{message}")]
     #[diagnostic(code(fennel::type_mismatch))]
-    TypeMismatch(String),
+    TypeMismatch {
+        message: String,
+        #[source]
+        source: Box<mlua::Error>,
+    },
 }
 
 /// Owns a Lua VM with the Fennel compiler registered as a module.
@@ -71,15 +99,11 @@ impl Fennel {
             .load(FENNEL_LUA)
             .set_name("fennel.lua")
             .eval()
-            .map_err(|e| FennelError::Internal {
-                message: format!("failed to load fennel compiler: {e}"),
-            })?;
+            .map_err(|e| FennelError::Internal(Box::new(InternalError::CompilerLoad(e))))?;
 
         lua.globals()
             .set("fennel", fennel_module)
-            .map_err(|e| FennelError::Internal {
-                message: format!("failed to register fennel module: {e}"),
-            })?;
+            .map_err(|e| FennelError::Internal(Box::new(InternalError::ModuleRegister(e))))?;
 
         Ok(Self { lua })
     }
@@ -102,32 +126,29 @@ impl Fennel {
             });
         }
 
-        setup(&self.lua).map_err(|e| FennelError::from_lua(source, name, &e))?;
+        setup(&self.lua).map_err(|e| FennelError::from_lua(source, name, e))?;
 
-        let fennel: mlua::Table =
-            self.lua
-                .globals()
-                .get("fennel")
-                .map_err(|e| FennelError::Internal {
-                    message: format!("fennel module not found: {e}"),
-                })?;
+        let fennel: mlua::Table = self
+            .lua
+            .globals()
+            .get("fennel")
+            .map_err(|e| FennelError::Internal(Box::new(InternalError::ModuleNotFound(e))))?;
 
-        let eval: mlua::Function = fennel.get("eval").map_err(|e| FennelError::Internal {
-            message: format!("fennel.eval not found: {e}"),
-        })?;
+        let eval: mlua::Function = fennel
+            .get("eval")
+            .map_err(|e| FennelError::Internal(Box::new(InternalError::EvalNotFound(e))))?;
 
-        let opts = self.lua.create_table().map_err(|e| FennelError::Internal {
-            message: format!("failed to create options table: {e}"),
-        })?;
+        let opts = self
+            .lua
+            .create_table()
+            .map_err(|e| FennelError::Internal(Box::new(InternalError::OptionsTable(e))))?;
 
         opts.set("filename", name)
-            .map_err(|e| FennelError::Internal {
-                message: format!("failed to set filename option: {e}"),
-            })?;
+            .map_err(|e| FennelError::Internal(Box::new(InternalError::FilenameOption(e))))?;
 
         let result = eval
             .call::<mlua::Value>((source, opts))
-            .map_err(|e| FennelError::from_lua(source, name, &e))?;
+            .map_err(|e| FennelError::from_lua(source, name, e))?;
 
         Ok(result)
     }
@@ -153,7 +174,10 @@ impl Fennel {
 
         self.lua
             .from_value(result)
-            .map_err(|e| FennelError::TypeMismatch(format!("{name}: {e}")))
+            .map_err(|e| FennelError::TypeMismatch {
+                message: format!("{name}: {e}"),
+                source: Box::new(e),
+            })
     }
 
     /// Load and evaluate a Fennel file from disk, deserializing the result
@@ -174,11 +198,11 @@ impl Fennel {
 impl FennelError {
     /// Construct an `Eval` error from an mlua error, extracting line
     /// information when available.
-    pub(crate) fn from_lua(source: &str, name: &str, err: &mlua::Error) -> Self {
+    pub(crate) fn from_lua(source: &str, name: &str, err: mlua::Error) -> Self {
         let message = format!("{name}: {err}");
 
         // Try to extract a line number from the Lua error for a label.
-        let offset = extract_line_offset(err)
+        let offset = extract_line_offset(&err)
             .and_then(|line| line_offset(source, line))
             .unwrap_or(SourceOffset::from(0));
 
@@ -186,6 +210,7 @@ impl FennelError {
             message,
             source_code: source.to_string(),
             label: offset,
+            source: Box::new(err),
         }
     }
 }