wisp icon indicating copy to clipboard operation
wisp copied to clipboard

A lisp👽 written in C++

wisp

A light lisp written in C++ Wisp

Why write a lisp?

Lisp is one of those niche, beautiful languages that people only really use to either

  1. Write a lisp interpreter
  2. Show off how "code is data!!!"

So why add to the list of infinite lisp interpreters?

The answer is simple: I'm bored out of my mind in quarantine. If you were looking to find out why this particular lisp is special, you're fresh out of luck.

But isn't the fact that it's a lisp enough?

Lisp

yes.

Syntax and Special Forms

Like every other lisp, this language uses s-expressions for code syntax and data syntax. So, for example, the s-expression (print 5) is both a valid code snippet, and a valid list containing the items print and 5.

When the data (print 5) is evaluated by the interpreter, it evaluates print and 5, and then applies print to 5.

Here's the result.

>>> (print 5)
5
 => 5

That's super cool! But what if we want to define our own functions? We can use the builtin function defun!

; define a function `fact` that takes an argument `n`
(defun fact (n)
  (if (<= n 1)
     1
     (* n (fact (- n 1)))
   ))

Thats awesome! But did you notice anything different about the defun function? It doesn't evaluate its arguments. If the atom fact were evaluated, it would throw an error like so:

>>> fact
error: the expression `fact` failed in scope { } with message "atom not defined"

This is known as a special form, where certain functions "quote" their arguments. We can quote things ourselves too, but the language automatically quotes arguments to special forms itself.

If you want to "quote" a value yourself, you can do it like this.

; quote the s-expression (1 2 3) so it's not evaluated
>>> (print '(1 2 3))
(1 2 3)
 => (1 2 3)

As you can see, quote negates an evaluation. For example, whenever the expression ''a is evaluated, it becomes 'a. This can be useful for when you want to write long lists of data or variable names without wanting to evaluate them as code.

Special Form Argument Evaluations Purpose
(if cond a b) if only evaluates its cond argument. If cond is truthy (non-zero), then a is evaluated. Otherwise, b is evaluated. This special form is the main method of control flow.
(do a b c ...) do takes a list of s-expressions and evaluates them in the order they were given (in the current scope), and then returns the result of the last s-expression. This special form allows lambda functions to have multi-step bodies.
(scope a b c ...) scope takes a list of s-expressions and evaluates them in the order they were given in a new scope, and then returns the result of the last s-expression. This special form allows the user to evaluate blocks of code in new scopes.
(defun name params body) defun evaluates none of its arguments. This special form allows the user to conveniently define functions.
(define name value) define evaluates the value argument, which is then assigned to name in the current scope. This special form allows the user to bind atoms to values in a scope.
(lambda params body) lambda evaluates none of its arguments. This special form allows the user to define anonymous functions.
(quote x) quote evaluates none of its arguments. This is equivalent to the 'expr syntactic sugar.
(for x list ...) for evaluates only its list argument. for iterates through the list storing each element in x, and then evaluating all of the rest of the values in the for body. It then returns the last value evaluated.
(while cond ...) while evaluates only its cond argument. while evaluates its condition expression every iteration before running. If it is true, it continues to evaluate every expression in the while body. It then returns the last value evaluated.

Examples

Here are some example math-y functions to wrap your head around.

; quicksort
(defun qs (l)
    (if (<= (len l) 1)
        l
        (do
            (define pivot (first l))
            (+
                (qs (filter (lambda (n) (> pivot n)) l))
                (list pivot)
                (qs (tail (filter (lambda (n) (<= pivot n)) l)))
            ))
    ))

; decrement a number
(defun dec (n) (- n 1))
; increment a number
(defun inc (n) (+ n 1))
; not a bool
(defun not (x) (if x 0 1))

; negate a number
(defun neg (n) (- 0 n))

; is a number positive?
(defun is-pos? (n) (> n 0))
; is a number negative?
(defun is-neg? (n) (< n 0))

Usage

Using and compiling wisp

Dependencies

Compile with your C++ compiler of choice. This is compatible with all standard versions of C++ since ANSI C++.

$ git clone https://github.com/adam-mcdaniel/wisp
$ cd wisp
$ g++ wisp.cpp -o wisp

Using the binary

Run wisp in interactive mode:

$ ./wisp
>>> (print "Hello world!")
Hello world!
 => "Hello world!"

Interpret a file:

$ ./wisp -f "examples/hello_world.lisp"
Hello world!

Interpret from command line argument:

$ ./wisp -c '(print "Hello world!")'
Hello world!