Customizing Lisp REPLs

By Artyom Bologov Soft lylac thumbnail. On it, “CuStOm REpLs” is written in wiggly and misplaced letters. In the corners, “ArTYom BoLOgOV” and “aArtAKa.Me” are written as attributions in the same style. All the text resembles graffiti more than something readable.

I am a portability freak. If something can be done with an existing tool, I’ll go for it. If the program can be portable across systems, then it should be. If I can get rid of a tool or a whole class of tools, then off with its head! Sometimes that costs me, but that’s what I am.

You can already see why I might dislike custom/wrapper/proxy REPLs. They are a new layer of tools reinventing what’s already there in the underlying REPL, and doing so in an ad-hoc incompatible way. A nightmare, albeit sometimes a comfy one. Still, we don’t have to live with the horrors of proxy REPLs! We can have existing REPLs incrementally improved for lasting and universal benefit. So let’s do just that.

SBCL SBCL SBCL

For some reason, default REPL in SBCL is extremely simple and has no extension points. Some of the customizations below won’t work, unless you enable, say, sb-aclrepl extension:

(require "sb-aclrepl")
Getting an Allegro-like REPL in SBCL

Welcoming Prompt #

The first thing you see when you launch a Common Lisp REPL is the prompt. Sometimes ugly, sometimes not so. But always open for improvement.

That improvement is what my Trivial Toplevel Prompt library does. Allowing one to define regular, debug, inspector, and otherwise special REPL prompts. All in a portable way, because, apparently, most implementations allow customizing the prompt. (Except for maybe SBCL, because customizing REPL there requires redefining a bunch of functions or using sb-aclrepl.)

Details varying between implementations, prompts can include:

All of these are portably supported by Trivial Toplevel Prompt. Without the need for any REPL preprocessing or function redefinition! Here’s the prompt I ended up with for my personal use:

(trivial-toplevel-prompt:set-toplevel-prompt "~*~a~*~@[/D~d~]~*~@[/I~*~]? ")
;; In CL-USER, debugging level 2, inspecting something
;; CL-USER/D2/I?
trivial-toplevel-prompt definition (can be a function instead of a format string) and final prompt look

Commands #

Most Common Lisp (and some Scheme) implementations converged on this idea: Commands. Short inline meta-programs instructing the REPL to perform an action. Be it loading a file, listing documentation for an entity, or running a shell command.

So we have a feature that’s uniformly accessible in most REPLs. The only thing we need is a portable way to use it. And a bit of courage to live with the consequences of this extensibility. I can help with the former, at least. Trivial Toplevel Commands is a library allowing to portably define REPL commands. Here’s how a shell-invoking command might look like with this library:

(define-command/string (:sh :!) (command)
 "Run shell command synchronously"
 (ignore-errors
  (uiop:run-program command :output t :error-output t)))
Trivial Toplevel Commands used for shell pass-through

And here’s a more involved command, inspired by DOS dir:

