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 let print_fn = lua.create_function(|lua_inner, args: mlua::MultiValue| {
259 use mlua::prelude::LuaValue;
260 let parts: Vec<String> = args
261 .iter()
262 .map(|v| match v {
263 LuaValue::Nil => "nil".to_string(),
264 LuaValue::Boolean(b) => b.to_string(),
265 LuaValue::Integer(n) => n.to_string(),
266 LuaValue::Number(n) => {
267 if n.fract() == 0.0 && n.abs() < 1e15_f64 {
270 format!("{n:.1}")
271 } else {
272 format!("{n}")
273 }
274 }
275 other => lua_inner
276 .coerce_string(other.clone())
277 .ok()
278 .flatten()
279 .and_then(|s| s.to_str().ok().map(|r| r.to_string()))
280 .unwrap_or_else(|| format!("{other:?}")),
281 })
282 .collect();
283 let line = parts.join("\t");
284 tracing::info!(target: "alc.lua.print", "{}", line);
285 Ok(())
286 })?;
287 lua.globals().set("print", print_fn)?;
288
289 Ok("ok".to_string())
292 })
293 .await
294 .map_err(|e| format!("Session setup failed: {e}"))?;
295
296 let wrapped_code = format!("return alc.json_encode((function()\n{lua_code}\nend)())");
298 let exec_task = session_isle.spawn_coroutine_eval(&wrapped_code);
299
300 drop(session_isle);
303
304 Ok(Session::new(llm_rx, exec_task, metrics, session_driver))
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use std::fs;
312
313 fn make_pkg_dir(parent: &std::path::Path, pkg_name: &str, init_lua: &str) -> PathBuf {
315 let pkg_dir = parent.join(pkg_name);
316 fs::create_dir_all(&pkg_dir).unwrap();
317 fs::write(pkg_dir.join("init.lua"), init_lua).unwrap();
318 parent.to_path_buf()
319 }
320
321 #[tokio::test]
323 async fn no_extra_lib_paths_eval_simple() {
324 let executor = Executor::new(vec![]).await.unwrap();
325 let result = executor.eval_simple("return 42".to_string()).await.unwrap();
326 assert_eq!(result, serde_json::json!(42));
327 }
328
329 #[tokio::test]
334 async fn extra_lib_paths_reachable_via_eval_simple_with_paths() {
335 let tmp = tempfile::tempdir().unwrap();
336 let pkg_root = make_pkg_dir(tmp.path(), "test_pkg", "return { value = 99 }");
337
338 let executor = Executor::new(vec![]).await.unwrap();
339 let code = r#"
340 local pkg = require("test_pkg")
341 return pkg.value
342 "#
343 .to_string();
344
345 let result = executor
346 .eval_simple_with_paths(code, vec![pkg_root], vec![])
347 .await
348 .unwrap();
349
350 assert_eq!(result, serde_json::json!(99));
351 }
352
353 #[tokio::test]
356 async fn variant_pkg_resolves_root_and_submodule() {
357 let tmp = tempfile::tempdir().unwrap();
358 let pkg_dir = tmp.path().join("physical-dir");
361 fs::create_dir_all(&pkg_dir).unwrap();
362 fs::write(
363 pkg_dir.join("init.lua"),
364 "return { greet = function(n) return 'hi-' .. n end, sub = require('logical_name.sub') }",
365 )
366 .unwrap();
367 fs::write(pkg_dir.join("sub.lua"), "return { value = 7 }").unwrap();
368
369 let executor = Executor::new(vec![]).await.unwrap();
370 let code = r#"
371 local pkg = require("logical_name")
372 return { msg = pkg.greet("there"), sub_value = pkg.sub.value }
373 "#
374 .to_string();
375
376 let result = executor
377 .eval_simple_with_paths(code, vec![], vec![VariantPkg::new("logical_name", pkg_dir)])
378 .await
379 .unwrap();
380
381 assert_eq!(result["msg"], serde_json::json!("hi-there"));
382 assert_eq!(result["sub_value"], serde_json::json!(7));
383 }
384
385 #[tokio::test]
387 async fn variant_pkg_overrides_global_same_name() {
388 let global_tmp = tempfile::tempdir().unwrap();
389 let variant_tmp = tempfile::tempdir().unwrap();
390
391 make_pkg_dir(global_tmp.path(), "my_pkg", "return { value = 1 }");
393 let variant_dir = variant_tmp.path().join("my_pkg");
395 fs::create_dir_all(&variant_dir).unwrap();
396 fs::write(variant_dir.join("init.lua"), "return { value = 2 }").unwrap();
397
398 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
399 .await
400 .unwrap();
401
402 let code = r#"
403 local pkg = require("my_pkg")
404 return pkg.value
405 "#
406 .to_string();
407
408 let result = executor
409 .eval_simple_with_paths(code, vec![], vec![VariantPkg::new("my_pkg", variant_dir)])
410 .await
411 .unwrap();
412
413 assert_eq!(result, serde_json::json!(2));
414 }
415
416 #[tokio::test]
419 async fn extra_lib_paths_priority_over_default() {
420 let global_tmp = tempfile::tempdir().unwrap();
421 let extra_tmp = tempfile::tempdir().unwrap();
422
423 make_pkg_dir(global_tmp.path(), "test_pkg", "return { value = 1 }");
425 let extra_root = make_pkg_dir(extra_tmp.path(), "test_pkg", "return { value = 2 }");
427
428 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
430 .await
431 .unwrap();
432
433 let code = r#"
434 local pkg = require("test_pkg")
435 return pkg.value
436 "#
437 .to_string();
438
439 let result = executor
440 .eval_simple_with_paths(code, vec![extra_root], vec![])
441 .await
442 .unwrap();
443
444 assert_eq!(result, serde_json::json!(2));
446 }
447}