A high-performance Scheme implementation, designed for embedding in applications. Supports VM interpretation as well as AOT compilation via Zig.
  • Zig 86.2%
  • Scheme 10.2%
  • Python 1.7%
  • Shell 1.3%
  • Tree-sitter Query 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mikael Säker f051ec2036
All checks were successful
test / test (push) Successful in 26m39s
Merge branch 'dev'
2026-10-24 19:18:18 +02:00
.forgejo/workflows Retire the Zig AOT compiler: the code, its build steps and tests 2026-10-15 11:44:36 +02:00
bench bench: peak memory against Chez (bench/memory.sh); BENCHMARKS.md Memory section 2026-10-17 11:26:25 +02:00
docs context, DESIGN, plan: stage 6 done, the hot blocks, natives as values 2026-10-24 17:09:04 +02:00
examples The R7RS 5.2 rule inside define-library bodies 2026-10-17 21:24:46 +02:00
scripts build.zig.zon: .paths for a fetched lamp; the gate builds it so 2026-10-24 19:15:05 +02:00
src ci: suspend mode and the dylib test where they can run 2026-10-24 17:07:05 +02:00
test build.zig.zon: .paths for a fetched lamp; the gate builds it so 2026-10-24 19:15:05 +02:00
.devkit.conf release: devkit publish makes the forge release page 2026-10-06 16:23:03 +02:00
.gitignore build: caret is a pinned dependency, not a clone of a branch 2026-10-06 02:05:07 +02:00
build.zig Native: a compiled unit runs on a host's session (Session.runNative) 2026-10-17 17:55:07 +02:00
build.zig.zon Merge branch 'dev' 2026-10-24 19:18:18 +02:00
build_native.zig native: char-alphabetic? and its kin inline for an ASCII character 2026-10-24 16:35:41 +02:00
build_vm.zig dap.Adapter: the debug adapter without a transport; Server frames over it 2026-10-18 20:22:20 +02:00
CHANGELOG.md Merge branch 'dev' 2026-10-24 19:18:18 +02:00
CLAUDE.md CLAUDE.md: we are building Lamp 2026-10-17 08:16:50 +02:00
context.md context, DESIGN, plan: stage 6 done, the hot blocks, natives as values 2026-10-24 17:09:04 +02:00
lamp.svg docs: outline the logo wordmark 2026-09-02 12:08:49 +02:00
LICENSE Add required library imports to benchmarks and tests 2026-02-04 22:46:20 +01:00
README.md bench: LuaJIT translations and runner deleted; README and TODO without it 2026-10-16 18:03:42 +02:00
TODO.md The R7RS 5.2 rule inside define-library bodies 2026-10-17 21:24:46 +02:00

Lamp Scheme

Lamp Scheme

A Scheme implementation in Zig, built for embedding.

