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 bridge_config = bridge::BridgeConfig {
200 llm_tx: Some(llm_tx),
201 ns: spec.namespace.clone(),
202 custom_metrics: metrics.custom_metrics_handle(),
203 budget: metrics.budget_handle(),
204 progress: metrics.progress_handle(),
205 lib_paths: effective.clone(), variant_pkgs: variant_pkgs.clone(), state_store,
208 card_store,
209 scenarios_dir,
210 };
211 let lua_ctx = spec.ctx.clone();
212 let lua_code = spec.code.clone();
213
214 let (session_isle, session_driver) = AsyncIsle::spawn(move |lua| {
216 let mut reg = Registry::new();
217 register_variant_pkgs(&mut reg, &variant_pkgs);
219 for path in &effective {
220 if let Some(resolver) = make_resolver(path) {
221 reg.add(resolver);
222 }
223 }
224 reg.install(lua)?;
225 Ok(())
226 })
227 .await
228 .map_err(|e| format!("Session VM spawn failed: {e}"))?;
229
230 session_isle
233 .exec(move |lua| {
234 let alc_table = lua.create_table()?;
235 bridge::register(lua, &alc_table, bridge_config)?;
236 lua.globals().set("alc", alc_table)?;
237
238 let ctx_value = lua.to_value(&lua_ctx)?;
239 lua.globals().set("ctx", ctx_value)?;
240
241 lua.load(PRELUDE)
242 .exec()
243 .map_err(|e| IsleError::Lua(format!("Prelude load failed: {e}")))?;
244
245 Ok("ok".to_string())
248 })
249 .await
250 .map_err(|e| format!("Session setup failed: {e}"))?;
251
252 let wrapped_code = format!("return alc.json_encode((function()\n{lua_code}\nend)())");
254 let exec_task = session_isle.spawn_coroutine_eval(&wrapped_code);
255
256 drop(session_isle);
259
260 Ok(Session::new(llm_rx, exec_task, metrics, session_driver))
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use std::fs;
268
269 fn make_pkg_dir(parent: &std::path::Path, pkg_name: &str, init_lua: &str) -> PathBuf {
271 let pkg_dir = parent.join(pkg_name);
272 fs::create_dir_all(&pkg_dir).unwrap();
273 fs::write(pkg_dir.join("init.lua"), init_lua).unwrap();
274 parent.to_path_buf()
275 }
276
277 #[tokio::test]
279 async fn no_extra_lib_paths_eval_simple() {
280 let executor = Executor::new(vec![]).await.unwrap();
281 let result = executor.eval_simple("return 42".to_string()).await.unwrap();
282 assert_eq!(result, serde_json::json!(42));
283 }
284
285 #[tokio::test]
290 async fn extra_lib_paths_reachable_via_eval_simple_with_paths() {
291 let tmp = tempfile::tempdir().unwrap();
292 let pkg_root = make_pkg_dir(tmp.path(), "test_pkg", "return { value = 99 }");
293
294 let executor = Executor::new(vec![]).await.unwrap();
295 let code = r#"
296 local pkg = require("test_pkg")
297 return pkg.value
298 "#
299 .to_string();
300
301 let result = executor
302 .eval_simple_with_paths(code, vec![pkg_root], vec![])
303 .await
304 .unwrap();
305
306 assert_eq!(result, serde_json::json!(99));
307 }
308
309 #[tokio::test]
312 async fn variant_pkg_resolves_root_and_submodule() {
313 let tmp = tempfile::tempdir().unwrap();
314 let pkg_dir = tmp.path().join("physical-dir");
317 fs::create_dir_all(&pkg_dir).unwrap();
318 fs::write(
319 pkg_dir.join("init.lua"),
320 "return { greet = function(n) return 'hi-' .. n end, sub = require('logical_name.sub') }",
321 )
322 .unwrap();
323 fs::write(pkg_dir.join("sub.lua"), "return { value = 7 }").unwrap();
324
325 let executor = Executor::new(vec![]).await.unwrap();
326 let code = r#"
327 local pkg = require("logical_name")
328 return { msg = pkg.greet("there"), sub_value = pkg.sub.value }
329 "#
330 .to_string();
331
332 let result = executor
333 .eval_simple_with_paths(code, vec![], vec![VariantPkg::new("logical_name", pkg_dir)])
334 .await
335 .unwrap();
336
337 assert_eq!(result["msg"], serde_json::json!("hi-there"));
338 assert_eq!(result["sub_value"], serde_json::json!(7));
339 }
340
341 #[tokio::test]
343 async fn variant_pkg_overrides_global_same_name() {
344 let global_tmp = tempfile::tempdir().unwrap();
345 let variant_tmp = tempfile::tempdir().unwrap();
346
347 make_pkg_dir(global_tmp.path(), "my_pkg", "return { value = 1 }");
349 let variant_dir = variant_tmp.path().join("my_pkg");
351 fs::create_dir_all(&variant_dir).unwrap();
352 fs::write(variant_dir.join("init.lua"), "return { value = 2 }").unwrap();
353
354 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
355 .await
356 .unwrap();
357
358 let code = r#"
359 local pkg = require("my_pkg")
360 return pkg.value
361 "#
362 .to_string();
363
364 let result = executor
365 .eval_simple_with_paths(code, vec![], vec![VariantPkg::new("my_pkg", variant_dir)])
366 .await
367 .unwrap();
368
369 assert_eq!(result, serde_json::json!(2));
370 }
371
372 #[tokio::test]
375 async fn extra_lib_paths_priority_over_default() {
376 let global_tmp = tempfile::tempdir().unwrap();
377 let extra_tmp = tempfile::tempdir().unwrap();
378
379 make_pkg_dir(global_tmp.path(), "test_pkg", "return { value = 1 }");
381 let extra_root = make_pkg_dir(extra_tmp.path(), "test_pkg", "return { value = 2 }");
383
384 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
386 .await
387 .unwrap();
388
389 let code = r#"
390 local pkg = require("test_pkg")
391 return pkg.value
392 "#
393 .to_string();
394
395 let result = executor
396 .eval_simple_with_paths(code, vec![extra_root], vec![])
397 .await
398 .unwrap();
399
400 assert_eq!(result, serde_json::json!(2));
402 }
403}