Centralize Fennel evaluation behind eval_raw
ci.rs managed its own Lua VM, duplicated Fennel loading, and had
separate error mapping logic. Now Fennel exposes eval_raw which
handles VM setup, source evaluation, and error conversion; load_string
delegates to it with a no-op setup callback. eval_ci uses eval_raw to
inject the job macro and extracts the registry from the shared VM.

Assisted-by: GLM-5.1 via pi
change ovynszxzrwrzkmvvkytnklspovuqqlmv
commit b0a4be9fcaa18f2591e0f8c8a9f5409e24e03946
author Alpha Chen <alpha@kejadlen.dev>
date
parent ymvnqzmo
diff --git a/src/ci.rs b/src/ci.rs
index aaba216..f85bd51 100644
--- a/src/ci.rs
+++ b/src/ci.rs
@@ -1,7 +1,5 @@
 use std::path::{Path, PathBuf};
 
-use mlua::Lua;
-
 use crate::Result;
 use crate::event::PushEvent;
 
@@ -306,71 +304,53 @@ pub struct EvalResult {
 
 /// Evaluate a ci.fnl source string, registering jobs via the `job` macro.
 ///
-/// Creates a fresh Lua VM with Fennel loaded, injects a `job` global
-/// that accumulates into a registration table, evaluates the source,
-/// and extracts the registered jobs.
+/// Injects a `job` global that accumulates into a registration table,
+/// evaluates the source, and extracts the registered jobs.
 pub fn eval_ci(
-    _fennel: &crate::fennel::Fennel,
+    fennel: &crate::fennel::Fennel,
     source: &str,
     name: &str,
 ) -> crate::Result<EvalResult> {
-    eval_ci_inner(source, name).map_err(|e| {
-        // Convert mlua errors into rich FennelError with source snippets.
-        match e {
-            mlua::Error::RuntimeError(_) | mlua::Error::SyntaxError { .. } => {
-                crate::fennel::FennelError::from_lua(source, name, &e).into()
-            }
-            _ => crate::Error::Fennel(crate::fennel::FennelError::Eval {
-                message: format!("{name}: {e}"),
-                source_code: source.to_string(),
-                label: miette::SourceOffset::from(0),
-            }),
-        }
-    })
-}
+    fennel.eval_raw(source, name, |lua| {
+        // Create a registration table. `job` will push into this.
+        let registry: mlua::Table = lua.create_table()?;
+        lua.globals().set("_quire_jobs", registry)?;
+
+        // Define the `job` global: (job id inputs run-fn)
+        let job_fn = lua.create_function(
+            |lua, (id, inputs, run_fn): (mlua::String, mlua::Table, mlua::Function)| {
+                let registry: mlua::Table = lua.globals().get("_quire_jobs")?;
+                let entry = lua.create_table()?;
+                entry.set("id", id)?;
+                entry.set("inputs", inputs)?;
+                entry.set("run", run_fn)?;
+                registry.push(entry)?;
+                Ok(())
+            },
+        )?;
+        lua.globals().set("job", job_fn)?;
 
-fn eval_ci_inner(source: &str, name: &str) -> mlua::Result<EvalResult> {
-    // Create a fresh VM with Fennel loaded.
-    let lua = unsafe { Lua::unsafe_new() };
-    let fennel_lua: &str = include_str!("../vendor/fennel.lua");
-    let fennel_module: mlua::Table = lua.load(fennel_lua).set_name("fennel.lua").eval()?;
-    lua.globals().set("fennel", fennel_module)?;
-
-    // Create a registration table. `job` will push into this.
-    let registry: mlua::Table = lua.create_table()?;
-    lua.globals().set("_quire_jobs", registry)?;
-
-    // Define the `job` global: (job id inputs run-fn)
-    let job_fn = lua.create_function(
-        |lua, (id, inputs, run_fn): (mlua::String, mlua::Table, mlua::Function)| {
-            let registry: mlua::Table = lua.globals().get("_quire_jobs")?;
-            let entry = lua.create_table()?;
-            entry.set("id", id)?;
-            entry.set("inputs", inputs)?;
-            entry.set("run", run_fn)?;
-            registry.push(entry)?;
-            Ok(())
-        },
-    )?;
-    lua.globals().set("job", job_fn)?;
-
-    // Eval the ci.fnl source via Fennel.
-    let fennel: mlua::Table = lua.globals().get("fennel")?;
-    let eval: mlua::Function = fennel.get("eval")?;
-    let opts = lua.create_table()?;
-    opts.set("filename", name)?;
-    eval.call::<mlua::MultiValue>((source, opts))?;
+        Ok(())
+    })?;
 
     // Extract the registration table.
-    let registry: mlua::Table = lua.globals().get("_quire_jobs")?;
+    let registry: mlua::Table = fennel
+        .lua()
+        .globals()
+        .get("_quire_jobs")
+        .map_err(|e| crate::fennel::FennelError::from_lua(source, name, &e))?;
     let mut jobs = Vec::new();
     for entry in registry.sequence_values::<mlua::Table>() {
-        let entry = entry?;
-        let id: String = entry.get("id")?;
-        let inputs_table: mlua::Table = entry.get("inputs")?;
+        let entry = entry.map_err(|e| crate::fennel::FennelError::from_lua(source, name, &e))?;
+        let id: String = entry
+            .get("id")
+            .map_err(|e| crate::fennel::FennelError::from_lua(source, name, &e))?;
+        let inputs_table: mlua::Table = entry
+            .get("inputs")
+            .map_err(|e| crate::fennel::FennelError::from_lua(source, name, &e))?;
         let mut inputs = Vec::new();
         for input in inputs_table.sequence_values::<String>() {
-            inputs.push(input?);
+            inputs.push(input.map_err(|e| crate::fennel::FennelError::from_lua(source, name, &e))?);
         }
         jobs.push(JobDef { id, inputs });
     }
diff --git a/src/fennel.rs b/src/fennel.rs
index b0518eb..2c66e95 100644
--- a/src/fennel.rs
+++ b/src/fennel.rs
@@ -42,6 +42,14 @@ pub struct Fennel {
 }
 
 impl Fennel {
+    /// Access the underlying Lua VM.
+    ///
+    /// Needed for extracting evaluation results that don't deserialize
+    /// directly into a Rust type (e.g. the CI job registry).
+    pub(crate) fn lua(&self) -> &Lua {
+        &self.lua
+    }
+
     /// Create a new Fennel instance.
     ///
     /// Loads the vendored `fennel.lua` into a fresh Lua VM and registers it
@@ -76,21 +84,26 @@ impl Fennel {
         Ok(Self { lua })
     }
 
-    /// Compile and evaluate a Fennel source string, deserializing the result
-    /// into `T`.
+    /// Compile and evaluate a Fennel source string, returning the raw
+    /// Lua value.
     ///
-    /// `name` is used in error messages — typically a filename or a synthetic
-    /// label like `HEAD:.quire/config.fnl`.
-    pub fn load_string<T>(&self, source: &str, name: &str) -> Result<T, FennelError>
-    where
-        T: serde::de::DeserializeOwned,
-    {
+    /// `setup` is called before evaluation and can inject globals or
+    /// modify the VM. `name` is used in error messages — typically a
+    /// filename or a synthetic label like `HEAD:.quire/config.fnl`.
+    pub fn eval_raw(
+        &self,
+        source: &str,
+        name: &str,
+        setup: impl Fn(&Lua) -> mlua::Result<()>,
+    ) -> Result<mlua::Value, FennelError> {
         if source.trim().is_empty() {
             return Err(FennelError::Empty {
                 name: name.to_string(),
             });
         }
 
+        setup(&self.lua).map_err(|e| FennelError::from_lua(source, name, &e))?;
+
         let fennel: mlua::Table =
             self.lua
                 .globals()
@@ -123,6 +136,20 @@ impl Fennel {
             .call::<mlua::Value>((source, opts))
             .map_err(|e| FennelError::from_lua(source, name, &e))?;
 
+        Ok(result)
+    }
+
+    /// Compile and evaluate a Fennel source string, deserializing the result
+    /// into `T`.
+    ///
+    /// `name` is used in error messages — typically a filename or a synthetic
+    /// label like `HEAD:.quire/config.fnl`.
+    pub fn load_string<T>(&self, source: &str, name: &str) -> Result<T, FennelError>
+    where
+        T: serde::de::DeserializeOwned,
+    {
+        let result = self.eval_raw(source, name, |_| Ok(()))?;
+
         // Reject nil results — a config file that evaluates to nothing is
         // almost always a mistake.
         if matches!(result, mlua::Value::Nil) {