An R7RS Scheme interpreter written in Rust — the R7RS-small (2013) core plus the ratified R7RS-large libraries (Red + Tangerine editions). Built primarily as an experiment in autonomous AI-assisted development; not (yet) a production-grade implementation.
- Reading the code? Start with TOUR.md — a guided tour of the source with suggested reading orders.
- Curious how it was built? PROJECT.md is the retrospective on the AI-assisted build process.
- What can it do? docs/showcase/ is a dozen tiny programs that show off the R7RS-large libraries.
- How conformant is it? docs/CONFORMANCE.md — each library runs its actual upstream SRFI reference suite.
- Contributing or extending it? docs/STYLE.md describes the in-code commentary conventions; docs/ holds the architecture decision records.
- A tree-walking interpreter for Scheme — Lisp's oldest standardized dialect.
- The language target is R7RS-small (2013) — the 88-page revised report most modern Schemes track — plus the R7RS-large library set: 21 SRFI libraries from the ratified Red (2016) and Tangerine (2019) editions, loaded from
lib/as.sldfiles. Examples:(scheme list),(scheme comparator),(scheme mapping),(scheme set),(scheme regex),(scheme generator),(scheme stream),(scheme show). See docs/CONFORMANCE.md for the full roster. - Implemented in Rust, single binary, no runtime dependencies beyond what
cargopulls in at build time.
If you don't already have Rust, the official installer is rustup:
# Pulls the rustup script and runs it. Installs Rust into ~/.cargo
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shThis installs three commands you'll need:
rustc— the Rust compilercargo— Rust's build tool and package manager (analogous tonpm,pip, orgo)rustup— the toolchain manager itself
After install, restart your shell (or source ~/.cargo/env) so the new commands are on your PATH. Verify with:
rustc --version # Should print "rustc 1.85+" or newer
cargo --versionnscheme is developed against Rust 1.85 or newer (edition 2024).
Clone the repository and build a release binary:
git clone <repository-url>
cd nscheme
cargo build --releaseThe first build downloads dependencies (thiserror, rustyline, and a few transitive crates) and may take a minute or two. Subsequent builds are incremental.
The compiled binary lands at:
target/release/nscheme
You can copy it anywhere on your PATH, or run it directly from that path. For the rest of this README we'll write ./target/release/nscheme — substitute nscheme if you've put it on your PATH.
Note:
cargo build(without--release) produces a debug binary attarget/debug/nscheme. It's slower but compiles faster — useful while editing the interpreter itself.
Run with no arguments to enter the read-eval-print loop:
./target/release/nschemenscheme 0.1.0 — R7RS Scheme interpreter. Type (exit) or press Ctrl-D to quit.
> (+ 1 2 3)
6
> (define (square x) (* x x))
> (square 7)
49
> (exit)
The REPL handles multi-line input automatically — if your parens aren't balanced when you hit Enter, the prompt changes to … and waits for the rest:
> (define (factorial n)
… (if (= n 0)
… 1
… (* n (factorial (- n 1)))))
> (factorial 10)
3628800
History is saved to ~/.nscheme_history. Press the up-arrow to recall previous expressions.
./target/release/nscheme path/to/program.scmEvaluates each top-level expression in the file in order. (display …) / (write …) print to stdout; errors go to stderr.
./target/release/nscheme -i path/to/program.scmEvaluates the file (one or more files) into the session, then drops you into the REPL with all of its definitions — and its imports — still live. You can also (load "file.scm") from inside a running REPL. See docs/showcase/ for worked examples.
./target/release/nscheme -e '(* 6 7)'
# prints: 42If you've never used a Lisp before: every expression is a parenthesized list where the first element is the operator and the rest are operands. (+ 1 2) means "call + with arguments 1 and 2". There is no precedence and no infix notation — every operator is prefix.
; Comments start with semicolons and run to end of line.
; Arithmetic
(+ 1 2 3 4) ; → 10
(* (- 5 1) (+ 2 3)) ; → 20
; Defining a variable
(define greeting "hello, world")
greeting ; → "hello, world"
; Defining a function (note the (name . params) shorthand)
(define (double x) (* x 2))
(double 21) ; → 42
; Anonymous functions
((lambda (x y) (+ x y)) 3 4) ; → 7
; Conditionals
(if (> 5 3) 'yes 'no) ; → yes
(cond ((= 1 2) 'never)
((= 1 1) 'always)
(else 'fallback))
; → always
; Lists are the central data structure
(define xs '(1 2 3 4 5))
(car xs) ; → 1 (first element)
(cdr xs) ; → (2 3 4 5) (everything but first)
(length xs) ; → 5
(reverse xs) ; → (5 4 3 2 1)
; Higher-order functions
(map (lambda (x) (* x x)) xs) ; → (1 4 9 16 25)
; Recursion is the canonical loop in Scheme
(define (sum-to n)
(if (= n 0) 0 (+ n (sum-to (- n 1)))))
(sum-to 100) ; → 5050
; nscheme has proper tail-call optimization, so this won't overflow:
(define (count-down n)
(if (= n 0) 'done (count-down (- n 1))))
(count-down 1000000) ; → doneA more thorough Scheme primer: The Scheme Programming Language by Kent Dybvig — free online, written for the previous revision (R6RS) but the core language is similar.
All of R7RS-small, plus the R7RS-large library set. See the bd issue tracker (bd list --status=open) for the remaining backlog (mostly refinements — see "Known gaps" below).
Currently working:
- Lexer + parser (including
[ ]as a synonym for( )) - Evaluator with proper tail calls and lexical closures
- Special forms:
quote,if,lambda,define,set!,begin,let,let*,letrec/letrec*, namedlet,cond(with=>clauses),case,and/or,when/unless,do,quasiquote,case-lambda,define-values,define-record-type,let-values/let*-values,parameterize,guard,delay/delay-force apply,eval, andloadare first-class procedures (passable as values, e.g.(map (lambda (e) (eval e env)) forms)), not special forms- Base library: arithmetic with exact/inexact promotion, all the type predicates, equality (
eq?/eqv?/equal?), list operations (cons,car,cdr,length,reverse,append,list-ref,member/assocfamilies),map,for-each
- Full numeric tower (
i64/ arbitrary-precisionBigInt/ exactBigRational/f64/Complex) with R7RS exact/inexact promotion; complex literals (1+2i) parse and evaluate - String / char / symbol / vector / bytevector operations with Unicode-aware string indexing
- Textual ports (string and file),
display/write/read-char/ datumread,eof-object,call-with-port,call-with-output-file/call-with-input-file;parameterize-ablecurrent-output-port/current-input-portwith the redirection family (with-output-to-file,with-input-from-file,with-output-to-string,with-input-from-string) - Hygienic
syntax-rules(scope-based) —define-syntax,let-syntax,letrec-syntax define-library,import(withonly/except/prefix/rename),cond-expand, and a filesystem loader that finds(import (foo bar))on disk asfoo/bar.sld(search path:NSCHEME_LIB_PATH→ compiled-in default →./lib)call/cc,call-with-current-continuation,dynamic-wind,apply- Exception handling:
raise,raise-continuable,with-exception-handler,guard, error objects delay/force/make-promise, lazy evaluationvalues/call-with-values/let-values/let*-valuesmake-parameter/parameterize- Error diagnostics: parse and runtime errors report
line:colwith a source caret, plus a (tail-call-correct) call-chain backtrace
21 SRFI libraries from the ratified Red (2016) and Tangerine (2019) editions ship as .sld files under lib/ and are loaded on demand. Among them: list (SRFI 1), comparator (128), set/bag (113), hash-table (125), mapping (146), regex (115), generator (158), stream (41), ideque (134), lseq (127), list-queue (117), bitwise (151), fixnum/flonum (143/144), division (141), box (111), charset (14), show (159), sort (132), bytevector (160). 18 of the 21 run their actual upstream SRFI reference suites green (~5,400 assertions); see docs/CONFORMANCE.md.
Tracked in the bd issue tracker, with deeper detail in docs/:
- Hygiene is scope-based and passes the canonical tests, but the full sets-of-scopes algorithm is not yet complete (
nscheme-d6o) - Environments aren't reified:
eval's environment argument andnull-environment/scheme-report-environmentall resolve to the interaction environment - Error locations point at the nearest enclosing recorded form, so an error inside macro-expanded code reports the macro call site, not the original template (
nscheme-tn3follow-up: span-preserving macro expansion) - Performance: it's a tree-walking interpreter with no bytecode VM, so heavy loops are slow (
nscheme-6mp) - User-defined error categories beyond
read-error?/file-error?(nscheme-1o2)
See docs/ for architecture decision records:
- 0001 — Tree-walking interpreter with explicit step-loop
- 0002 — Numeric tower
- 0003 —
syntax-ruleshygiene (originally alpha-renaming; since reworked to scope-based — see the ADR's update note) - 0004 — Continuations as cloned frame stacks
- 0005 — Exception handling (incl. how primitive errors flow as raises)
- 0006 — Library / module system
- 0007 — Filesystem-loaded libraries for R7RS-large
- 0008 — Hygiene beyond alpha-renaming (def-site
SyntaxRef+ per-expansion scope; supersedes 0003's mechanism) - 0009 — First-class control procedures (
apply/eval/load) - 0010 — Current ports as parameters
- 0011 — Error diagnostics: source locations and backtraces
ADR 0001 is the load-bearing one: it explains why the evaluator is a step-loop with continuation frames rather than recursive eval calls, and why that choice makes TCO and call/cc cheap.
cargo testThat runs about 676 tests across ~46 files in a few seconds (the slow SRFI 132 sort suite is #[ignore]d — run it with -- --ignored). The suite covers each module's unit tests, end-to-end integration tests (evaluation, tail calls, special forms, the base library, I/O, macros, libraries, continuations, exceptions, lazy evaluation, multiple values, parameters), the in-house R7RS conformance corpus, and the R7RS-large reference-suite harness described below.
tests/conformance.rs runs each R7RS-large library against its actual upstream SRFI reference test suite (vendored under tests/r7rs-large-corpus/, adapted only at the non-portable preamble). 18 of the 21 libraries have a portable upstream suite and all 18 run green — roughly 5,400 verbatim upstream assertions. The full story, including the bugs these suites surfaced, is in docs/CONFORMANCE.md.
cargo test --test conformance # the fast suites
cargo test --test conformance -- --ignored # also the ~2-min SRFI 132 sort suitecargo test --test r7rs_conformance # the in-house R7RS suite
cargo test --test tail_calls # tail-position regression suite
cargo test --test syntax_rules # macros
cargo test --test continuations # call/cc
cargo test factorial # any test name matching "factorial"
cargo test -- --nocapture # show println! / eprintln! outputtests/r7rs_chibi.rs runs chibi-scheme's r7rs-tests.scm — the de facto standard R7RS-small conformance suite — through nscheme. To see the baseline:
cargo test --test r7rs_chibi -- --nocaptureSample output:
=== chibi r7rs-tests.scm baseline ===
Top-level forms in corpus:
total: 1180
evaluated cleanly: 1180
raised an error: 0
Test assertions run inside those forms:
total: 1225
passed: 1225
failed: 0
Duration: ~230ms
All 1225 chibi (test …) / (test-assert …) / (test-error …) assertions pass; every one of the 1180 top-level forms evaluates without raising an uncaught exception.
The BASELINE_MIN_PASSES constant in the test guards against regressions — lowering it requires triage in bead nscheme-i0p.
cargo clippy --all-targets -- -D warnings # lint (treats warnings as errors)
cargo fmt # auto-format
cargo fmt --check # verify formatted without changingbd ready # tasks ready to work on
bd list --status=open # all open tasks
bd show <id> # details on a specific taskMIT OR Apache-2.0. The chibi-scheme test corpus under tests/r7rs-corpus/chibi-r7rs-tests.scm is redistributed under its original BSD 3-clause license — see tests/r7rs-corpus/COPYING-chibi-scheme.