Rook Lisp — or — The Compulsion To Design Languages

Lisp fascinates me. I think it's the axiomatic, constructive nature of the thing. From a handful of operators and special forms spring ten thousand functions and countless libraries. The only other language that comes close to the simple-complexity of Lisp is C.

I think every Lisper tries their hand at implementing Lisp, via a meta-circular evaluator. That is, use a Lisp to implement a Lisp interpreter. Some even go so far as to implement Lisp on top of another language runtime, like Python or LLVM.

I've been programming for over two decades now, in about a dozen different languages. I've seen what I like in these languages, and I remember what I dislike. So the time has come (... melodramatic pause...) to build a better mousetrap language.

It's going to be Lisp, and it's going to be bootstrapped in C.

I call it Rook.

It has all the hallmarks of a "classic" Lisp:

It also has lots of things that I think are important, that don't seem to make it into language specifications:

Future writings will cover these topics in more detail. For now, I want to show you my plans for the language, in 100% vaproware code snippets!

Hello, World!

You just can't implement a language without this snippet:

(import io)

(fn (main)
    (io.printf "Hello, World!\n"))

io is a standard library for doing input / output. The io.printf function derives its name from the import.

Standalone binaries need an entrypoint, and following the traditions of C, we call that entrypoint main.

Recursively Fibonacci'n

(fn (fib n)
    "Calculate the n'th number in the Fibonacci sequence"
    (when
      (< 1)     (bail "invalid!")
      (eq? n 1) 1
      (eq? n 2) 1
      #t        (+ (fib (- n 1))
                   (fib (- n 2)))))

This is a naïve recursive implementation that finds Fibonacci numbers, but you already knew that because of the helpful documentation string. The compiler will remove that, by the way, since it has no effect on either the computation or the outside world.

The (when ...) construct is just a multi-branch if ... then ... else if ... as you might find in other languages. Conditionals are evaluated in-order until a true value is found, and then the paired consequent clause is evaluated.

Anonymous Functions (Lambdas, really)

First-class functions are super useful.

(fn (evens lst)
    (filter lst
            (lambda (x) (even? x))))

Here, we pass a lambda to the (filter lst f) call, which will apply f to each item in lst, and return the subset for which f returned #t.

Variable Bindings

(fn (foo)
    (with (x 3
           y 4)
      (+ x y)))

The (with ...) form introduces new variable bindings, shadowing any lexically "outer" bindings for the duration of the with form.

Communicating Sequential Processes

(fn (main)
    (let (ch (chan))
      (thread
        (for n ch
          (-> ch (* 2 n))))
      (for n (list 1 2 3 4)
          (-> ch n)
          (printf "%d x 2 = %d\n" n (<- ch)))))

This (rather contrived) example demonstrates some co-processing capabilities, which is all based on passing messages via channels. (chan) creates a new channel, which we store in the ch variable.

Then, the (thread ...) form steps in and starts a new thread, executing the contained statements, which is just a for loop over the channel. The (-> ch x) form sends the value of x to the channel ch. On the flip-side, (<- ch) returns the next value available in the channel.

The main thread then loops over the list (1 2 3 4), sending each number off to the thread for processing, and printing the results.

An Alternate Syntax

Some people don't like S-expressions. That's fine. If you're willing to sacrifice macros (which more or less require S-exprs), there's no reason you can't still use Rook!

This S-expr program:

(fn (main)
    (printf "Hello, World!\n"))

Is equivalent to this alt-syntax program:

fn main() {
  printf("Hello, World!\n");
}

All Rook needs is an alternate lexer/parser that can turn the latter into the former. The compiler, of course, never sees the alternate syntax, which means a program can use libraries written in either style! Win!

Libraries, Libraries, Libraries!

What good is a language without standard libraries?

(import io)
(io.printf "hello, world!")

Imports the standard input/output library. Functions will be prefixed with io., like io.printf.

(import net/http)
(http.connect "https://jameshunt.us")

Here, we've imported a multi-label library, net/http. The prefix will be taken from the final component of the directory path.

(import (web net/http))
(web.connect "https://jameshunt.us")

If you want, you can provide your own namepsace, by using the two-element list form of the (import ...) call.

(import (. io))
(printf "hello, world!\n")

The special prefix . tells Rook to import symbols directly into the main namespace, so you don't even need a prefix!

You can even combine these imports into one call:

(import
  io
  (web net/http)
  (. string/utils))

What good is a language without user-defined libraries?

(lib base64)

(export fn (decode s)
  (implementation-wanted!))

(export fn (encode s)
  (implementation-wanted!))

The (lib ...) form defines that the following declarations belong in a library. The export decorator for the (fn ...) form defines a function that will be available after an import.

The Future...

This is all just dreams at this point, but I'll be working in earnest on an implementation of Rook. Stay tuned!

James (@iamjameshunt) works on the Internet, spends his weekends developing new and interesting bits of software and his nights trying to make sense of research papers.

Currently working on Rook.