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::{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        let custom_handle = metrics.custom_handle();
105
106        let (llm_tx, llm_rx) = tokio::sync::mpsc::channel::<LlmRequest>(16);
107
108        let ns = spec.namespace.clone();
109        let lua_ctx = spec.ctx.clone();
110        let lua_code = spec.code.clone();
111
112        // 1. Spawn a dedicated VM for this session.
113        let lib_paths = self.lib_paths.clone();
114        let (session_isle, session_driver) = AsyncIsle::spawn(move |lua| {
115            let mut reg = Registry::new();
116            for path in &lib_paths {
117                if let Ok(resolver) = FsResolver::new(path) {
118                    reg.add(resolver);
119                }
120            }
121            reg.install(lua)?;
122            Ok(())
123        })
124        .await
125        .map_err(|e| format!("Session VM spawn failed: {e}"))?;
126
127        // 2. Setup: register alc.* StdLib, set ctx, load prelude.
128        //    Safe to set globals — this VM is exclusively ours.
129        session_isle
130            .exec(move |lua| {
131                let alc_table = lua.create_table()?;
132                bridge::register(lua, &alc_table, Some(llm_tx), ns, custom_handle)?;
133                lua.globals().set("alc", alc_table)?;
134
135                let ctx_value = lua.to_value(&lua_ctx)?;
136                lua.globals().set("ctx", ctx_value)?;
137
138                lua.load(PRELUDE)
139                    .exec()
140                    .map_err(|e| IsleError::Lua(format!("Prelude load failed: {e}")))?;
141
142                // No need to clear package.loaded — fresh VM.
143
144                Ok("ok".to_string())
145            })
146            .await
147            .map_err(|e| format!("Session setup failed: {e}"))?;
148
149        // 3. Execute user code as a coroutine on the session VM.
150        let wrapped_code = format!("return alc.json_encode((function()\n{lua_code}\nend)())");
151        let exec_task = session_isle.spawn_coroutine_eval(&wrapped_code);
152
153        // Handle no longer needed — all requests have been sent.
154        // The driver keeps the channel alive until the session completes.
155        drop(session_isle);
156
157        Ok(Session::new(llm_rx, exec_task, metrics, session_driver))
158    }
159}