1 - 0. Evaluation

Starting Moonli either using the included binary or using VS Code will start a Read–Eval–Print Loop (REPL).

In VS Code, you will need to switch to a separate tab that contains the REPL. TODO: Elaborate more on evaluation in VS Code.

MOONLI-USER> 

A REPL is how many interactive programming languages work. It’s a simple cycle: the computer reads what you type, evaluates it (figures out what it means and runs it), prints the result, and then loops back to wait for your next command. For example, if you type 2 + 3 in the REPL, it reads the expression, evaluates it to get 5, prints that result, and waits for more input.

MOONLI-USER> 2 + 3
[OUT]: 5

MOONLI-USER> 

This loop makes programming feel conversational – you can test ideas instantly, explore code step by step, and see exactly how the language thinks and responds.

For any input expression, you can prevent evaluation by prefixing it with $. This is known as quote-ing.

MOONLI-USER> $x
[OUT]: x
MOONLI-USER> $(1 + 2)
[OUT]: (+ 1 2)

Note that for $(1 + 2), even though evaluation has been avoided, the expression is printed as it appears to Moonli internally, just before the step of evaluation. What you type at the REPL is converted to an internal representation that Moonli can then work with.

2 - 1. Literal Objects

In a programming language, a literal object is something the evaluator doesn’t need to compute—it already is its own value. When the REPL reads a literal like 5, “hello”, or #t, the eval step simply returns it unchanged, because it directly represents itself.

For example, since 5 is already a number:

MOONLI-USER> 5
[OUT]: 5

By contrast, when you type an expression like 2 + 3, the result is a new value obtained through the process of evaluation. Literals skip that process entirely.

MOONLI-USER> 2 + 3
[OUT]: 5

Moonli has several kinds of literal objects.

Literal Objects in Moonli

Symbols

A symbol is a name that represents something else, like a label attached to a value or concept. In Moonli, symbols are the basic building blocks of code.

For example, recall the x that you typed at the REPL. It was a symbol. When you had quoted the symbol x, Moonli returned the symbol itself.

MOONLI-USER> $x
[OUT]: x

A special kind of symbols that do not require to be quoted are keywords.

MOONLI-USER> :i-am-a-keyword
[OUT]: :i-am-a-keyword

However, keywords require to be prefixed with a colon :. Note also that symbols in Moonli can contain hyphens.

We will look at symbols in more detail in the next chapter.

Numbers

Recall from high school mathematics, that there can be different kinds of numbers, like integers, and real numbers. Programming languages can also represent different kinds of numbers. Two important ones include integers and floats.

Integers are whole numbers without fractions – like -3, 0, or 42. They’re exact and good for counting or discrete steps.

MOONLI-USER> 42
[OUT]: 42

Floating-point numbers (or floats) represent real numbers that may include decimals – like 3.14 or -0.001. They’re useful for measurements or continuous values but can lose precision because they’re stored in binary form.

When evaluated, both kinds of numbers are literal objects – they evaluate to themselves. So typing 3.14 in a REPL simply returns 3.14, already fully evaluated.

MOONLI-USER> 3.14
[OUT]: 3.14
MOONLI-USER> -0.001
[OUT]: -0.001

Note that it is important that there is no space between - and 0.001 for Moonli to understand it as -0.001.

Strings

Strings are sequences of characters used to represent text – like “hello”, “42”, or “Moonli rocks!”. They’re written between quotation marks so the evaluator knows they’re text, not symbols or code.

MOONLI-USER> "Moonli rocks!"
[OUT]: "Moonli rocks!"

When the REPL reads a string, it treats it as a literal object, meaning it already represents its own value and needs no further evaluation. For example, typing "cat" simply returns "cat".

MOONLI-USER> "cat"
[OUT]: "cat"

Strings can contain letters, digits, spaces, or even special symbols, and most languages let you combine (concatenate) or inspect them with built-in functions. They’re essential for displaying messages, storing words, or communicating with users.

While symbols are useful for working with code, strings are useful for working with text.

Characters

Characters can be understood as blocks of strings. Each string is a sequence of characters. An individual character can be input to the REPL using single quotation marks.

