1 - Installation

Method 1: Binaries - Best for checking out

Grab a binary from the latest release. If you want an interactive REPL as in the above gif, grab the binaries with "repl" in their name.

If you want to run a few moonli files, grab the regular binaries.

However, the repl binaries depend on readline. This can be installed as follows:

  • Ubuntu: sudo apt install libreadline-dev
  • Mac OS: brew link readline --force
  • Windows: pacman -S mingw-w64-x86_64-readline

In case of any installation issues, please create an issue on github or gitlab.

Method 2: From fresh compilers - Best for serious development

Step 0. Install a package manager for your OS

Mac OS: brew

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Windows:

  • MSYS2: this will install the pacman package manager
  • Or choco

Step 1. Install a Common Lisp compiler and some tools

  • Ubuntu: sudo apt install git sbcl
  • Mac OS: brew install git sbcl
  • Windows:
    • pacman -S git mingw-w64-x86_64-sbcl
    • choco install git sbcl

Once this step is successful, you should be able to type sbcl --help and see something similar to the following:

Usage: sbcl [runtime-options] [toplevel-options] [user-options]
Common runtime options:
  --help                     Print this message and exit.
  --version                  Print version information and exit.
  --core <filename>          Use the specified core file instead of the default.
  --dynamic-space-size <MiB> Size of reserved dynamic space in megabytes.
  --control-stack-size <MiB> Size of reserved control stack in megabytes.
  --tls-limit                Maximum number of thread-local symbols.

Common toplevel options:
  --sysinit <filename>       System-wide init-file to use instead of default.
  --userinit <filename>      Per-user init-file to use instead of default.
  --no-sysinit               Inhibit processing of any system-wide init-file.
  --no-userinit              Inhibit processing of any per-user init-file.
  --disable-debugger         Invoke sb-ext:disable-debugger.
  --noprint                  Run a Read-Eval Loop without printing results.
  --script [<filename>]      Skip #! line, disable debugger, avoid verbosity.
  --quit                     Exit with code 0 after option processing.
  --non-interactive          Sets both --quit and --disable-debugger.
Common toplevel options that are processed in order:
  --eval <form>              Form to eval when processing this option.
  --load <filename>          File to load when processing this option.

User options are not processed by SBCL. All runtime options must
appear before toplevel options, and all toplevel options must
appear before user options.

For more information please refer to the SBCL User Manual, which
should be installed along with SBCL, and is also available from the
website <http://www.sbcl.org/>.

Step 2. Install ocicl

Follow the instructions at https://github.com/ocicl/ocicl.

Briefly:

  • MacOS: brew install ocicl && ocicl setup
  • Others:
git clone https://github.com/ocicl/ocicl
cd ocicl
sbcl --load setup.lisp
ocicl setup

Edit the compiler init file (eg. ~/.sbclrc) to include the current directory, or

