The CLI and the REPL
Seven subcommands, and each one prints exactly one representation from Claim 1 of the mental model. That correspondence is the design, and it is why the CLI is a learning tool as much as a shipping one.
The Commands
ember run script.ember # execute (VM backend, the default)
ember run --interp script.ember # execute with the tree walker — the oracle, exposed
ember repl # interactive
ember tokens script.ember # LEXER output
ember ast script.ember # PARSER output
ember disassemble script.ember # COMPILER output
ember trace script.ember # VM output: one line per instruction
ember compile script.ember -o x.embc # serialized bytecode (Lab 22)
| Command | Representation | Built in |
|---|---|---|
tokens | Vec<Token> | Lab 1 |
ast | Block | Lab 2 |
disassemble | Chunk | Lab 9 |
trace | VM state per instruction | Lab 11 |
run | Value | Lab 3 |
run --interp | the same Value, a different way | Lab 12 |
run --interp is a user-facing feature, not a test hook. When a user reports "this gives the
wrong answer", the first question is "does --interp agree?", and it localizes the bug to one of
two implementations before you have read any code.
Global Flags
--no-color for golden tests, CI logs, and NO_COLOR
--stats print the stats block on exit
--max-instructions N --max-memory BYTES --max-depth N
--gc-stress collect on every allocation (~1000× slower; finds root bugs)
--trace-gc collection log with pause times
--trace-tables array/hash transitions and migrations
--trace-upvalues the open list and every close event
--trace-modules cache hits, misses, and the loading chain
--trace-host every marshal in and out, with types
--trace-ic inline-cache state transitions (§7)
Four design rules for all of them, from the teaching method:
- Trace output goes to stderr. Stdout is what golden tests compare, and a
--trace-gcthat broke the test suite is a real thing that happens. - Switchable at run time, not by recompiling.
- Cheap when off —
if self.trace.is_some()around the formatting, not a formatted string thrown away. In the dispatch loop this is measurable. - Replayable. Anything you can dump, you can feed back in.
The REPL
The REPL is the most hostile input source in the project — a human, typing, interactively — and it must be impossible to crash.
#![allow(unused)] fn main() { loop { let line = match editor.readline("> ") { Ok(l) => l, Err(Interrupted) => continue, // Ctrl-C cancels the LINE, not the session Err(Eof) => break, // Ctrl-D exits Err(e) => { eprintln!("input error: {e}"); break } }; match engine.eval_repl(&line) { Ok(values) => print_values(&values), Err(e) if e.is_incomplete() => { buffer.push_str(&line); continue } // multi-line Err(e) => eprintln!("{}", render(&e, engine.sources())), } } }
Five properties, each of which is a decision:
- Every REPL line is its own
SourceFile, named<repl:3>. Without that, every traceback says line 1, and a traceback through three definitions is useless. It also means theSourceMapgrows — bound it, or evict and degrade gracefully to "location unavailable". - Incomplete input continues rather than erroring.
function f()with noendshould prompt>>, not report a syntax error. The lexer/parser must distinguish "unexpected EOF" from "unexpected token", which is one extraErrorKindpredicate and a much better experience. - A bare expression prints its value.
1 + 1→2. Implemented by trying to parse the line as an expression first, and falling back to a statement — the same "parse first, classify second" move asexpr_stat. - State persists across lines, but limits reset per line. A REPL user should not have to think
about the instruction budget, and an accidental
while true do endshould return the prompt, not the process. - Errors never terminate the session. Not a syntax error, not a runtime error, not a limit, not a host panic. Every one is caught, rendered, and the prompt returns.
Plus the dot commands, which are where the REPL becomes a teaching tool:
.help .exit
.tokens EXPR .ast EXPR .disasm EXPR .trace EXPR
.stats .gc .heap .globals
.interp EXPR — evaluate under the tree walker and DIFF against the VM
.interp is worth building. Typing an expression and seeing both backends agree — or not — is the
most direct demonstration of
ADR-003 available.
Exit Codes
0 success
1 a script error (syntax, runtime, or limit)
2 a usage error (bad flags, missing file)
70 an internal error (a bug in Ember)
Exit code 1 rather than a signal is the deliverable of every limit test. echo $? after a
runaway script must print 1, not 139.
Separating 70 from 1 matters operationally: a CI pipeline running policies wants to distinguish "the policy is wrong" from "the runtime is broken", and only the second one should page anybody.
Things to Notice
- One subcommand per representation is the design. If you add a representation, add a subcommand.
--interpis a user-facing debugging tool, and it exists only because the tree walker was kept.- The REPL is your most hostile input source. Treat it as a fuzz target with a human driving.
- Incomplete-input detection is one predicate and a large experience difference.
- Trace output goes to stderr. The one rule that breaks the test suite when violated.
- Exit code 70 separates "your policy is wrong" from "our runtime is broken."
Validation / Self-check
- Map each subcommand to the representation it prints and the lab that built it.
- Why is
run --interpa user-facing feature rather than a test hook? - Give the four instrumentation rules and the failure each one prevents.
- Why is every REPL line its own
SourceFile? What breaks without it, and what must be bounded? - How does the REPL distinguish incomplete input from a syntax error, and why does it matter?
- Why do limits reset per REPL line but state persist?
- What are the four exit codes, and which distinction is operationally load-bearing?
Next: Testing Strategy.