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
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) {