---
title: "Building a REPL OS in Common Lisp, Part 1: The Foundation"
date: "2026-06-02"
description: "Part 1 of a series where we build a custom operating system that runs inside a Lisp REPL. Starting with the most fundamental piece: the REPL itself."
tags: ["lisp"]
series: "Building a REPL OS in Common Lisp"
part: 1
language: "en"
draft: false
---

*Part 1 of a series where we build a custom operating system that runs inside a Lisp REPL.*

---

## What Are We Building?

Over this series, we'll build something unusual: an operating system that lives inside a REPL. Not a real OS that boots on hardware, but a simulation but a playground where we can explore OS concepts like process management, filesystems, and command interpreters, all while staying in the comfortable world of Lisp.

By the end of the series, our REPL OS will have:

- A command system with custom commands
- Background threads (processes)
- A virtual filesystem with persistence
- The ability to mount real directories
- Self-modifying capabilities

But we're going to build it incrementally. Each article adds one layer. Today, we start with the most fundamental piece: the REPL itself.

## What is a REPL?

If you are here I assume that you at least have some experience using an interactive shell. REPL stands for Read-Eval-Print Loop. A shell is a form of REPL, and it's the interactive shell that Lisp programmers live in. You type an expression, the system evaluates it, prints the result, and waits for the next input. If you've spent any time prompting an AI agent lately, you've already been inside one too. Same pattern: you type, it responds, you type again. The interaction model isn't new at all. Lisp programmers have been living in it for sixty years.

```lisp
(+ 1 2)
=> 3
(cons 'a '(b c))
=> (A B C)
```

Every Lisp environment already has a REPL. So why build our own?

Because we want to intercept that loop. We want to:

1. **Add custom commands** that aren't standard Lisp functions
2. **Control what happens** before and after evaluation
3. **Handle errors** our way
4. **Track history** and state across evaluations

Our REPL will wrap the standard Lisp evaluator. When you type something, we'll eventually check if it's one of our commands. If not, we pass it through to regular Lisp `eval`. This means everything you can do in normal Lisp still works and we're just adding capabilities, not replacing them.

## Starting Simple

Let's build the REPL step by step, starting with the absolute minimum. And the minimum is surprisingly small when you use Lisp.

In Common Lisp, we have built-in functions that do exactly what a REPL needs: `read` parses input into a Lisp form, `eval` evaluates it, and `print` displays the result. The simplest possible REPL is just these three nested in a loop:

```lisp
(loop (print (eval (read))))
```

That's it. Three functions and a loop. Nothing else is required. You could run this right now and it would work, or sort of. Try typing `(+ 1 2)` and you'd get `3`. But there are problems: there is no prompt (you stare at a blank line), there is no error handling (one mistake crashes everything), and there is no way to exit cleanly.

Let's fix these one at a time.

## Adding a Prompt and Better Output

First, let's give users a visual cue that we're ready for input, and make results visually distinct:

```lisp
(loop
  (format t "~%λ ")
  (force-output)
  (format t "~%=> ~S" (eval (read))))
```

The `format` function prints to standard output. `~%` means newline, `~S` prints the result in readable form. We use `λ` as our prompt because  what better symbol for a Lisp environment? The `force-output` ensures the prompt appears before `read` blocks waiting for input.

Now sessions look like:

```
λ (+ 1 2)
=> 3
```

## Error Handling

Right now, `(/ 1 0)` crashes us back to the shell. We need to guard against this type of situations, and for that we wrap the evaluation in a `handler-case`:

```lisp
(loop
  (format t "~%λ ")
  (force-output)
  (handler-case
      (format t "~%=> ~S" (eval (read)))
    (error (e)
      (format t "~%ERROR: ~A" e))))
```

If any error occurs, we catch it and print a message instead of crashing. Now we can make mistakes and keep going, which is essential for an interactive environment.

## Clean Exit

We need a way out. For that, let's check for `(quit)` before evaluating and:

```lisp
(loop
  (format t "~%λ ")
  (force-output)
  (let ((form (read)))
    (when (and (consp form) (eq (car form) 'quit))
      (format t "~%Goodbye.~%")
      (return))
    (handler-case
        (format t "~%=> ~S" (eval form))
      (error (e)
        (format t "~%ERROR: ~A" e)))))
```

We read the form first, then check: is it a list whose first element is `quit`? If so, `return` from the loop.

Why `(quit)` instead of just `quit`? Consistency and simplicity. Everything in our REPL will look like a function call: `(command arg1 arg2)`. The command is always `car`, arguments are `cdr`. This uniform syntax will make parsing easy when we add more commands.

## The Complete Function

Wrapping it in a function with a welcome message:

```lisp
(defun my-repl ()
  "Start the REPL. Type (quit) to exit."
  (format t "~%Welcome to REPL OS v1")
  (format t "~%Type any Lisp expression. Use (quit) to exit.~%")
  (loop
    (format t "~%λ ")
    (force-output)
    (let ((form (read)))
      (when (and (consp form) (eq (car form) 'quit))
        (format t "~%Goodbye.~%")
        (return))
      (handler-case
          (format t "~%=> ~S" (eval form))
        (error (e)
          (format t "~%ERROR: ~A" e))))))
```

We tell users how to exit right away: nothing's more frustrating than being trapped in a program.

## Project Structure

