Step 5 — REPL, Tests, and CLI

Goal: wire lexer + parser + evaluator into a real tool, with a test suite and an interactive REPL.

The CLI Driver — main.cpp

int main(int argc, char** argv) {
    if (argc == 1) return arith::repl();
    // gather argv[1..] into one expression for unquoted use
    std::ostringstream oss;
    for (int i = 1; i < argc; ++i) {
        if (i > 1) oss << ' ';
        oss << argv[i];
    }
    return arith::evaluateOnce(oss.str());
}

Two modes:

  • One-shot: ./eval "1 + 2 * 3" or ./eval 1 + 2 + 3.
  • REPL: ./eval with no args.

The REPL Loop

static int repl() {
    std::cout << "arith repl (cp-02). type 'quit' or Ctrl-D to exit.\n";
    std::string line;
    for (;;) {
        std::cout << "> " << std::flush;
        if (!std::getline(std::cin, line)) { std::cout << "\n"; return 0; }
        if (line == "quit" || line == "exit") return 0;
        if (line.empty()) continue;
        evaluateOnce(line);
    }
}

Every iteration: read a line, send it through the whole pipeline, print. No persistent state between lines — variables and bindings come in cp-03.

Building It

cd src/cpp
cmake -B build           # configure once
cmake --build build      # build (incremental)

CMake breakdown:

  • arithlib — static library with lexer.cpp, parser.cpp, evaluator.cpp.
  • eval — executable that links arithlib and main.cpp.
  • test_eval — executable that links arithlib and the test file.

Separating library from main lets us reuse the same compiler internals from tests — a pattern we keep in every later lab.

The Test Suite — test_eval.cpp

Assert-based, no test-framework dependency. 19 tests across 5 categories:

// arithmetic
assert(APPROX(eval("1 + 2"),    3.0));
assert(APPROX(eval("10 - 4"),   6.0));
// precedence
assert(APPROX(eval("1 + 2 * 3"), 7.0));
// left-associativity
assert(APPROX(eval("10 - 3 - 2"), 5.0));
// parens
assert(APPROX(eval("(1 + 2) * 3"), 9.0));
// unary
assert(APPROX(eval("-(3 + 4)"), -7.0));
// floats / whitespace
assert(APPROX(eval("3.14 + 0.86"), 4.0));
// error cases
assert(throws(""));
assert(throws("1 +"));
assert(throws("1 / 0"));
assert(throws("(1 + 2"));

The macro APPROX(a, b) uses std::fabs((a) - (b)) < 1e-9 because comparing floats with == is fragile. throws(...) runs a snippet inside a try/catch and returns whether any std::exception was thrown.

Running The Tests

ctest --test-dir build

Or directly:

./build/tests/test_eval
# cp-02 tests: 19/19 PASS

Expected: 19/19 tests pass. If anything fails, the failing assert aborts immediately with a line number.

Why No gtest / catch2?

Pulling in a framework would mean a find_package (often a git submodule), a CMake config file, and 10 MB of headers. For 19 trivial tests, plain assert is shorter, faster to build, and removes a teaching distraction.

We'll graduate to a real framework around cp-08, when individual test cases benefit from structured fixtures and parameterization.

Manual Sanity Checks

The classic checklist:

./build/eval "1 + 2 * 3"           # 7        (precedence)
./build/eval "(1 + 2) * 3"         # 9        (parens override)
./build/eval "10 - 3 - 2"          # 5        (left associativity)
./build/eval "3 * -4"              # -12      (unary in factor)
./build/eval "((((42))))"          # 42       (parens nest)
./build/eval "1 / 0" 2>&1          # division by zero
./build/eval "1 +" 2>&1            # parse error

If any of these don't match, suspect: parser precedence (Step 3), evaluator dispatch (Step 4), error propagation (parser → eval).

A Tour Of A Failure

Suppose you accidentally swapped left-associativity for right in parseExpr:

// WRONG:
ExprPtr right = parseExpr();   // recursion instead of loop

Then:

  • 10 - 3 - 2 → tree: (10 - (3 - 2)) = 10 - 1 = 9 (not 5!).
  • Test assert(APPROX(eval("10 - 3 - 2"), 5.0)); fails immediately.

This is exactly why we write tests for associativity — the bug is subtle and silent without them.

Outcomes

You now have:

  • A working evaluator binary, both REPL and one-shot.
  • A 19-test test suite proving correctness across precedence, associativity, parens, unary, floats, whitespace, and 4 error categories.
  • A CMake project structured for reuse — the same arithlib will be linked from cp-03's expanded language.

Next

06-extensions.md — optional extension exercises that deepen each concept.