MOONLI-USER> 'a'
[OUT]: #\a

3 - 2. Symbols, Variables, and Values

A critical aspect of programming is building abstractions. The first step to such abstractions involves using variables to stand in for literal values that we studied in the last section.

Programmatically, variables are symbols that can be eval-uated to obtain the value they are bound to. You have already seen a few symbols. Below, x and :i-am-a-keyword are two symbols.

MOONLI-USER> $x
[OUT]: x
MOONLI-USER> :i-am-a-keyword
[OUT]: :i-am-a-keyword

Recall that the $-prefix was used to quote the symbols to prevent their eval-uation. (Recall the initial section on evaluation.) Recall also that keywords are symbols that begin with a colon :, and they do not need to be quoted. What happens when you omit the $-prefix for non-keywords?

MOONLI-USER> x
unbound-variable: The variable x is unbound.

This time, instead of receiving the output, we received an error message. It tells us that the REPL does not know how to evaluate x. We can provide REPL this information by defparameter.

MOONLI-USER> defparameter x = 42
[OUT]: x

This tells the REPL to bind the symbol x to the literal value 400. After this, if you type the unquoted x, even without the $-prefix, you do not receive an error. Instead, the REPL tells you the value that the symbol is bound to.

MOONLI-USER> x
[OUT]: 42

You can check whether a symbol is bound to using boundp. The output t means true. Indeed the symbol x is bound to some value!

MOONLI-USER> boundp($x)
[OUT]: t

On the other hand, unless you had defined the value of y using defparameter beforehand, the symbol y would be unbound. This is indicated by the output nil which means false. The symbol y is not bound to any value.

MOONLI-USER> boundp($y)
[OUT]: nil

The idea of a variable is conceptual. It is something we use to talk about or describe programming. On the other hand, for Moonli (and for Lisps in general), symbols are programmatic objects that we can manipulate. We will revisit this idea later.

The bindings of the variables can be updated. In other words, variables can be assigned new values. This is a frequent part of programming. For example:

MOONLI-USER> x = 84
[OUT]: 84
MOONLI-USER> x
[OUT]: 84

Global and Local

Variables defined using defparameter are global variables.

Global variables are accessible from anywhere within the program. This means anyone can assign them new values or change the values they are assigned to. So, if used frequently, programs can be hard to understand because it will be difficult to figure out where a particular variable is being reassigned.

That is why, the more common approach to using a variable is by using local variables. In Moonli, These can be defined using let and let+. We show an example below.

MOONLI-USER> let a = 1, b = 2:
  a + b
end
[OUT]: 3
MOONLI-USER> let+ a = 1, b = 2:
  a + b
end
[OUT]: 3

One difference between let and let+ is that let can be described as performing parallel binding, while let+ can be described as performing sequential binding.

The following works with let+ but not let.

MOONLI-USER> let+ a = 1, b = a:
  a + b
end
[OUT]: 2

MOONLI-USER> let a = 1, b = a:
  a + b
end
; in: progn (let ((a 1) (b a))
;          (+ a b))
;     (MOONLI-USER::B MOONLI-USER::A)
;
; caught warning:
;   undefined variable: moonli-user::a
;
; compilation unit finished
;   Undefined variable:
;     a
;   caught 1 WARNING condition
unbound-variable: The variable a is unbound.

With let, we get the error that the variable a is unbound. This is because let binds all its variables as if they are made at once. In let a = 1, b = a, we are asking the REPL to bind b to its value at the same time as a. But the value of b is told to be a. At that point in the program, the value of a is unavailable, resulting in the error.

The algorithm for let can be written as:

  1. Compute the values of all the expressions assigned to the variables, without assigning them.
  2. Bind the variables to the values of the respective expressions
  3. Execute the body. (In this case the body is simply a + b. But it could be any valid Moonli code.)
  4. Unbind the variables.

In contrast the algorithm for let+ can be written as:

  1. Bind the first variable to the value of the corresponding expression.
  2. Bind the second variable to the value of the corresponding expression. … Repeat the same for all variables …
  3. Execute the body. (In this case the body is simply a + b. But it could be any valid Moonli code.)
  4. Unbind the variables.

