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.

$cargo add sema-lang · npm i @sema-lang/sema

9 sandbox capabilities · in-browser VFS · LSP + DAP built in · CDN-ready

Two targets, one language

Same (sema), different host.

Rust
native binary · real FS · LLM providers
eval · stdlib · LLM
Browser (WASM)
in-memory VFS · fetch() · no build step

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.

main.rsquick start
use sema::{Interpreter, Value};

fn main() -> sema::Result<()> {
    let interp = Interpreter::new();
    let result = interp
        .eval_str("(+ 1 2 3)")?;
    println!("{result}"); // 6
    Ok(())
}
sandboxed.rsbuilder API
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.
SHELLcommand execution
NETWORKHTTP requests
FS_WRITEfile writes
FS_READfile reads
ENV_WRITEenv mutations
ENV_READenv access
PROCESSprocess control
LLMAPI calls
SERIALserial port

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).

register.rsRust closure
// 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()))
});
register.jsJS function
// 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. evalAsync uses fetch() 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) streams println output line-by-line to the main thread in Worker mode.
index.htmlCDN, no build
<!-- 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() and flush() — sync to any remote store.
  • Quota-managed. 1 MB per file, 16 MB total, 256 files. vfsStats() reports usage.
vfs.jsseed + load + persist
// 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.

LSP Server
completionshovergo-to-defrenameformatcode lens
Neovim · Helix · Zed · Emacs · Sublime
DAP Debugger
breakpointsstep in/outstep overlocalsstack tracewatch
VS Code · Helix · Neovim · Emacs
WASM Headless Debugger
debugStartdebugStepIntodebugGetLocalsdebugGetStackTracedebugSetBreakpointsdebugPoll
Browser playground · custom IDEs

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.

JSON / CSV / TOML
Parse and serialize out of the box
HTTP client
Synchronous in Rust, fetch() in WASM
Regex
Pattern matching, no crate to add
File I/O
Real FS in Rust, VFS in browser
LLM primitives
complete, chat, extract, embed, rerank
LSP server
Completions, hover, go-to-def, rename
DAP debugger
Breakpoints, stepping, variable inspection
WASM target
CDN-ready, in-browser VFS, headless debugger
Sandbox
9 capability flags, path restriction

Embed it in 30 seconds.

Two install paths. Same language. Same primitives.

rust$cargo add sema-lang
js$npm i @sema-lang/sema