README.md

# 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
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/1` builds a sandbox surface — `(scheme base)` is pre-imported (so `string-append`, `lambda`, `*` are in scope without an explicit `(import ...)`), plus an anonymous host library exposing `shout` as a Scheme procedure backed by an Elixir function.
- `Schooner.eval/2` returns `{:ok, value}` on success. `Schooner.Host.to_string!/2` extracts the underlying binary; `Schooner.Host.string/1` constructs a Scheme string for the return.
- `Schooner.apply/2` invokes any Scheme procedure value (closure, primitive, or parameter) from Elixir.

See the [Embedding](https://hexdocs.pm/schooner/embedding.html) and [Host Functions](https://hexdocs.pm/schooner/host-functions.html) guides for the full story.

## Installation

Add `schooner` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:schooner, "~> 0.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](guides/embedding.md)
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](guides/deviations.md) 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           | `if`, `let`, `cond`'s `=>`, etc. cannot be lexically rebound as ordinary variables. The expander dispatches them on the literal symbol before consulting the lexical environment. |
| Macro hygiene                | `(syntax-rules <id> () ...)` custom-ellipsis identifier and `define-syntax` introduced by another macro template are not supported.                                                                          |
| `define-syntax` placement    | Top-level only — a `define-syntax` inside a `(let () ...)` body 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](guides/host-functions.md)). |
| 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. |