# `Schooner`
[🔗](https://github.com/ausimian/schooner/blob/1.1.0/lib/schooner.ex#L1)

An embeddable, sandboxed Scheme interpreter for the BEAM, targeting
the r7rs-small language minus its mutable operations.

The pipeline is Lexer → Reader → Expander → Eval. The
`Schooner.Value` module provides the tagged-term value model and
`Schooner.Env` the immutable-with-mutable-globals runtime
environment. Macro bindings live in a separate expansion-time
syntax env threaded through expansion only.

## Choosing an entry point

Schooner's entry points come in pairs following the Elixir
convention: a tagged-tuple form (`run/1`, `eval/2,3`) and a bang
form (`run!/1`, `eval!/2,3`) that raises on failure. Choosing
between `run*` and `eval*` is a **trust decision**, and the names
can mislead: `eval*` is the sandbox-safe family, `run*` the
permissive one.

| Family        | Auto-imports                                                                              | Trust posture                                       | Use for                                                            |
| ---           | ---                                                                                       | ---                                                 | --- |
| `run/1`/`run!/1`     | injects `(import ...)` of every shipped standard library when the script declares none | **Not sandbox-safe.** Every shipped primitive is in scope by default. | tests, REPL-style use, your own scripts where you control the source |
| `eval/2,3`/`eval!/2,3` | none — bindings come exclusively from the `env` argument and the script's own `(import ...)` declarations | **Sandbox-safe.** The embedder controls the surface. | embedding untrusted or semi-trusted scripts |

`run`/`run!` evaluate against a fresh `Schooner.Env` with the
`implicit_imports: :all` option of `eval/3` and `eval!/3`. To get
the same implicit imports against your own `Schooner.Env`, call
`eval/3` or `eval!/3` with that option directly.

Within each family, the bang form raises one of:

  * `Schooner.Error` — uncaught Scheme `(raise ...)` in the script
  * `Schooner.Eval.Error` — runtime evaluation failure
  * `Schooner.Primitive.Error` — primitive type / arity / domain error
  * `Schooner.Library.NotFoundError` — `(import ...)` of a missing library
  * `Schooner.Lexer.Error`, `Schooner.Reader.Error`,
    `Schooner.Expander.Error` — source-level failures

The non-bang form returns `{:ok, value}` on success or
`{:error, exception}` for any of the above. `ArgumentError`
raised by malformed options is **not** caught — that is an
embedder bug, not a script-level failure.

### Embedding untrusted code

Use `eval/2` (or `eval!/2`) and require the script to declare
exactly which libraries it needs. A script that omits
`(import ...)` cannot reach any primitive — even `+` is unbound:

    iex> Schooner.eval!("(import (only (scheme base) +)) (+ 1 2)", Schooner.Env.new())
    3

Pair this with BEAM-level resource limits — run `eval!/2` inside
a spawned process with `:max_heap_size` and a `Task.shutdown/2`
timeout so a runaway script cannot exhaust the host. Build the env
inside that process: an env can only be used by the process that
created it. See the Running Untrusted Scheme guide.

For richer sandbox composition (registering host libraries,
pre-imports, etc.), construct a `Schooner.Environment` via
`Schooner.Environment.new/1` and pass it to `eval/2`.

# `apply`

```elixir
@spec apply(Schooner.Value.t(), [Schooner.Value.t()]) ::
  {:ok, Schooner.Value.t()} | {:error, Exception.t()}
```

Invoke a Scheme procedure value from Elixir. Returns
`{:ok, value}` on success, `{:error, exception}` for any
script-level failure. Use `apply!/2` for the raising variant.

`proc` may be any procedure value — a closure (returned by
evaluating a `lambda` or a `define` form), a primitive, or a
parameter. `args` is a list of `t:Schooner.Value.t/0` arguments.

This is the host-side hook for callback patterns: pass a Scheme
procedure into a host function, capture it, and invoke it later
via `apply/2`.

# `apply!`

```elixir
@spec apply!(Schooner.Value.t(), [Schooner.Value.t()]) :: Schooner.Value.t()
```

Bang form of `apply/2` — raises on script-level failure. See
`apply/2` for arguments.

# `compile`

```elixir
@spec compile(binary()) :: {:ok, Schooner.Compiled.t()} | {:error, Exception.t()}
```

Compile `source` against a fresh `Schooner.Environment.new/0`, in
which every shipped standard library is available to import. See
`compile/2`.

# `compile`

```elixir
@spec compile(binary(), Schooner.Environment.t()) ::
  {:ok, Schooner.Compiled.t()} | {:error, Exception.t()}
```

Read, expand, and pre-resolve `source` against `env_struct`'s
registry. Returns `{:ok, %Schooner.Compiled{}}` on success,
`{:error, exception}` on any source-level failure. Use
`compile!/1,2` for the raising variant.

The compiled artifact can be passed to `run_compiled/2`
repeatedly against any compatible environment. Macros are
expanded at compile time; variable bindings from `(import ...)`
declarations are pre-resolved and baked into the artifact.

# `compile!`

```elixir
@spec compile!(binary()) :: Schooner.Compiled.t()
```

Bang form of `compile/1` — raises on source-level failure.

# `compile!`

```elixir
@spec compile!(binary(), Schooner.Environment.t()) :: Schooner.Compiled.t()
```

Bang form of `compile/2` — raises on source-level failure.

# `eval`

```elixir
@spec eval(binary(), Schooner.Env.t() | Schooner.Environment.t()) ::
  {:ok, Schooner.Value.t()} | {:error, Exception.t()}
```

Read and evaluate `source` against `env`, threading top-level
definitions and any explicit `(import ...)` bindings into it.

Returns `{:ok, value}` on success, `{:error, exception}` for
any script-level failure. Use `eval!/2` for the raising variant.

`env` may be either a `Schooner.Env` (low-level) or a
`Schooner.Environment` (the embedding-friendly bundle of env +
registry + syntax env produced by `Schooner.Environment.new/1`).
When given an `Env`, no implicit imports are added — bindings
come exclusively from `env` and the script's own `(import ...)`
declarations.

This is the strict path and the right entry point for scripts
whose source the host does not control. See
"Choosing an entry point" in the moduledoc.

# `eval`

```elixir
@spec eval(binary(), Schooner.Env.t(), keyword()) ::
  {:ok, Schooner.Value.t()} | {:error, Exception.t()}
```

Read and evaluate `source` against `env` with `opts`. Returns
`{:ok, value}` on success, `{:error, exception}` for any
script-level failure. Use `eval!/3` for the raising variant.

Options match `eval!/3`. The `Environment` overload of `eval/2`
does not accept options — pre-imports and library composition
are baked into the `Environment` at construction time.

# `eval!`

```elixir
@spec eval!(binary(), Schooner.Env.t() | Schooner.Environment.t()) ::
  Schooner.Value.t()
```

Bang form of `eval/2` — raises on script-level failure.

# `eval!`

```elixir
@spec eval!(binary(), Schooner.Env.t(), keyword()) :: Schooner.Value.t()
```

Bang form of `eval/3` — raises on script-level failure.

Options:

  * `:implicit_imports` — controls implicit imports prepended
    to a script that declares none of its own.
      * `:none` (default) — no implicit imports. Bindings come
        exclusively from `env` and the script's own
        `(import ...)` declarations. Use this for untrusted
        input.
      * `:all` — implicitly import every shipped standard
        library. Skipped if the script declares any
        `(import ...)` of its own: an explicit import means
        the script has chosen a narrower surface.

# `run`

```elixir
@spec run(binary()) :: {:ok, Schooner.Value.t()} | {:error, Exception.t()}
```

Read and evaluate `source` in a fresh empty env, with every shipped
standard library implicitly imported when the script declares no
imports of its own.

Returns `{:ok, value}` on success, `{:error, exception}` for
any script-level failure. Use `run!/1` for the raising variant.

**Not for untrusted input.** A script with no `(import ...)`
form reaches every primitive Schooner ships with. Use `eval/2`
for embedding scripts you do not control — see "Choosing an
entry point" in the moduledoc.

# `run!`

```elixir
@spec run!(binary()) :: Schooner.Value.t()
```

Bang form of `run/1` — raises on script-level failure.

# `run_compiled`

```elixir
@spec run_compiled(Schooner.Compiled.t(), Schooner.Environment.t()) ::
  {:ok, Schooner.Value.t()} | {:error, Exception.t()}
```

Evaluate a `%Schooner.Compiled{}` against `env_struct`. Returns
`{:ok, value}` on success, `{:error, exception}` for any
script-level failure. Use `run_compiled!/2` for the raising
variant.

The compiled program's pre-resolved variable bindings are
re-applied to `env_struct.env` on every call, so the
`(import ...)` surface the program was compiled against is
always in scope regardless of `env_struct`'s registry. Macros
are not re-expanded — the program's macro shape is frozen at
compile time.

# `run_compiled!`

```elixir
@spec run_compiled!(Schooner.Compiled.t(), Schooner.Environment.t()) ::
  Schooner.Value.t()
```

Bang form of `run_compiled/2` — raises on script-level failure.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
