Step 06 · Building a REPL
A REPL ("read-eval-print loop") is the most useful tool a language ships. Our implementation in repl.cpp is ~50 lines because all the heavy lifting is reused from the CLI.
void runRepl(in, out, err, opts) {
EvalState st; // persists across lines
std::string buffer;
while (getline(in, line)) {
buffer += line + "\n";
SourceFile src("<repl>", buffer);
auto l = lex(src);
auto p = parse(l.tokens);
if (needsContinuation(...)) continue; // accumulate
for (auto& d : l.diagnostics) renderTo(err, d, src);
for (auto& d : p.diagnostics) renderTo(err, d, src);
if (no errors) eval(st, p.program, out);
buffer.clear();
}
}
Multi-line continuation
The REPL recognises unfinished input — unbalanced parens, dangling
operators — and doesn't evaluate yet. It keeps reading lines into
a buffer, showing a | continuation prompt, until the input
type-checks as a complete program.
Heuristic in needsContinuation:
( > )count → expect more.- Last diagnostic mentions "got end of input" → expect more.
A more rigorous approach: have the parser return a distinguished "unexpected EOF" error type rather than scanning messages. We chose strings here for simplicity; it's the kind of decision easy to revisit once you feel the friction.
State preservation
EvalState st lives outside the loop, so:
> let x = 10;
> print x;
10
works as expected. The semantics is "each REPL line is appended to a notional program that you've been building all along".
Error recovery, REPL-style
When evaluation fails, we discard the buffer and re-prompt. The alternative — keeping the buffer so the user can edit — is what IPython / Jupyter offer, but requires terminal-line-editor integration (readline / replxx) outside the scope of this lab.
Things real REPLs add
- Line editing & history (libedit, readline, replxx).
- Tab completion (introspect
st.envfor variable names). - Special commands (
:type,:reset,:doc). - Pretty-printing of last value (Python's
_). - Persistent history file.
Each is a half-day; they're orthogonal to the core REPL loop.