#-ocicl
(when (probe-file #P"/Users/user/.local/share/ocicl/ocicl-runtime.lisp")
  (load #P"/Users/user/.local/share/ocicl/ocicl-runtime.lisp"))
(asdf:initialize-source-registry
 ;; (list :source-registry
 ;;       (list :tree (uiop:strcat (uiop:getenv "HOME") "/Common Lisp/"))
 ;;       :inherit-configuration)
 (list :source-registry
       (list :directory (uiop:getcwd))
       :inherit-configuration))

Step 3. Install Moonli

  1. 3.1. Obtain the source

    git clone https://github.com/moonli-lang/moonli
    
  2. 3.2. Start the REPL

    sbcl --eval '(asdf:load-system "moonli/repl")' --eval '(cl-repl:main)'
    
  3. 3.3a. (Optional) Building basic binary

    The following should create a moonli binary in the root directory of moonli.

    (asdf:make :moonli)
    
    ./moonli --help
    A basic moonli transpiler over SBCL
    Available options:
      -h, --help                 Print this help text
      -l, --load-lisp ARG        Load lisp file
      -m, --load-moonli ARG      Load moonli file
      -t, --transpile-moonli ARG
                                 Transpile moonli file to lisp file
    
  4. 3.3b. (Optional) Build REPL

    The following should create a moonli.repl binary in the root directory of moonli.

    (asdf:make :moonli/repl)
    

Step 4. Set up VS Code

Install VS Code or VSCodium if they are not already installed.

Once the editor is installed, install the Alive Moonli extension.

2 - Tutorial

2.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.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

2.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.

2.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.

2.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)

2.6 - 5. Systems and Libraries

Once you have someone else’ code, you need to tell the REPL how to load their code. Or when you share your code with someone, you also need to tell them how to tell their REPL to load your code. At its simplest, the shared code is in the form of a single file. You can load a Moonli file using moonli:load-moonli-file and Common Lisp files using cl:load:

MOONLI-USER> moonli:load-moonli-file("my-code.moonli")
...
MOONLI-USER> cl:load("my-code.lisp")
...

However, often times, shared code is in the form of multiple files. The shared code is essentially what is called a library. Different programming languages have different ways of defining or sharing libraries. In Moonli (and Common Lisp), libraries and tools for managing them are implemented in ASDF.

ASDF stands for Another System Definition Facility and is the standard build-and-load system for Common Lisp. It lets you group files into systems, declare dependencies (on other libraries), and provide metadata (author, version, license, etc.).

If you’re writing a library (or project) that others might use — or you want to reuse your own code across different projects — you put an .asd file at the root describing that system.

Anatomy of a system definition

At the root of your library directory, create a file named .asd. Example of my-awesome-lib.asd:

;;; my-awesome-lib.asd
(defsystem "my-awesome-lib"
  :version "0.1.0"
  :author "Your Name <you@example.com>"
  :description "Utilities for awesome tasks"
  :license "MIT"
  :depends-on ("alexandria")
  :serial t
  :pathname "./"
  :components ((:moonli-file "package")
               (:moonli-file "core")
               (:moonli-file "utils")))

Here:

  • “my-awesome-lib” is the system name. ASDF expects the file my-awesome-lib.asd.
  • :components lists the source files (without extension). ASDF will compile and load them in the right order according to dependencies.
  • You can declare external dependencies via :depends-on, naming other ASDF systems your code needs. For example, the above system (= library) says that it depends on a library called “alexandria”.

Optionally you can include metadata such as version, author, license, long description. This is useful for distribution/packaging if you share code.

Putting code and system together

The above definition corresponds to the following directory structure:

my-awesome-lib/
  ├── my-awesome-lib.asd
  ├── package.moonli       ;; package definition
  ├── core.moonli          ;; core functions
  └── utils.moonli         ;; utility functions

Loading and using your system

Once your .asd is in a directory ASDF can find, you can load your library from the REPL:

MOONLI-USER> asdf:load-system("my-awesome-lib")
...

ASDF ensures that the dependencies (in this case, alexandria) and their dependencies are loaded exactly once in the right order. Once the dependencies are loaded, ASDF loads the :components of your system in :serial order. This means, ASDF will first load package.moonli, then core.moonli and finally utils.moonli.

That will compile and load the system. From there you can (in-package :my-awesome-lib) (or whatever package you defined) and use its functions.

Systems and Packages

Note that there are two senses of systems. Firstly, they are a collection of moonli (or lisp) files, along with an .asd file. However, this information is also available within the language in the form of an object:

MOONLI-USER> asdf:find-system("my-awesome-lib")
[OUT]: #<system "my-awesome-lib">

MOONLI-USER> describe(*)

#<system "my-awesome-lib">
  [standard-object]

Slots with :instance allocation:
  name                           = "sample-asdf"
  source-file                    = #P"/Users/user/moonli/my-awesome-lib/my-awesome-lib.asd"
  definition-dependency-list     = nil
  definition-dependency-set      = {..
  version                        = nil
  description                    = nil
  long-description               = nil
  sideway-dependencies           = nil
  if-feature                     = nil
  in-order-to                    = nil
  inline-methods                 = nil
  relative-pathname              = #P"/Users/user/moonli/my-awesome-lib/"
  absolute-pathname              = #P"/Users/user/moonli/my-awesome-lib/"
...

You can reload the library by supplying additional option :force as t:

MOONLI-USER> asdf:load-system("my-awesome-lib", :force, t)
...

Configuring where ASDF looks for systems

By default, ASDF will search some standard directories (e.g. ~/common-lisp/) for .asd files.

If you prefer a custom layout, you can configure ASDF’s “source registry” by creating a configuration file (e.g. in ~/.config/common-lisp/source-registry.conf.d/) that tells ASDF to scan your custom code directories. asdf.common-lisp.dev

Once configured, ASDF will find your libraries automatically — so you just load them by name, without worrying about full paths.

Systems and Packages

It’s useful to remember the conceptual distinction:

  • A package (in Moonli or Common Lisp) groups symbols (names)
  • A system (in ASDF) groups files, code, dependencies, metadata

You use packages to manage symbol namespaces in code, and ASDF systems to manage your project’s files and dependencies.

Package Managers

ASDF defines how systems are organized and loaded, but it doesn’t tell you where to get them. This is the job of library managers. Below, we list a few of them

1. Quicklisp - The Standard Library Manager

Quicklisp is the de-facto ecosystem for installing and managing Common Lisp libraries. It provides:

  • A large, curated set of stable libraries
  • Automatic dependency resolution
  • One-line installation and loading
  • A reproducible snapshot each month

Once you have quicklisp installed and loaded, you can install new libraries simply by quickload-ing them:

ql:quickload("dexador")

Quicklisp automatically configures ASDF to locate the offline library after it is downloaded. Thus, ASDF knows where the system lives, so you can use it in your own project’s :depends-on.

2. Ultralisp - A Fast, Community-Driven Repository

Ultralisp is a complementary distribution to Quicklisp.

It focuses on:

  • Very fast updates (often every few minutes)
  • Automatically including systems from GitHub/GitLab
  • A broader, more experimental set of libraries

After installing quicklisp, you can install ultralisp simply by:

ql-dist:install-dist("http://dist.ultralisp.org/")

When to use Ultralisp

Use it when:

  • You want bleeding-edge versions of a library
  • You need something not yet in Quicklisp
  • You want Quicklisp-style automatic dependency handling but with faster turnaround

Moonli developers can publish their own libraries to Ultralisp to reach users quickly.

3. OCICL - OCI-based ASDF system distribution

OCICL (pronounced osicl) is a more modern alternative to quicklisp and ultralisp. In addition to loading libraries, it also provides tools to version-lock the libraries.

Summary

ToolPurposeWhen to Use
ASDFBuild system and system loaderEvery project; defines your system structure
QuicklispStable, curated dependency managerMost users; everyday library installation
UltralispRapid updates, large community indexFast-moving libraries; newest versions
OCICLModern alternative that also allows dependency-lockingProviding reproducible build environments

2.7 - 6. Classes and Methods

Recall that while discussing literal objects, we discussed different kinds of literal objects. Each object in Moonli (and Common Lisp) has a class.

class-of("hello")
#=> #<built-in-class simple-character-string>

class-of(2)
#=> #<built-in-class fixnum>

class-of(42.0)
#=> #<built-in-class single-float>

This corresponds to how the object is implemented in the programming environment. Classes have instances. For example, above,

  • the string "hello" is an instance of the built-in class simple-character-string
  • 2 is an instance of the built-in class fixnum
  • 42.0 is an instance of the built-in class single-float

Most programming languages provide a way for the user to define their own classes. A paradigm of programming that centers around classes and objects is known as Object-Oriented Programming. This involves defining new classes of objects that mimic the structure of the real-world you want to represent.

For example, suppose we want to program basic geometry. We can start with a class rectangle:

defclass rectangle():
  slots:
    length:
      accessor: height,
      initarg: :height;
    breadth:
      accessor: breadth,
      initarg: :breadth;
  end
end

This creates a class with two slots: height and breadth.

We can create an instance using the following. Note that :height and :breadth were specified as the respective initarg.

defparameter *shape-1* = make-instance($rectangle, :height, 6, :breadth, 3)

And access the slots using the specified accessor.

height(*shape-1*)
#=> 6
breadth(*shape-1*)
#=> 3

Methods and generic functions

You may want some functions to have different behaviors depending on the class of the object they are called with. For example, area of a triangle may be computed differently than a square, which in turn may be computed differently than a circle.

We can achieve this by using generic functions. A generic function can be declared using:

defgeneric area(shape)

This introduces the function name but not its behavior yet.

Defining methods

A method specializes a generic function on specific classes. Below, the generic function area is specialized with the first argument shape being of class rectangle.

defmethod area(shape :: rectangle):
  height(shape) * breadth(shape)
end

We can call it just like any other normal function.

area(*shape-1*)
#=> 18

Moonli dispatches to the correct method based on the argument’s class.

Extensibility

We can also define a class and a method corresponding to a circle class:

defclass circle():
  slots:
    radius:
      initarg: :radius,
      accessor: radius;
  end
end

defmethod area(shape :: circle):
  pi * radius(shape) ^ 2
end

pi is a constant provided by Moonli (and Common Lisp).

pi
#=> 3.141592653589793d0
defparameter *shape-2* = make-instance($circle, :radius, 7)

area(*shape-2*)

Note how we were able to extend the generic function area without touching the earlier implementations. This extensibility is one of the crucial benefits provided by generic functions.

Multiple dispatch

Moonli (and Common Lisp) support multiple dispatch. This means methods can specialize on not just one parameter, like Python and Java do, but multiple parameters at once(!)

# Check whether shape-in fits inside shape-out
defgeneric fits-inside-p(shape-in, shape-out)

The following specialized fits-inside-p on rectangle and rectangle.

defmethod fits-inside-p(s1 :: rectangle, s2 :: rectangle):
  let h1 = height(s1), h2 = height(s2),
      b1 = breadth(s1), b2 = breadth(s2):
    if ((min(h1,b1) <= min(h2,b2)) 
        and (max(h1,b1) <= max(h2,b2))):  
      t
    else:
      nil
    end
  end 
end

While the following specializes fits-inside-p on rectangle and circle:

defmethod fits-inside-p(s1 :: rectangle, s2 :: circle):
  let h = height(s1), b = breadth(s1), r = radius(s2):
    let d = sqrt(h ^ 2 + b ^ 2):
      if (d <= 2 * r):
        t
      else:
        nil
      end if
    end let
  end let
end defmethod

Inheritance

Classes can inherit slots from its super classes. To help us organize our code better, we can define a shape class with a slot label which we want to be common across all shapes.

defparameter *shape-index* = -1;

defclass shape():
  slots:
    index:
      initarg: :index,
      reader: index,
      initform: incf(*shape-index*);
  end
end

We can now add this label slot to the rectangle and circle class above by redefining rectangle and circle to have shape as one of its superclasses.

Dynamic Redefinition

Before we see how to specify shape as one of the direct superclass of rectangle, let us briefly ponder over *shape-1 we had defined earlier:

describe(*shape-1*)

#<rectangle {700A674383}>
  [standard-object]

Slots with :instance allocation:
  length                         = 6
  breadth                        = 3

What do you think will happen to the rectangle instance bound to *shape-1* if we redefine rectangle?

We can redefine rectangle using:

defclass rectangle(shape):
  slots:
    length:
      accessor: height,
      initarg: :height;
    breadth:
      accessor: breadth,
      initarg: :breadth;
  end
end

This specifies shape as one of the direct superclasses of rectangle class.

Now, if you check the object bound to *shape-1* once more, you will find that the index slot has already been added!

describe(*shape-1*)

#<rectangle {700A674383}>
  [standard-object]

Slots with :instance allocation:
  index                          = 0
  length                         = 6
  breadth                        = 3

We can repeat the same with the circle class.

  1. Check the object bound to *shape-2* before update:

    describe(*shape-2*)
    
    #<circle {700A497AB3}>
      [standard-object]
    
    Slots with :instance allocation:
      radius                         = 7
    
  2. Update the circle class to include shape in its list of direct superclasses.

    defclass circle(shape):
      slots:
        radius:
          initarg: :radius,
          accessor: radius;
      end
    end
    
  3. Check the object bound to *shape-2* after update:

    describe(*shape-2*)
    
    #<circle {700A497AB3}>
      [standard-object]
    
    Slots with :instance allocation:
      index                          = 1
      radius                         = 7
    

Now that shape is a superclass of circle and rectangle, you can also add other slots that would be common across all shapes to the shape class. Perhaps, this could be center-location, or color, or something else. These changes will be automatically propagated to all instances of the subclasses of shape. You do not need to restart your Moonli REPL or load all files again! You can play with classes and their instances very much on the fly.

Of course, once you have reached a state where the code in the REPL reflects what you had in mind, you also want to make sure the code is written down in the files in appropriate order. This is necessary both for sharing it with others, as well as for your own self when you restart the REPL.

But, by and large, Moonli (and Common Lisp) provide a very interactive object system. There are also a large number of options for more fine-grained control for object updation as well as initiation. But these are outside the scope of this tutorial, and readers are requested to consult to appropriate resources to learn and explore more on these topics.

Method modifiers

A last point of note would be method modifers. Methods can be modified by prefixing their names with :before, :after, and :around modifiers to customize method behavior.

Example:

defmethod :before fits-inside-p(s1, s2):
  format(t, "Checking if ~S fits inside ~S...", s1, s2)
end
  
defmethod :after fits-inside-p(s1, s2):
  format(t, "Done~%")
end

:before methods run before the main methods. :after runs after the main methods.

Metaclasses

One can find the class associated with a symbol using find-class:

find-class($string)
#=> #<built-in-class common-lisp:string>

find-class($rectangle)
#=> #<standard-class rectangle>

Further, in Moonli (and Common Lisp), one can find the class of the class by using find-class followed by class-of.

class-of(find-class($string))
#=> #<standard-class built-in-class>

class-of(find-class($rectangle))
#=> #<standard-class standard-class>

Class of a class is called a metaclass. They define how the class itself behaves. We do not dive into metaclasses in this tutorial. But to note, many Common Lisp implementations (and thus, Moonli) provide what is called a Meta-Object Protocol, which can be used to modify the behavior of classes, their instances, and methods and generic functions.

Here, we merely point to the existence of metaclasses. The built-in-class is one metaclass and standard-class is another. In the next chapter, we will dive into objects and classes corresponding to the metaclass structure-class.

Summary

Moonli transpiles directly to Common Lisp’s CLOS:

  • Methods belong to generic functions, not classes – encouraging extensible design.
  • You get full multiple dispatch.
  • Classes and methods can be redefined at the REPL.
  • Multiple inheritance is allowed and sane.
  • Method combination allows fine-grained customization of behavior.

Moonli gives you a simple, readable syntax while inheriting the dynamic power of the Lisp object system beneath it.

2.8 - 7. Structures and Performance

As we saw last, classes in Moonli (and Common Lisp) are very dynamic. A lot many things take place at run-time. Unfortunately, this also incurs a run-time cost.

In some cases, you may not need all that dynamicity, but may instead need better performance. This is achieved through structures.

A structure is a simple container for data. They have fixed fields and no multiple inheritance. Structure of instances of classes with metaclass structure-class. For example, the below defines an instance point of structure-class.

If you are using the Moonli REPL for trying out the code in this tutorial series, you should start the REPL with --enable-debugger option for this tutorial. You can also do this by starting the REPL the usual way and then calling cl-repl:enable-debugger().

defstruct point:
  x = 0;
  y = 0;
end

This defines:

  • a constructor make-point(:x, …, :y, …)
  • accessors point-x(obj) and point-y(obj)
  • a predicate point-p
  • a printed representation

Creating a structure instance:

defparameter *point* = make-point(:x, 3, :y, 4)

Accessing fields:

point-x(*point*)
#=> 3

point-y(*point*)
#=> 4

point-p(*point*)
#=> t

Redefining structures

There’s no standard way to redefine structures. If you redefine point class to include a third slot for z, the existing instances as well as the code associated with the older (before update) class may behave unpredictably. This is quite unlike the classes that we discussed in the last chapter.

defstruct point:
  x = 0;
  y = 0;
  z = 0;
end

In fact, if you are using the Moonli REPL in its default settings, you will simply get an error message if you try to run the new definition of point:

warning: change in instance length of class point:
  current length: 3
  new length: 4
simple-error: attempt to redefine the structure-object class point incompatibly
              with the current definition
Backtrace for: #<SB-THREAD:THREAD tid=259 "main thread" RUNNING {70054D05F3}>
0: ((LAMBDA NIL :IN UIOP/IMAGE:PRINT-BACKTRACE))
1: ((FLET "THUNK" :IN UIOP/STREAM:CALL-WITH-SAFE-IO-SYNTAX))
2: (SB-IMPL::%WITH-STANDARD-IO-SYNTAX #<FUNCTION (FLET "THUNK" :IN UIOP/STREAM:CALL-WITH-SAFE-IO-SYNTAX) {1014D13BB}>)
...

On the other hand, if you have enabled the debugger either by running cl-repl:enable-debugger() or by starting Moonli REPL with --enable-debugger, you will be dropped into the debugger:

warning: change in instance length of class point:
  current length: 3
  new length: 4
attempt to redefine the structure-object class point incompatibly with the
current definition
 [Condition of type simple-error]

Restarts:
 0: [continue] Use the new definition of point, invalidating already-loaded
                   code and instances.
 1: [recklessly-continue] Use the new definition of point as if it were
                          compatible, allowing old accessors to use new
                          instances and allowing new accessors to use old
                          instances.
 ...
 
Backtrace:
 0: (sb-kernel::%redefine-defstruct #<sb-kernel:structure-classoid point> #<sb-kernel:layout (ID=376) for point {700A180063}> #<sb-kernel:layout for point, INVALID=:uninitialized {7007551CE3}>)
 1: (sb-kernel::%defstruct #<sb-kernel:defstruct-description point {70074B6DA3}> #(#<sb-kernel:layout for t {7003033803}> #<sb-kernel:layout (ID=1) for structure-object {7003033883}>) #S(sb-c:definition-source-location :namestring nil :indices 0))
 ...

The debugger has essentially paused code execution, and is waiting for you to select a restart. You can select the restart by pressing Ctrl + r and then pressing 0 or 1 (or another restart number). Suppose we select the restart numbered 0.

The redefinition of the point structure-class would proceed. However

*point*
#=> #<UNPRINTABLE instance of #<structure-classoid point> {700A37C8B3}>

point-p(*point*)
#=> nil

Unlike classes, where the redefinition of class resulted in a clean updation of instances, redefinition of structures requires a fair bit of manual work. To actually use the new definition of point structure-class, you will need to redefine the binding for *point*.

defparameter *point* = make-point(:x, 3, :y, 4)
*point*
#=> #S(point :x 3 :y 4 :z 5)

point-p(*point*)
#=> t

Performance

To actually compare the performance of classes and structures, let us define an equivalent class and a structure:

defclass point-class():
  slots:
    x:
      initarg: :x,
      accessor: class-x;
    y:
      initarg: :y,
      accessor: class-y;
  end
end

defstruct point-struct:
  x;
  y;
end

Then one can run a loop summing up the x and y a million times.

For the class:

let point = make-instance($point-class, :x, 3, :y, 4),
    num-iter = 1e9,
    sum = 0:
  time loop :repeat num-iter :do
    sum = sum + class-x(point) + class-y(point)
  end
  sum
end
Evaluation took:
  13.849 seconds of real time
  13.865638 seconds of total run time (13.843339 user, 0.022299 system)
  100.12% CPU
  0 bytes consed

For the structure:

let point = make-point-struct(:x, 3, :y, 4),
    num-iter = 1e9,
    sum = 0:
  time loop :repeat num-iter :do
    sum = sum + point-struct-x(point) + point-struct-y(point)
  end
  sum  
end
Evaluation took:
  12.229 seconds of real time
  12.229539 seconds of total run time (12.215391 user, 0.014148 system)
  100.01% CPU
  0 bytes consed

At first, this looks comparable. However, one can specify the types of the structure slots:

defstruct point-struct:
  (x = 0) :: fixnum;
  (y = 0) :: fixnum;
end

Then

let point = make-point-struct(:x, 3, :y, 4),
    num-iter = 1e9,
    sum = 0:
  declare type(fixnum, sum)
  time loop :repeat num-iter :do
    sum = sum + point-struct-x(point) + point-struct-y(point)
  end
  sum
end

We are down to a third of the time!

Evaluation took:
  4.223 seconds of real time
  4.223535 seconds of total run time (4.219177 user, 0.004358 system)
  100.02% CPU
  0 bytes consed

Meanwhile, type specification on classes have little impact:

defclass point-class():
  slots:
    x:
      type: fixnum,
      initarg: :x,
      accessor: class-x;
    y:
      type: fixnum,
      initarg: :y,
      accessor: class-y;
  end
end
let point = make-instance($point-class, :x, 3, :y, 4),
    num-iter = 1e9,
    sum = 0:
  declare type(fixnum, sum)
  time loop :repeat num-iter :do
    sum = sum + class-x(point) + class-y(point)
  end
  sum
end
Evaluation took:
  15.313 seconds of real time
  15.305151 seconds of total run time (15.280037 user, 0.025114 system)
  100.01% CPU
  0 bytes consed

The gap becomes even more pronounced when one considers construction of new instances and more complex read or write operations.

In practice, there are projects such as static-dispatch and fast-generic-functions that attempt to overcome the performance limitations of generic functions and standard classes, trying to give you the best of both worlds. However, these are not standard. The standard way to obtain fast Moonli (or Common Lisp) code is to use structures.

Classes vs Structures

Classes are perfect for flexible, evolving designs. Structures are ideal for performance-critical code.

FeatureClassesStructures
Single inheritance
Multiple inheritance
Dynamic redefinition
Performance
Use with generic functions

2.9 - 8. Types, Classes and Structures

So far, we have seen a number of types. Built-in types such as string, fixnum, symbol, as well as user-defined types such as point, shape, rectangle and `circle.

Classes – either standard-classes or structure-classes – and Types have a close correspondence. Every class defines a type. That is why, even though point, shape, etc were defined as classes, they are also types. Most types also have a corresponding class. The Common Lisp Hyperspec page on Integrating Types and Classes go into this in detail.

To begin with, one can check whether an object is of a particular type using typep.

typep(2, $fixnum)
#=> t

typep(2, $integer)
#=> t

All objects are of type t. This is also the boolean value true.

typep(2, t)
#=> t

No objects are of type nil. This is also the boolean value false.

typep(2, nil)
#=> nil

In contrast to classes and structures, however, some type specifiers also allow us to check whether an object is of a more specific type. The following checks whether the object 2 is an integer between 1 and 5.

typep(2, $integer(1, 5))
#=> t

Note that the second argument to typep is quoted using $. However, t and nil do not need to be quoted.

The following checks whether the object "hello" and "hello world" are strings of length 5.

typep("hello", $string(5))
#=> t

typep("hello world", $string(5))
#=> nil

Classes and structures do not allow this detailed specification.

One can also define new types that are combinations of existing types. The following defines rectangle-or-circle as a type. Objects are of this type if they are a rectangle or a circle. In other words, rectangle-or-circle type is a disjunction of the types rectangle and circle.

deftype rectangle-or-circle():
  $(rectangle or circle)
end

Like disjunction, one can define types corresponding to conjunctions using the and operator.

One can also talk about types corresponding to a specific object. These are eql-types. The type eql(5) only includes the object 5 and nothing else(!)

typep(5, $eql(5))
#=> t

typep(5.0, $eql(5))
#=> nil

One can also talk about negative types. not(integer) includes all objects that are not integers.

typep(5, $not(integer))
#=> nil

typep(5.0, $not(integer))
#=> t

typep("hello", $not(integer))
#=> t

Types also have subtypep relations between them. A type t1 is a subtype of type t2 if all members of t1 are also members of t2. Thus, every type is a subtype of t.

subtypep($not(integer), t)
#=> t t

Subtypep returns two return values:

  • the first indicates whether the first argument is a subtype of the second
  • the second indicates whether the subtype relation was determinable

For most common types, the second value is t, but eventually, you can expect to run into cases where the second value is nil.

While types allow powerful expressive capabilities, in general, they cannot all be used as the method specializers in generic functions. Only eql-types and types that correspond exactly to a class are allowed method specializers of the generic functions. Thus, there are no standard ways to make the behavior of a function depend on the detailed types of its objects.

However, there are again projects such as peltadot and polymorphic-functions that attempt to provide functions that dispatch on arbitrary type specifiers.

2.10 - 9. Miscellaneous

So far, in this tutorial series, we have seen different aspects of Moonli (and Common Lisp) without seeing constructs to write your own code. Here we cover some of those constructs.

Conditional Execution

Moonli supports the standard conditional form if..elif..else. This is translated to the Common Lisp form cond.

if x > 10:
  format(t, "Large")
elif x > 5:
  format(t, "Medium")
else:
  format(t, "Small")
end
  • Conditions evaluate top-to-bottom.
  • The first true condition’s block is executed.
  • The elif and else branches are optional.

Looping

Moonli provides two main looping constructs

for …

This is based on Shinmera’s for and corresponds to the standard for loops in other languages. These are useful for iterating over a data structures, or for iterating a fixed number of times.

For has a number of clauses. A generic clause is over that allows iteration over lists as well as vectors.

# Iterate over lists
for x in (1, 2, 3):
  print(x)
end

# Iterate over vectors
for x in [1, 2, 3]:
  print(x)
end

To iterate over ranges:

for i repeat 10:
  print(i)
end

loop

Because Moonli runs on Common Lisp, you also have full access to the powerful loop. This is a mini-language for writing concise iteration and accumulation logic.

1. Simple Repetition

The simplest form counts from 1 to N:

loop :for i :from 1 :to 5 :do
  print(i)
end
loop :for i :from 0 :to 20 :by 5 do
  print(i)
end

2. Iterating Over Lists

You can loop directly over a list:

loop :for x :in (10, 20, 30) :do
  print(x)
end

Or over any sequence:

loop :for ch :across "moonli" :do
  print(ch)
end

3. Conditional Execution Inside loop

The code following :do can be arbitrary Moonli code. This can include conditional statements. However, you can also put conditional statements with the loop itself:

loop :for n :from 1 :to 10
     :when (rem(n, 2) == 0) :do
  format(t, "~d is even~%", n)
end

4. Collect, Sum, Maximizing

One of the most powerful features is accumulation. loop can build lists, sums, and more without extra variables.

loop :for i :in (1, 2, 3, 4)
     :collect i * 2
end
#=> (2, 4, 6, 8)


loop :for x :in (1, 2, 3, 4)
     :sum x
end
#=> 10


loop :for x :in (2, 3, 4, 1)
     :maximizing x
end
#=> 4

5. Finally

A finally clause runs after the iteration and lets you return a final value:

loop :for word :in ("hello", "world", "Moonli", "is", "powerful")
     :count word :into n
     :finally return(format(nil, "Found ~d words", n))
end
#=> "Found 5 words"

6. Using Multiple Clauses

loop shines when you combine iteration, conditionals, and accumulation:

loop :for x :in (-1, 2, 3, -1, 5)
     :when x > 0
       :collect x :into positives
     :finally return({
       :positives : positives,
       :total : apply(function(+), positives)
     })
end
#=> {
     :positives : (2, 3, 5),
     :total : 10
}

This runs in one pass but builds structured results.

Format – Producing Structured Output

Moonli’s format follows a simplified Lisp-style template mechanism. It allows inserting variables into strings or writing formatted text to output.

The first argument to format indicates the stream. This can be t which corresponds to the standard-output stream. Or nil which obtains the result as a string. Or any variable or expression that evaluates to a stream.

3.1 Basic formatting

format(nil, "Hello, ~a!", "Moonli")
#=> "Hello, Moonli!"

~a inserts the argument using its “human-friendly” representation.

3.2 Multiple arguments

format(nil, "~a + ~a = ~a", a, b, a + b)

3.3 Common directives

DirectiveMeaning
~aInsert readable form
~sInsert literal/escaped form
~dInsert decimal integer
~fInsert floating-point number
~%Insert newline

Examples:

format("Count: ~d", n)
format("Value: ~f", pi)
format("Debug: ~s", obj)

3.4 List Iteration with a joiner

format(t, "~{~A~^, ~}", (1,2,3))
#=> (prints) 1, 2, 3

More directives

The wikipedia page on format lists the variety of directives supported by format.

3 - Introduction to Moonli for Common Lispers

Moonli is a syntax layer that transpiles to Common Lisp.

For example,

defun sum(args):
  if null(args):
    0
  else:
    first(args) + sum(rest(args))
  end
end

transpiles to:

(defun sum (args)
   (cond ((null args)
          0)
         (t
          (+ (first args)
             (sum (rest args))))))

See moonli-sample.asd and sample/sample.moonli to include in your project.

Table of Contents

Due to tools like paredit and lispy (beyond macros and metaprogramming), s-expression (Lists) based syntax of lisps is very powerful. However, not every one has the time or patience to become comfortable with them, especially when it comes to reading code, or sharing it with your colleagues.

In the 21st century, very many more people are familiar with python, matlab and julia than they are with lisps. Given the power and flexibility of common lisp, moonli is an attempt to provide a thin syntax layer over common lisp. It is thin in the sense it can be easily transpiled to common lisp. The semantics remain the same and clean as common lisp. (Common Lisp is also good for reasons beyond macros.)

Contrasts with Common Lisp

  • Case sensitive, but invert-case reader to maintain common lisp compatibility
  • Extensible using moonli:define-moonli-macro and moonli:define-short-moonli-macro. See src/macros/ directory for examples.
  • Inability to access internal symbols of another package through "A::B" syntax; this syntax rather translates to (the B A)

Syntax

As with lisp, everything is an expression.

Moonli’s syntax can be understood in terms of a (i) Core Syntax, and (ii) Macros. The core syntax makes space for macros, and macros provide extensibility. A third part concerns the with-special macros.

Core Syntax

  Lisp                       Moonli
  -------------------------- -------------------------
  #\a                        'a'
  "hello world"              "hello world"
  2, 2.0, 2d-3, 2.0d-3       2, 2.0, 2d-3, 2.0d-3
  'quoted-symbol             $quoted-symbol
  package:exported-symbol    package:exported-symbol
  package::internal-symbol   <WONTDO>
  (the type expr)            expr :: type
  (list form-1 form-2)       (form-1, form-2)
  (fn arg1 arg2)             fn(arg1, arg2)
  #c(re, im)                 <TODO>

Macros

One defines a Moonli Macro or a Moonli Short Macro that expands to a Common Lisp macro or special form. These can be defined by moonli:define-moonli-macro and moonli:define-moonli-short-macro respectively. The difference between a Moonli Macro and a Moonli Short Macro is that the former end with end and can stretch over multiple lines, while the latter are expected to either span a single line or have their components be separated by non-newline whitespaces. See src/macros for examples.

Several Moonli macros are predefined as part of Moonli system, and you can add more Moonli macros as part of your own library or application.

Example transpilations for these predefined Moonli macros are given below:

declaim

declaim inline(foo)

transpiles to

(declaim (inline foo))
declaim type(hash-table, *map*)

transpiles to

(declaim (type hash-table *map*))

declare

declare type(single-float, x, y)

transpiles to

(declare (type single-float x y))
declare type(single-float, x, y), optimize(debug(3))

transpiles to

(declare (type single-float x y)
         (optimize (debug 3)))

defclass

defclass point():
end

transpiles to

(defclass point nil nil)
defclass point():
  options:
    metaclass: standard-class;
  end
end

transpiles to

(defclass point nil nil (:metaclass standard-class))
defclass point():
  options:
    metaclass: standard-class;
    documentation: "A class for Points!";
  end
end

transpiles to

(defclass point nil nil (:metaclass standard-class)
          (:documentation "A class for Points!"))
defclass point():
  slots:
  end
end

transpiles to

(defclass point nil nil)
defclass point():
  slots:
    x;
    y;
  end
end

transpiles to

(defclass point nil ((x) (y)))
defclass point():
  slots:
    x:
      initform: 2.0,
      type: single-float,
      accessor: point-x;
  end
end

transpiles to

(defclass point nil ((x :initform 2.0 :type single-float :accessor point-x)))
defclass point():
  slots:
    x:
      initform: 2.0,
      type: single-float,
      accessor: point-x;
    y:
      initform: 2.0,
      type: single-float,
      accessor: point-y;
  end
end

transpiles to

(defclass point nil
          ((x :initform 2.0 :type single-float :accessor point-x)
           (y :initform 2.0 :type single-float :accessor point-y)))
defclass point():
  slots:
    x:
      initform: 2.0,
      type: single-float,
      accessor: point-x;
    y:
      initform: 2.0,
      type: single-float,
      accessor: point-y;
  end
  options:
    metaclass: standard-class;

    documentation: "Two dimensional points.";

  end
end

transpiles to

(defclass point nil
          ((x :initform 2.0 :type single-float :accessor point-x)
           (y :initform 2.0 :type single-float :accessor point-y))
          (:metaclass standard-class)
          (:documentation "Two dimensional points."))

defgeneric

defgeneric area(shape)

transpiles to

(defgeneric area
    (shape))

defmethod

defmethod our-identity(x): x end

transpiles to

(defmethod our-identity (x) x)
defmethod :before our-identity(x):
  format(t, "Returning identity~%")
end

transpiles to

(defmethod :before our-identity (x) (format t "Returning identity~%"))
defmethod :after our-identity(x):
  format(t, "Returned identity~%")
end

transpiles to

(defmethod :after our-identity (x) (format t "Returned identity~%"))
defmethod add (x :: number, y :: number):
 x + y
end

transpiles to

(defmethod add ((x number) (y number)) (+ x y))
defmethod add (x :: number, y :: number, &rest, others):
  x + if null(others):
    y
  else:
    apply(function(add), y, others)
  end
end

transpiles to

(defmethod add ((x number) (y number) &rest others)
  (+ x (cond ((null others) y) (t (apply #'add y others)))))
defmethod add (x :: number, y :: number, &rest, others):
  x + (if null(others):
    y
  else:
    apply(function(add), y, others)
  end)
end

transpiles to

(defmethod add ((x number) (y number) &rest others)
  (+ x (cond ((null others) y) (t (apply #'add y others)))))
defmethod add (x :: string, y):
  uiop:strcat(x, y)
end

transpiles to

(defmethod add ((x string) y) (uiop/utility:strcat x y))

defpackage

defpackage foo
  :use cl;
end

transpiles to

(defpackage foo
  (:use cl))

defparameter

defparameter a = 5

transpiles to

(defparameter a 5)

defstruct

defstruct foo:
  a;
  b;
end

transpiles to

(defstruct foo a b)
defstruct foo:
  (a = 4) :: number;
  b;
end

transpiles to

(defstruct foo (a 4 :type number) b)
defstruct foo:
  (a = 4), :read-only = t;
  b;
end

transpiles to

(defstruct foo (a 4 :read-only t) b)
defstruct foo:
  (a = 4), :read-only = t;
  (b = 2.0) :: single-float, :read-only = t;
end

transpiles to

(defstruct foo (a 4 :read-only t) (b 2.0 :type single-float :read-only t))
defstruct foo:
  a = 4;
  b = 2.0;
end

transpiles to

(defstruct foo (a 4) (b 2.0))

deftype

defun

defun our-identity(x): x end

transpiles to

(defun our-identity (x) x)
defun add (&rest, args):
 args
end defun

transpiles to

(defun add (&rest args) args)
defun add(args):
  if null(args):
    0
  else:
    first(args) + add(rest(args))
  end if
end

transpiles to

(defun add (args) (cond ((null args) 0) (t (+ (first args) (add (rest args))))))
defun foo(&optional, a = 5): a end

transpiles to

(defun foo (&optional (a 5)) a)

defvar

defvar a = 5

transpiles to

(defvar a 5)

for

for:for (i,j) in ((1,2),(3,4)):
  print(i + j)
end

transpiles to

(for-minimal:for (((i j) in (list (list 1 2) (list 3 4))))
  (print (+ i j)))
for:for i in (1,2,3), j in (2,3,4):
  print(i + j)
end

transpiles to

(for-minimal:for ((i in (list 1 2 3)) (j in (list 2 3 4)))
  (print (+ i j)))

if

if a: b end if

transpiles to

(cond (a b) (t nil))
if a:
  b; c
end

transpiles to

(cond (a b c) (t nil))
if a: b
else: c
end if

transpiles to

(cond (a b) (t c))
if a:
   b; d
else:
   c; e
end if

transpiles to

(cond (a b d) (t c e))
if a: b
elif c: d; e
else: f
end if

transpiles to

(cond (a b) (c d e) (t f))
(if a: b else: c; end)::boolean

transpiles to

(the boolean (cond (a b) (t c)))
if null(args): 0; else: 1 end

transpiles to

(cond ((null args) 0) (t 1))
if null(args):
    0
else:
    first(args)
end if

transpiles to

(cond ((null args) 0) (t (first args)))
if null(args):
  0
else:
  2 + 3
end if

transpiles to

(cond ((null args) 0) (t (+ 2 3)))
if null(args):
  0
else:
  first(args) + add(rest(args))
end if

transpiles to

(cond ((null args) 0) (t (+ (first args) (add (rest args)))))

ifelse

ifelse a 5

transpiles to

(if a
    5
    nil)
ifelse a :hello :bye

transpiles to

(if a
    :hello
    :bye)

in-package

labels

labels foo(x):
         bar(x - 1)
       end,
       bar(x):
         if (x < 0): nil else: foo(x - 1) end
       end:
  foo(42)
end

transpiles to

(labels ((foo (x)
           (bar (- x 1)))
         (bar (x)
           (cond ((< x 0) nil) (t (foo (- x 1))))))
  (foo 42))
labels foo(x):
         if (x < 0): nil else: foo(x - 1) end
       end:
  foo(42)
end

transpiles to

(labels ((foo (x)
           (cond ((< x 0) nil) (t (foo (- x 1))))))
  (foo 42))
labels :
  nil
end

transpiles to

(labels ()
  nil)

lambda

lambda (): nil end

transpiles to

(lambda () nil)
lambda (x):
  x
end

transpiles to

(lambda (x) x)
lambda (x, y):
  let sum = x + y:
    sum ^ 2
  end
end

transpiles to

(lambda (x y)
  (let ((sum (+ x y)))
    (expt sum 2)))

let

let a = 2, b = 3:
   a + b
end

transpiles to

(let ((a 2) (b 3))
  (+ a b))
let a = 2, b = 3:
   a + b
end let

transpiles to

(let ((a 2) (b 3))
  (+ a b))

let+

let-plus:let+ x = 42: x
end

transpiles to

(let+ ((x 42))
  x)
let-plus:let+ (a,b) = list(1,2):
  a + b
end

transpiles to

(let+ (((a b) (list 1 2)))
  (+ a b))
let-plus:let+ let-plus:&values(a,b) = list(1,2):
  a + b
end

transpiles to

(let+ (((&values a b) (list 1 2)))
  (+ a b))
let-plus:let+
  let-plus:&values(a,b) = list(1,2),
  (c,d,e) = list(1,2,3):
  {a,b,c,d,e}
end

transpiles to

(let+ (((&values a b) (list 1 2)) ((c d e) (list 1 2 3)))
  (fill-hash-set a b c d e))

lm

lm (): nil

transpiles to

(lambda () nil)
lm (x): x

transpiles to

(lambda (x) x)
lm (x, y): x + y

transpiles to

(lambda (x y) (+ x y))

loop

loop end loop

transpiles to

(loop)
loop :repeat n :do
  print("hello")
end

transpiles to

(loop :repeat n
      :do (print "hello"))
loop :for i :below n :do
  print(i + 1)
end

transpiles to

(loop :for i :below n
      :do (print (+ i 1)))

time

time length("hello world")

transpiles to

(time (length "hello world"))

with

Standard Common Lisp code uses a number of with- macros. These can all be generated using the with- special macro of Moonli. This is “special” because

  1. Unlike standard macros which are identified by symbols, the with-block is identified using the keyword “with”, regardless of the package under consideration.

  2. with pkg:symbol expands into pkg:with-symbol without the interning of pkg:symbol.

Example transpilations:

with open-file(f, "/tmp/a.txt"):
  f
end

transpiles to

(with-open-file (f "/tmp/a.txt") f)
with-open-file(f, "/tmp/a.txt"):
  f
end

transpiles to

(with-open-file (f "/tmp/a.txt") f)
with output-to-string(*standard-output*),
     open-file(f, "/tmp/a.txt"):
  write-line(read-line(f))
end

transpiles to

(with-output-to-string (*standard-output*)
  (with-open-file (f "/tmp/a.txt")
    (write-line (read-line f))))
with alexandria:gensyms(a,b,c):
  list(a,b,c)
end

transpiles to

(alexandria:with-gensyms (a b c)
  (list a b c))
with alexandria:gensyms(a,b,c),
     open-file(f, "/tmp/a.txt", :direction, :output):
  write(list(a,b,c), f)
end

transpiles to

(alexandria:with-gensyms (a b c)
  (with-open-file (f "/tmp/a.txt" :direction :output)
    (write (list a b c) f)))
defstruct pair:
  x;
  y;
end

with access:dot():
  let pair = make-pair(:x, 2, :y, 3):
     format(t, "~&x + y = ~a~%", pair.x + pair.y)
  end
end

transpiles to

(defstruct pair x y)

(access:with-dot
  (let ((pair (make-pair :x 2 :y 3)))
    (format t "~&x + y = ~a~%" (+ pair.x pair.y))))

4 - Feature comparison across different programming languages

Here’s a brief comparison of features across different languages.

    FEATURES                             MOONLI       COMMON LISP       JULIA       HASKELL         RUST         PYTHON       JAVASCRIPT         C  
  ----------------------------------- ------------ ----------------- ----------- ------------- -------------- ------------ ---------------- -----------
  Syntax                                    +              +              +            +            ---             +              -             -
  Interactivity (Rapid Prototyping)       High         Very High      Moderate        Low           None        Moderate       Moderate        None
  Typing (Strong/Weak)                   Strong         Strong         Strong       Strong         Strong        Strong          Weak          Weak
  Typing (Static/Dynamic)               Flexible       Flexible       Flexible      Static         Static       Dynamic        Dynamic        Dynamic
  Typing (Expressivity)                 Flexible       Flexible       Moderate     Very High     Very High        Low            Low            Low
  Compiler Speed                        Flexible       Flexible         Slow       Moderate         Slow        Moderate       Moderate      Moderate
  Runtime Speed                         Flexible       Flexible         Fast       Moderate         Fast          Slow         Moderate        Fast
  Runtime Error Recovery                Advanced       Advanced        Limited     Moderate         None        Moderate       Moderate        None
  Binary Size                           Flexible       Flexible         Large          ?           Small          None           None          Small
  User Extensibility                      High           High         Moderate        Low           Low           None           None          None
  Compiler built-in optimizations         Low             Low         Very High        ?         Very High        Low          Moderate      Very High
  Long Term Support                       Low          Very High      Moderate         ?          Moderate      Moderate         Low         Very High
  Ecosystem (without interop)            Small           Small        Moderate       Small        Moderate       Large          Large          Large
  Memory Management                       Heap           Heap         Reference      Heap       Compile Time   Reference          ?           Manual