# Host Functions

Schooner ships only pure Scheme primitives by default. Anything
that touches the outside world — logging, reading config, looking
up data, talking to a database — comes from **host functions**:
Elixir functions you register as Scheme procedures via
`Schooner.Host.library/1`.

This guide walks the host-function authoring API: building a
library, the conversion helpers that move between Scheme values
and Elixir terms, foreign payloads for opaque host data, the
callback pattern (Scheme → host → Scheme), and how to map errors
between the two sides.

> **Worked reference:** `Schooner.Time` (in `lib/schooner/time.ex`)
> is the r7rs `(scheme time)` library shipped as an opt-in host
> library. Read it alongside this guide for a concrete, runnable
> example of the patterns described here — a single public
> `library/0` that returns a `%Schooner.Library{}`, a small set of
> primitive impls following the standard ABI, and a sandbox-safe
> canonical name (`["scheme", "time"]`).

## A first host library

```elixir
defmodule MyApp.SchemeLib do
  alias Schooner.Host

  def lib do
    Host.library(
      name: ["myapp", "log"],
      primitives: [
        {"info", 1, &info/1},
        {"error", 1, &error_/1}
      ]
    )
  end

  defp info([msg]) do
    text = Host.to_string!(msg, op: "myapp/log/info")
    Logger.info(text)
    :unspecified
  end

  defp error_([msg]) do
    text = Host.to_string!(msg, op: "myapp/log/error")
    Logger.error(text)
    :unspecified
  end
end
```

The shape of a host primitive is `{name :: binary, arity ::
Schooner.Value.arity_spec, fun :: ([Schooner.Value.t] ->
Schooner.Value.t)}` — the same ABI used by every built-in
primitive Schooner ships with.

To use the library, list it on `Schooner.Environment.new/1`:

```elixir
env = Schooner.Environment.new(libraries: [MyApp.SchemeLib.lib()])
Schooner.eval!(~s|(import (myapp log)) (info "starting")|, env)
```

Because the library has a non-empty name, it is **named** — the
script must `(import (myapp log))` to see its bindings. That's
the sandbox-safe default: the embedder controls the menu, the
script controls which dishes it orders.

## Anonymous libraries (no import needed)

For trivial cases — a single helper, a constant — a host
library with `name: []` is **anonymous**: its bindings are
applied directly to the runtime env at construction time, with
no `(import ...)` required:

```elixir
inject =
  Schooner.Host.library(
    primitives: [
      {"now-ms", 0, fn [] -> System.system_time(:millisecond) end}
    ]
  )

env = Schooner.Environment.new(libraries: [inject])
Schooner.eval!("(now-ms)", env)
# => some integer
```

There is no Scheme syntax that produces an empty library-name
datum, so anonymous libraries are unreachable from script code;
the only path their bindings enter scope is the embedder
listing them. **This is sandbox-loosening** — list anonymous
libraries deliberately.

## Conversion helpers

Schooner does not auto-marshal across the host boundary. Host
functions consume `t:Schooner.Value.t/0` and return
`t:Schooner.Value.t/0`. To go between Scheme values and idiomatic
Elixir terms, use the helpers on `Schooner.Host`.

### Naming convention

- `to_*!/2` — **asserting**: raises `Schooner.Host.TypeError` on
  shape mismatch. Takes a keyword list with `:op` so the error
  points at the host call site. The bang in the name is the
  Elixir convention for "raises rather than returns a tagged
  result".
- `to_*/1` — **total**: returns `{:ok, term} | :error`. Use for
  the "branch on shape" case.

```elixir
# Asserting — the typical case in a host primitive body
text = Host.to_string!(msg, op: "myapp/log/info")

# Total — when a type mismatch is not an error
case Host.to_string(msg) do
  {:ok, text} -> Logger.info(text)
  :error      -> Logger.info(Schooner.Value.write(msg))
end
```

### Avoid `import Schooner.Host`

