Schooner
An embeddable, sandboxed Scheme interpreter for the BEAM, targeting the
r7rs-small language minus its mutable operations. Schooner is intended as
a scripting layer for Elixir applications: hosts hand a script source
to one of the entry points below, get back an Elixir term, and
resource-bound the work with the standard process tools
(:max_heap_size, Task.shutdown/2).
Quick example
iex> alias Schooner.Host
iex> env =
...> Schooner.Environment.new(
...> pre_imports: [["scheme", "base"]],
...> libraries: [
...> Host.library(
...> primitives: [
...> {"shout", 1, fn [msg] ->
...> text = Host.to_string!(msg, op: "shout")
...> Host.string(String.upcase(text) <> "!")
...> end}
...> ]
...> )
...> ]
...> )
iex> Schooner.eval(~s|(shout (string-append "hello, " "world"))|, env)
{:ok, "HELLO, WORLD!"}
iex> {:ok, double} = Schooner.eval("(lambda (x) (* x 2))", env)
iex> Schooner.apply(double, [21])
{:ok, 42}
What's happening:
Schooner.Environment.new/1builds a sandbox surface —(scheme base)is pre-imported (sostring-append,lambda,*are in scope without an explicit(import ...)), plus an anonymous host library exposingshoutas a Scheme procedure backed by an Elixir function.Schooner.eval/2returns{:ok, value}on success.Schooner.Host.to_string!/2extracts the underlying binary;Schooner.Host.string/1constructs a Scheme string for the return.Schooner.apply/2invokes any Scheme procedure value (closure, primitive, or parameter) from Elixir.
The Embedding and Host Functions guides cover these APIs in detail.
Installation
Add schooner to your list of dependencies in mix.exs:
def deps do
[
{:schooner, "~> 1.0"}
]
end
Documentation is published at https://hexdocs.pm/schooner.
Choosing an entry point
Schooner has two top-level evaluation entry points. They differ in what's in scope when the script starts, and picking the wrong one for untrusted input is a sandbox hole.
| Entry point | Auto-imports | Trust posture | Use for |
|---|---|---|---|
Schooner.run/1 |
injects (import ...) of every shipped standard library when the script declares none |
Not sandbox-safe. Every primitive is in scope. | tests, REPL-style use, your own scripts |
Schooner.eval/2 |
none — bindings come from env and the script's own (import ...) declarations |
Sandbox-safe. The embedder controls the surface. | embedding scripts you do not control |
For untrusted input, use Schooner.eval/2. A script that omits
(import ...) cannot reach any primitive, so the embedder sets the
surface deliberately.
Both functions also have raising bang variants (run!/1, eval!/2,3)
and a richer environment-construction path via
Schooner.Environment.new/1 — see the Embedding
guide.
Deviations from r7rs-small
Schooner targets r7rs-small but deliberately ships a smaller surface.
The table below summarises every intentional gap; conformance tests
under test/conformance/ cover the surface that is shipped, with
each excluded upstream case annotated inline. The
Deviations guide expands every row with a
runnable example and a workaround.
| Area | Schooner |
|---|---|
| Mutation | None. set!, set-car!, set-cdr!, string-set!, vector-set!, bytevector-u8-set!, record mutators, string-fill!/copy!, vector-fill!/copy!, list-set! are not defined. |
| Numeric procedures | Inexact reals are double-precision only. |
| Special-form names | Core special forms (if, lambda, quote, define, etc.) are always the special form at the head of a form, even where a local variable of the same name is in scope. A top-level define does not shadow a macro keyword such as when, and syntax-rules literals such as cond's else and => match by name even when locally bound. |
| Macro hygiene | The (syntax-rules <id> () ...) custom-ellipsis form is not supported; the ellipsis is always .... |
define-syntax placement |
Top-level only — a define-syntax anywhere else, whether written in a body or produced by a macro, is rejected. |
call/cc |
Escape-only. A captured continuation invoked after its dynamic extent has ended raises a Schooner error. Multi-shot continuations and dynamic-wind re-entry are deferred to v2.0. |
| Parameter objects | make-parameter and parameterize are implemented. Calling a parameter with arguments is rejected (Schooner has no mutation, so the implementation-defined "set the parameter's current value" reading is inapplicable); use parameterize instead. |
| Primitive errors | Type / arity / domain errors raised by primitives surface as Schooner.Primitive.Error on the Elixir side and are not catchable from Scheme guard / with-exception-handler. Only Scheme-level (raise ...) / (error ...) enter the handler chain. |
| Libraries shipped (default) | (scheme base), (scheme cxr), (scheme char), (scheme inexact), (scheme complex), (scheme case-lambda), (scheme lazy), (scheme write), (scheme read). |
| Libraries shipped (opt-in) | (scheme time) via Schooner.Time — embedders pass Schooner.Time.library() to Schooner.Environment.new/1. Not in the default registry so the sandbox stays pure unless wall-clock access is deliberately granted. Also a worked example of the embeddable-library pattern (see Host Functions). |
| Libraries omitted | (scheme file), (scheme load), (scheme repl), (scheme process-context), (scheme eval), (scheme r5rs). |
| I/O | No file ports, no string ports beyond what (scheme read) needs internally, no read-line. display / write / newline / write-string are present in the string-port flavour: they return the rendered text instead of writing to a port. |
Tooling
The Tooling guide covers writing, checking and debugging scripts: error locations, a static checker, tracing, backtraces, a REPL and a macro expander.