Skip to main content

sema/
lib.rs

1//! Sema — a Lisp with LLM primitives.
2//!
3//! This module provides a clean embedding API for the Sema interpreter.
4//!
5//! # Quick Start
6//!
7//! ```no_run
8//! use sema::{Interpreter, InterpreterBuilder, Value};
9//!
10//! let interp = InterpreterBuilder::new().build();
11//! let result = interp.eval_str("(+ 1 2)").unwrap();
12//! assert_eq!(result, Value::int(3));
13//! ```
14
15use std::rc::Rc;
16
17// Re-export core types.
18pub use sema_core::{intern, resolve, with_resolved, Caps, Env, Sandbox, SemaError, Value};
19/// Result of evaluating a Sema expression.
20pub type EvalResult = Result<Value>;
21
22pub type Result<T> = std::result::Result<T, SemaError>;
23
24/// Builder for configuring and constructing an [`Interpreter`].
25///
26/// By default, both the standard library and LLM builtins are enabled.
27pub struct InterpreterBuilder {
28    stdlib: bool,
29    llm: bool,
30    sandbox: Sandbox,
31}
32
33impl Default for InterpreterBuilder {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl InterpreterBuilder {
40    /// Create a new builder.
41    pub fn new() -> Self {
42        Self {
43            stdlib: true,
44            llm: true,
45            sandbox: Sandbox::allow_all(),
46        }
47    }
48
49    /// Enable or disable the standard library (default: `true`).
50    pub fn with_stdlib(mut self, enable: bool) -> Self {
51        self.stdlib = enable;
52        self
53    }
54
55    /// Enable or disable the LLM builtins (default: `true`).
56    pub fn with_llm(mut self, enable: bool) -> Self {
57        self.llm = enable;
58        self
59    }
60
61    /// Set the sandbox configuration to restrict dangerous operations.
62    pub fn with_sandbox(mut self, sandbox: Sandbox) -> Self {
63        self.sandbox = sandbox;
64        self
65    }
66
67    /// Restrict file operations to the given directories.
68    pub fn with_allowed_paths(mut self, paths: Vec<std::path::PathBuf>) -> Self {
69        self.sandbox = self.sandbox.with_allowed_paths(paths);
70        self
71    }
72
73    /// Disable the standard library.
74    pub fn without_stdlib(self) -> Self {
75        self.with_stdlib(false)
76    }
77
78    /// Disable the LLM builtins.
79    pub fn without_llm(self) -> Self {
80        self.with_llm(false)
81    }
82
83    /// Build the [`Interpreter`] with the configured options.
84    pub fn build(self) -> Interpreter {
85        sema_llm::builtins::reset_runtime_state();
86
87        let env = Env::new();
88        let ctx = sema_eval::EvalContext::new();
89
90        sema_core::set_eval_callback(&ctx, sema_eval::eval_value);
91        sema_core::set_call_callback(&ctx, sema_eval::call_value);
92
93        if self.stdlib {
94            sema_stdlib::register_stdlib(&env, &self.sandbox);
95        }
96
97        if self.llm {
98            sema_llm::builtins::register_llm_builtins(&env, &self.sandbox);
99            sema_llm::builtins::set_eval_callback(sema_eval::eval_value);
100        }
101
102        Interpreter {
103            inner: sema_eval::Interpreter {
104                global_env: Rc::new(env),
105                ctx,
106            },
107        }
108    }
109}
110
111/// A Sema Lisp interpreter instance.
112///
113/// Use [`InterpreterBuilder`] for fine-grained control, or call
114/// [`Interpreter::new`] for a default interpreter with stdlib enabled.
115pub struct Interpreter {
116    inner: sema_eval::Interpreter,
117}
118
119impl Default for Interpreter {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl Interpreter {
126    pub fn new() -> Self {
127        InterpreterBuilder::new().build()
128    }
129
130    /// Create an [`InterpreterBuilder`] for fine-grained configuration.
131    pub fn builder() -> InterpreterBuilder {
132        InterpreterBuilder::new()
133    }
134
135    /// Evaluate a single parsed [`Value`] expression.
136    ///
137    /// Definitions (`define`) persist across calls.
138    pub fn eval(&self, expr: &Value) -> EvalResult {
139        self.inner.eval_in_global(expr)
140    }
141
142    /// Parse and evaluate a string containing one or more Sema expressions.
143    ///
144    /// Definitions (`define`) persist across calls, so you can define a
145    /// function in one call and use it in the next.
146    pub fn eval_str(&self, input: &str) -> EvalResult {
147        self.inner.eval_str_in_global(input)
148    }
149
150    /// Register a native function that can be called from Sema code.
151    ///
152    /// # Example
153    ///
154    /// ```no_run
155    /// use sema::{Interpreter, Value, SemaError};
156    ///
157    /// let interp = Interpreter::new();
158    /// interp.register_fn("square", |args: &[Value]| {
159    ///     if let Some(n) = args[0].as_int() {
160    ///         Ok(Value::int(n * n))
161    ///     } else {
162    ///         Err(SemaError::type_error("integer", args[0].type_name()))
163    ///     }
164    /// });
165    /// ```
166    pub fn register_fn<F>(&self, name: &str, f: F)
167    where
168        F: Fn(&[Value]) -> Result<Value> + 'static,
169    {
170        use sema_core::NativeFn;
171
172        let native = NativeFn::simple(name, f);
173        self.inner
174            .global_env
175            .set_str(name, Value::native_fn(native));
176    }
177
178    /// Load and evaluate a `.sema` file.
179    ///
180    /// Definitions persist in the global environment, just like [`eval_str`].
181    ///
182    /// ```no_run
183    /// # use sema::Interpreter;
184    /// let interp = Interpreter::new();
185    /// interp.load_file("prelude.sema").unwrap();
186    /// interp.eval_str("(my-prelude-fn 42)").unwrap();
187    /// ```
188    pub fn load_file(&self, path: impl AsRef<std::path::Path>) -> EvalResult {
189        let path = path.as_ref();
190        let content = std::fs::read_to_string(path)
191            .map_err(|e| SemaError::eval(format!("load_file {}: {e}", path.display())))?;
192        self.eval_str(&content)
193    }
194
195    /// Pre-load a module into the module cache so that `(import "name")`
196    /// resolves without reading from disk.
197    ///
198    /// The `name` is the string users pass to `import`. The `source` is
199    /// evaluated in an isolated module environment, and all top-level
200    /// bindings (or only `export`-ed ones) are cached.
201    ///
202    /// ```no_run
203    /// # use sema::Interpreter;
204    /// let interp = Interpreter::new();
205    /// interp.preload_module("utils", r#"
206    ///     (define (double x) (* x 2))
207    /// "#).unwrap();
208    ///
209    /// interp.eval_str(r#"(import "utils")"#).unwrap();
210    /// interp.eval_str("(double 21)").unwrap(); // => 42
211    /// ```
212    ///
213    /// Use `(module name (export ...) ...)` to control which bindings are visible:
214    ///
215    /// ```no_run
216    /// # use sema::Interpreter;
217    /// let interp = Interpreter::new();
218    /// interp.preload_module("math", r#"
219    ///     (module math (export square)
220    ///       (define (square x) (* x x))
221    ///       (define internal 42))
222    /// "#).unwrap();
223    /// ```
224    pub fn preload_module(&self, name: &str, source: &str) -> Result<()> {
225        use sema_core::resolve;
226        use std::collections::BTreeMap;
227
228        let (exprs, spans) = sema_reader::read_many_with_spans(source)?;
229        self.inner.ctx.merge_span_table(spans);
230
231        // Evaluate in an isolated child env (like a real import does).
232        let module_env = Env::with_parent(self.inner.global_env.clone());
233        self.inner.ctx.clear_module_exports();
234
235        for expr in &exprs {
236            sema_eval::eval_value(&self.inner.ctx, expr, &module_env)?;
237        }
238
239        let declared = self.inner.ctx.take_module_exports();
240
241        // Collect exports: if (export ...) was used, only those; else all bindings.
242        let bindings = module_env.bindings.borrow();
243        let exports: BTreeMap<String, Value> = match declared {
244            Some(names) => names
245                .iter()
246                .filter_map(|n| {
247                    let spur = intern(n);
248                    bindings.get(&spur).map(|v| (n.clone(), v.clone()))
249                })
250                .collect(),
251            None => bindings
252                .iter()
253                .map(|(spur, val)| (resolve(*spur), val.clone()))
254                .collect(),
255        };
256        drop(bindings);
257
258        // Cache under the bare name so `(import "name")` resolves it
259        // before attempting to canonicalize a real file path.
260        let key = std::path::PathBuf::from(name);
261        self.inner.ctx.cache_module(key, exports);
262
263        Ok(())
264    }
265
266    /// Return a reference to the global environment.
267    pub fn global_env(&self) -> &Rc<Env> {
268        &self.inner.global_env
269    }
270
271    pub fn env(&self) -> &Rc<Env> {
272        self.global_env()
273    }
274}