All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

1.1.0 - 2026-10-03

Changed

  • The evaluator is substantially faster: roughly 3-7x on call-heavy code. Core forms are now analysed once into an internal representation, variable references are resolved to lexical slots ahead of time, and the result is compiled into closures, so evaluation no longer re-parses forms or looks variables up by name. Each global binding has its own cell, read directly at run time. Two-argument integer + - * = < > <= >= run inline while those names still mean the standard procedures. A named let, or any letrec* whose lambdas are only ever called directly, runs as direct calls with no closures.

    workload1.0.0this release
    fib(20)20.4 ms3.6 ms
    named-let loop 100k120.7 ms16.2 ms
    closures 10k23.6 ms4.0 ms
    list build+map+fold 10k25.3 ms5.9 ms
    string build 2k2.6 ms0.8 ms

    (Medians from bench/eval_bench.exs on OTP 28.5 with JIT.)

  • %Schooner.Compiled{} now holds the analysed program rather than the expanded source forms. It is still plain data, safe to cache or persist, and is still opaque. A %Compiled{} produced by 1.0.x cannot be run by this release: running one raises. Recompile cached or persisted programs with Schooner.compile/2 after upgrading.

  • Imports and library loading are stricter. An only, except, or rename import that names an identifier the library doesn't export, or a rename whose new name collides with another binding, now fails with Schooner.Eval.Error instead of being silently ignored or overwriting a binding. Schooner.Library.Loader.load_string/2,3 without :base_dir now rejects absolute include paths. Scripts and libraries that relied on the old behaviour need updating; the corresponding entries under Fixed have the details.

Added

  • Schooner.Host.raise_error/2 and Schooner.Host.raise_value/1 let host functions raise errors that scripts can catch with guard or with-exception-handler.

  • bench/eval_bench.exs, an evaluator throughput benchmark with no extra dependencies (MIX_ENV=prod mix run bench/eval_bench.exs).

Fixed

  • Macros defined inside top-level begin forms now remain visible to later forms even when the begin also contains ordinary forms, including nested begin forms and compiled programs.

  • Import rename clauses now apply simultaneously, preserving bindings in swaps and rotations. Duplicate destinations and renames that would overwrite an export retained under its original name now return script errors.

  • Import modifiers only, except, and rename now reject identifiers absent from the inner import set. Scripts that previously imported an unknown name silently now fail with a script error.

  • Loading a library with base_dir: "/" no longer rejects includes beneath the filesystem root.

  • digit-value now returns the correct decimal value for adjacent Unicode digit sets, including mathematical styled digits.

  • Library sources loaded without a base directory now reject absolute include paths as well as relative ones. Load from a file or pass :base_dir to confine includes to the library root directory.

  • Tail loops with internal definitions, including bodies that create callbacks, no longer grow the stack or allocate recursive process-dictionary slots when binding targets are distinct and initializer references only target earlier bindings. Closures in these forms retain their captured lexical values, including when saved by host primitives. Self and forward references retain recursive binding behavior.

  • Rational asin/acos outside the real domain and negative bases raised to fractional powers now return complex values. Inexact real expt and exp handle overflow and underflow, and numeric domain or range failures return script errors instead of leaking Elixir arithmetic exceptions.

  • The error for a define-syntax outside the top level now says that only top-level definitions are supported and suggests let-syntax or letrec-syntax; it previously described only macro-introduced definitions. The expired-continuation error no longer refers to an internal planning document.

  • Module documentation and guides no longer contradict the implementation in several places, including the representation of promises and the empty list, equal? on foreign values, how out-of-range asin/acos and negative sqrt behave, and how include paths are confined when loading libraries.

  • The sandboxing examples in the "Running Untrusted Scheme" guide now work. They built the environment in one process and evaluated it in another, which fails. The first example also passed :max_heap_size to Task.Supervisor.async_nolink/3, which ignores it, so no heap limit applied. The examples now set the limit inside the task, give it in words rather than bytes, and set include_shared_binaries: true so that large strings and bytevectors count towards it.

  • The "Host Functions" guide told host code to raise Schooner.Error for errors a script can catch. That exception bypasses Scheme handlers. The guide now uses Schooner.Host.raise_error/2, which reaches guard and with-exception-handler.

  • The "Special-form names" deviation now describes the actual behaviour: rebinding is accepted, but core special forms keep their meaning at the head of a form.

1.0.0 - 2026-05-01

Added

  • Initial release.