use std::path::PathBuf;
use std::sync::Arc;
use algocline_core::{BudgetHandle, CustomMetricsHandle, ProgressHandle};
use mlua::prelude::*;
mod data;
mod fork;
mod fuzzy;
mod llm;
mod text;
use crate::card::FileCardStore;
use crate::llm_bridge::LlmRequest;
use crate::state::JsonFileStore;
use crate::variant_pkg::VariantPkg;
pub(crate) const PRELUDE: &str = include_str!("../prelude.lua");
pub struct BridgeConfig {
pub llm_tx: Option<tokio::sync::mpsc::Sender<LlmRequest>>,
pub ns: String,
pub custom_metrics: CustomMetricsHandle,
pub budget: BudgetHandle,
pub progress: ProgressHandle,
pub lib_paths: Vec<PathBuf>,
pub variant_pkgs: Vec<VariantPkg>,
pub state_store: Arc<JsonFileStore>,
pub card_store: Arc<FileCardStore>,
pub scenarios_dir: PathBuf,
}
pub fn register(lua: &Lua, alc_table: &LuaTable, config: BridgeConfig) -> LuaResult<()> {
data::register_json(lua, alc_table)?;
fuzzy::register_fuzzy(lua, alc_table)?;
data::register_log(lua, alc_table)?;
data::register_state(lua, alc_table, config.ns, Arc::clone(&config.state_store))?;
data::register_card(lua, alc_table, Arc::clone(&config.card_store))?;
data::register_dirs(
lua,
alc_table,
config.state_store.root(),
config.card_store.root(),
&config.scenarios_dir,
)?;
text::register_chunk(lua, alc_table)?;
data::register_stats(lua, alc_table, config.custom_metrics)?;
register_time(lua, alc_table)?;
register_math(lua, alc_table)?;
llm::register_budget_remaining(lua, alc_table, config.budget.clone())?;
llm::register_progress(lua, alc_table, config.progress)?;
if let Some(tx) = config.llm_tx {
llm::register_llm(lua, alc_table, tx.clone(), config.budget.clone())?;
llm::register_llm_batch(lua, alc_table, tx.clone(), config.budget.clone())?;
fork::register_fork(
lua,
alc_table,
tx,
config.budget,
config.lib_paths,
config.variant_pkgs,
config.state_store,
config.card_store,
config.scenarios_dir,
)?;
}
Ok(())
}
fn register_math(lua: &Lua, alc_table: &LuaTable) -> LuaResult<()> {
let math_table = mlua_mathlib::module(lua)?;
alc_table.set("math", math_table)?;
Ok(())
}
fn register_time(lua: &Lua, alc_table: &LuaTable) -> LuaResult<()> {
let time_fn = lua.create_function(|_, ()| {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(mlua::Error::external)?;
Ok(now.as_secs_f64())
})?;
alc_table.set("time", time_fn)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use algocline_core::ExecutionMetrics;
fn test_config() -> BridgeConfig {
let metrics = ExecutionMetrics::new();
let tmp = tempfile::tempdir().expect("test tempdir");
let root = tmp.path().to_path_buf();
std::mem::forget(tmp);
BridgeConfig {
llm_tx: None,
ns: "default".into(),
custom_metrics: metrics.custom_metrics_handle(),
budget: metrics.budget_handle(),
progress: metrics.progress_handle(),
lib_paths: vec![],
variant_pkgs: vec![],
state_store: Arc::new(JsonFileStore::new(root.join("state"))),
card_store: Arc::new(FileCardStore::new(root.join("cards"))),
scenarios_dir: root.join("scenarios"),
}
}
fn setup_with_prelude() -> Lua {
let lua = Lua::new();
let t = lua.create_table().unwrap();
register(&lua, &t, test_config()).unwrap();
lua.globals().set("alc", t).unwrap();
lua.load(PRELUDE).exec().unwrap();
lua
}
#[test]
fn cache_info_initial_state() {
let lua = setup_with_prelude();
let result: LuaValue = lua.load("return alc.cache_info()").eval().unwrap();
let tbl = result.as_table().unwrap();
assert_eq!(tbl.get::<i64>("entries").unwrap(), 0);
assert_eq!(tbl.get::<i64>("hits").unwrap(), 0);
assert_eq!(tbl.get::<i64>("misses").unwrap(), 0);
}
#[test]
fn cache_clear_resets_state() {
let lua = setup_with_prelude();
lua.load(
r#"
-- Simulate cache state by calling cache_info before/after clear
local info1 = alc.cache_info()
alc.cache_clear()
local info2 = alc.cache_info()
assert(info2.entries == 0)
assert(info2.hits == 0)
assert(info2.misses == 0)
"#,
)
.exec()
.unwrap();
}
#[test]
fn parallel_rejects_empty_items() {
let lua = setup_with_prelude();
let result: Result<LuaValue, _> = lua
.load(r#"return alc.parallel({}, function(x) return x end)"#)
.eval();
let err = result.unwrap_err().to_string();
assert!(
err.contains("non-empty array"),
"expected non-empty array error, got: {err}"
);
}
#[test]
fn parallel_rejects_non_function_prompt_fn() {
let lua = setup_with_prelude();
let result: Result<LuaValue, _> = lua
.load(r#"return alc.parallel({"a", "b"}, "not a function")"#)
.eval();
let err = result.unwrap_err().to_string();
assert!(
err.contains("prompt_fn must be a function"),
"expected function error, got: {err}"
);
}
#[test]
fn parallel_rejects_invalid_prompt_fn_return() {
let lua = setup_with_prelude();
let result: Result<LuaValue, _> = lua
.load(r#"return alc.parallel({"a"}, function(x) return 42 end)"#)
.eval();
let err = result.unwrap_err().to_string();
assert!(
err.contains("must return string or table"),
"expected type error, got: {err}"
);
}
#[test]
fn parallel_rejects_table_without_prompt() {
let lua = setup_with_prelude();
let result: Result<LuaValue, _> = lua
.load(r#"return alc.parallel({"a"}, function(x) return { system = "hi" } end)"#)
.eval();
let err = result.unwrap_err().to_string();
assert!(
err.contains("without .prompt"),
"expected prompt field error, got: {err}"
);
}
#[test]
fn fingerprint_deterministic() {
let lua = setup_with_prelude();
let result: bool = lua
.load(r#"return alc.fingerprint("hello") == alc.fingerprint("hello")"#)
.eval()
.unwrap();
assert!(result);
}
#[test]
fn fingerprint_normalized() {
let lua = setup_with_prelude();
let result: bool = lua
.load(r#"return alc.fingerprint(" Hello World ") == alc.fingerprint("hello world")"#)
.eval()
.unwrap();
assert!(result);
}
#[test]
fn parse_number_basic() {
let lua = setup_with_prelude();
let result: f64 = lua
.load(r#"return alc.parse_number("Found 3 subtasks to implement")"#)
.eval()
.unwrap();
assert!((result - 3.0).abs() < f64::EPSILON);
}
#[test]
fn parse_number_decimal() {
let lua = setup_with_prelude();
let result: f64 = lua
.load(r#"return alc.parse_number("Score: 7.5/10")"#)
.eval()
.unwrap();
assert!((result - 7.5).abs() < f64::EPSILON);
}
#[test]
fn parse_number_with_pattern() {
let lua = setup_with_prelude();
let result: f64 = lua
.load(r#"return alc.parse_number("Created 3 subtasks for implementation", "(%d+)%s+subtask")"#)
.eval()
.unwrap();
assert!((result - 3.0).abs() < f64::EPSILON);
}
#[test]
fn parse_number_nil_on_no_match() {
let lua = setup_with_prelude();
let result: LuaValue = lua
.load(r#"return alc.parse_number("no numbers here")"#)
.eval()
.unwrap();
assert!(result.is_nil());
}
#[test]
fn parse_number_negative() {
let lua = setup_with_prelude();
let result: f64 = lua
.load(r#"return alc.parse_number("Temperature: -5 degrees")"#)
.eval()
.unwrap();
assert!((result - (-5.0)).abs() < f64::EPSILON);
}
}