Feature·Runtime·Embedding
Put a Lisp in your app.
Embed Sema in Rust as a native scripting engine, or in JavaScript via WASM in the browser. Same language, two host targets. Sandboxed when you need it, scriptable by design, with LLM primitives built in.
9 sandbox capabilities · in-browser VFS · LSP + DAP built in · CDN-ready
Two targets, one language
Same (sema), different host.
Rust embedding
Five lines to a working interpreter.
Interpreter::new() gives you the full stdlib and LLM support. eval_str() parses and evaluates. That's it. Use the builder for finer control — toggle stdlib, toggle LLM, configure the sandbox, restrict file paths.
use sema::{Interpreter, Value}; fn main() -> sema::Result<()> { let interp = Interpreter::new(); let result = interp .eval_str("(+ 1 2 3)")?; println!("{result}"); // 6 Ok(()) }
use sema::{Interpreter, Sandbox, Caps}; let interp = Interpreter::builder() .without_llm() .with_sandbox(Sandbox::deny( Caps::SHELL .union(Caps::NETWORK) .union(Caps::FS_WRITE) )) .build(); interp.eval_str("(+ 1 2)")?; // => 3 (always works) interp.eval_str(r#"(shell "ls")"#)?; // => PermissionDenied
- Builder API.
.with_stdlib(),.without_llm(),.with_sandbox(),.with_allowed_paths()— only what you need. - Multiple interpreters. Each has fully isolated state — module cache, call stack, environment. Spin up as many as you want on the same thread.
- Preload modules.
interp.preload_module("utils", source)— inject virtual modules without a filesystem.
Sandboxing
Nine capabilities. Deny what you don't need.
Sandboxed functions remain discoverable and tab-completable — they return PermissionDenied when invoked. Path-restricted file operations confine I/O to specific directories with .. traversal protection.
- STRICT preset. Denies shell, fs-write, network, env-write, process, LLM, serial. Allows reads.
- Path restriction.
.with_allowed_paths(vec!["./workspace"])— canonicalizes and rejects traversal. - Graceful degradation. Denied functions are still visible — scripts can detect and adapt with
try/catch.
Native function registration
Expose your functions to the script.
Register Rust closures or JavaScript functions — Sema scripts call them like any other function. Automatic value conversion both ways. Capture state in Rc<RefCell<T>> (Rust) or closures (JS).
// Simple function interp.register_fn("add1", |args| { let n = args[0].as_int() .ok_or_else(|| { SemaError::type_error( "int", args[0].type_name()) })?; Ok(Value::Int(n + 1)) }); // Captured state let counter = Rc::new( RefCell::new(0_i64)); let c = counter.clone(); interp.register_fn("inc!", move |_| { *c.borrow_mut() += 1; Ok(Value::Int(*c.borrow())) });
// Simple function interp.registerFunction( 'add1', (n) => n + 1); interp.evalGlobal( '(add1 41)').value; // => "42" // Returning structured data interp.registerFunction( 'get-user', (id) => { return JSON.stringify({ name: "Alice", age: 30 }); }); interp.evalGlobal( '(:name (get-user 1))' ).value; // => "Alice"
Browser & WASM
CDN-ready. No build step.
Import from a CDN in a <script> tag. new SemaInterpreter() — that's it. evalGlobal for persistent definitions, evalAsync for HTTP via fetch().
- Async HTTP.
evalAsyncusesfetch()with a replay-with-cache strategy. Works on the main thread. - 10M step limit. Infinite loops can't freeze the browser tab. Web Worker mode gets real blocking via
Atomics.wait. - Live output streaming.
setOutputSink(fn)streamsprintlnoutput line-by-line to the main thread in Worker mode.
<!-- 30-second setup --> <script type="module"> import init, { SemaInterpreter } from 'https://cdn.jsdelivr.net/ npm/@sema-lang/sema-wasm/ sema_wasm.js'; await init('...sema_wasm_bg.wasm'); const interp = new SemaInterpreter(); const result = interp.evalGlobal( '(+ 40 2)'); console.log(result.value); // => "42" // Errors carry stack traces if (result.error) { console.error(result.error); } </script>
Virtual filesystem
Files that survive a reload.
The in-browser VFS lets scripts use file/read and file/write normally. Seed files from JavaScript, let scripts read them. Four built-in persistence backends, or plug in your own — S3, a REST API, a service worker cache.
- Four backends. Memory (ephemeral), LocalStorage (per-origin), SessionStorage (per-tab), IndexedDB (hundreds of MB).
- Custom backend interface. Implement
hydrate()andflush()— sync to any remote store. - Quota-managed. 1 MB per file, 16 MB total, 256 files.
vfsStats()reports usage.
// Seed files from JS sema.writeFile("/lib/math.sema", "(define (square x) (* x x))"); sema.writeFile("/main.sema", '(import "/lib/math") (square 7)'); // Script loads from VFS sema.evalGlobal('(load "/main.sema")'); // => 49 // Persist across reloads import { SemaInterpreter, IndexedDBBackend } from "@sema-lang/sema"; const sema = await SemaInterpreter.create({ vfs: new IndexedDBBackend({ namespace: "my-project" }) });
Developer tooling
LSP, DAP, and a debugger in the box.
The language server and debug adapter ship with the binary — no external plugins to install. Any editor that speaks LSP/DAP gets full IDE support. The WASM bindings expose a headless debugger for in-browser stepping, breakpoints, and variable inspection.
What's in the box
One dependency. Everything included.
The stdlib ships with JSON, HTTP, regex, file I/O, crypto, and more. LLM primitives are built in. LSP and DAP ship with the binary. The WASM target brings it to the browser. No extra dependencies to evaluate, wire up, or maintain.
Embed it in 30 seconds.
Two install paths. Same language. Same primitives.