Moonli Update (September 2026)

Updates about Moonli v0.0.10

Moonli v0.0.10 is released this weekend. It’s been just 2 months since the last Moonli release, however, there were lots of changes and additions in this period, that made a release hard to delay. (If you want to install from source, see here.)

Table of Contents


1. Binaries compiled against glibc 2.28

This one has a short description, but a fair bit of work :). glibc versions are a major pain point for distributing binaries on linux. We now have docker images with glibc 2.28, openssl 3.5 (LTS), SBCL and ECL. These are based on the manylinux docker images and can be used to compile sbcl-goodies, isocline-repl and also Moonli itself, with statically compiled libssl and the likes! You can check out the build scripts for each of these to adapt to your own programs!

2. More helpful error messages

This one is a bit silly. Nonetheless, it’s one factor that draws me to Moonli. And of course, I’m biased :).

Languages that end with end have a problem. To see this, consider:

defun map-array2(fn, a1, a2):
  declare type(array, a1, a2);

  assert(equal(array-dimensions(a1), array-dimensions(a2)))

  let result = make-array(array-dimensions(a1)):
    loop :for i :below array-total-size(result)
      :do row-major-aref(result, i) = funcall(
        fn,
        row-major-aref(a1, i),
        row-major-aref(a2, i),
      )
    end
  end

  result
end

This seems correct at the outset, but upon seeing the compiler error that result is undefined, one is left a bit confused. And this disconnect grows as the size of the function grows. In particular, this can look horrifying:

        end
      end
    end
  end
end

It can become difficult to keep track of where some block ends. Editors can be helpful. But here’s an alternative:

      end if
    end loop
  end let
end defun

This can be verbose. But it is optional. With Moonli 0.0.10, mismatched block ends now signal a better message, instead of continuing the parse and ending up with something confusing.

    end
  end loop

  result
end

For example, with the above map-array2, with the added loop after the but-last end, we get the error.

Parse error at line 13, column 7. Expected 'LET'
  .
  .
  .
    end
  end loop
------^---

  result
end

Likewise, there are several small ways the error messages have been improved, including but not limited to shorter backtraces through isocline-repl and color coded funcall arguments on the stack through styled-strings.

3. There are no symbols at the terminal

Something I had explored last time was being able to call moonli/lisp functions from the terminal.

$ moonli --no-init --eval 'defun add(x,y): x + y end' -f add 2.0 3.4
5.4

Earlier, the arguments were simply kept as they were in string format, or they were parsed into numbers. With this release, Moonli goes full featured.

To understand how, one must ask: What’s the difference between typing commands at the shell vs at the REPL of a programming language? Well, free symbols at the REPL are variables in that language. But free symbols in the shell are strings. Thus, if something cannot be parsed as an element of the language, then it is a string.

This leads to a natural grammar

(esrap:defrule moonsh-atomic-expression
    ;; This is identical to moonli's atomic-expression, but without any symbol,
    ;; string or chain
    (or bracketed-expression
        quoted-expression
        expr:character
        number
        expr:vector
        expr:cons
        expr:list
        expr:hash-table
        expr:hash-set))

This allows

$ moonli --no-init --funcall alexandria:hash-table-keys '{"a": 2, "b": [2,3,4]}'
("b", "a")
$ moonli --no-init --funcall 'lm (x): gethash("b", x)' '{"a": 2, "b": "zebra"}'
zebra, t
$ moonli --no-init --funcall str:split " " "The quick brown fox jumped over the lazy dog"
("The", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog")
$ moonli --no-init --funcall string-downcase "The quick brown fox jumped over the lazy dog"
the quick brown fox jumped over the lazy dog

And indeed, you can load lisp or moonli files or libraries prior to --funcall!

4. Curated libraries

Last but not the least.

A big advantage of Moonli is that you can simply use Common Lisp libraries. And while there aren’t that much, there are lots of useful ones. However, scavenging for libraries can be difficult.

One approach is CIEL (see here for libraries). As much as I appreciate the effort, I have my own peculiarities, so I’m not exactly happy with the library selection.

But now, there is another collection! I wanted to mirror the Python standard-library, which is an ambituous goal in itself. But here’s where we start. However, many of these rely on foreign-libraries, so if you use the moonli binary, these may not work as expected! Perhaps, some day in the future, all those foreign libraries become statically baked into the lisp image :).

;; Threading
"bordeaux-threads"

;; Text Processing Services
"cl-ppcre"
"str"

;; Numeric and Mathematical Libraries
"array-operations"
"nibbles"

;; Functional Programming Libraries
"alexandria"
"coalton"
"fset"

;; File and Directory Access
"file-finder"
;; "trivial-posix-fs" ; experimental
"file-attributes"

;; Data Persistence
"postmodern"
"mito"
(:feature (:not :windows) "clsql")
;; "bknr.datastore" ; does not load on windows, also unmaintained now

;; TODO: Data Compression and Archiving

