The Lust Core

This document describes the Lust core language and should familiarize you with what Lust can do without its standard library.


In lust all functions are anonymous. In effect this means that functions have similar semantics to normal pieces of data like strings and numbers. Here's a simple function that takes zero arguments and prints "hello":

(fn () (println 'hello))
Functions have the form (fn ARG-LIST BODY) where ARG-LIST is a list of symbols and BODY is an expression to evaluate when the function is called.

When a function is called two things happen. First, a new environment is created where each symbol in the function's ARG-LIST is bound to its corresponding value in the function call. Second, the body is executed in the new environment. The value yielded by that evaluation is returned from the function.

To call a function place it at the front of a list and then place its arguments afterwards. For example, consider the following Lust REPL session:

lust> ((fn (a b) (add a b)) 1 2)
=> 3

Let's walk through what happened there. First, Lust saw that it was evaluating a list so it looked in the first position for the function to call. In the first position it saw (fn (a b) (add a b)) which it recognized to be a user defined function. Having determined it was calling that function, it bound its arguments 1 and 2 to the symbols a and b respectively and then evaluated the function's body, returning 3.

Varadic Functions

In Lust functions can also take a variable number of arguments. We call functions that do this varadic functions. In order to declare a function that takes a variable number of arguments we can add an & symbol before the last argument in the function. If Lust see's the & symbol in that position it will then bind all remaining arguments to the function to the last value in the form of a list.

Lets take a look at this in action:

lust> ((fn (& vargs) vargs) 1 2)
=> (1 2)

As we can see, because we used the & symbol, all of the remaining arguments after binding the preceding ones were bound to vargs in the form of a list.

Varadic functions are a very powerful part of Lust and enable the creation of many very interesting functions. You can take a look at the standard library function +. In the meantime, here's an example REPL session to demonstrate some additional varadic function capabilities:

lust> ((fn (first & rest) first) 1 2)
=> 1
lust> ((fn (first & rest) rest) 1 2)
=> (2)
lust> ((fn (first & rest) rest) 1)
=> ()


In Lust variables are a way of referring to a piece of data. When you evaluate a variable you'll get the piece of data that the variable is referring to. When you declare a variable it will only be accessible in the scope that you declare it to be in. Lust has two ways to chose that scope:

  1. Using the let function to declare a variable in the current local scope
  2. Using the set function to declare a variable in the current global scope
Scopes are created whenever you perform a new function call. Function arguments are always in the local scope.

Here are some examples of variable scoping in Lust:

Control Flow

The Lust core has only one control flow expression, that being the venerable if expression. If expressions are in the form (if COND THEN ELSE). Their evaluation is pretty standard, but if you're interested in the details, they are evaluated as follows:

  1. The COND expression is evaluated
  2. If COND did not evaluate to the empty list, the THEN expression is evaluated in a new enviorments with a parent environment equal to the enviorments in which COND was evaluated.
  3. If COND did evaluate to the empty list, the ELSE expression is evaluated in a new enviorments and with a parent enviorments equal to the enviorments in which COND was evaluated.
  4. The return value of the if statement is the result of evaluating THEN or ELSE, depending on which one was chosen.

Here's an example of if expression evaluation:

lust> (if 1 2 3)
=> 2
lust> (if () 2 3)
=> 3


Lust has single line comments and comments are entirely ignored by the evaluator. Comments start with a semicolon and cause the rest of their line to be ignored. Here's an example:

lust> (if 1 2 3) ; this is a comment
=> 2


Everything in Lust is a list. When the evaluator sees a list it expects the first item in the list to be a function and the remaining items to be arguments for that function.

Here are some examples of lists:

(add 1 2)
(fn (a b c) c)
(() (1 2 3) 'hello (()))

The Empty List

The exception to that rule is the empty list. When the evaluator sees an empty list it evaluates it to another empty list. The empty list is the only value in Lust that is considered to be false. See control flow for an example of this.

Operations On Lists

Because Lists are such an essential part of Lust, Lust has three powerful functions to assist in manipulating them car, cdr, and cons. If you've used a Lisp language before these will be very familiar.

  1. car takes one argument, a list, and returns the first item in that list. If the list is empty it just returns the empty list.

    lust> (car '(1 2 3))
    => 1
    lust> (car '())
    => ()
  2. cdr takes one argument, a list, and returns a new list containing all but the last item in the list.

    lust> (cdr ())
    => ()
    lust> (cdr '(1 2 3))
    => (2 3)
  3. cons takes two arguments, an item and a list and returns a new list consisting of the first argument prepended to the second.

    lust> (cons 1 ())
    => (1)
    lust> (cons 1 '(2 3))
    => (1 2 3)


Numbers in Lust probably work exactly the way you expect. All numbers in Lust are represented by 32 bit floating point numbers. You can perform a variety of operations on numbers with the Lust core. Here are some example numbers:

lust> -1.5
=> -1.5
lust> 10
=> 10
lust> 1.5
=> 1.5
lust> 300000000
=> 300000000
lust> -40.6
=> -40.6

Operations On Numbers

The Lust core supports addition, subtraction, multiplication, and division of numbers. Lust then leaves it up to the standard library to extend these operations. Here are some examples of operations on numbers:

lust> (add 1 1)
=> 2
lust> (div 5 2)
=> 2.5
lust> (mul 2 5)
=> 10
lust> (sub 2 1)
=> 1


In Lust strings are syntactic sugar for lists of characters. If you print a list of characters Lust will figure out that you mean to print a string and print a regular string. Here's an example demonstrating that behavior:

lust> (let foo "hello")
=> "hello"
lust> (let foo (cons 1 foo))
=> (1 'h' 'e' 'l' 'l' 'o')
lust> foo
=> (1 'h' 'e' 'l' 'l' 'o')
lust> (let foo "hello")
=> "hello"
lust> (println foo)
=> ()
lust> (let foo (cons 1 foo))
=> (1 'h' 'e' 'l' 'l' 'o')
lust> (println foo)
(1 'h' 'e' 'l' 'l' 'o')
=> ()

Notice that before we appended a number to foo Lust recognized that foo was a string and displayed it as such.


You might notice that despite strings being lists of characters, you can't directly input characters into Lust. For example, 'a' won't parse as a character. This is not an intentional design decision as much as it is just a feature that hasn't been implemented yet.

Luckily enough for us though, Lust's flexibility makes it easy enough to write a macro to take a string with one character and convert it to a char. Here it is:

(let char (macro (s)
     	   `(if (eq (len ,s) 1)
	       (car ,s)
	     (error "can not convert to char"))))


In Lust, quotation is a way to indicate to the evaluator that an expression should evaluate to itself. Quotation can be accomplished via regular function calls and via some nice syntactic sugar. Let's start with a small example. Notice how below when foo is evaluated normally it evaluates to the value it references, but when it is quoted it just evaluates to foo.

lust> (let foo 10)
=> 10
lust> foo
=> 10
lust> (quote foo)
=> foo

In the above example we used the quote function which takes an argument, and does nothing to it. Another way to use the quote function is to prepend a ' symbol to something. Here's that same example but rewritten to use the quote special form:

lust> (let foo 10)
=> 10
lust> foo
=> 10
lust> 'foo
=> foo

There are times where quotation isn't quite what we want though. Sometimes we want to quote part of an expression but not the rest. For example, say we'd like to write a importq function that doesn't evaluate its first argument so we're not always needing to quote it. A first attempt might look something like this:

(set 'importq (macro (target value)
           '(import (quote target) value)))

This looks good, but sadly does not work. Expanding an invocation of it in the REPL looks like this:

lust> (macroexpand (importq foo))
=> (import (quote target))

What we really want to do here is selectively evaluate parts of this list. Specifically, we'd like to evaluate target and value so that the macro expands to the code we expect. Quote alone won't do that, but lucky for us, Lust has the quaziquote function.

quaziquote allows us to quote most of a list. When a list is quaziquoted it everything inside will be evaluated except calls to a special comma function which behaves the same as eval when inside a quaziquote and runs while nothing else does. quaziquote has ` as its syntactic sugar and comma has ,. We can use quaziquote to fix our letq function as follows:

(set 'importq (macro (target value)
           `(import (quote ,target))))
           ;; (quaziquote (import (quote (comma target))))

lust> (macroexpand (importq foo))
=> (import (quote foo))


Macros are probably the most powerful and interesting part of Lust. This manual saves the best for last. Macros are functions that are evaluated before the evaluator actually runs and who's arguments aren't evaluated when passed in. In other languages this is essentially what the preprocessor does, but in Lust and most Lisp languages because lists are simultaneously code and data, it is trivial to use a macro to generate complex code at compile time.

Macro's are probably best demonstrated with an example. Let's take a look at the when macro from the Lust homepage. Here's what it looks like:

(setq when (macro (cond body) `(if ,cond ,body ())))

Let's walk through how something like (when 1 2) is evaluated.

  1. The when macro is evaluated. In this case it returns a list in the form (if 1 2 ())
  2. After the macro has been evaluated, the evaluator runs on the generated code. In this case the evaluator sees (if 1 2 ()) and returns 1 as 1 is a truthy value in Lust.

This seems simple, and it is, but it can take quite some time to wrap your head around. If you're still confused I've found this write up to be extremely helpful. For debugging macros you'll also find the macroexpand function to be helpful.


macroexpand is a function that expands and does not evaluate a macro. It can be extremely useful when debugging issues with macros. Here's an example of it being used in the Lust REPL.

lust> (when 1 2)
=> 2
lust> (macroexpand (when 1 2))
=> (if 1 2 ())