Two execution modes share one frontend: a register VM with an aarch64 JIT, and a native backend that compiles the VM's code ahead of time to arm64. Both pass the same suites, and a differential corpus asserts they agree byte-for-byte.

  • R5RS — VM 316/316, native 314/314 (the suite's two eval tests run only where there is an evaluator)
  • R7RS-small — VM 1210/1210, native 1205/1205 (a compiled program has no eval or environments); no known failures
  • Speed — on the R7RS benchmark suite the VM is faster than Guile 3.0 (it takes 0.54x Guile's time, geometric mean) and native code is level with Chez 10.4 (1.00x Chez's time); see docs/BENCHMARKS.md

Other things worth knowing: the full real numeric tower — fixnums that promote to bignums on overflow and exact rationals, so (/ 1 3) is 1/3 and (exact 0.75) is 3/4 — hygienic syntax-rules macros, call/cc and dynamic-wind, proper tail calls, full Unicode (tables generated from the Unicode 16.0 database), a generational mark-sweep GC over typed arenas, a source-level debugger built for DAP integration, and native value types: host structs as Scheme values.

Build

Requires Zig 0.17.0 on the PATH (ZIG overrides it in the scripts). scripts/setup-tools.sh installs the developer tools (devkit, simgrep, zigmap). The REPL's line editor, caret, is a pinned dependency in build.zig.zon that zig build fetches.

zig build                          # debug
zig build -Doptimize=fast   # release
scripts/gate                                         # the full test gate

Use

./zig-out/bin/lamp                    # REPL
./zig-out/bin/lamp script.scm [args]  # script; args land in *args*
./zig-out/bin/lamp < script.scm       # a program on stdin
./zig-out/bin/lamp --repl             # the REPL even when stdin is a pipe
./zig-out/bin/lamp --dap              # Debug Adapter Protocol on stdin/stdout
./zig-out/bin/lamp --no-jit ...       # interpret only
./zig-out/bin/lamp --disasm file.scm  # list the compiled code (--disasm=NAME: one function)

lamp runs the register VM with an aarch64 JIT (src/vm/). A script's status is its exit (0 if it has none); an error prints error: ... to stderr and exits 1.

> (define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))
> (map fib '(1 2 3 4 5 6 7 8 9 10))
(1 1 2 3 5 8 13 21 34 55)

To compile ahead of time, the native backend (arm64; src/native/):

zig build native -Doptimize=ReleaseFast     # zig-out/bin/lamp-native, zig-out/lib/liblamp-rt.a
./zig-out/bin/lamp-native compile app.scm app.o
zig cc app.o zig-out/lib/liblamp-rt.a -o app

A compiled program has no evaluator: eval, environments and runtime load are absent ((scheme eval) reads as missing in cond-expand; use lamp for those). Everything else compiles, re-entrant call/cc included.

Libraries can live in files. (import (my utils)) looks for my/utils.sld then my/utils.scm under each search root:

lamp -I lib app.scm                  # repeatable, highest precedence
LAMP_LIBRARY_PATH=lib:vendor lamp app.scm

An embedder adds roots with .library_paths (Session.init's options). With no roots configured nothing is loaded from disk, so import sees only the built-in libraries and what the source defines.

Performance

Apple M1 Pro, ecraven's R7RS benchmark suite (56 programs), identical sources, inputs and iteration counts for every implementation. Each ratio is Lamp's time divided by the peer's, so below 1.00 means Lamp is faster (0.50x: Lamp takes half the time):

geometric mean median Lamp faster on
native vs Chez 10.4 0.90x 1.01x 27 of 56
native vs Guile 3.0 0.33x 0.43x 51 of 55
VM vs Guile 3.0 0.52x 0.70x 41 of 55
VM vs Chez 10.4 1.40x 1.68x 17 of 56

Native code beats Chez outright on unboxed float loops (mbrot 0.12x, sumfp 0.21x), call/cc used as a return (ctak 0.13x), strings (string 0.23x) and buffered I/O, and loses on allocation-heavy and higher-order code (graphs 2.9x, sboyer 2.8x, gcbench 2.6x, scheme 2.2x) and re-entrant continuations (fibc 2.9x). The full tables and the method are in docs/BENCHMARKS.md; single rows carry several percent of noise.

Embedding

A host program runs Scheme through a lamp.Session: one growing program, each evalText/evalSource a unit compiled into it (the VM, JIT on). The whole guide is docs/EMBEDDING.md; the surface is src/embed.zig.

const lamp = @import("lamp");

const s = try lamp.Session.init(gpa, io, .{
    // Sandbox: only these libraries (besides (scheme base)) can be imported;
    // the rest stay hidden. Omit for all of them.
    .libraries = &.{"(scheme write)"},
});
defer s.deinit();

_ = try s.evalSource("game.scm", text);          // errors report game.scm:line
const tick = s.global("on-tick") orelse return;  // a callback, by name
_ = try s.call(tick, &.{lamp.embed.Value.fixnum(1)});

Zig functions become procedures: plain functions with typed parameters, in a library a session adds with its context (lamp.native):

fn hostLog(host: *Host, msg: []const u8) void {
    host.out.print("[host] {s}\n", .{msg}) catch {};
}
const host_lib = lamp.native.Library(.{
    .name = "(my host)",
    .Context = Host,
    .natives = .{.{ "host-log", hostLog }},
});
try s.addLibrary(host_lib, &host);   // scripts: (import (my host))

Lamp reads each argument into its type, refusing a wrong one with a message naming the procedure and the argument, and boxes the result.

An error is error.Faulted (or ExpandFailed for a unit that does not compile); s.errorMessage(err, &buf) says what and where, and the session goes on. A script's (exit n) is s.exitStatus().

Native value types

A host type -- a Vec3, a colour, a handle -- is a first-class Scheme value carrying its bytes inline, with overloadable + - * /, its own printed form and equal?:

(define v (+ (make-vec3 1.0 2.0 3.0) (make-vec3 0.5 0.5 0.5)))
(vec3-x v)   ; 1.5

It is a native.Type in the same library declaration. examples/native-types/ shows it (zig build native-types-interp); examples/callback/ is a host and Scheme calling each other (zig build callback); examples/embed/ the sandbox. Embedding natively compiled code is not built yet; the compiled halves of the first two examples (main.zig) wait for it.

Libraries

Library Provides
(scheme base) Core language — arithmetic, lists, strings, control flow
(scheme write) display, write, newline
(scheme read) read
(scheme char) Unicode predicates and case conversion
(scheme inexact) sqrt, sin, cos, exp, log, …
(scheme file) open-input-file, open-output-file, …
(scheme time) current-jiffy, jiffies-per-second
(scheme cxr) caar, cadr, caddr, …
(scheme eval) eval, environment
(scheme load) load
(scheme lazy) delay, force, make-promise
(scheme process-context) command-line, exit
(scheme case-lambda) case-lambda
(scheme repl) interaction-environment
(scheme r5rs) the whole R5RS set in one import
(lamp debug) disassemble, debug-break
(lamp meta) library-list, library-exports, symbol-doc, symbol-signature

Scheme source embeds as proper R7RS libraries via @embedFile and define-library.

Docstrings

#doc attaches documentation to a definition. It is stored in the library table at compile time, not compiled into code, and queried through (lamp meta):

(define (square x)
  #doc "Return the square of x."
  (* x x))

(symbol-doc '(example math) 'square)  ; "Return the square of x."

Not implemented

Complex numbers ((scheme complex)) and syntax-case. A compiled program has no eval, environments or runtime load (the interpreter has all three).

Dev tools

devkit (the release mechanism — see .devkit.conf), simgrep (duplicate-code search) and zigmap (codemaps) live in .tools/. None is a dependency:

scripts/setup-tools.sh      # devkit, simgrep + zigmap (Zig 0.17.0 comes from your PATH)
scripts/setup-tools.sh -u   # pull first

Work happens on dev; main only advances via .tools/devkit/devkit promote, which merges, runs scripts/gate, and rolls back on any failure.

License

MIT. See LICENSE.