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/constbindings.fndeclarations (named and anonymous-then-let-bound).struct/enum/ method declarations.useimports 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:
| Command | Action |
|---|---|
:help | Print the meta-command list |
:vars | List all currently-bound names (sorted) |
:reset / :clear | Drop every binding and start fresh |
:quit / :q / :exit | Exit 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.