Skip to content
Lesson 13 of 13

Step 1 of 5 · Reading · ~3 min

Read

Advanced Topics

A Robust REPL

A real REPL must not crash on a single bad input. If you type:

(+ 1 2)
foo
(/ 10 0)
(1 2 3)
(+ 3 4)

A good interpreter should report three errors — an unbound symbol, a division by zero, and an attempt to call a non-function — and still evaluate the well-formed forms around them, ending with 7 for the last line:

3
ERROR: unbound foo
ERROR: div by zero
ERROR: not callable
7

What we add

Every evaluator call can fail. Wrap each top-level form in try/except. The environment persists across forms — variables defined before an error are still bound after it.

Error categories

  • Lex errors — unterminated string, garbage characters.
  • Parse errors — missing ), unexpected ).
  • Eval errors:
    • unbound symbol
    • arity mismatch (wrong number of args)
    • applying a non-callable
    • division by zero

A single LispError exception class with a message is enough. Catch it, print ERROR: <message>, and decide what happens next — which depends on where the error came from.

Parse errors are not like eval errors

This is the one subtlety worth calling out: a parse error is not recoverable per-form the way an eval error is.

An eval error happens after a form has already been read successfully — the token stream is still in a clean state, so the REPL can simply move on to the next form. But a parse error (like a missing )) happens while consuming tokens — the reader may already have eaten tokens belonging to what would have been later forms, chasing a closing paren that never arrives. Once that happens, the remaining token stream can't be trusted to represent the forms that follow. So a parse failure ends processing for the rest of the input, not just the one broken form:

(+ 1 2
foo
(/ 10 0)
(+ 3 4)

Here the very first ( never finds its matching ) — the reader keeps consuming tokens (including the ones that were meant to be foo, (/ 10 0), and (+ 3 4)) looking for it, runs out of input, and reports a single ERROR: missing ). Nothing after it runs, because nothing after it was ever safely parsed in the first place.

Why this matters

Production languages must keep going in the face of bad values — compilers report dozens of errors per build by design, IDEs evaluate snippets continuously. But they're far more cautious about bad syntax: once the parser loses sync with the token stream, most tools bail rather than guess at what was meant. The robust-REPL pattern here — recover from eval errors per form, but stop at the first parse error — mirrors this same split in every Jupyter kernel, browser console, and database client.

Architecture

src -> tokens -> AST -> values
        ^         ^      ^
        |         |      |
        +— a failure here (parse) halts remaining input;
           a failure here (eval) only aborts the current form.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…