Schooner.Debug (schooner v1.2.0)

Copy Markdown View Source

The opt-in (schooner debug) library: tracing, printing and assertions for script authors.

display, write and newline return their rendered text rather than writing it to a port (see Deviations), so a (display x) dropped into a procedure body shows nothing. This library sends output to a sink the embedder chooses instead. Like Schooner.Time, it is not in the standard registry: a script can only import it from an environment built with it, so the default sandbox stays free of side effects.

env =
  Schooner.Environment.new(
    pre_imports: [["scheme", "base"]],
    libraries: [Schooner.Debug.library(sink: {:logger, :debug})]
  )

Schooner.eval!(~s|(import (schooner debug)) (trace "sum" (+ 1 2))|, env)
# logs "sum: 3" and returns 3

Syntax

All three exports are syntax, not procedures, so each one knows where it was used:

  • (trace label expr) evaluates expr, sends "<label>: <value>" to the sink and returns the value unchanged. The label is rendered as by display and the value as by write; multiple values are written space-separated. Wrap any expression in place without restructuring the code around it. trace has to see the value, so expr is not in tail position, but tail calls within expr still are: wrapping a call to a long-running loop does not make the loop grow the stack.

  • (print obj ...) sends the display rendering of its arguments, separated by spaces, to the sink. Its value is unspecified.

  • (assert expr) and (assert expr message) return the value of expr when it is true. When it is #f they raise an error object, as (error ...) does, whose message is "assertion failed: <expr>" with the source text of expr, followed by ": <message>" when a message is given. message is only evaluated when the assertion fails. A guard catches the error like any other; if nothing does, it reaches the host as a Schooner.Error.

Because they are syntax, the names cannot be passed around as values: write (lambda (x) (print x)) rather than print.

Sinks

library/1 takes a required :sink:

  • {:logger, level} logs each message through Logger at level, with the script location in the :schooner_location metadata and the kind of message (:trace or :print) in :schooner_debug.
  • a pid is sent {:schooner_debug, kind, text, location}.
  • a 1-arity function is called with %{kind: kind, text: text, location: location}; its return value is ignored.

location is the Schooner.Location of the trace or print form. Like the locations of errors, it is only recorded when the script is evaluated with :file, locations: true or debug: true; otherwise it is nil. The same holds for the location of a failed assertion.

The sink runs in the evaluating process. If it raises, exits or throws, evaluation stops with a Schooner.Primitive.Error, which Schooner.eval/3 returns as {:error, exception}. A script's guard does not catch it.

Compiled scripts

Schooner.compile/3 expands macros when it compiles, so a compiled script keeps the sink of the environment it was compiled against. Compile against an environment with the sink you want to run with.

Summary

Types

What a function sink is called with.

Where trace and print send their output.

Functions

Build the (schooner debug) library. Pass it to Schooner.Environment.new/1 in :libraries to let scripts evaluated against the environment (import (schooner debug)).

Types

message()

@type message() :: %{
  kind: :trace | :print,
  text: binary(),
  location: Schooner.Location.t() | nil
}

What a function sink is called with.

sink()

@type sink() :: {:logger, Logger.level()} | pid() | (message() -> any())

Where trace and print send their output.

Functions

library(opts)

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

Build the (schooner debug) library. Pass it to Schooner.Environment.new/1 in :libraries to let scripts evaluated against the environment (import (schooner debug)).

Options

  • :sink (required) — where trace and print send their output: {:logger, level}, a pid, or a 1-arity function. See "Sinks" above.