It is generally recommended to use let since it often allows you to think about the value of each variable independently of the other variables. But when you can’t use let, feel free to use let+.

Global variables should have earmuffs

Suppose you see an arbitrary variable in some code. How can you tell whether it is global or local? Different programming languages or projects have different conventions. For Moonli (and Lisps), it is recommended that global variables should have earmuffs *...* around them.

Thus one should write

MOONLI-USER> defparameter *x* = 42
[OUT]: *x*
MOONLI-USER> *x*
[OUT]: 42

Instead of

MOONLI-USER> defparameter x = 42
[OUT]: x

Unbinding using makunbound

One can remove the binding of a variable using makunbound:

MOONLI-USER> makunbound($x)
[OUT]: x
MOONLI-USER> x
unbound-variable: The variable x is unbound.
MOONLI-USER> boundp($x)
[OUT]: nil

While it is okay to use makunbound in the REPL, it is recommended to avoid using it in the code you write and save in files and share with others. Creation, deletion, re-creation is harder to understand that a single creation. Use local variables wherever possible.

Variables and Abstraction

Suppose you had a program to multiply 23 with itself thrice. You program would then be a single file with the following single line of code:

print(23 * 23 * 23)

Now, suppose you wanted to change this program to multiply 47 by itself thrice. You’d need to make changes in three places. The new code would look like this.

print(47 * 47 * 47)

But, with the use of variables, you can achieve the same with just a single change!

The following program multiples 23 with itself thrice:

let x = 23:
  print(x * x * x)
end

It can be changed to multiply 47 with itself thrice by making a single change!

let x = 47:
  print(x * x * x)
end

Instead of saying “multiply 23 by itself”, now you are saying “multiply x by itself (whatever x may be)”. This replacement of something specific (23) by something general (a variable x) is essentially abstraction.

Abstractions can help to keep code simple – it allows code to be reused.

4 - 3. Functions and Abstractions

Besides variables that we studied in the last chapter, the second key element to abstraction are functions.

Even if you write abstract code such as the one in the last chapter, you will need to write it again and again for different values of x:

let x = 23:
  print(x * x * x)
end

let x = 47:
  print(x * x * x)
end

Functions help us get rid of this code repetition and further enable reuse. Functions are characterized by parameters, which are essentially lists of variables. For example, the below code defines a function multiply-thrice that has a single parameter x:

defun multiply-thrice(x):
  print(x * x * x)
end

It takes this parameter, multiplies it with itself thrice, and returns the result.

MOONLI-USER> multiply-thrice(23)

12167
[OUT]: 12167
MOONLI-USER> multiply-thrice(47)

103823
[OUT]: 103823

Thus, the lines of code multiply-thrice(23) or multiply-thrice(47) achieves the same effect as the two blocks of let we used earlier.

The full code looks something like the following:

defun multiply-thrice(x):
  print(x * x * x)
end

multiply-thrice(23)
multiply-thrice(47)

There are many built-in functions that Moonli provides. print is one such function. You may have noticed that the result is printed twice in the REPL. This is because of print. If you omit the print, the REPL would look something like this:

MOONLI-USER> defun new-multiply-thrice(x):
  x * x * x
end
[OUT]: new-multiply-thrice
MOONLI-USER> new-multiply-thrice(23)
[OUT]: 12167
MOONLI-USER> new-multiply-thrice(47)
[OUT]: 103823

A function can span multiple lines of code. By default, it returns the value resulting from the evaluation of the last line of code (excluding end). For our new-multiply-thrice, this refers to the value of x * x * x. For our old multiply-thrice, it would have been print(x * x * x).

print(x * x * x) first multiplies x with itself thrice. Prints the result. And returns it. You can see this in action if you instantiate x with a concrete value such as 23 or 47:

MOONLI-USER> print(23 * 23 * 23)
12167
[OUT]: 12167
MOONLI-USER> print(47 * 47 * 47)
103823
[OUT]: 103823