;; File Formats
;; "file-formats" ; experimental
"com.inuoe.jzon"
"shasht"
"fare-csv"

;; Cryptographic Services
"ironclad"

;; TODO: Generic Operating System Services

;; Command-line Interface
"unix-opts"
"clingon"

;; Concurrent Execution
"lparallel"

;; TODO: Internet Data Handling

;; TODO: Structured Markup Processing Tools

;; Internet Protocols and Support
"usocket"
"hunchentoot"
"hunchensocket"
"dexador"

;; TODO: Multimedia Services

;; Internationalization

"local-time"

;; Graphical User Interfaces
(:feature (:not :windows) "clog") ; depends on libsqlite3, does not load on windows, not out of the box anyways
"ltk"
"isocline-repl"

;; Development Tools
"esrap"
"quicksearch"
"quickproject"
(:feature (:not :ql-https) "ql-https")
(:feature (:not :swank) "swank")
"trivial-package-local-nicknames"

;; Debugging and Profiling
"swank"
"log4cl"

;; Software Packaging and Distribution
"deploy"

;; TODO: Runtime Services
"closer-mop"

;; TODO: Language Services
"trivial-features"
"policy-cond"
"cl-environments"
"cl-form-types"
"float-features"
"trivial-garbage"

;; Foreign Libraries
"cffi"
"cl-autowrap"
;; "py4cl2" ; experimental
;; "py4cl2-cffi" ; experimental

;; Iteration
"iterate"
"series"
"picl"

;; Pattern Matching
"optima"

;; Code Generation
"cl-who"
"parenscript"

;; Documentation
"mgl-pax"
"docsearch"


;; Testing
"fiveam"

;; TODO: Windows specific

;; Unix Specific
(:feature (:not :windows) "osicat") ; apparantly, it's supposed to work on windows, but it does not, not out of the box anyways

5. help

Example: How do you look up a function’s documentation in lisp when you are not using emacs? Eh, either use describe or documentation.

Ugh, so verbose.

Duh, we should just define help that macroexpands to describe.

MOONLI-USER> help(describe)
describe
  [symbol]

describe names a compiled function:
  Lambda-list: (object, &optional,
                (stream-designator, *standard-output*))
  Declared type: (function, (t, &optional, (or, stream, boolean)),
                  (values, &optional))
  Documentation:

    Print a description of OBJECT to STREAM-DESIGNATOR.

  Known attributes: unwind, any
  Source file: SYS:SRC;CODE;DESCRIBE.LISP
#=>

6. docsearch

Example: How do I read a json file?

MOONLI-USER> docsearch("json")
 .
 .
 .
SHASHT:READ-JSON
  Function:
    Read a JSON value. Reading is influenced by the dynamic variables
    *read-default-true-value*, *read-default-false-value*, *read-default-null-value*,
    *read-default-array-format*, *read-default-object-format*, *read-hash-table-test* and
    common-lisp:*read-default-float-format* which each determine the default values
    and formats used. The following arguments also control the behavior of the read.

    * input-stream-or-string - a stream, a string or t. If t is passed then
      *standard-input* is used.
    * eof-error-p - if true signal eof with error, otherwise return eof-value.
    * eof-value - value used if eof-error-p is nil.
    * single-value-p - Check for trailing junk after read is complete.
 .
 .
 .
MOONLI-USER> help(shasht:read-json)
read-json
  [symbol]

read-json names a compiled function:
  Lambda-list: (&optional, (input-stream-or-string, t),
                (eof-error-p, t), eof-value, single-value-p)
  Derived type: (function, (&optional, t, boolean, t, boolean),
                 (values, t, &optional))
  Documentation:
    Read a JSON value. Reading is influenced by the dynamic variables
    *read-default-true-value*, *read-default-false-value*, *read-default-null-value*,
    *read-default-array-format*, *read-default-object-format*, *read-hash-table-test* and
    common-lisp:*read-default-float-format* which each determine the default values
    and formats used. The following arguments also control the behavior of the read.

    * input-stream-or-string - a stream, a string or t. If t is passed then
      *standard-input* is used.
    * eof-error-p - if true signal eof with error, otherwise return eof-value.
    * eof-value - value used if eof-error-p is nil.
    * single-value-p - Check for trailing junk after read is complete.

MOONLI-USER> *print-pretty* = nil
#=> nil

MOONLI-USER> with open-file(f, "~/ram-disk/sample.json"):
  shasht:read-json(f)
end
#=> #<hash-table :TEST equal :COUNT 1 {12091262A3}>

MOONLI-USER> alexandria:hash-table-alist(*)
#=> (("glossary" . #<hash-table :TEST equal :COUNT 2 {1209126423}>))

Enjoy :)

Moonli Update (July 2026)

Updates about Moonli v0.0.9

Moonli v0.0.9 is released this weekend. It’s been 5 months since the last Moonli release. (If you want to install from source, see here.)

