REPL reference

The exact behavior of Cruft's interactive REPL: session state that persists across lines, the _ last-result variable, expression-versus-statement parsing, multi-line continuation, the dot-commands, history and tab completion, and where it diverges from Node's (no top-level await, no _error).

cruft with no entry point on a terminal starts the interactive REPL; cruft -i (--interactive) forces it even when stdin is piped. The REPL is node-shaped where node's behavior is the ecosystem expectation, and this page states its exact mechanics.

$ cruft
Welcome to cruft 0.0.10.
Type ".help" for more information.
>

Session model

One persistent runtime holds state across the whole session (node parity): top-level var/let/const/class/function declarations persist across lines, because the REPL's script mode publishes top-level declarations to globalThis natively in the compiler, there is no source rewriting.

Each line evaluates and auto-prints its completion value:

> 1 + 1
2
> "hi"
'hi'
> let x = 10
undefined
> x * 2
20

Details of the printing rules:

  • Values render via the engine's inspector (console.log formatting), except top-level strings, which are quoted ('hi'), matching node's REPL, and deliberately distinct from -p/console.log (which print strings bare).
  • Statements complete with undefined, like node.

Re-declaration is allowed across lines: entering let x = 10 and later let x = 99 does not raise "identifier already declared" the way a single script would. The REPL treats each fresh-prompt declaration as a rebind of the session global, matching node's REPL convenience.

Expression vs. statement

The REPL decides how to treat a line by parsing it: if (<line>) parses as an expression, it evaluates as one; otherwise the line runs as statements. Parsing first matters because a run-and-catch strategy would double-evaluate side effects; Cruft's parse-first approach never executes a line twice.

One node-parity subtlety is handled explicitly: a leading function, class, or async function is forced down the statement path (a parenthesized declaration would parse as a named expression and bind nothing, node treats it as a declaration so the name persists, and so does Cruft).

The _ variable

_ holds the last expression result, updated on every evaluation (undefined after a statement line):

> x * 2
20
> _
20

_ is an ordinary session global, not a special accessor. You can assign to it (_ = 100), but the next expression you evaluate overwrites it again; there is no node-style "assignment to _ now disabled" mode. There is also no _error variable (node exposes the last uncaught error as _error; Cruft does not, so referencing _error throws ReferenceError).

Multi-line continuation

An incomplete line switches the prompt to ... and accumulates until the buffer parses as complete:

> const o = {
... a: 1
... }
undefined
> o.a
1

The continue-vs-error decision is made by classifying the parse failure: ran-out-of-input errors (an unclosed brace/paren/bracket, an expression cut off at end of input) continue; a genuine mid-stream syntax error, or an at-end error that no further input could repair, evaluates immediately so the error surfaces. The classification follows node's decisions.

Promises and top-level await

This is the sharpest divergence from node's REPL, so know it before you reach for it. Top-level await is not supported. A line that begins with await is a syntax error, not an awaited value:

> await Promise.resolve(42)
Uncaught SyntaxError: expected semicolon or line terminator

And a promise value is not awaited or specially rendered for display. Node's REPL prints Promise { 42 } (resolving pending ones); Cruft prints the promise through the ordinary inspector, so you see its object form:

> (async () => 7)()
{}

To get a resolved value at the REPL today, drive the promise yourself, either assign it and read it on a later line, or use .then with a console.log:

> (async () => 7)().then(v => console.log("got", v))
{}
> got 7

The {} is the promise printing immediately; the got 7 line is the callback firing on the next tick. If you need await ergonomics, run the code from a file instead, where module top-level await is supported.

Dot-commands

Recognized at a fresh > prompt (so a continuation line like .map(x => x) is never eaten):

CommandEffect
.helpPrint the command list
.exitEnd the session
.breakAbandon an in-progress multi-line buffer
.clearAlias for .break
.save <path>Write every evaluated command this session to a file
.load <path>Evaluate a file's contents in the session

.break and .clear are additionally recognized mid-continuation, the one place you need them. Anything else starting with . at a fresh prompt prints Invalid REPL keyword, including node's .editor (multi-line editor mode), which Cruft does not have. .save writes every line you entered this session (verbatim, including dot-command-free input) to the path; .load reads a file and evaluates its contents line by line in the current session.

Line editing, history, completion (TTY)

On a terminal the REPL runs a raw-mode line editor:

  • Cursor editing on the current line; ↑/↓ navigate history, with the in-progress draft preserved when you arrow back down.
  • History persists to ~/.cruft_repl_history (capped at 1000 entries) across sessions.
  • Tab completion, engine-native: a bare prefix completes against globalThis (including its prototype chain) plus JavaScript keywords; <expr>.<prefix> completes against the object's keys. Property completion only engages when <expr> is a side-effect-free dotted-identifier path, no calls, no bracket indexing, so Tab never invokes a function. On multiple candidates, the longest common prefix is filled and candidates listed.
  • Ctrl+C abandons the current line/continuation and returns to a fresh prompt; Ctrl+D (or closed stdin) evaluates any pending buffer and ends the session.

When stdin is not a terminal (piped input under -i), the editor falls back to plain line reads, same evaluation semantics, no raw-mode features, which makes scripted REPL sessions reproducible.

Errors

A thrown or engine error prints as Uncaught <Name>: <message>, on stdout, inline with results, exactly as node's REPL does (so a captured session interleaves correctly). Cruft-internal decorations (byte offsets, internal URL tags, compile: parse: prefixes) are stripped for the REPL surface:

> nope
Uncaught ReferenceError: nope is not defined

The session recovers and continues after any error.

Capabilities and the REPL

The REPL honors the same capability flags as file execution: Cruft --sealed -i gives a sealed interactive session (I/O denied unless granted), --audit logs the session's capability use, and --allow-net-loopback applies. There is no separate REPL policy; an interactive line has exactly the authority the process mode grants.

Where it diverges from node's REPL

The REPL is node-shaped by default; these are the deliberate or not-yet-built differences a node user will notice, all covered above:

  • No top-level await (syntax error), and promises print as their object form rather than being awaited.
  • No _error variable for the last uncaught error.
  • No .editor dot-command.
  • Assigning _ does not switch off its auto-update the way node's does.

Everything else, session persistence, the _ result, parse-based expression detection, multi-line continuation, the other dot-commands, history, editing, completion, and error rendering, follows node's observable behavior.

Line identity

Each evaluated line gets a synthetic source URL [repl:N], which is what appears in stack traces originating from REPL input.

Source: the REPL driver and its raw-mode editor with history.