Several helper names — `Schooner.Host.to_string/1` and
`Schooner.Host.to_string!/2` — shadow `Kernel.to_string/1`.
**Use `alias Schooner.Host`** and call the helpers as
`Host.to_string!(value, op: "...")`. Don't `import` the module.

### Available accessors

| Helper | Accepts | Returns |
| --- | --- | --- |
| `Schooner.Host.to_integer!/2` | bare exact integer | Elixir integer |
| `Schooner.Host.to_float!/2` | bare Elixir float | Elixir float |
| `Schooner.Host.to_real!/2` | integer / float / rational | Elixir number (rationals coerced to float) |
| `Schooner.Host.to_string!/2` | bare binary (Scheme string) | Elixir binary |
| `Schooner.Host.to_symbol_name!/2` | `{:sym, name}` | Elixir binary (the name) |
| `Schooner.Host.to_char!/2` | `{:char, cp}` | non-negative integer codepoint |
| `Schooner.Host.to_bool!/2` | `true \| false` | Elixir bool |
| `Schooner.Host.to_list!/2` | proper Scheme list | Elixir list |
| `Schooner.Host.to_vector!/2` | `{:vector, tuple}` | Elixir list |
| `Schooner.Host.to_bytevector!/2` | `{:bytevector, binary}` | Elixir binary |
| `Schooner.Host.to_foreign_ref!/2` | `{:foreign, term}` | the wrapped host term |
| `Schooner.Host.to_proc!/2` | closure / primitive / parameter | the value unchanged (callable assertion) |

The `Schooner.Host.to_real!/2` vs `Schooner.Host.to_float!/2`
split is deliberate: `to_float!` rejects integers (the host
should say what it wants), `to_real!` is the lenient "give me
whatever number it is" form.

### Constructors

The same module re-exports `Schooner.Value`'s constructors:
`Host.string/1`, `Host.symbol/1`, `Host.list/1`, `Host.vector/1`,
`Host.foreign/1`, `Host.primitive/3`, etc. Use them instead of
naming the internal tag shape directly — that way future
representation changes only touch `Schooner.Value`, not your
host code.

## Foreign payloads

When the host wants to expose an opaque handle — a `pid`, a
`Reference`, an internal struct — wrap it as a **foreign value**:

```elixir
defmodule MyApp.DbLib do
  alias Schooner.Host

  def lib(db_pid) do
    Host.library(
      name: ["myapp", "db"],
      values: [
        {"connection", Host.foreign(db_pid)}
      ],
      primitives: [
        {"query", 2, &query/1}
      ]
    )
  end

  defp query([conn, sql]) do
    pid = Host.to_foreign_ref!(conn, op: "db/query")
    sql_text = Host.to_string!(sql, op: "db/query")
    rows = MyApp.DB.query(pid, sql_text)
    Host.list(Enum.map(rows, &row_to_scheme/1))
  end

  defp row_to_scheme(row), do: # ...
end
```

Foreign values are **opaque to Scheme**: the script can pass
them around (bind them, return them, store them in pairs and
vectors) but cannot inspect or forge them. `write` redacts the
contents to `#<foreign>`. `eq?` / `eqv?` / `equal?` compare the
wrapped Elixir terms with `===`, so two foreigns wrapping the
same pid are equal.

This is the right vehicle for any "host handle a script needs
to reference but shouldn't see inside".

## Callback pattern (Scheme → host → Scheme)

A host function can receive a Scheme procedure as an argument
and invoke it back via `Schooner.apply!/2`:

```elixir
defp map_over_rows([fun, rows_value]) do
  fun = Host.to_proc!(fun, op: "myapp/map-over-rows")
  rows = Host.to_list!(rows_value, op: "myapp/map-over-rows")

  rows
  |> Enum.map(&Schooner.apply!(fun, [&1]))
  |> Host.list()
end
```

Used from Scheme:

```scheme
(import (myapp util))
(map-over-rows (lambda (row) (cons "got" row)) '(1 2 3))
;; => (("got" . 1) ("got" . 2) ("got" . 3))
```

