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)
CommandRepresentationBuilt in
tokensVec<Token>Lab 1
astBlockLab 2
disassembleChunkLab 9
traceVM state per instructionLab 11
runValueLab 3
run --interpthe same Value, a different wayLab 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:

  1. Trace output goes to stderr. Stdout is what golden tests compare, and a --trace-gc that broke the test suite is a real thing that happens.
  2. Switchable at run time, not by recompiling.
  3. Cheap when off — if self.trace.is_some() around the formatting, not a formatted string thrown away. In the dispatch loop this is measurable.
  4. 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:

  1. 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 the SourceMap grows — bound it, or evict and degrade gracefully to "location unavailable".
  2. Incomplete input continues rather than erroring. function f() with no end should prompt >> , not report a syntax error. The lexer/parser must distinguish "unexpected EOF" from "unexpected token", which is one extra ErrorKind predicate and a much better experience.
  3. 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 as expr_stat.
  4. 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 end should return the prompt, not the process.
  5. 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.
  • --interp is 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

  1. Map each subcommand to the representation it prints and the lab that built it.
  2. Why is run --interp a user-facing feature rather than a test hook?
  3. Give the four instrumentation rules and the failure each one prevents.
  4. Why is every REPL line its own SourceFile? What breaks without it, and what must be bounded?
  5. How does the REPL distinguish incomplete input from a syntax error, and why does it matter?
  6. Why do limits reset per REPL line but state persist?
  7. What are the four exit codes, and which distinction is operationally load-bearing?

Next: Testing Strategy.