Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

REPL

The bop CLI ships an interactive REPL that carries state across submissions, echoes bare-expression results, supports multi-line input, and persists history across sessions.

$ bop repl
> let x = 5
> let f = fn(n) { return n * x }
> f(7)
35
> :quit

Starting the REPL

bop            # default subcommand is `repl`
bop repl

Ctrl-D on an empty prompt exits. Ctrl-C clears the current line without touching the session.

What persists across submissions

Everything that shows up in the program’s scope:

  • let / const bindings.
  • fn declarations (named and anonymous-then-let-bound).
  • struct / enum / method declarations.
  • use imports and module aliases.
  • The rand() seed — same starting seed, same sequence (helpful for reproducing bugs).

Resource limits (BopLimits::standard() by default) reset per submission, so the step budget doesn’t accumulate across lines.

Bare expressions echo

When the last statement in a submission is a bare expression, the REPL prints its value:

> 1 + 2
3
> let x = 5      // `let` has no value — no echo
> x
5

print(...) returns none; the REPL suppresses none echoes so print(42) only shows 42 once (from the host), not “42” followed by “none”.

Multi-line input

When a line parses with “end of code” — unclosed brace, trailing +, unfinished match — the REPL keeps the buffer open and prompts for more. Paste a multi-line block and it runs as one submission:

> fn greet(name) {
...   return "hi " + name
... }
> greet("Bop")
hi Bop

A different parse error (typo, unexpected token) submits immediately so you can see the error rather than hunting through a stale buffer.

Tab completion

Hit Tab on an identifier prefix to see matches from:

  • Bop keywords (let, fn, match, use, …)
  • Built-in functions (print, len, range, int, float, sqrt, …)
  • Names currently in the session (let my_var = … shows up after declaration)
  • Identifiers the REPL has seen you type in previous submissions (covers fn parameters, struct field names)

Meta-commands

Lines starting with : are REPL commands, not Bop code:

CommandAction
:helpPrint the meta-command list
:varsList all currently-bound names (sorted)
:reset / :clearDrop every binding and start fresh
:quit / :q / :exitExit the REPL

Unknown commands surface a friendly “try :help” hint rather than being silently ignored.

History

Arrow keys browse history. ~/.bop_history ($USERPROFILE\.bop_history on Windows) persists history across sessions. History save on exit is best-effort — the REPL doesn’t error if it can’t write the file.

Error handling

Runtime and parse errors render with the same source-snippet + carat as errors from bop run:

> let f = fn(n) { return missing(n) }
> f(5)
error: I don't know what 'missing' is
  --> line 1:23
  |
1 | let f = fn(n) { return missing(n) }
  |                        ^
hint: Did you forget to create it with `let`?

The error doesn’t reset the session — subsequent submissions still see the prior bindings.

Piped / non-TTY input

When stdin isn’t a terminal (piped, heredoc, test harness), the REPL accumulates stdin line by line using the same incomplete-input heuristic, so a script like:

bop repl <<EOF
fn double(n) {
  return n + n
}
print(double(21))
EOF

prints 42 and exits 0. Errors during a piped session exit 1 but don’t abort the remaining input — if you pipe five submissions and #2 fails, #3 through #5 still run. That matches how a user would experience the same sequence interactively.

Using the session from Rust

bop::ReplSession is the same session type the CLI uses, exposed for embedders that want to drive Bop as a scripting layer from their own app. See Embedding → Stateful REPL sessions.