
\documentclass[12pt]{article}
\usepackage[T2A,OT1]{fontenc}
\usepackage[default]{cantarell}
\usepackage[a4paper, top=20mm, bottom=20mm, left=20mm, right=20mm]{geometry}
\usepackage[utf8]{inputenc}
\usepackage[russian, english]{babel}
\usepackage{tabu}
\usepackage{hyperref}
\usepackage{parskip}
\usepackage{graphicx}
\usepackage{tabularx}
\usepackage[normalem]{ulem}
\usepackage{float}
\floatstyle{boxed}
\restylefloat{figure}
\usepackage{setspace}
\onehalfspacing
\author{Artyom Bologov \href{mailto:customize-repl@aartaka.me}{(email)}}
\date{\today}
\title{Customizing Lisp REPLs}
\makeatletter
\def\endenv{\expandafter\end\expandafter{\@currenvir}}
\makeatother
\begin{document}
\maketitle

\includegraphics[width=\textwidth,height=\textheight,keepaspectratio]{./assets/customize-repl.png}

I am a portability freak.
\href{run:paren-regex}{If something can be done with an existing tool, I’ll go for it}.
\href{https://codeberg.org/aartaka/lisp-config/commit/887e7b3}{If the program can be portable across systems, then it should be}.
\href{run:postmodern-front-end}{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.

\paragraph{SBCL SBCL SBCL} \begin{quote}
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, \verb|sb-aclrepl| extension:

\begin{figure}[h!]\begin{verbatim}
(require "sb-aclrepl")
\end{verbatim}\caption{Getting an Allegro-like REPL in SBCL}\end{figure}
\end{quote}

\section*{Welcoming Prompt} \label{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
\href{https://codeberg.org/aartaka/trivial-toplevel-prompt}{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 \verb|sb-aclrepl|.)

Details varying between implementations, prompts can include:

\begin{itemize}\item Process/thread name
\item Evaluation package
\item Command number
\item Debug level
\item Stepping and inspection indicators
\end{itemize}

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:

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

\section*{Commands} \label{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.
\href{https://codeberg.org/aartaka/trivial-toplevel-commands}{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:

\begin{figure}[h!]\begin{verbatim}
(define-command/string (:sh :!) (command)
 "Run shell command synchronously"
 (ignore-errors
  (uiop:run-program command :output t :error-output t)))
\end{verbatim}\caption{Trivial Toplevel Commands used for shell pass-through}\end{figure}

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

\begin{figure}[h!]\begin{verbatim}
(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))))))
\end{verbatim}\caption{Directory listing command defined with Trivial Toplevel Commands}\end{figure}

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: \verb|: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…

\section*{Reader Syntax} \label{reader}

You want some syntax in your Lisp?
Imagine… two chars to enable any notation, including
\href{https://github.com/y2q-actionman/with-c-syntax}{a C-resembling one}.
CL can do it all, given enough will on the side of the programmer.
\href{https://lispworks.com/documentation/HyperSpec/Body/f_set_ma.htm}{And a couple of standard APIs}.
So one doesn’t really need much special REPL syntax if they have reader macros.

Shell passthrough: \verb|#!cat passwords.txt|.
Inline docs: \verb|#?(+ function)|.
Convenient hash tables: \verb|#{:reader T :commands T :prompt T}|.
Short lambdas: \verb|#^kv.(print (list k v))|
Anything really.
\href{https://codeberg.org/aartaka/lisp-config/src/branch/main/reader.lisp}{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 \verb|#?documentation|.
Portable (once defined) across implementations, compatible with all the existing REPLs, integrated into the language.

\section*{GUI Debuggers} \label{debugger}

One of the things I worked on when I was on Nyxt team was the graphical debugger.
\href{https://github.com/atlas-engineer/nyxt/commit/6fdf502}{You can check the latest state before it was removed in January 2025}.
The underlying libraries are
\href{https://github.com/atlas-engineer/ndebug}{Ndebug, a framework for GUI debugger construction}, and
\href{https://github.com/Shinmera/dissect}{Dissect, Shinmera’s condition/stack inspection library}, and the fundamental
\href{https://github.com/trivial-gray-streams/trivial-gray-streams}{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):

\begin{itemize}\item Restart interaction,
\item Backtrace listing,
\item Rudimentary local variable inspection,
\item Eval in frame,
\item And multi-threaded persistent debug window that’ll wait until the decision is made.
\end{itemize}

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

\begin{itemize}\item Ability to switch to an erroring process and act on it (everywhere, same as eval-in-frame?),
\item Disassembly and code listing for any stack frame/function (ECL, SBCL),
\item Listing of both Lisp (everywhere) and native backtraces (ECL, ABCL maybe?)
\item Alter local variable (CCL) and function argument (CCL) values
\item Simulate returns and nested calls for any stack frame (CCL)
\item Set frames and other condition info to REPL-reachable variables (CCL, ABCL)
\item Step the execution from the moment of condition raising (SBCL)
\end{itemize}

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?

\section*{Readline Heresy} \label{readline}

Now I said proxy REPLs are bad.
And most proxy REPLs rely heavily on Readline/\verb|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 \verb|rlwrap| is a good way to enhance implementation-specific REPLs.

Some useful features \verb|rlwrap| provides without messing with the REPL:

\begin{itemize}\item Completions;
\item Line editing;
\item Emacs and vi keybindings, whichever you prefer;
\item Keyboard macros
\item Filters (including syntax highlighting-supporting ones!)
\end{itemize}

So I often (or, at least, when I’m able to remember it…) do \verb|rlwrap|
\begin{figure}[h!]\begin{verbatim}
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
\end{verbatim}\caption{rlwrap invocations for better REPL experience}\end{figure}

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

\section*{Batteries Included} \label{batteries}

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:

\begin{figure}[h!]\begin{verbatim}
(require "asdf")
\href{https://quicklisp.org/beta}{;; Install Quicklisp first}
(ql:quickload :alexandria :cl-ppcre :serapeum)
\end{verbatim}\caption{Loading essential libraries via Quicklisp}\end{figure}

One can also use alternative package managers, like
\href{https://clpm.dev}{CLPM, Common Lisp Project Manager},
\href{https://qlot.tech}{Qlot, a project-local library manager},
\href{https://guix.gnu.org}{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:

\begin{itemize}\item Pinning versions/commits of your libraries.
\item Loading libraries without Internet connection.
\item Distributing necessary libraries together with your project.
\end{itemize}

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

\begin{figure}[h!]\begin{verbatim}
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
...
\end{verbatim}\caption{My config directory structure}\end{figure}

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


\begin{figure}[h!]\begin{verbatim}
(:tree (:home ".config/common-lisp/"))
\end{verbatim}\caption{Adding the submodule path to ASDF central registry}\end{figure}

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.

\section*{Against Proxy REPLs?} \label{proxy}


This post started as an attack on
\href{https://github.com/ciel-lang/CIEL}{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:

\begin{itemize}\item Easier script running,
\item Readline by default,
\item Completion,
\item Error prettification (making debugger off/useless in case of CIEL),
\item Shell and documentation commands / syntaxes,
\item A set opinionated functions and macros,
\item A bunch of libraries pre-loaded.
\end{itemize}

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

\begin{itemize}\item REPL-native commands,
\item Implementation debugger,
\item Ability to customize things using implementation APIs,
\item Minimal and standard syntax.
\end{itemize}

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.

\par\noindent\rule{\textwidth}{0.4pt}

Most of these things I listed are already put to use
\href{https://codeberg.org/aartaka/lisp-config}{in my personal config},
so go check it out and adapt to your needs!


\par\noindent\rule{\textwidth}{0.4pt}
\href{https://creativecommons.org/licenses/by/4.0}{CC-BY 4.0} 2022-2026 by Artyom Bologov (aartaka,)
\href{https://codeberg.org/aartaka/pages/commit/a91befa}{with one commit remixing Claude-generated code}.
Any and all opinions listed here are my own and not representative of my employers; future, past and present.
\end{document}