To make this a proper loadable system, we'll use ASDF, the standard Common Lisp build system. Create a folder with three files:

```
repl/
  repl.asd       # System definition
  packages.lisp  # Package definition
  repl.lisp      # The REPL itself
```

### repl.asd

The system definition tells ASDF what files to load and in what order:

```lisp title="repl.asd"
(asdf:defsystem :repl
  :description "A custom REPL in Common Lisp - Version 1: The Foundation"
  :version "1.0.0"
  :serial t
  :components
  ((:file "packages")
   (:file "repl")))
```

The `:serial t` option means files are loaded in order. `packages.lisp` first, then `repl.lisp`.

### packages.lisp

We define a package to keep our code organized:

```lisp title="packages.lisp"
(defpackage :repl
  (:use :cl)
  (:export #:my-repl))
```

We `:use :cl` to inherit all standard Common Lisp symbols. We export `my-repl`, which will be our entry point.

### repl.lisp

Our REPL function from above, wrapped in the package with `(in-package :repl)` at the top.

## Running It

To use the REPL, you need a Common Lisp implementation. SBCL is a good choice. Here's how to get started:

### The Standard Way

This is how you'd typically load a Common Lisp project:

1. **Navigate to your repl directory and start SBCL:**
   ```bash
   cd repl
   sbcl
   ```

2. **Load the system definition and the system itself:**
   ```lisp
   (load "repl.asd")
   (asdf:load-system :repl)
   ```

3. **Start the REPL:**
   ```lisp
   (repl:my-repl)
   ```

This three-step process is standard in Common Lisp development. The `.asd` file tells ASDF about our system, then `load-system` compiles and loads all the components.

### The Quick Way

If you want a one-command launch, create a `run.lisp` file:

```lisp title="run.lisp"
(load (merge-pathnames "repl.asd" *load-truename*))
(asdf:load-system :repl)
(repl:my-repl)
(quit)
```

Then just:

```bash
sbcl --load run.lisp
```

This loads the system, starts the REPL, and cleanly exits SBCL when you quit. The `*load-truename*` trick ensures paths resolve correctly regardless of where you invoke the command from.

You should see:

```
Welcome to REPL OS v1
Type any Lisp expression. Use (quit) to exit.

λ
```

Try some expressions:

```
λ (+ 1 2 3)
=> 6

λ (reverse '(a b c))
=> (C B A)

λ (defun square (x) (* x x))
=> SQUARE

λ (square 5)
=> 25

λ (/ 1 0)
ERROR: arithmetic error DIVISION-BY-ZERO signalled

λ (quit)
Goodbye.
```

Notice that errors don't crash us. We catch them, report them, and keep going.

## What We Built

Up to this point it does not look like much, but starting from a one-liner, we incrementally added:

1. **A prompt**: so users know when to type
2. **Output flushing**: so the prompt appears before we block on input
3. **Better formatting**: `=> ` prefix for results
4. **Error handling**: mistakes don't crash the session
5. **Clean exit**: `(quit)` to leave gracefully
6. **Project structure**: proper ASDF system for easy loading

We can evaluate any Lisp expression. We can define functions and use them. We can make mistakes and recover. It's a foundation.

## What's Next

Our REPL is functional but primitive. Here's what we'll add in future versions:

**Version 2** introduces a command system. Instead of hardcoding `quit`, we'll have a registry of commands. Each command will be a struct with a name, description, and function. We'll be able to add new commands at runtime, and we'll implement `help` to list them.

**Version 3** adds threading. We'll use Bordeaux Threads to run background tasks: timers, schedulers, anything that needs to happen while the REPL is waiting for input. We'll add `ps` to list threads, `kill` to stop them, and commands like `after` and `every` to schedule work.

**Version 4** builds a virtual filesystem in memory. We'll create directories and files as a tree of nodes, and write the usual commands to navigate it: `ls`, `mkdir`, `touch`, `cat`, `write`, `rm`.

**Version 5** adds persistence. The filesystem survives restarts, serialized as S-expressions. We'll also write `tree` and `find` to walk and search what we've built.

**Version 6** introduces mounts: the ability to map a virtual path to a real directory on disk. Suddenly the OS can read and edit its own source code from within.

**Version 7** unifies the two worlds. Virtual files and mounted files end up behind a single abstraction, so every command sees the whole filesystem regardless of where the bytes really live.

Each version builds on the last. By the end, we'll have something that feels surprisingly like a real operating system, all running inside a Lisp REPL.

## The Philosophy

There's a deeper reason to build a REPL OS: to understand abstractions.

An operating system is a collection of abstractions. Processes, files, permissions, none of which exist in hardware. They're ideas, implemented in software, that make computers usable.

By building our own, we see how these abstractions are constructed. We see that a "file" is just a node in a tree with a name and contents. We see that a "process" is just a thread with an entry in a registry. We see that a "command" is just a function with metadata.

These aren't just simplifications, this is genuinely how it works. Real operating systems are more complex because they handle more edge cases at a bigger scale, but the core ideas are the same.

Lisp is fun to use, and I think it is the perfect language for this exploration. Some people feel intimidated by the amount of parenthesis, but its simplicity and uniform syntax means we can extend the language in a very natural way. Its interactive nature means we can experiment in real time. Its homoiconicity means our OS can manipulate its own code.

The foundation is laid. Now we start building.

---

*Next: Part 2: The Command System*