(define-command/string (:directory :dir) (#+clozure &optional dir)
  "(Switch to DIR, if provided) and list all the files in the current directory"
  (block dir
    (unless (uiop:emptyp dir)
      (let* ((tilde (eql #\~ (elt dir 0)))
             (resolved-dir (merge-pathnames
                            (uiop:parse-native-namestring
                             (if tilde
                                 (subseq dir 2)
                                 dir))
                            (if tilde
                                (user-homedir-pathname)
                                (uiop:getcwd)))))
        (unless (uiop:directory-exists-p resolved-dir)
          (if (yes-or-no-p "Create a ~a directory?" dir)
              (ensure-directories-exist (uiop:ensure-directory-pathname resolved-dir))
              (return-from dir)))
        (unless (uiop:emptyp dir)
          (uiop:chdir resolved-dir))))
    (format t "~:[Switched to d~;D~]irectory ~a~:[~;:~{~&~a/~}~{~&~a~}~]"
            (uiop:emptyp dir)
            (uiop:getcwd)
            (uiop:emptyp dir)
            (mapcar (lambda (d)
                      (car (last (pathname-directory d))))
                    (uiop:subdirectories (uiop:getcwd)))
            (mapcar #'file-namestring
                    (uiop:directory-files (uiop:getcwd))))))
Directory listing command defined with Trivial Toplevel Commands

The only purpose of this intimidating listing is to prove: yes, you can put whatever into commands. And this code is ran when you call the defined command from the REPL: :dir ~/web/.well-known/. Essentially making commands what they are: functions with benefits! Callable with a keyword, needing no packages, possessing any syntax you can come up with. Speaking of syntax…

Reader Syntax #

You want some syntax in your Lisp? Imagine… two chars to enable any notation, including a C-resembling one. CL can do it all, given enough will on the side of the programmer. And a couple of standard APIs. So one doesn’t really need much special REPL syntax if they have reader macros.

Shell passthrough: #!cat passwords.txt. Inline docs: #?(+ function). Convenient hash tables: #{:reader T :commands T :prompt T}. Short lambdas: #^kv.(print (list k v)) Anything really. I have a lot of them. And I love it.

I don’t advocate for excessive reader macro use though! Reader macros are too powerful to be used lightly. But it’s fine for personal use and quick REPL jots. Especially if you need to quickly look up documentation of some function. Just fire up ECL or SBCL and do #?documentation. Portable (once defined) across implementations, compatible with all the existing REPLs, integrated into the language.

GUI Debuggers #

One of the things I worked on when I was on Nyxt team was the graphical debugger. You can check the latest state before it was removed in January 2025. The underlying libraries are Ndebug, a framework for GUI debugger construction, and Dissect, Shinmera’s condition/stack inspection library, and the fundamental trivial-gray-streams.

Another notable example of a GUI debugger is McCLIM Debugger. SLIME/Sly debuggers are there too. McCLIM Debugger, SLIME/Sly, and Nyxt debugger have (or used to have):

Which is a lot, but much less than what the implementation-native REPLs provide:

Many of these require a really strong coupling and some diffusion between the REPL and the debugger (i.e. break loop/REPL.) It’s unrealistic expecting all of these features to be there in every external graphical debugger. So maybe it’s better to use a more powerful and REPL-native one instead, improving it with e.g. commands?

Readline Heresy #

Now I said proxy REPLs are bad. And most proxy REPLs rely heavily on Readline/rlwrap. Meaning Readline is bad?

No, Readline is a blessing, actually. It’s non-invasive, it’s simple, it’s extensible. So using something like rlwrap is a good way to enhance implementation-specific REPLs.

Some useful features rlwrap provides without messing with the REPL:

So I often (or, at least, when I’m able to remember it…) do rlwrap

rlwrap ecl
# Or, with better break chars and history
rlwrap --remember -c -b "(){}[],^%$#@"";''|\" ecl
# Or something with custom completion lists,
rlwrap -f ~/.ecl_completions --remember -c -b "(){}[],^%$#@"";''|\" ecl
rlwrap invocations for better REPL experience

So, if you follow my advice and try out implementation REPLs, don’t be overly cautious about rlwrap. Do use it.

Batteries Included #

Common Lisp standard library is imperfect (which standard library is?) There are de-facto standard libraries, like ASDF/UIOP (bundled with every implementation), Alexandria, CL-PPCRE, Serapeum. But these need loading. The easiest way to load libraries is Quicklisp:

(require "asdf")
;; Load Quicklisp somehow (https://www.quicklisp.org/beta/)…
(ql:quickload :alexandria :cl-ppcre :serapeum)
Loading essential libraries via Quicklisp

One can also use alternative package managers, like CLPM, Common Lisp Project Manager, Qlot, a project-local library manager, or Guix, external project manager made with Scheme. But the approach I like the most is using submodules. Include git submodules into your project/config directory. And load them with ASDF. This allows:

This requires a certain project structure, with submodules sprinkled around it. In my case, I drop them all in the root of my config:

tree
.
├── README.org
├── closer-mop
│   ├── LICENSE.md
│   ├── README.md
...
│   ├── closer-mop.asd
│   ├── closer-sbcl.lisp
│   ├── closer-scl.lisp
│   ├── features.lisp
│   └── features.txt
├── commands.lisp
...
├── config.lisp
├── documentation.lisp
├── ed.lisp
├── gimage.lisp
├── graven-image
...
├── inputrc
├── install.lisp
├── prompt.lisp
├── reader.lisp
├── source-registry.conf.d
│   └── asdf.conf
├── talkative.lisp
├── time.lisp
├── trivial-arguments
...
My config directory structure

And then add the path to config directory to source-registry.conf.d/asdf.conf.

(:tree (:home ".config/common-lisp/"))
Adding the submodule path to ASDF central registry

This is fine for personal config, but project deployment might require some more ASDF code. You can put it into Makefile when building the project or run a couple of functions in the REPL.

Against Proxy REPLs? #

This post started as an attack on CIEL, a batteries-included custom Lisp image and REPL. Because I consider any attempt to wrap, proxy, and “prettify” the existing tool inferior to using the tool itself. But, for better or worse, the post ended up quite impartial!

In the spirit of impartiality, here’s what CIEL and other proxy REPLs provide:

But, providing that, proxy REPLs lose important native capabilities I already highlighted:

Trade-offs might be negligible for something like C++ or Python with their REPLs. But for CL, where REPL is a fundamental piece of the running image… it’s surrendering a lot. A lot of things one can mold to their liking, making Lisp more personal and comfy. One doesn’t have to surrender native REPLs to have a modern and ergonomic experience. Trivial Toplevel libraries and Readline already get one quite far!

Use native REPLs.


Most of these things I listed are already put to use in my personal config, so go check it out and adapt to your needs!

Leave feedback! (via email)