Skip to main content

proof_engine/scripting/
host.rs

1//! Host bridge — exposes Rust functions and objects to the scripting VM.
2//!
3//! `ScriptHost` wraps a `Vm` and provides a clean API for:
4//!   - Registering host functions callable from scripts
5//!   - Binding host objects as script tables
6//!   - Executing scripts and retrieving results
7//!   - Event-driven scripting (hooks / callbacks)
8
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use super::compiler::Compiler;
13use super::parser::Parser;
14use super::stdlib;
15use super::vm::{NativeFunc, ScriptError, Table, Value, Vm};
16
17// ── HostFunction type alias ───────────────────────────────────────────────────
18
19/// A Rust function callable from scripts.
20pub type HostFunction =
21    Arc<dyn Fn(&mut Vm, Vec<Value>) -> Result<Vec<Value>, ScriptError> + Send + Sync>;
22
23// ── ScriptHost ────────────────────────────────────────────────────────────────
24
25/// High-level scripting host. Owns the VM and provides ergonomic registration APIs.
26pub struct ScriptHost {
27    vm:      Vm,
28    modules: HashMap<String, Value>,
29}
30
31impl ScriptHost {
32    /// Create a new ScriptHost with the standard library pre-registered.
33    pub fn new() -> Self {
34        let mut vm = Vm::new();
35        stdlib::register_all(&mut vm);
36        ScriptHost { vm, modules: HashMap::new() }
37    }
38
39    /// Create a sandboxed host with no standard library.
40    pub fn sandboxed() -> Self {
41        ScriptHost { vm: Vm::new(), modules: HashMap::new() }
42    }
43
44    // ── Registration API ──────────────────────────────────────────────────
45
46    /// Register a Rust closure as a global script function.
47    pub fn register<F>(&mut self, name: &str, f: F)
48    where
49        F: Fn(&mut Vm, Vec<Value>) -> Result<Vec<Value>, ScriptError> + Send + Sync + 'static,
50    {
51        self.vm.register_native(name, f);
52    }
53
54    /// Register a simple 1-argument Rust function.
55    pub fn register_fn<F>(&mut self, name: &str, f: F)
56    where
57        F: Fn(Vec<Value>) -> Result<Vec<Value>, ScriptError> + Send + Sync + 'static,
58    {
59        let f = Arc::new(f);
60        self.vm.register_native(name, move |_vm, args| f(args));
61    }
62
63    /// Set a global variable.
64    pub fn set(&mut self, name: &str, value: Value) {
65        self.vm.set_global(name, value);
66    }
67
68    /// Get a global variable.
69    pub fn get(&self, name: &str) -> Value {
70        self.vm.get_global(name)
71    }
72
73    /// Expose a Rust table of functions as a named module.
74    ///
75    /// ```ignore
76    /// host.register_module("engine", vec![
77    ///     ("spawn", Arc::new(|_vm, args| { ... })),
78    /// ]);
79    /// ```
80    pub fn register_module(&mut self, name: &str, funcs: Vec<(&str, HostFunction)>) {
81        let table = Table::new();
82        for (fname, f) in funcs {
83            let fname = fname.to_string();
84            let func_arc = Arc::clone(&f);
85            table.rawset_str(&fname, Value::NativeFunction(Arc::new(NativeFunc {
86                name: format!("{}.{}", name, fname),
87                func: Box::new(move |vm, args| (func_arc)(vm, args)),
88            })));
89        }
90        let v = Value::Table(table.clone());
91        self.vm.set_global(name, v.clone());
92        self.modules.insert(name.to_string(), v);
93    }
94
95    // ── Execution API ─────────────────────────────────────────────────────
96
97    /// Execute a script string, returning all return values.
98    pub fn exec(&mut self, source: &str) -> Result<Vec<Value>, ScriptError> {
99        let script = Parser::from_source("<inline>", source)
100            .map_err(|e| ScriptError::new(e.to_string()))?;
101        let chunk = Compiler::compile_script(&script);
102        self.vm.execute(chunk)
103    }
104
105    /// Execute a named script (for better error messages).
106    pub fn exec_named(&mut self, name: &str, source: &str) -> Result<Vec<Value>, ScriptError> {
107        let script = Parser::from_source(name, source)
108            .map_err(|e| ScriptError::new(e.to_string()))?;
109        let chunk = Compiler::compile_script(&script);
110        self.vm.execute(chunk)
111    }
112
113    /// Call a previously defined script function by name.
114    pub fn call(&mut self, func_name: &str, args: Vec<Value>) -> Result<Vec<Value>, ScriptError> {
115        let func = self.vm.get_global(func_name);
116        self.vm.call(func, args)
117    }
118
119    /// Call a method on a table: `table_name.method_name(args)`.
120    pub fn call_method(
121        &mut self,
122        table_name: &str,
123        method_name: &str,
124        args: Vec<Value>,
125    ) -> Result<Vec<Value>, ScriptError> {
126        let table = self.vm.get_global(table_name);
127        match &table {
128            Value::Table(t) => {
129                let method = t.rawget_str(method_name);
130                self.vm.call(method, args)
131            }
132            other => Err(ScriptError::new(format!(
133                "call_method: {} is not a table (got {})", table_name, other.type_name()
134            ))),
135        }
136    }
137
138    // ── Convenience getters ───────────────────────────────────────────────
139
140    /// Get a global as an integer.
141    pub fn get_int(&self, name: &str) -> Option<i64> {
142        self.vm.get_global(name).to_int()
143    }
144
145    /// Get a global as a float.
146    pub fn get_float(&self, name: &str) -> Option<f64> {
147        self.vm.get_global(name).to_float()
148    }
149
150    /// Get a global as a string.
151    pub fn get_string(&self, name: &str) -> Option<String> {
152        self.vm.get_global(name).to_str_repr()
153    }
154
155    /// Get a global as a boolean.
156    pub fn get_bool(&self, name: &str) -> Option<bool> {
157        match self.vm.get_global(name) {
158            Value::Bool(b) => Some(b),
159            _ => None,
160        }
161    }
162
163    // ── Captured output ───────────────────────────────────────────────────
164
165    /// Drain all lines produced by `print()` calls.
166    pub fn drain_output(&mut self) -> Vec<String> {
167        std::mem::take(&mut self.vm.output)
168    }
169
170    /// Access the underlying VM directly.
171    pub fn vm(&mut self) -> &mut Vm {
172        &mut self.vm
173    }
174}
175
176impl Default for ScriptHost {
177    fn default() -> Self {
178        ScriptHost::new()
179    }
180}
181
182// ── EventBus ──────────────────────────────────────────────────────────────────
183
184/// Simple event system for triggering script callbacks.
185pub struct EventBus {
186    handlers: HashMap<String, Vec<String>>, // event_name -> [script function names]
187}
188
189impl EventBus {
190    pub fn new() -> Self {
191        EventBus { handlers: HashMap::new() }
192    }
193
194    /// Register a script function to handle a named event.
195    pub fn on(&mut self, event: &str, func_name: &str) {
196        self.handlers
197            .entry(event.to_string())
198            .or_default()
199            .push(func_name.to_string());
200    }
201
202    /// Fire an event, calling all registered handlers.
203    pub fn emit(
204        &self,
205        event: &str,
206        host: &mut ScriptHost,
207        args: Vec<Value>,
208    ) -> Result<(), ScriptError> {
209        if let Some(handlers) = self.handlers.get(event) {
210            for func_name in handlers {
211                host.call(func_name, args.clone())?;
212            }
213        }
214        Ok(())
215    }
216
217    /// Remove all handlers for an event.
218    pub fn clear(&mut self, event: &str) {
219        self.handlers.remove(event);
220    }
221}
222
223// ── ScriptObject ──────────────────────────────────────────────────────────────
224
225/// A Rust-owned object exposed to scripts via a table interface.
226pub trait ScriptObject: Send + Sync {
227    fn script_type_name(&self) -> &'static str;
228    fn get_field(&self, name: &str) -> Value;
229    fn set_field(&mut self, name: &str, value: Value);
230    fn call_method(&mut self, name: &str, args: Vec<Value>) -> Result<Vec<Value>, ScriptError>;
231}
232
233/// Wrap a `ScriptObject` as a script table. Changes to the table are NOT
234/// reflected back to the Rust object — use `bind_object` for two-way binding.
235pub fn object_to_table<O: ScriptObject>(obj: &O) -> Table {
236    let t = Table::new();
237    // Expose type name
238    t.rawset_str("__type", Value::Str(Arc::new(obj.script_type_name().to_string())));
239    t
240}
241
242// ── ScriptComponent ──────────────────────────────────────────────────────────
243
244/// A script component: holds a pre-compiled chunk + per-instance table.
245pub struct ScriptComponent {
246    source:  String,
247    globals: Table,
248}
249
250impl ScriptComponent {
251    pub fn new(source: impl Into<String>) -> Self {
252        ScriptComponent {
253            source:  source.into(),
254            globals: Table::new(),
255        }
256    }
257
258    /// Run the script body, populating the component's globals table.
259    pub fn init(&mut self, host: &mut ScriptHost) -> Result<(), ScriptError> {
260        // Expose 'self' table to the script
261        host.set("self", Value::Table(self.globals.clone()));
262        host.exec_named("<component>", &self.source)?;
263        Ok(())
264    }
265
266    /// Call a method defined by this component's script.
267    pub fn call(&mut self, host: &mut ScriptHost, method: &str, args: Vec<Value>) -> Result<Vec<Value>, ScriptError> {
268        let func = self.globals.rawget_str(method);
269        if matches!(func, Value::Nil) {
270            return Ok(vec![]);
271        }
272        host.vm().call(func, args)
273    }
274
275    pub fn table(&self) -> &Table { &self.globals }
276}
277
278// ── Tests ─────────────────────────────────────────────────────────────────────
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_host_exec() {
286        let mut host = ScriptHost::new();
287        let result = host.exec("return 1 + 2").unwrap();
288        assert_eq!(result[0], Value::Int(3));
289    }
290
291    #[test]
292    fn test_host_set_get() {
293        let mut host = ScriptHost::new();
294        host.set("x", Value::Int(99));
295        let result = host.exec("return x").unwrap();
296        assert_eq!(result[0], Value::Int(99));
297    }
298
299    #[test]
300    fn test_host_register_fn() {
301        let mut host = ScriptHost::new();
302        host.register("double", |_vm, args| {
303            let n = args.first().and_then(|v| v.to_int()).unwrap_or(0);
304            Ok(vec![Value::Int(n * 2)])
305        });
306        let result = host.exec("return double(21)").unwrap();
307        assert_eq!(result[0], Value::Int(42));
308    }
309
310    #[test]
311    fn test_host_call_function() {
312        let mut host = ScriptHost::new();
313        host.exec("function greet(name) return \"Hello, \" .. name end").unwrap();
314        let result = host.call("greet", vec![Value::Str(Arc::new("World".to_string()))]).unwrap();
315        assert!(matches!(&result[0], Value::Str(s) if s.as_ref() == "Hello, World"));
316    }
317
318    #[test]
319    fn test_host_print_capture() {
320        let mut host = ScriptHost::new();
321        host.exec("print(\"hello\")").unwrap();
322        let out = host.drain_output();
323        assert_eq!(out, vec!["hello"]);
324    }
325
326    #[test]
327    fn test_event_bus() {
328        let mut host = ScriptHost::new();
329        let mut bus  = EventBus::new();
330        host.exec("function on_tick() end").unwrap();
331        bus.on("tick", "on_tick");
332        assert!(bus.emit("tick", &mut host, vec![]).is_ok());
333    }
334
335    #[test]
336    fn test_register_module() {
337        let mut host = ScriptHost::new();
338        let add_fn: HostFunction = Arc::new(|_vm, args| {
339            let a = args.first().and_then(|v| v.to_int()).unwrap_or(0);
340            let b = args.get(1).and_then(|v| v.to_int()).unwrap_or(0);
341            Ok(vec![Value::Int(a + b)])
342        });
343        host.register_module("mymod", vec![("add", add_fn)]);
344        let result = host.exec("return mymod.add(3, 4)").unwrap();
345        assert_eq!(result[0], Value::Int(7));
346    }
347
348    #[test]
349    fn test_sandboxed_no_stdlib() {
350        let mut host = ScriptHost::sandboxed();
351        // math shouldn't be available
352        let result = host.exec("return type(math)");
353        // Either an error or nil is acceptable
354        match result {
355            Ok(r)  => assert!(matches!(&r[0], Value::Str(s) if s.as_ref() == "nil")),
356            Err(_) => {} // also fine
357        }
358    }
359}