d6 docs

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. ISIG is cleared specifically so Ctrl-C is read as an ordinary byte (0x03) rather than raising a real SIGINT — this is unrelated to, and doesn't interact with, the Signal.trap language feature described in docs/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.

Edit this page on GitHub