Besides some bug fixes, addition of new macros like define-condition handler-bind print-unreadable-object, a matrix chat room at #moonli:matrix-for-lispers.net, this release packs three important updates:


1. MIT licensed isocline

Perhaps the greatest change is the change from libreadline to isocline. Readline has been GPL licensed. The (positively) infectious nature of GPL poses problems in software distribution. By contrast, isocline is MIT licensed. So you can do whatever you want with it. But that’s not all. It comes with a multiline REPL, completions, and because it uses a minimal subset of ANSI escape sequences, it works across all of Unix, Windows and MacOS.

The Moonli binary releases link statically against isocline (as well as libssl and libtls and libcrypto). This means you can just download the binary and just run, without running into foreign library linking issues.

Now, you don’t need to use Moonli in order to use isocline. The repository containing isocline bindings for Common Lisp itself contains isocline-repl that is statically linked against libisocline (and others). Something I experimented with but haven’t thoroughly tested is that you can take statically linked SBCL binary, use it to compile lisp code and dump the image with the new code. That new image is still a statically linked SBCL binary!

The static linking itself is thanks to sbcl-goodies, which now also has an experimental cross-platform variant. These work across Linux, MacOS, Windows, across x86-64 as well as arm64.

2. Calling functions from terminal

Returning back to Moonli, the binary itself can be used to call arbitrary Common Lisp functions from the terminal. (I got this inspiration from kiln.) This means you can do something like:

$ moonli -f + 2 3
5
$ moonli -f round 2.345
2
$ moonli --eval 'defun add(x,y): x + y end' -f add 2.0 3.4
5.4
$ moonli -f uiop:strcat hello world
"helloworld"

It’s actually fairly trivial to implement this. That said, I haven’t finalized the interface for this yet. The main limitation of this is that everything in the shell is a string. (But, there’s SHCL – and several other projects.)

3. Extensible Infix Macros

Macros and Short Macros

Moonli already has had extension points for macros and short-macros since the first release. Much of Moonli is actually defined using define-moonli-macro and define-moonli-short-macro. For example, here is a moonli macro for optima:match:

(esrap:defrule match/clauses
    (* (and *whitespace
            chain
            *whitespace/internal
            ":"
            *whitespace
            moonli-expression
            ";"))
  (:function (lambda (expr)
               (mapcar (lambda (expr)
                         (list (second expr)
                               (sixth expr)))
                       expr))))


(define-moonli-macro optima:match

  ((arg moonli-expression)
   (_ (and *whitespace/internal ":" *whitespace))
   (clauses match/clauses))

  `(optima:match ,arg
    ,@clauses))

With that defined, the Moonli can now transpile the below code

optima:match [2,3,4]:
  vector(x, y, z) : x + y + z;
  sym : sym;
end

to Lisp:

(optima:match (vector 2 3 4)
  ((vector x y z)
   (+ (+ x y) z))
  (sym sym))

When the end feels annoying at the end, you can define a short macro:

(define-moonli-short-macro ifelse

  ((test moonli-expression)
   (_ +whitespace/internal)
   (then moonli-expression)
   (_ (esrap:? +whitespace/internal))
   (else (esrap:? moonli-expression)))

  `(if ,test ,then ,else))

Now ifelse a :hello :bye transpiles to (if a :hello :bye).

Extensible Infix Macros

However, having used R, I felt a jealous at the lack of extensible infix macros. But we have them now. To define a new infix macro, simply:

(define-moonli-infix-macro binding-arrows:->)

Now you can:

## Assumes moonli/repl is loaded which defines 
## and exports additional macros including binding-arrows:->
in-package :moonli-user

t -> format(\"hello\");

- (3 + 4) 
  -> add(3)
  -> expt(2);

And these will be transpiled to:

;; Assumes moonli/repl is loaded which defines and 
;; exports additional macros including binding-arrows:->
(in-package :moonli)

(-> t (format "hello"))

(-> (->
      (- (+ 3 4))
      (add 3))
  (expt 2))

The precedence for user defined infix macros is higher than unary minus but lower than other operators. All the user defined infix macros have the same precedence however.

Next Plans

I’m mainly using Moonli for one of my own projects that I cannot publish about at the moment. So, its development is mainly in service of that.

However, I expect to improve the interface to calling functions from terminal. It can be possible to supply keyword arguments too!

The REPL itself needs more bugfixes. I still run into a number of bugs related to completion and unicode characters that should not exist in production.

The other aspect is libraries and documentation. In principle, this can be delegated to CIEL; however, CIEL also includes a number of libraries that rely on foreign libraries. Ideally, these need to be statically linked in.

Yet another aspect is actually making the binaries trustworthy for Windows and MacOS users. However, this can be ugly.

There’s also an open request for coalton support. The main issue I currently have with this is defining the syntax itself. It’s relatively easy to add it to Moonli using Parsing Expression Grammar implemented by esrap once the syntax is ready.