Schooner.Host (schooner v1.2.0)

Copy Markdown View Source

Helpers for authoring Elixir-side host functions exposed to Scheme.

Schooner does not auto-marshal across the host boundary. A host function has the same shape as a built-in primitive — it consumes a list of Schooner.Value.t/0 arguments and returns a Schooner.Value.t/0 — and uses the helpers in this module to move between Scheme values and idiomatic Elixir terms. Going through the named constructors and accessors keeps host code independent of the value representation, even where a helper is currently the identity (Schooner strings are bare Elixir binaries today).

Use raise_error/2 or raise_value/1 to raise failures that a script can catch with guard or with-exception-handler.

Naming convention

Asserting accessors (to_*!/2) raise Schooner.Host.TypeError when the input does not match the expected shape. They take a keyword argument with :op set to a host-supplied label for the call site so the error points at the wrapper, not the bare predicate.

Total accessors (to_*/1) return {:ok, term()} | :error for the "branch on shape" case where a type mismatch is not an error.

Schooner.Host.to_string/1 collides with Kernel.to_string/1. Don't import Schooner.Host. Use alias Schooner.Host and call the accessors as Host.to_string! etc.

Worked example

defmodule MyApp.SchemeLib do
  alias Schooner.Host

  @spec specs() :: [{binary(), Schooner.Value.arity_spec(), fun()}]
  def specs do
    [
      {"info", 1, &info/1},
      {"now-ms", 0, &now_ms/1}
    ]
  end

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

  defp now_ms([]) do
    System.system_time(:millisecond)
  end
end

Summary

Functions

Build a Schooner.Library from host-supplied primitive specs and arbitrary value bindings.

Raise a Scheme error object of kind :user with message and irritants.

Raise any Scheme value, including a symbol or a host-built error object.

Assert value is a Scheme boolean and return the Elixir true | false. This is a type check, not a truthiness test: every value other than #f is truthy in Scheme, but only #t and #f are booleans. Use Schooner.Value.truthy?/1 for truthiness.

Assert value is a Scheme bytevector and return the underlying Elixir binary.

Assert value is a Scheme character and return the codepoint integer.

Assert value is a bare inexact real and return the Elixir float. Rejects integers, rationals, the float specials (+inf.0, -inf.0, +nan.0), and complex. Hosts that want "any real number, coerced" should call to_real!/2 instead.

Assert value is a foreign-wrapped host term and return the wrapped term. Mirrors Schooner.Value.foreign_ref/1 but raises the host-facing TypeError instead of ArgumentError.

Assert value is an exact integer and return the bare Elixir integer. Rejects rationals, floats, the float specials, and complex.

Assert value is a proper Scheme list and return the underlying Elixir list. Raises on improper lists and non-list values.

Assert value is callable from Elixir — a closure, a primitive, or a parameter — and return it unchanged. Use this to validate the shape of a callback argument before passing it to Schooner.apply/2.

Assert value is any real number and return it as an Elixir number. Integers and floats pass through; rationals are coerced to their float approximation; the float specials are rejected (no plain BEAM-float representation).

Assert value is a Scheme string and return the underlying Elixir binary. Rejects symbols, bytevectors, and any non-string.

Assert value is a Scheme symbol and return its name as an Elixir binary.

Assert value is a Scheme vector and return its elements as an Elixir list, which suits Enum-based host code. Use List.to_tuple/1 on the result if a tuple is needed.

Functions

bool(b)

@spec bool(boolean()) :: Schooner.Value.bool_v()

See Schooner.Value.bool/1.

bytevector(bs)

@spec bytevector([byte()] | binary()) :: Schooner.Value.bytevector_v()

See Schooner.Value.bytevector/1.

char(cp)

See Schooner.Value.char/1.

foreign(term)

@spec foreign(term()) :: Schooner.Value.foreign_v()

See Schooner.Value.foreign/1.

library(opts)

@spec library(keyword()) :: Schooner.Library.t()

Build a Schooner.Library from host-supplied primitive specs and arbitrary value bindings.

Options

  • :name — canonical library name as a list of binary or non-negative-integer segments. Defaults to [], which marks the library as anonymous: it cannot be reached via Scheme (import ...), and its bindings are applied directly to the runtime environment when the embedder lists it. Named libraries (e.g. ["myapp", "log"]) are sandbox-safe — the script must explicitly (import (myapp log)) to see them.

  • :primitives — list of {name, arity, fun} triples registered as Schooner.Value.primitive/3 bindings. arity may be an integer, {:at_least, n}, or {:between, lo, hi}. fun must be (list() -> Schooner.Value.t()) — the underlying primitive ABI.

  • :values — list of {name, value} pairs for arbitrary Schooner.Value.t/0 bindings (constants, foreign-wrapped service handles, pre-built closures). Useful when the host wants to expose data, not just procedures.

Names within a library must be unique across :primitives and :values combined; a collision raises ArgumentError.

Examples

Named library — script must import:

Schooner.Host.library(
  name: ["myapp", "log"],
  primitives: [
    {"info", 1, &MyApp.SchemeLib.info/1}
  ]
)

Anonymous library — bindings auto-applied, no import required. Sandbox-loosening; list deliberately:

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

list(items)

@spec list([Schooner.Value.t()]) :: Schooner.Value.t()

See Schooner.Value.list/1.

pair(car, cdr)

See Schooner.Value.pair/2.

primitive(name, arity, fun)

See Schooner.Value.primitive/3.

raise_error(message, irritants \\ [])

@spec raise_error(binary(), [Schooner.Value.t()]) :: no_return()

Raise a Scheme error object of kind :user with message and irritants.

