1use std::path::PathBuf;
21use std::sync::Arc;
22
23use algocline_core::{Budget, ExecutionMetrics, ExecutionSpec};
24use mlua::LuaSerdeExt;
25use mlua_isle::{AsyncIsle, AsyncIsleDriver, IsleError};
26use mlua_pkg::Registry;
27
28use crate::bridge;
29use crate::card::FileCardStore;
30use crate::llm_bridge::LlmRequest;
31use crate::resolver_factory::make_resolver;
32use crate::session::Session;
33use crate::state::JsonFileStore;
34use crate::variant_pkg::{register_variant_pkgs, VariantPkg};
35
36const PRELUDE: &str = include_str!("prelude.lua");
39
40pub struct Executor {
49 isle: AsyncIsle,
51 _driver: AsyncIsleDriver,
52 lib_paths: Vec<PathBuf>,
54}
55
56impl Executor {
57 pub async fn new(lib_paths: Vec<PathBuf>) -> anyhow::Result<Self> {
58 let paths_for_shared = lib_paths.clone();
59 let (isle, driver) = AsyncIsle::spawn(move |lua| {
60 let mut reg = Registry::new();
61 for path in &paths_for_shared {
62 if let Some(resolver) = make_resolver(path) {
63 reg.add(resolver);
64 }
65 }
66 reg.install(lua)?;
67 Ok(())
68 })
69 .await?;
70
71 Ok(Self {
72 isle,
73 _driver: driver,
74 lib_paths,
75 })
76 }
77
78 pub async fn eval_simple(&self, code: String) -> Result<serde_json::Value, String> {
84 self.eval_simple_with_paths(code, vec![], vec![]).await
85 }
86
87 pub async fn eval_simple_with_paths(
100 &self,
101 code: String,
102 extra_lib_paths: Vec<PathBuf>,
103 variant_pkgs: Vec<VariantPkg>,
104 ) -> Result<serde_json::Value, String> {
105 if extra_lib_paths.is_empty() && variant_pkgs.is_empty() {
106 let task = self.isle.spawn_exec(move |lua| {
108 let result: mlua::Value = lua
109 .load(&code)
110 .eval()
111 .map_err(|e| IsleError::Lua(e.to_string()))?;
112 let json: serde_json::Value = lua
113 .from_value(result)
114 .map_err(|e| IsleError::Lua(e.to_string()))?;
115 serde_json::to_string(&json)
116 .map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
117 });
118 let json_str = task.await.map_err(|e| e.to_string())?;
119 return serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"));
120 }
121
122 let mut effective = extra_lib_paths;
124 effective.extend(self.lib_paths.iter().cloned());
125
126 let (tmp_isle, _tmp_driver) = AsyncIsle::spawn(move |lua| {
127 let mut reg = Registry::new();
128 register_variant_pkgs(&mut reg, &variant_pkgs);
130 for path in &effective {
131 if let Some(resolver) = make_resolver(path) {
132 reg.add(resolver);
133 }
134 }
135 reg.install(lua)?;
136 Ok(())
137 })
138 .await
139 .map_err(|e| format!("eval_simple VM spawn failed: {e}"))?;
140
141 let task = tmp_isle.spawn_exec(move |lua| {
142 let result: mlua::Value = lua
143 .load(&code)
144 .eval()
145 .map_err(|e| IsleError::Lua(e.to_string()))?;
146 let json: serde_json::Value = lua
147 .from_value(result)
148 .map_err(|e| IsleError::Lua(e.to_string()))?;
149 serde_json::to_string(&json).map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
150 });
151
152 let json_str = task.await.map_err(|e| e.to_string())?;
153 serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"))
154 }
155
156 #[allow(clippy::too_many_arguments)]
173 pub async fn start_session(
174 &self,
175 code: String,
176 ctx: serde_json::Value,
177 extra_lib_paths: Vec<PathBuf>,
178 variant_pkgs: Vec<VariantPkg>,
179 state_store: Arc<JsonFileStore>,
180 card_store: Arc<FileCardStore>,
181 scenarios_dir: PathBuf,
182 ) -> Result<Session, String> {
183 let spec = ExecutionSpec::new(code, ctx);
184 let metrics = ExecutionMetrics::new();
185
186 if let Some(budget) = Budget::from_ctx(&spec.ctx) {
188 metrics.set_budget(budget);
189 }
190
191 let (llm_tx, llm_rx) = tokio::sync::mpsc::channel::<LlmRequest>(16);
192
193 let mut effective = extra_lib_paths;
197 effective.extend(self.lib_paths.iter().cloned());
198
199 let log_sink = metrics.log_sink_handle();
202
203 let bridge_config = bridge::BridgeConfig {
204 llm_tx: Some(llm_tx),
205 ns: spec.namespace.clone(),
206 custom_metrics: metrics.custom_metrics_handle(),
207 budget: metrics.budget_handle(),
208 progress: metrics.progress_handle(),
209 lib_paths: effective.clone(), variant_pkgs: variant_pkgs.clone(), state_store,
212 card_store,
213 scenarios_dir,
214 log_sink: Some(log_sink.clone()),
215 };
216 let lua_ctx = spec.ctx.clone();
217 let lua_code = spec.code.clone();
218
219 let (session_isle, session_driver) = AsyncIsle::spawn(move |lua| {
221 let mut reg = Registry::new();
222 register_variant_pkgs(&mut reg, &variant_pkgs);
224 for path in &effective {
225 if let Some(resolver) = make_resolver(path) {
226 reg.add(resolver);
227 }
228 }
229 reg.install(lua)?;
230 Ok(())
231 })
232 .await
233 .map_err(|e| format!("Session VM spawn failed: {e}"))?;
234
235 session_isle
238 .exec(move |lua| {
239 let alc_table = lua.create_table()?;
240 bridge::register(lua, &alc_table, bridge_config)?;
241 lua.globals().set("alc", alc_table)?;
242
243 let ctx_value = lua.to_value(&lua_ctx)?;
244 lua.globals().set("ctx", ctx_value)?;
245
246 lua.load(PRELUDE)
247 .exec()
248 .map_err(|e| IsleError::Lua(format!("Prelude load failed: {e}")))?;
249
250 Ok("ok".to_string())
262 })
263 .await
264 .map_err(|e| format!("Session setup failed: {e}"))?;
265
266 let wrapped_code = format!("return alc.json_encode((function()\n{lua_code}\nend)())");
268 let exec_task = session_isle.spawn_coroutine_eval(&wrapped_code);
269
270 drop(session_isle);
273
274 Ok(Session::new(llm_rx, exec_task, metrics, session_driver))
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use std::fs;
282
283 fn make_pkg_dir(parent: &std::path::Path, pkg_name: &str, init_lua: &str) -> PathBuf {
285 let pkg_dir = parent.join(pkg_name);
286 fs::create_dir_all(&pkg_dir).unwrap();
287 fs::write(pkg_dir.join("init.lua"), init_lua).unwrap();
288 parent.to_path_buf()
289 }
290
291 #[tokio::test]
293 async fn no_extra_lib_paths_eval_simple() {
294 let executor = Executor::new(vec![]).await.unwrap();
295 let result = executor.eval_simple("return 42".to_string()).await.unwrap();
296 assert_eq!(result, serde_json::json!(42));
297 }
298
299 #[tokio::test]
304 async fn extra_lib_paths_reachable_via_eval_simple_with_paths() {
305 let tmp = tempfile::tempdir().unwrap();
306 let pkg_root = make_pkg_dir(tmp.path(), "test_pkg", "return { value = 99 }");
307
308 let executor = Executor::new(vec![]).await.unwrap();
309 let code = r#"
310 local pkg = require("test_pkg")
311 return pkg.value
312 "#
313 .to_string();
314
315 let result = executor
316 .eval_simple_with_paths(code, vec![pkg_root], vec![])
317 .await
318 .unwrap();
319
320 assert_eq!(result, serde_json::json!(99));
321 }
322
323 #[tokio::test]
326 async fn variant_pkg_resolves_root_and_submodule() {
327 let tmp = tempfile::tempdir().unwrap();
328 let pkg_dir = tmp.path().join("physical-dir");
331 fs::create_dir_all(&pkg_dir).unwrap();
332 fs::write(
333 pkg_dir.join("init.lua"),
334 "return { greet = function(n) return 'hi-' .. n end, sub = require('logical_name.sub') }",
335 )
336 .unwrap();
337 fs::write(pkg_dir.join("sub.lua"), "return { value = 7 }").unwrap();
338
339 let executor = Executor::new(vec![]).await.unwrap();
340 let code = r#"
341 local pkg = require("logical_name")
342 return { msg = pkg.greet("there"), sub_value = pkg.sub.value }
343 "#
344 .to_string();
345
346 let result = executor
347 .eval_simple_with_paths(code, vec![], vec![VariantPkg::new("logical_name", pkg_dir)])
348 .await
349 .unwrap();
350
351 assert_eq!(result["msg"], serde_json::json!("hi-there"));
352 assert_eq!(result["sub_value"], serde_json::json!(7));
353 }
354
355 #[tokio::test]
357 async fn variant_pkg_overrides_global_same_name() {
358 let global_tmp = tempfile::tempdir().unwrap();
359 let variant_tmp = tempfile::tempdir().unwrap();
360
361 make_pkg_dir(global_tmp.path(), "my_pkg", "return { value = 1 }");
363 let variant_dir = variant_tmp.path().join("my_pkg");
365 fs::create_dir_all(&variant_dir).unwrap();
366 fs::write(variant_dir.join("init.lua"), "return { value = 2 }").unwrap();
367
368 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
369 .await
370 .unwrap();
371
372 let code = r#"
373 local pkg = require("my_pkg")
374 return pkg.value
375 "#
376 .to_string();
377
378 let result = executor
379 .eval_simple_with_paths(code, vec![], vec![VariantPkg::new("my_pkg", variant_dir)])
380 .await
381 .unwrap();
382
383 assert_eq!(result, serde_json::json!(2));
384 }
385
386 #[tokio::test]
389 async fn extra_lib_paths_priority_over_default() {
390 let global_tmp = tempfile::tempdir().unwrap();
391 let extra_tmp = tempfile::tempdir().unwrap();
392
393 make_pkg_dir(global_tmp.path(), "test_pkg", "return { value = 1 }");
395 let extra_root = make_pkg_dir(extra_tmp.path(), "test_pkg", "return { value = 2 }");
397
398 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
400 .await
401 .unwrap();
402
403 let code = r#"
404 local pkg = require("test_pkg")
405 return pkg.value
406 "#
407 .to_string();
408
409 let result = executor
410 .eval_simple_with_paths(code, vec![extra_root], vec![])
411 .await
412 .unwrap();
413
414 assert_eq!(result, serde_json::json!(2));
416 }
417}