Skip to main content

algocline_engine/
executor.rs

1//! Lua execution engine.
2//!
3//! Orchestrates StdLib injection and Lua execution for each session:
4//!
5//! 1. **Layer 0** — [`bridge::register`] injects Rust-backed `alc.*` primitives
6//! 2. **Layer 1** — [`PRELUDE`] adds Lua-based combinators (`alc.map`, etc.)
7//! 3. **Layer 2** — [`mlua_pkg::Registry`] makes `require("ucb")` etc.
8//!    resolve from `~/.algocline/packages/`
9//!
10//! ## Execution models
11//!
12//! - **`eval_simple`** — sync eval on a shared VM (no LLM bridge).
13//!   For lightweight ops like reading package metadata.
14//! - **`start_session`** — spawns a **dedicated VM per session**.
15//!   Each session gets an isolated Lua VM so concurrent sessions
16//!   cannot interfere with each other's globals (`alc`, `ctx`).
17//!   `alc.llm()` yields the coroutine, and the VM is cleaned up
18//!   when the session completes or is abandoned.
19
20use std::path::PathBuf;
21
22use algocline_core::{Budget, ExecutionMetrics, ExecutionSpec};
23use mlua::LuaSerdeExt;
24use mlua_isle::{AsyncIsle, AsyncIsleDriver, IsleError};
25use mlua_pkg::{resolvers::FsResolver, Registry};
26
27use crate::bridge;
28use crate::llm_bridge::LlmRequest;
29use crate::session::Session;
30
31/// Layer 1: Prelude combinators (map, reduce, vote, filter).
32/// Embedded at compile time and loaded into every session.
33const PRELUDE: &str = include_str!("prelude.lua");
34
35/// Lua execution engine.
36///
37/// Holds a **shared VM** for lightweight stateless operations (`eval_simple`)
38/// and spawns **per-session VMs** for coroutine-based execution (`start_session`).
39///
40/// Per-session VMs eliminate global namespace pollution between concurrent
41/// sessions — each session's `alc`, `ctx`, and `package.loaded` are fully
42/// isolated.
43pub struct Executor {
44    /// Shared VM for eval_simple (stateless, no session globals).
45    isle: AsyncIsle,
46    _driver: AsyncIsleDriver,
47    /// Package resolver paths, cloned into each per-session VM.
48    lib_paths: Vec<PathBuf>,
49}
50
51impl Executor {
52    pub async fn new(lib_paths: Vec<PathBuf>) -> anyhow::Result<Self> {
53        let paths_for_shared = lib_paths.clone();
54        let (isle, driver) = AsyncIsle::spawn(move |lua| {
55            let mut reg = Registry::new();
56            for path in &paths_for_shared {
57                if let Ok(resolver) = FsResolver::new(path) {
58                    reg.add(resolver);
59                }
60            }
61            reg.install(lua)?;
62            Ok(())
63        })
64        .await?;
65
66        Ok(Self {
67            isle,
68            _driver: driver,
69            lib_paths,
70        })
71    }
72
73    /// Evaluate Lua code without LLM bridge. For lightweight operations
74    /// like reading package metadata.
75    pub async fn eval_simple(&self, code: String) -> Result<serde_json::Value, String> {
76        let task = self.isle.spawn_exec(move |lua| {
77            let result: mlua::Value = lua
78                .load(&code)
79                .eval()
80                .map_err(|e| IsleError::Lua(e.to_string()))?;
81            let json: serde_json::Value = lua
82                .from_value(result)
83                .map_err(|e| IsleError::Lua(e.to_string()))?;
84            serde_json::to_string(&json).map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
85        });
86
87        let json_str = task.await.map_err(|e| e.to_string())?;
88        serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"))
89    }
90
91    /// Start a new Lua execution session on a **dedicated VM**.
92    ///
93    /// Each session gets its own Lua VM (OS thread + mlua instance) so
94    /// concurrent sessions cannot interfere with each other's globals.
95    /// The VM is cleaned up automatically when the session completes or
96    /// is abandoned (all senders drop → channel closes → thread exits).
97    pub async fn start_session(
98        &self,
99        code: String,
100        ctx: serde_json::Value,
101    ) -> Result<Session, String> {
102        let spec = ExecutionSpec::new(code, ctx);
103        let metrics = ExecutionMetrics::new();
104
105        // Extract and apply budget from ctx.budget
106        if let Some(budget) = Budget::from_ctx(&spec.ctx) {
107            metrics.set_budget(budget);
108        }
109
110        let (llm_tx, llm_rx) = tokio::sync::mpsc::channel::<LlmRequest>(16);
111
112        let bridge_config = bridge::BridgeConfig {
113            llm_tx: Some(llm_tx),
114            ns: spec.namespace.clone(),
115            custom_metrics: metrics.custom_metrics_handle(),
116            budget: metrics.budget_handle(),
117            progress: metrics.progress_handle(),
118        };
119        let lua_ctx = spec.ctx.clone();
120        let lua_code = spec.code.clone();
121
122        // 1. Spawn a dedicated VM for this session.
123        let lib_paths = self.lib_paths.clone();
124        let (session_isle, session_driver) = AsyncIsle::spawn(move |lua| {
125            let mut reg = Registry::new();
126            for path in &lib_paths {
127                if let Ok(resolver) = FsResolver::new(path) {
128                    reg.add(resolver);
129                }
130            }
131            reg.install(lua)?;
132            Ok(())
133        })
134        .await
135        .map_err(|e| format!("Session VM spawn failed: {e}"))?;
136
137        // 2. Setup: register alc.* StdLib, set ctx, load prelude.
138        //    Safe to set globals — this VM is exclusively ours.
139        session_isle
140            .exec(move |lua| {
141                let alc_table = lua.create_table()?;
142                bridge::register(lua, &alc_table, bridge_config)?;
143                lua.globals().set("alc", alc_table)?;
144
145                let ctx_value = lua.to_value(&lua_ctx)?;
146                lua.globals().set("ctx", ctx_value)?;
147
148                lua.load(PRELUDE)
149                    .exec()
150                    .map_err(|e| IsleError::Lua(format!("Prelude load failed: {e}")))?;
151
152                // No need to clear package.loaded — fresh VM.
153
154                Ok("ok".to_string())
155            })
156            .await
157            .map_err(|e| format!("Session setup failed: {e}"))?;
158
159        // 3. Execute user code as a coroutine on the session VM.
160        let wrapped_code = format!("return alc.json_encode((function()\n{lua_code}\nend)())");
161        let exec_task = session_isle.spawn_coroutine_eval(&wrapped_code);
162
163        // Handle no longer needed — all requests have been sent.
164        // The driver keeps the channel alive until the session completes.
165        drop(session_isle);
166
167        Ok(Session::new(llm_rx, exec_task, metrics, session_driver))
168    }
169}