use std::path::PathBuf;
use std::sync::Arc;
use algocline_core::{Budget, ExecutionMetrics, ExecutionSpec};
use mlua::LuaSerdeExt;
use mlua_isle::{AsyncIsle, AsyncIsleDriver, IsleError};
use mlua_pkg::Registry;
use crate::bridge;
use crate::card::FileCardStore;
use crate::llm_bridge::LlmRequest;
use crate::resolver_factory::make_resolver;
use crate::session::Session;
use crate::state::JsonFileStore;
use crate::variant_pkg::{register_variant_pkgs, VariantPkg};
const PRELUDE: &str = include_str!("prelude.lua");
pub struct Executor {
isle: AsyncIsle,
_driver: AsyncIsleDriver,
lib_paths: Vec<PathBuf>,
}
impl Executor {
pub async fn new(lib_paths: Vec<PathBuf>) -> anyhow::Result<Self> {
let paths_for_shared = lib_paths.clone();
let (isle, driver) = AsyncIsle::spawn(move |lua| {
let mut reg = Registry::new();
for path in &paths_for_shared {
if let Some(resolver) = make_resolver(path) {
reg.add(resolver);
}
}
reg.install(lua)?;
Ok(())
})
.await?;
Ok(Self {
isle,
_driver: driver,
lib_paths,
})
}
pub async fn eval_simple(&self, code: String) -> Result<serde_json::Value, String> {
self.eval_simple_with_paths(code, vec![], vec![]).await
}
pub async fn eval_simple_with_paths(
&self,
code: String,
extra_lib_paths: Vec<PathBuf>,
variant_pkgs: Vec<VariantPkg>,
) -> Result<serde_json::Value, String> {
if extra_lib_paths.is_empty() && variant_pkgs.is_empty() {
let task = self.isle.spawn_exec(move |lua| {
let result: mlua::Value = lua
.load(&code)
.eval()
.map_err(|e| IsleError::Lua(e.to_string()))?;
let json: serde_json::Value = lua
.from_value(result)
.map_err(|e| IsleError::Lua(e.to_string()))?;
serde_json::to_string(&json)
.map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
});
let json_str = task.await.map_err(|e| e.to_string())?;
return serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"));
}
let mut effective = extra_lib_paths;
effective.extend(self.lib_paths.iter().cloned());
let (tmp_isle, _tmp_driver) = AsyncIsle::spawn(move |lua| {
let mut reg = Registry::new();
register_variant_pkgs(&mut reg, &variant_pkgs);
for path in &effective {
if let Some(resolver) = make_resolver(path) {
reg.add(resolver);
}
}
reg.install(lua)?;
Ok(())
})
.await
.map_err(|e| format!("eval_simple VM spawn failed: {e}"))?;
let task = tmp_isle.spawn_exec(move |lua| {
let result: mlua::Value = lua
.load(&code)
.eval()
.map_err(|e| IsleError::Lua(e.to_string()))?;
let json: serde_json::Value = lua
.from_value(result)
.map_err(|e| IsleError::Lua(e.to_string()))?;
serde_json::to_string(&json).map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
});
let json_str = task.await.map_err(|e| e.to_string())?;
serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"))
}
#[allow(clippy::too_many_arguments)]
pub async fn start_session(
&self,
code: String,
ctx: serde_json::Value,
extra_lib_paths: Vec<PathBuf>,
variant_pkgs: Vec<VariantPkg>,
state_store: Arc<JsonFileStore>,
card_store: Arc<FileCardStore>,
scenarios_dir: PathBuf,
) -> Result<Session, String> {
let spec = ExecutionSpec::new(code, ctx);
let metrics = ExecutionMetrics::new();
if let Some(budget) = Budget::from_ctx(&spec.ctx) {
metrics.set_budget(budget);
}
let (llm_tx, llm_rx) = tokio::sync::mpsc::channel::<LlmRequest>(16);
let mut effective = extra_lib_paths;
effective.extend(self.lib_paths.iter().cloned());
let log_sink = metrics.log_sink_handle();
let bridge_config = bridge::BridgeConfig {
llm_tx: Some(llm_tx),
ns: spec.namespace.clone(),
custom_metrics: metrics.custom_metrics_handle(),
budget: metrics.budget_handle(),
progress: metrics.progress_handle(),
lib_paths: effective.clone(), variant_pkgs: variant_pkgs.clone(), state_store,
card_store,
scenarios_dir,
log_sink: Some(log_sink.clone()),
};
let lua_ctx = spec.ctx.clone();
let lua_code = spec.code.clone();
let (session_isle, session_driver) = AsyncIsle::spawn(move |lua| {
let mut reg = Registry::new();
register_variant_pkgs(&mut reg, &variant_pkgs);
for path in &effective {
if let Some(resolver) = make_resolver(path) {
reg.add(resolver);
}
}
reg.install(lua)?;
Ok(())
})
.await
.map_err(|e| format!("Session VM spawn failed: {e}"))?;
session_isle
.exec(move |lua| {
let alc_table = lua.create_table()?;
bridge::register(lua, &alc_table, bridge_config)?;
lua.globals().set("alc", alc_table)?;
let ctx_value = lua.to_value(&lua_ctx)?;
lua.globals().set("ctx", ctx_value)?;
lua.load(PRELUDE)
.exec()
.map_err(|e| IsleError::Lua(format!("Prelude load failed: {e}")))?;
Ok("ok".to_string())
})
.await
.map_err(|e| format!("Session setup failed: {e}"))?;
let wrapped_code = format!("return alc.json_encode((function()\n{lua_code}\nend)())");
let exec_task = session_isle.spawn_coroutine_eval(&wrapped_code);
drop(session_isle);
Ok(Session::new(llm_rx, exec_task, metrics, session_driver))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn make_pkg_dir(parent: &std::path::Path, pkg_name: &str, init_lua: &str) -> PathBuf {
let pkg_dir = parent.join(pkg_name);
fs::create_dir_all(&pkg_dir).unwrap();
fs::write(pkg_dir.join("init.lua"), init_lua).unwrap();
parent.to_path_buf()
}
#[tokio::test]
async fn no_extra_lib_paths_eval_simple() {
let executor = Executor::new(vec![]).await.unwrap();
let result = executor.eval_simple("return 42".to_string()).await.unwrap();
assert_eq!(result, serde_json::json!(42));
}
#[tokio::test]
async fn extra_lib_paths_reachable_via_eval_simple_with_paths() {
let tmp = tempfile::tempdir().unwrap();
let pkg_root = make_pkg_dir(tmp.path(), "test_pkg", "return { value = 99 }");
let executor = Executor::new(vec![]).await.unwrap();
let code = r#"
local pkg = require("test_pkg")
return pkg.value
"#
.to_string();
let result = executor
.eval_simple_with_paths(code, vec![pkg_root], vec![])
.await
.unwrap();
assert_eq!(result, serde_json::json!(99));
}
#[tokio::test]
async fn variant_pkg_resolves_root_and_submodule() {
let tmp = tempfile::tempdir().unwrap();
let pkg_dir = tmp.path().join("physical-dir");
fs::create_dir_all(&pkg_dir).unwrap();
fs::write(
pkg_dir.join("init.lua"),
"return { greet = function(n) return 'hi-' .. n end, sub = require('logical_name.sub') }",
)
.unwrap();
fs::write(pkg_dir.join("sub.lua"), "return { value = 7 }").unwrap();
let executor = Executor::new(vec![]).await.unwrap();
let code = r#"
local pkg = require("logical_name")
return { msg = pkg.greet("there"), sub_value = pkg.sub.value }
"#
.to_string();
let result = executor
.eval_simple_with_paths(code, vec![], vec![VariantPkg::new("logical_name", pkg_dir)])
.await
.unwrap();
assert_eq!(result["msg"], serde_json::json!("hi-there"));
assert_eq!(result["sub_value"], serde_json::json!(7));
}
#[tokio::test]
async fn variant_pkg_overrides_global_same_name() {
let global_tmp = tempfile::tempdir().unwrap();
let variant_tmp = tempfile::tempdir().unwrap();
make_pkg_dir(global_tmp.path(), "my_pkg", "return { value = 1 }");
let variant_dir = variant_tmp.path().join("my_pkg");
fs::create_dir_all(&variant_dir).unwrap();
fs::write(variant_dir.join("init.lua"), "return { value = 2 }").unwrap();
let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
.await
.unwrap();
let code = r#"
local pkg = require("my_pkg")
return pkg.value
"#
.to_string();
let result = executor
.eval_simple_with_paths(code, vec![], vec![VariantPkg::new("my_pkg", variant_dir)])
.await
.unwrap();
assert_eq!(result, serde_json::json!(2));
}
#[tokio::test]
async fn extra_lib_paths_priority_over_default() {
let global_tmp = tempfile::tempdir().unwrap();
let extra_tmp = tempfile::tempdir().unwrap();
make_pkg_dir(global_tmp.path(), "test_pkg", "return { value = 1 }");
let extra_root = make_pkg_dir(extra_tmp.path(), "test_pkg", "return { value = 2 }");
let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
.await
.unwrap();
let code = r#"
local pkg = require("test_pkg")
return pkg.value
"#
.to_string();
let result = executor
.eval_simple_with_paths(code, vec![extra_root], vec![])
.await
.unwrap();
assert_eq!(result, serde_json::json!(2));
}
}