A script's guard or with-exception-handler catches the error. With no handler, it surfaces to the embedder as Schooner.Error: Schooner.eval/2 returns {:error, %Schooner.Error{}}. Raising Schooner.Error directly with Kernel.raise/2 bypasses Scheme handlers.

message must be an Elixir string and irritants a list of Scheme values. A non-binary message or non-list irritants raises ArgumentError, indicating a host programming error.

alias Schooner.Host

Host.raise_error("db query failed", [Host.foreign(:timeout)])

raise_value(value)

@spec raise_value(Schooner.Value.t()) :: no_return()

Raise any Scheme value, including a symbol or a host-built error object.

A script's guard or with-exception-handler catches the value. With no handler, it surfaces to the embedder as Schooner.Error: Schooner.eval/2 returns {:error, %Schooner.Error{}}. Raising Schooner.Error directly with Kernel.raise/2 bypasses Scheme handlers.

alias Schooner.Host

Host.raise_value(Host.symbol("service-unavailable"))

string(s)

@spec string(binary()) :: Schooner.Value.string_v()

See Schooner.Value.string/1.

symbol(name)

@spec symbol(binary()) :: Schooner.Value.sym_v()

See Schooner.Value.symbol/1.

to_bool(value)

@spec to_bool(Schooner.Value.t()) :: {:ok, boolean()} | :error

to_bool!(value, opts)

@spec to_bool!(
  Schooner.Value.t(),
  keyword()
) :: boolean()

Assert value is a Scheme boolean and return the Elixir true | false. This is a type check, not a truthiness test: every value other than #f is truthy in Scheme, but only #t and #f are booleans. Use Schooner.Value.truthy?/1 for truthiness.

to_bytevector(arg1)

@spec to_bytevector(Schooner.Value.t()) :: {:ok, binary()} | :error

to_bytevector!(value, opts)

@spec to_bytevector!(
  Schooner.Value.t(),
  keyword()
) :: binary()

Assert value is a Scheme bytevector and return the underlying Elixir binary.

to_char(arg1)

@spec to_char(Schooner.Value.t()) :: {:ok, non_neg_integer()} | :error

to_char!(value, opts)

@spec to_char!(
  Schooner.Value.t(),
  keyword()
) :: non_neg_integer()

Assert value is a Scheme character and return the codepoint integer.

to_float(value)

@spec to_float(Schooner.Value.t()) :: {:ok, float()} | :error

to_float!(value, opts)

@spec to_float!(
  Schooner.Value.t(),
  keyword()
) :: float()

Assert value is a bare inexact real and return the Elixir float. Rejects integers, rationals, the float specials (+inf.0, -inf.0, +nan.0), and complex. Hosts that want "any real number, coerced" should call to_real!/2 instead.

to_foreign_ref(arg1)

@spec to_foreign_ref(Schooner.Value.t()) :: {:ok, term()} | :error

to_foreign_ref!(value, opts)

@spec to_foreign_ref!(
  Schooner.Value.t(),
  keyword()
) :: term()

Assert value is a foreign-wrapped host term and return the wrapped term. Mirrors Schooner.Value.foreign_ref/1 but raises the host-facing TypeError instead of ArgumentError.

to_integer(value)

@spec to_integer(Schooner.Value.t()) :: {:ok, integer()} | :error

to_integer!(value, opts)

@spec to_integer!(
  Schooner.Value.t(),
  keyword()
) :: integer()

Assert value is an exact integer and return the bare Elixir integer. Rejects rationals, floats, the float specials, and complex.

to_list(list)

@spec to_list(Schooner.Value.t()) :: {:ok, [Schooner.Value.t()]} | :error

to_list!(list, opts)

@spec to_list!(
  Schooner.Value.t(),
  keyword()
) :: [Schooner.Value.t()]

Assert value is a proper Scheme list and return the underlying Elixir list. Raises on improper lists and non-list values.

to_proc(value)

@spec to_proc(Schooner.Value.t()) :: {:ok, Schooner.Value.t()} | :error

to_proc!(value, opts)

@spec to_proc!(
  Schooner.Value.t(),
  keyword()
) :: Schooner.Value.t()

Assert value is callable from Elixir — a closure, a primitive, or a parameter — and return it unchanged. Use this to validate the shape of a callback argument before passing it to Schooner.apply/2.

to_real(value)

@spec to_real(Schooner.Value.t()) :: {:ok, number()} | :error

to_real!(value, opts)

@spec to_real!(
  Schooner.Value.t(),
  keyword()
) :: number()

Assert value is any real number and return it as an Elixir number. Integers and floats pass through; rationals are coerced to their float approximation; the float specials are rejected (no plain BEAM-float representation).

to_string(value)

@spec to_string(Schooner.Value.t()) :: {:ok, binary()} | :error

to_string!(value, opts)

@spec to_string!(
  Schooner.Value.t(),
  keyword()
) :: binary()

Assert value is a Scheme string and return the underlying Elixir binary. Rejects symbols, bytevectors, and any non-string.

to_symbol_name(arg1)

@spec to_symbol_name(Schooner.Value.t()) :: {:ok, binary()} | :error

to_symbol_name!(value, opts)

@spec to_symbol_name!(
  Schooner.Value.t(),
  keyword()
) :: binary()

Assert value is a Scheme symbol and return its name as an Elixir binary.

to_vector(arg1)

@spec to_vector(Schooner.Value.t()) :: {:ok, [Schooner.Value.t()]} | :error

to_vector!(value, opts)

@spec to_vector!(
  Schooner.Value.t(),
  keyword()
) :: [Schooner.Value.t()]

Assert value is a Scheme vector and return its elements as an Elixir list, which suits Enum-based host code. Use List.to_tuple/1 on the result if a tuple is needed.

vector(items)

@spec vector([Schooner.Value.t()]) :: Schooner.Value.vector_v()

See Schooner.Value.vector/1.