Mini-Lisp-R

A tiny Lisp that compiles to R language objects and runs on R’s own eval().

> lisp_eval("(define (fact n) (if (< n 2) 1 (* n (fact (- n 1))))) (fact 10)")
[1] 3628800

Why R’s own eval()

Most toy Lisps (SICP’s metacircular-evaluator, for example) spend most of their code on a hand-written evaluator: an eval/apply pair that manages its own environments, its own scoping rules, its own closures. This project skips that step on purpose. R already has all of it — lexically scoped environments, closures, a garbage collector — so once Lisp source has been turned into an R call object, plain eval() does the rest. What’s left to build is genuinely small: a reader and a syntactic translator, not an interpreter.

A pleasant consequence: if, while, function, <- and { are not special syntax in R, they’re ordinary calls with backtick-able names (`if`(cond, a, b) really does work). So most Lisp special forms need no special-casing in the translator at all — (if c a b) becomes as.call(list(as.name("if"), c, a, b)) and R runs it correctly. Only a handful of forms (define, lambda/fn, let, variadic arithmetic) need real translation logic; everything else falls through to a generic “S-expression to call” conversion.

Architecture

source text --tokenize--> tokens --read_all--> S-expressions --to_r--> R call objects --eval()--> value

Supported language

Form Example Notes
Arithmetic (+ 1 2 3), (* 2 (- 5 1)) + - * / are variadic, left-folded into R’s binary operators
Comparisons (< 1 2), (== 3 3) strictly binary, like R’s own </==
if (if (< 1 2) "yes" "no") maps directly to R’s `if` call
while (while (< i 10) ...) maps directly to R’s `while` call
begin (begin e1 e2 e3) explicit progn, becomes a { ... } block
define (define x 5), (define (f a b) (+ a b)) both variable and function-shorthand forms
lambda / fn (lambda (x y) (+ x y)) builds a real R closure at eval-time
let (let ((a 1) (b 2)) (+ a b)) desugars to an immediately-invoked lambda (IIFE)
Strings (paste "hello" "world") double-quoted; supports \n, \t, \", \\ escapes
Closures (define (adder n) (lambda (x) (+ x n))) lexical scoping, courtesy of R
Recursion (define (fact n) (if (< n 2) 1 (* n (fact (- n 1))))) plain self-reference via R’s own scoping

Any symbol that isn’t one of the forms above is passed straight through as a function call, so any R function is callable from Lisp for free — (paste "a" "b"), (nchar "lisp"), (sqrt 16), and so on.

Usage

From the command line

minilisp.r is executable and doubles as a Lisp file runner — it evaluates every top-level form in the given file in one shared environment and prints the value of the last one:

./minilisp.r program.lisp

This only fires when the file is run directly; test.r sources it as a plain R library instead, so the two uses don’t interfere with each other.

demo.lisp is a short tour of the language — arithmetic, if, lambda, let, closures, recursion, strings, while — runnable as-is:

./minilisp.r demo.lisp

minilisp.r never looks up its own location, so it works the same whether you run it in place or through a symlink. To make it available system-wide as minilispr, symlink it into a directory already on your PATH~/.local/bin is the standard per-user one on most Linux distros:

mkdir -p ~/.local/bin
ln -s "$(pwd)/minilisp.r" ~/.local/bin/minilispr

Then from anywhere:

minilispr demo.lisp

(If ~/.local/bin isn’t already on your PATH, add export PATH="$HOME/.local/bin:$PATH" to your shell’s rc file.)

From R

source("minilisp.r")

lisp_eval("(+ 1 2)")                        # 3
lisp_eval("(define (sq x) (* x x)) (sq 7)") # 49

# inspect the intermediate stages
tokenize("(+ 1 2)")            # "(" "+" "1" "2" ")"
read_all("(+ 1 2)")            # nested list: list(list(as.name("+"), 1, 2))
to_r(read_all("(+ 1 2)")[[1]]) # quote(1 + 2)

Forms accumulate in a shared environment by default, so a REPL-style session just keeps calling lisp_eval() with the same env:

env <- new.env(parent = globalenv())
lisp_eval("(define x 10)", env)
lisp_eval("(+ x 5)", env) # 15

Tests

./test.r
# or: Rscript test.r

Dependency-free TAP 13 output (~15-line test framework, no testthat), composable with any TAP consumer. Covers the tokenizer, reader, translator and driver end-to-end, plus a golden test comparing lisp_eval() output against the equivalent hand-written R expression evaluated natively.

Project layout

File Purpose
minilisp.r The whole implementation: tokenizer, reader, translator, driver, CLI entry point
test.r TAP test suite
demo.lisp Runnable example touring the supported language

Roadmap

Milestones so far: arithmetic + ifdefine/lambda/let → strings, closures, recursion, while. What’s next, in increasing order of how much fun it is:

Known limitations