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