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}