Customizing Lisp REPLs Oh, I see you disabled JavaScript. Keep up the good work, my fellow cleanweb person! 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: Process/thread name Evaluation package Command number Debug level Stepping and inspection indicators 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): Restart interaction, Backtrace listing, Rudimentary local variable inspection, Eval in frame, And multi-threaded persistent debug window that’ll wait until the decision is made. Which is a lot, but much less than what the implementation-native REPLs provide: Ability to switch to an erroring process and act on it (everywhere, same as eval-in-frame?), Disassembly and code listing for any stack frame/function (ECL, SBCL), Listing of both Lisp (everywhere) and native backtraces (ECL, ABCL maybe?) Alter local variable (CCL) and function argument (CCL) values Simulate returns and nested calls for any stack frame (CCL) Set frames and other condition info to REPL-reachable variables (CCL, ABCL) Step the execution from the moment of condition raising (SBCL) 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: Completions; Line editing; Emacs and vi keybindings, whichever you prefer; Keyboard macros Filters (including syntax highlighting-supporting ones!) 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: Pinning versions/commits of your libraries. Loading libraries without Internet connection. Distributing necessary libraries together with your project. 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.