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 stats: metrics.stats_handle(),
208 budget: metrics.budget_handle(),
209 progress: metrics.progress_handle(),
210 lib_paths: effective.clone(), variant_pkgs: variant_pkgs.clone(), state_store,
213 card_store,
214 scenarios_dir,
215 log_sink: Some(log_sink.clone()),
216 };
217 let lua_ctx = spec.ctx.clone();
218 let lua_code = spec.code.clone();
219
220 let (session_isle, session_driver) = AsyncIsle::spawn(move |lua| {
222 let mut reg = Registry::new();
223 register_variant_pkgs(&mut reg, &variant_pkgs);
225 for path in &effective {
226 if let Some(resolver) = make_resolver(path) {
227 reg.add(resolver);
228 }
229 }
230 reg.install(lua)?;
231 Ok(())
232 })
233 .await
234 .map_err(|e| format!("Session VM spawn failed: {e}"))?;
235
236 session_isle
239 .exec(move |lua| {
240 let alc_table = lua.create_table()?;
241 bridge::register(lua, &alc_table, bridge_config)?;
242 lua.globals().set("alc", alc_table)?;
243
244 let ctx_value = lua.to_value(&lua_ctx)?;
245 lua.globals().set("ctx", ctx_value)?;
246
247 lua.load(PRELUDE)
248 .exec()
249 .map_err(|e| IsleError::Lua(format!("Prelude load failed: {e}")))?;
250
251 Ok("ok".to_string())
263 })
264 .await
265 .map_err(|e| format!("Session setup failed: {e}"))?;
266
267 let wrapped_code = format!("return alc.json_encode((function()\n{lua_code}\nend)())");
269 let exec_task = session_isle.spawn_coroutine_eval(&wrapped_code);
270
271 drop(session_isle);
274
275 Ok(Session::new(llm_rx, exec_task, metrics, session_driver))
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use std::fs;
283
284 fn make_pkg_dir(parent: &std::path::Path, pkg_name: &str, init_lua: &str) -> PathBuf {
286 let pkg_dir = parent.join(pkg_name);
287 fs::create_dir_all(&pkg_dir).unwrap();
288 fs::write(pkg_dir.join("init.lua"), init_lua).unwrap();
289 parent.to_path_buf()
290 }
291
292 #[tokio::test]
294 async fn no_extra_lib_paths_eval_simple() {
295 let executor = Executor::new(vec![]).await.unwrap();
296 let result = executor.eval_simple("return 42".to_string()).await.unwrap();
297 assert_eq!(result, serde_json::json!(42));
298 }
299
300 #[tokio::test]
305 async fn extra_lib_paths_reachable_via_eval_simple_with_paths() {
306 let tmp = tempfile::tempdir().unwrap();
307 let pkg_root = make_pkg_dir(tmp.path(), "test_pkg", "return { value = 99 }");
308
309 let executor = Executor::new(vec![]).await.unwrap();
310 let code = r#"
311 local pkg = require("test_pkg")
312 return pkg.value
313 "#
314 .to_string();
315
316 let result = executor
317 .eval_simple_with_paths(code, vec![pkg_root], vec![])
318 .await
319 .unwrap();
320
321 assert_eq!(result, serde_json::json!(99));
322 }
323
324 #[tokio::test]
327 async fn variant_pkg_resolves_root_and_submodule() {
328 let tmp = tempfile::tempdir().unwrap();
329 let pkg_dir = tmp.path().join("physical-dir");
332 fs::create_dir_all(&pkg_dir).unwrap();
333 fs::write(
334 pkg_dir.join("init.lua"),
335 "return { greet = function(n) return 'hi-' .. n end, sub = require('logical_name.sub') }",
336 )
337 .unwrap();
338 fs::write(pkg_dir.join("sub.lua"), "return { value = 7 }").unwrap();
339
340 let executor = Executor::new(vec![]).await.unwrap();
341 let code = r#"
342 local pkg = require("logical_name")
343 return { msg = pkg.greet("there"), sub_value = pkg.sub.value }
344 "#
345 .to_string();
346
347 let result = executor
348 .eval_simple_with_paths(code, vec![], vec![VariantPkg::new("logical_name", pkg_dir)])
349 .await
350 .unwrap();
351
352 assert_eq!(result["msg"], serde_json::json!("hi-there"));
353 assert_eq!(result["sub_value"], serde_json::json!(7));
354 }
355
356 #[tokio::test]
358 async fn variant_pkg_overrides_global_same_name() {
359 let global_tmp = tempfile::tempdir().unwrap();
360 let variant_tmp = tempfile::tempdir().unwrap();
361
362 make_pkg_dir(global_tmp.path(), "my_pkg", "return { value = 1 }");
364 let variant_dir = variant_tmp.path().join("my_pkg");
366 fs::create_dir_all(&variant_dir).unwrap();
367 fs::write(variant_dir.join("init.lua"), "return { value = 2 }").unwrap();
368
369 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
370 .await
371 .unwrap();
372
373 let code = r#"
374 local pkg = require("my_pkg")
375 return pkg.value
376 "#
377 .to_string();
378
379 let result = executor
380 .eval_simple_with_paths(code, vec![], vec![VariantPkg::new("my_pkg", variant_dir)])
381 .await
382 .unwrap();
383
384 assert_eq!(result, serde_json::json!(2));
385 }
386
387 #[tokio::test]
390 async fn extra_lib_paths_priority_over_default() {
391 let global_tmp = tempfile::tempdir().unwrap();
392 let extra_tmp = tempfile::tempdir().unwrap();
393
394 make_pkg_dir(global_tmp.path(), "test_pkg", "return { value = 1 }");
396 let extra_root = make_pkg_dir(extra_tmp.path(), "test_pkg", "return { value = 2 }");
398
399 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
401 .await
402 .unwrap();
403
404 let code = r#"
405 local pkg = require("test_pkg")
406 return pkg.value
407 "#
408 .to_string();
409
410 let result = executor
411 .eval_simple_with_paths(code, vec![extra_root], vec![])
412 .await
413 .unwrap();
414
415 assert_eq!(result, serde_json::json!(2));
417 }
418}