Since print(x * x * x) was the last line of muliply-thrice, the return value of multiply-thrice is the same as the return value of print(x * x * x).

MOONLI-USER> multiply-thrice(23)

12167
[OUT]: 12167
MOONLI-USER> multiply-thrice(47)

103823
[OUT]: 103823

Similarly, the following code defines an add function that takes in two parameters x and y. It multiplies the values of these parameters and returns the result.

MOONLI-USER> defun add(x,y):
  x + y
end
[OUT]: add
MOONLI-USER> add(2,3)
[OUT]: 5

Error: Invalid number of arguments

What happens if you call add with just a single argument?

MOONLI-USER> add(2)
simple-program-error: invalid number of arguments: 1
Backtrace for: #<SB-THREAD:THREAD tid=259 "main thread" RUNNING {7005490613}>
0: (MOONLI-USER::ADD 2) [external]
1: (SB-INT:SIMPLE-EVAL-IN-LEXENV (MOONLI-USER::ADD 2) #<NULL-LEXENV>)
2: (SB-INT:SIMPLE-EVAL-IN-LEXENV (PROGN (MOONLI-USER::ADD 2)) #<NULL-LEXENV>)
...

The first line of this says that there was an error in the code you asked the REPL to evaluate. In particular, the code has invalid number of arguments. Similarly, what happens when if you call multiply-thrice with three arguments? This results in a similar error:

MOONLI-USER> multiply-thrice(2,3,4)
simple-program-error: invalid number of arguments: 3
Backtrace for: #<SB-THREAD:THREAD tid=259 "main thread" RUNNING {7005490613}>
0: (MOONLI-USER::MULTIPLY-THRICE 2 3 4) [external]
1: (SB-INT:SIMPLE-EVAL-IN-LEXENV (MOONLI-USER::MULTIPLY-THRICE 2 3 4) #<NULL-LEXENV>)
...

Calling other functions

Notice how you called print function from inside the multiply-thrice function? Calling other functions from one function is very common. You can also write another function that uses both new-multiply-thrice and add:

defun multiply-thrice-and-add(x,y):
  let x-cubed = new-multiply-thrice(x),
      y-cubed = new-multiply-thrice(y):
    add(x-cubed, y-cubed)
  end
end

The return value of multiply-thrice-and-add is determined by add(x-cubed, y-cubed) since that is the last line of code (excluding end).

You can see what this function is doing in more detail by printing the values of the variables in intermediate steps. This can be done using the format function. We will explain format in more detail later:

defun multiply-thrice-and-add(x,y):
  let x-cubed = new-multiply-thrice(x),
      y-cubed = new-multiply-thrice(y):
    format(t, "x-cubed has value: ~a~%", x-cubed)
    format(t, "y-cubed has value: ~a~%", y-cubed)
    add(x-cubed, y-cubed)
  end
end

You can see the intermediate steps:

MOONLI-USER> multiply-thrice-and-add(2,3)
x-cubed has value: 8
y-cubed has value: 27
[OUT]: 35

Usually, writing something as a function instead of repeating it needlessly everywhere is helpful because it enables reuse.

5 - 4. Packages and Namespaces

Suppose you write some code and share it with other people. Or you want to use code written by other people. It may happen that both of you are using the same function names but are doing different things. For example, your process-data function might be doing something different that somebody else’ process-data function.

To deal with this, most modern languages implement the concept of namespaces. The implementation of namespaces in Moonli (and Common Lisp) is made by a data structure called package. One way to find or identify packages is using strings.

MOONLI-USER> find-package("CL")
[OUT]: #<package "COMMON-LISP">
MOONLI-USER> find-package("COMMON-LISP")
[OUT]: #<package "COMMON-LISP">
MOONLI-USER> find-package("MOONLI")
[OUT]: #<package "MOONLI">

The above says that "CL" and "COMMON-LISP" are both two different names of the same package. On the other hand, the string "MOONLI" identifies a different package. Note that package names are case sensitive. This means "moonli" and "MOONLI" identify different packages. In fact, if you are following the tutorial, so far there is no package named "moonli", but only the package named "MOONLI":

MOONLI-USER> find-package("moonli")
[OUT]: nil

A package maps symbol names to symbols. Symbol names are again strings. You can use find-symbol to find a symbol in a particular package. For example, the following finds the symbol named "LIST" in package named "CL".

MOONLI-USER> find-symbol("LIST", "CL")
[OUT]: list
:external

Note that the second argument to find-symbol can be either a package name (a string) or the package itself, for example:

MOONLI-USER> let pkg = find-package("CL"):
  print(pkg)
  find-symbol("LIST", pkg)
end

#<package "COMMON-LISP">
[OUT]: list
:external

In fact, the second argument is optional. When you do not supply it, find-symbol finds the symbol in the package bound to the special variable *package*.

MOONLI-USER> find-symbol("LIST", "MOONLI-USER")
[OUT]: list
:external
MOONLI-USER> *package*
[OUT]: #<package "MOONLI-USER">
MOONLI-USER> find-symbol("LIST")
[OUT]: list
:external

You may wonder whether the symbol list above belongs to the package named "MOONLI-USER" or to the package named "COMMON-LISP" given that they look identical. To identify the package which a symbol belongs to, you can use the function symbol-package. Note that the symbol list is quoted by prefixing it with $, since we want to refer to the symbol itself and not the value it is bound to:

MOONLI-USER> symbol-package($list)
[OUT]: #<package "COMMON-LISP">

This says that the symbol list belongs to the package "COMMON-LISP". To confirm:

MOONLI-USER> let sym = find-symbol("LIST", "MOONLI-USER"):
  symbol-package(sym)
end
[OUT]: #<package "COMMON-LISP">

Indeed, the symbol named "LIST" in package "MOONLI-USER" has as its package "COMMON-LISP"!

In fact, packages provide much more than simple mapping. They provide a way to organize the symbols themselves. Packages also have the notion of internal and external symbols. Above, the second return value :external shows that the list symbol is external to the respective packages.

While writing your own code, you should define your own package as the first step. You can define new packages using defpackage. This takes in a number of options, a few of them include:

  • :use: which specifies preexisting packages the new package should use. This means that all the external symbols of these packages will be used in the new package.
  • :export: which specifies which should be the external symbols of this new package. These are the symbols that you want to make available to other users of your code.
MOONLI-USER> defpackage "TUTORIAL"
  :use "CL", "MOONLI-USER";
  :export "ADD";
end
[OUT]: #<package "TUTORIAL">

This specifies that the new package named "TUTORIAL" uses the external symbols of the existing packages "CL" and "MOONLI-USER". It also specifies that the package "TUTORIAL" has a symbol named "ADD" as an external symbol. We can switch to the new package using in-package.

MOONLI-USER> in-package tutorial
[OUT]: #<package "TUTORIAL">
TUTORIAL>

You will notice that the prompt MOONLI-USER> has changed to TUTORIAL>. The prompt also indicates the which package you are currently in. Once you have defined a new package, you can use in-package to switch to that package (or any other) no matter in which file you are in. This does not require quoting, because in-package is a special form (also called a macro).

You may note that we had said package names are case sensitive. Yet, we used the symbol tutorial to refer to the package named "TUTORIAL". In Moonli, the symbols are related to their names by case inversion. This is for backward compatibility with Common Lisp and to enable the use of Common Lisp libraries with minimal disruption. The Common Lisp convention is to distinguish symbols not by their case, but by making the names themselves distinct regardless of case. However, Moonli wants to provide better names while interfacing with C and Python libraries, thus, the Moonli parser is case sensitive.

The external symbols from another package can be used by prefixing the symbol name with the package name and using a colon : as a separator (without spaces). For example, the function ensure-list in package alexandria takes any object, and if it is not a list, it wraps it into a list. You can access this symbol using alexandria:ensure-list.

TUTORIAL> listp((1,2,3))
[OUT]: t
TUTORIAL> alexandria:ensure-list((1,2,3))
[OUT]: (1 2 3)
TUTORIAL> alexandria:ensure-list("a")
[OUT]: ("a")
TUTORIAL> alexandria:ensure-list(42.0)
[OUT]: (42.0)