Mate Scheme

A tiny Scheme interpreter, written in Scheme, in about 35 lines of code. It's a toy/educational "meta-circular interpreter": it defines a very minimal language and evaluates it through a classic tree-walking interpreter (ev), with no separate parsing or compilation phase — expressions are just s-expressions read by the host Scheme.

Repo layout

The language

The environment is represented as an association list (name . box), where box is a mutable cell (a 1-element vector). This is what enables the "tying the knot" trick used by letrec to implement recursion.

The forms supported by ev are:

Form Meaning
n (number) self-evaluating literal
s (symbol) variable lookup in the environment
(if test then else) 0 is false, anything else is true
(let name val body) single, non-recursive binding
(letrec name val body) recursive binding, for functions that call themselves
(lambda (param ...) body) creates a closure over the current environment
(f arg ...) function application (the default case)

Primitives available in global: +, -, *, < (the latter returns 1/0 instead of #t/#f, consistent with the language's truthiness convention).

Since there isn't much else, the language lacks: strings, lists, native booleans, a multi-clause cond, set!, multiple bindings in a single let, and any error handling beyond a generic error for unbound variables.

Running it

You need a host Scheme with load and (ice-9 format) (used to align the test output); the project was developed and tested with Guile.

guile tests.scm

To use the interpreter interactively:

guile
(load "mate.scm")
(display (ev '(letrec fact (lambda (n) (if (< n 2) 1 (* n (fact (- n 1)))))
                (fact 10))
             global))

To run a file of toy-language expressions and see each result, use matescm (a small executable Guile script, no quote needed — the file is read directly as data):

./matescm demo.lisp

Any file works, not just demo.lisp: matescm just reads one top-level expression at a time from the given path and evaluates each in global, so it doubles as a REPL-less way to try out your own snippets.

matescm is a plain executable text file, not a compiled binary. It opens with the classic portable shebang trick for Guile scripts:

#!/bin/sh
exec guile -q --no-auto-compile -s "$0" "$@"
!#

/bin/sh runs first — a fixed path present on virtually every Unix system — and immediately execs into guile -s, resolved via $PATH, passing itself ($0) as the script to run and forwarding any extra arguments ($@); the !# line closes that leading block so Guile's reader treats it as a comment and continues with the Scheme code below it. This two-line indirection is the standard portable shebang trick for Guile scripts: a plain #!/usr/bin/env guile ... shebang can't reliably take multiple flags (like -q --no-auto-compile -s here) on every system, since the kernel passes everything after the interpreter path as a single, unsplit argument. The rest of the file is ordinary Guile: it finds its own directory via (command-line) to load mate.scm from there (not from the caller's working directory), then reads and evaluates each expression from the file given as an argument.

To run matescm from anywhere without the leading ./, symlink it into a directory on your PATH, e.g. ~/.local/bin (from the repo root, creating the directory first if it doesn't exist yet):

mkdir -p ~/.local/bin
ln -s "$(pwd)/matescm" ~/.local/bin/matescm

With that in place (and ~/.local/bin on your $PATH), matescm demo.lisp works from any directory, system-wide for your user.

TODO

Some directions for extending the language or the interpreter:

 

Dedicated to my mates.