`Schooner.apply!/2` works on any procedure value — closures,
primitives, parameters. The callback runs in the same Elixir
process as the outer eval, so it inherits the current
exception/parameter state. A `with-exception-handler` installed
in the outer script catches an error raised three frames deep
across two host hops.

### Continuation barrier (documentation-only in v1)

A Scheme callback that captures a `call/cc` continuation, then
escapes via that continuation **after the host call has
returned**, is unsupported. Schooner v1's `call/cc` is
escape-only; a continuation invoked outside its dynamic extent
already raises a structured error, and the host-boundary case
fires the same guard. So the rule is:

> Capture-and-escape across the host boundary is undefined and
> raises `Schooner.Eval.Error`. Use `(raise ...)` and
> `with-exception-handler` for callbacks that need long-lived
> non-local exit — exceptions cross the host boundary cleanly
> through the existing handler stack.

This is forward-compatible with v2.0's first-class `call/cc`,
which will turn the rule into a hard runtime barrier.

## Error mapping

Two failure modes cross the boundary in opposite directions.

### Host raises a Scheme-catchable error

When you want the script's `with-exception-handler` / `guard` to
see the error, raise a Scheme exception via `Schooner.Error`:

```elixir
defp query([conn, sql]) do
  pid = Host.to_foreign_ref!(conn, op: "db/query")
  sql_text = Host.to_string!(sql, op: "db/query")

  case MyApp.DB.query(pid, sql_text) do
    {:ok, rows} ->
      Host.list(Enum.map(rows, &row_to_scheme/1))

    {:error, reason} ->
      raise Schooner.Error,
        value:
          Schooner.Value.error_object(
            :user,
            Host.string("db query failed"),
            [Host.foreign(reason)]
          )
  end
end
```

The script catches it like any other:

```scheme
(guard (e ((error-object? e) (handle-failure e)))
  (query connection "SELECT ..."))
```

### Host-side type errors are not script-catchable

`Schooner.Host.TypeError` (raised by `to_*!/2` helpers) and
`Schooner.Primitive.Error` (raised by built-in primitive type /
arity / domain checks) both surface to the **host**, not to the
script. They bubble out of `Schooner.eval!/2` as Elixir
exceptions, or land in the `{:error, _}` arm of `Schooner.eval/2`.

This is deliberate: a sandboxed script must not be able to paper
over its own type errors. If you want a host primitive to
produce a *script-catchable* failure, use the `Schooner.Error`
pattern above; if you want it to be a host-side bug report,
use `Schooner.Host.TypeError` (which is what the assertion
helpers produce automatically).

## Tying it all together

```elixir
defmodule MyApp.Embed do
  alias Schooner.Host

  def env(db_pid) do
    Schooner.Environment.new(
      standard_libraries: :default,
      libraries: [
        MyApp.SchemeLib.lib(),
        MyApp.DbLib.lib(db_pid)
      ],
      pre_imports: [
        ["scheme", "base"],   # define, length, string-append, number->string ...
        ["myapp", "log"]      # info, error
      ]
    )
  end

  def run_rule(source, db_pid) do
    Schooner.eval(source, env(db_pid))
  end
end
```

A script the host hands to `run_rule/2`:

```scheme
(import (myapp db))
(define rows (query connection "SELECT id FROM users WHERE active = TRUE"))
(info (string-append "got " (number->string (length rows)) " rows"))
rows
```

- `define`, `string-append`, `number->string`, `length` are in
  scope without import (pre-imported `(scheme base)`).
- `info` is in scope without import (pre-imported `(myapp log)`).
- `query` and `connection` are in scope after the explicit
  `(import (myapp db))`.

Decide which surface to pre-import based on trust. For untrusted
input you might lock down the standard libraries to a tighter
set (`standard_libraries: [:base, :char]`) so the script can't
even *attempt* to import things you don't want to expose — see
[Sandbox](sandbox.md).
