Skip to main content

ion_core/
engine.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::error::IonError;
5use crate::host_types::{HostEnumDef, HostStructDef, IonType, IonTypeDef};
6use crate::interpreter::{Interpreter, Limits};
7use crate::lexer::Lexer;
8use crate::module::Module;
9use crate::parser::Parser;
10use crate::stdlib::OutputHandler;
11use crate::value::Value;
12
13/// The public embedding API for the Ion interpreter.
14pub struct Engine {
15    interpreter: Interpreter,
16    output: Arc<dyn OutputHandler>,
17}
18
19impl Engine {
20    pub fn new() -> Self {
21        let output = crate::stdlib::missing_output_handler();
22        Self {
23            interpreter: Interpreter::with_output(Arc::clone(&output)),
24            output,
25        }
26    }
27
28    /// Create an engine with a host-provided output handler for `io::print*`.
29    pub fn with_output<H>(output: H) -> Self
30    where
31        H: OutputHandler + 'static,
32    {
33        Self::with_output_handler(Arc::new(output))
34    }
35
36    /// Create an engine with a shared host-provided output handler.
37    pub fn with_output_handler(output: Arc<dyn OutputHandler>) -> Self {
38        Self {
39            interpreter: Interpreter::with_output(Arc::clone(&output)),
40            output,
41        }
42    }
43
44    /// Evaluate a script, returning the last expression's value.
45    pub fn eval(&mut self, source: &str) -> Result<Value, IonError> {
46        let mut lexer = Lexer::new(source);
47        let tokens = lexer.tokenize()?;
48        let mut parser = Parser::new(tokens);
49        let program = parser.parse_program()?;
50        self.interpreter.eval_program(&program)
51    }
52
53    /// Inject a value into the script scope.
54    pub fn set(&mut self, name: &str, value: Value) {
55        self.interpreter.env.define(name.to_string(), value, false);
56    }
57
58    /// Read a variable from the script scope.
59    pub fn get(&self, name: &str) -> Option<Value> {
60        self.interpreter.env.get(name).cloned()
61    }
62
63    /// Get all top-level bindings.
64    pub fn get_all(&self) -> HashMap<String, Value> {
65        self.interpreter.env.top_level()
66    }
67
68    /// Set execution limits.
69    pub fn set_limits(&mut self, limits: Limits) {
70        self.interpreter.limits = limits;
71    }
72
73    /// Set the host output handler used by `io::print`, `io::println`, and
74    /// `io::eprintln`.
75    pub fn set_output<H>(&mut self, output: H)
76    where
77        H: OutputHandler + 'static,
78    {
79        self.set_output_handler(Arc::new(output));
80    }
81
82    /// Set a shared host output handler used by `io::print*`.
83    pub fn set_output_handler(&mut self, output: Arc<dyn OutputHandler>) {
84        self.output = Arc::clone(&output);
85        let io = crate::stdlib::io_module_with_output(output);
86        self.interpreter
87            .env
88            .define(io.name.clone(), io.to_value(), false);
89    }
90
91    /// Register a built-in function.
92    pub fn register_fn(&mut self, name: &str, func: fn(&[Value]) -> Result<Value, String>) {
93        self.interpreter.env.define(
94            name.to_string(),
95            Value::BuiltinFn(name.to_string(), func),
96            false,
97        );
98    }
99
100    /// Register a built-in backed by a closure. Unlike `register_fn`,
101    /// this accepts any `Fn` — including closures that capture
102    /// host-side state such as a `tokio::runtime::Handle`, a database
103    /// pool, or shared counters. See `docs/concurrency.md` for the
104    /// tokio embedding pattern.
105    pub fn register_closure<F>(&mut self, name: &str, func: F)
106    where
107        F: Fn(&[Value]) -> Result<Value, String> + Send + Sync + 'static,
108    {
109        self.interpreter.env.define(
110            name.to_string(),
111            Value::BuiltinClosure(name.to_string(), crate::value::BuiltinClosureFn::new(func)),
112            false,
113        );
114    }
115
116    /// Register a host struct type that scripts can construct and match on.
117    pub fn register_struct(&mut self, def: HostStructDef) {
118        self.interpreter.types.register_struct(def);
119    }
120
121    /// Register a host enum type that scripts can construct and match on.
122    pub fn register_enum(&mut self, def: HostEnumDef) {
123        self.interpreter.types.register_enum(def);
124    }
125
126    /// Register a module that scripts can access via `module::name` or `use module::*`.
127    pub fn register_module(&mut self, module: Module) {
128        let name = module.name.clone();
129        let value = module.to_value();
130        self.interpreter.env.define(name, value, false);
131    }
132
133    /// Register a type via the IonType trait (used with `#[derive(IonType)]`).
134    pub fn register_type<T: IonType>(&mut self) {
135        match T::ion_type_def() {
136            IonTypeDef::Struct(def) => self.interpreter.types.register_struct(def),
137            IonTypeDef::Enum(def) => self.interpreter.types.register_enum(def),
138        }
139    }
140
141    /// Inject a typed Rust value into the script scope.
142    pub fn set_typed<T: IonType>(&mut self, name: &str, value: &T) {
143        self.interpreter
144            .env
145            .define(name.to_string(), value.to_ion(), false);
146    }
147
148    /// Extract a typed Rust value from the script scope.
149    pub fn get_typed<T: IonType>(&self, name: &str) -> Result<T, String> {
150        let val = self.interpreter.env.get(name).ok_or_else(|| {
151            format!(
152                "{}{}{}",
153                ion_str!("variable '"),
154                name,
155                ion_str!("' not found")
156            )
157        })?;
158        T::from_ion(val)
159    }
160
161    /// Evaluate a script via the bytecode VM. Falls back to tree-walk for
162    /// unsupported features (concurrency).
163    #[cfg(feature = "vm")]
164    pub fn vm_eval(&mut self, source: &str) -> Result<Value, IonError> {
165        let mut lexer = Lexer::new(source);
166        let tokens = lexer.tokenize()?;
167        let mut parser = Parser::new(tokens);
168        let program = parser.parse_program()?;
169
170        // Try the bytecode path first
171        let compiler = crate::compiler::Compiler::new();
172        match compiler.compile_program(&program) {
173            Ok((chunk, fn_chunks)) => {
174                let mut vm = crate::vm::Vm::with_env_and_output(
175                    std::mem::take(&mut self.interpreter.env),
176                    Arc::clone(&self.output),
177                );
178                // Pre-populate the VM's function cache with compiled chunks
179                vm.preload_fn_chunks(fn_chunks);
180                // Pass host type registry to VM
181                vm.set_types(self.interpreter.types.clone());
182                let result = vm.execute(&chunk);
183                // Restore env back to interpreter
184                self.interpreter.env = std::mem::take(vm.env_mut());
185                result
186            }
187            Err(_) => {
188                // Compilation failed (unsupported feature) — fall back to tree-walk
189                self.interpreter.eval_program(&program)
190            }
191        }
192    }
193}
194
195impl Default for Engine {
196    fn default() -> Self {
197        Self::new()
198    }
199}