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.logformatting), 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):
| Command | Effect |
|---|---|
.help | Print the command list |
.exit | End the session |
.break | Abandon an in-progress multi-line buffer |
.clear | Alias 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
_errorvariable for the last uncaught error. - No
.editordot-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.