REPL
This document covers the interactive REPL (build/d6 with no file
argument, when stdin is a terminal) — line editing, history, and how it
falls back to plain-pipe behavior for scripted use.
A browser version of the same session model runs on getdiamond.codes from the WebAssembly build (internal notes); it has no line editing beyond the page's own, and fibers, threads, sockets and databases are unavailable there.
Multi-line, session-accumulating evaluation
Each round reads one or more physical lines and compiles the whole session
plus the pending input as one program. If the diagnostic indicates incomplete
input, such as an unclosed block, the REPL prompts for another line (... )
instead of reporting an error. After a successful compile, it prints the
result and retains the source so later rounds see earlier definitions and
variables.
Interactive line editing
When stdin is a real terminal (isatty(STDIN_FILENO)), the REPL puts the
terminal into raw mode (ECHO, ICANON, and ISIG all cleared) and
reads/edits one line at a time itself, rather than deferring to the
kernel's line discipline via getline:
- Printable characters insert at the cursor.
- Left/Right move the cursor; Home/End jump to the start/end of the line; Delete removes the character under the cursor; Backspace removes the one before it.
- Up/Down navigate command history (see below). The first Up on a freshly-typed, not-yet-submitted line stashes it so Down can restore it after navigating back past the newest history entry — matching bash/readline convention.
- Ctrl-C discards the current line and returns to a fresh prompt
without exiting the process. This is deliberately input-only: it does
not interrupt a running evaluation.
ISIGis cleared specifically so Ctrl-C is read as an ordinary byte (0x03) rather than raising a realSIGINT— this is unrelated to, and doesn't interact with, theSignal.traplanguage feature described indocs/io.md. - Ctrl-D exits the REPL, but only on an empty line — on a non-empty line it's a no-op, matching common shell convention (not a forced submit, not a deletion).
exit/quit, typed as a whole line by itself, also exit the REPL — not a language builtin (d6 has none), recognized only here, only when it's the first line of a fresh statement (so it can't misfire partway through a multi-line block, where it's just an ordinary, undefined identifier like any other word). Case-sensitive, matching the exact spelling every other REPL with this convention (irb, pry, the Python REPL, ...) actually recognizes.
Each edit redraws the prompt and input from scratch, not as a diff against what was drawn before. Input longer than the terminal is wide wraps onto further rows: the redraw moves back up to the prompt's row, clears to the end of the screen, rewrites everything and puts the cursor on the right row and column. The width is read from the terminal on each redraw (80 when it can't be read). Two limits remain: a wide character such as CJK text counts as one column, and resizing the terminal while a wrapped line is on screen can misplace redraws until the next prompt.
CSI (arrow-key and similar) escape sequences are parsed generically — parameter bytes collected, then dispatched on the final byte — so any recognized-shape-but-unhandled sequence (a key combination the editor doesn't act on) is still fully consumed rather than leaking its raw bytes into the buffer. A bare standalone Escape keypress, with no following CSI bytes at all, blocks waiting for the next byte rather than resolving immediately; this is a known limitation of not implementing read-with-a-timeout disambiguation, not something expected to matter in practice (every arrow/function key sends its CSI bytes back-to-back).
Tab-completion
Pressing Tab completes the identifier (or receiver.method) at the
cursor, reusing the LSP's own completion engine in-process (no
subprocess, no JSON-RPC transport) — the same candidate list an editor
connected to d6 lsp would see for the same position. Exactly one
match splices the unmatched remainder into the buffer at the cursor and
redraws, the same way typing it would have; two or more matches print
every candidate label on a fresh line below the prompt (unfiltered by
score or frequency) and redraw the prompt unchanged, for the user to
keep typing and press Tab again to narrow further — no arrow-key
menu navigation. Zero matches is a no-op. Completion only activates in
raw-mode interactive line editing (see above); the non-interactive
fallback below has no Tab handling at all.
History
Entries persist to $HOME/.diamond_history, one physical line per entry,
loaded at startup and appended (with an immediate fflush, not buffered
until exit) as each line is submitted — so history survives a crash or
kill -9, not only a clean Ctrl-D exit. A missing or unwritable $HOME
is not fatal: history persistence is silently skipped and in-memory
navigation still works for the rest of the session.
Blank/whitespace-only lines are never added. An exact repeat of the
immediately preceding entry is skipped too, matching bash's
ignoredups-like default — re-running the same command repeatedly
doesn't require stepping through identical copies to get further back.
History has no size cap (no HISTSIZE-style trimming, in memory or in
the file) — a known simplification rather than a deliberate stance;
revisit if REPL sessions long enough for this to matter in practice turn
out to be common.
Non-interactive fallback
When stdin is not a terminal (a pipe, a file redirect, or
DIAMOND_FORCE_REPL=1 driving the REPL over a coprocess as
tests/repl_test.sh does), none of the above activates. The REPL prints the
prompt and uses a plain
getline() read with no editing, no history, and Ctrl-C handled however
the shell/kernel already handles it for a backgrounded or piped process.
Raw-mode line editing is only meaningful for a real terminal, and the plain
path also makes the REPL predictable in scripts and tests.