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}