Add a stepping mode to your MSDscript interpreter, triggered by --step instead of --interp, which implements its own continuations instead of using the C++ stack.

Your interpreter in stepping mode should never crash by exhausting the C stack, and it should support looping via function calls as in the countdown example:

_let countdown = _fun(countdown)
                  _fun(n)
                  _if n == 0
                  _then 0
                  _else countdown(countdown)(n + -1)
_in countdown(countdown)(1000000)

In --interp mode, your MSDscript interpreter should behave as before, which almost certainly means a stack-overflow segfault when running the countdown example. For any program that msdscript --interp runs successfully, msdscript --step should produce the same result.

The lecture slides provide much of the code that you need, but in addition to figuring out how to organize and finish the code, beware that there are many small things that you will have to clean up. For example, Cont should be declared with CLASS, and continue_mode generally needs to be Step::continue_mode. Also, beware that setting Step::cont must be the last action in a Cont::step_continue method, because assigning to a static std::shared_ptr variable can destroy a previously referenced object even while that object’s method is still running (if there are no other shared references to the object).

If you declare static mode, expr, etc. fields in Step as in the slides in some .h file, you will need to “implement” those static fields with declarations like Step::mode_t Step::mode; and PTR(Expr) Step::expr; in a .cpp file, possibly step.cpp (similar to the way that declared methods have to be implemented).

Note that you probably will not need to add an equals method to the Cont classes (which will save you a lot of tedious work). See the next part about testing.

Testing

From a testing perspective, the Expr::step_interp and Cont::step_continue methods are strange: they read and modify global variables, and they have very little internal structure. Writing method-level tests for these (in the way we have been doing all semester) turns out to be painful and arguably not worthwhile.

Here’s one way you could write tests, if you really wanted, using a step_ok helper function (a kind of test harness) :

static bool step_ok(PTR(Expr) expr, PTR(Env) env, PTR(Cont) cont,
                    Step::mode_t expected_mode,
                    PTR(Val) expected_val, /* NULL if we expect interp_mode */
                    PTR(Expr) expected_expr, /* NULL if we expect continue_mode */
                    PTR(Cont) expected_cont) {
  Step::expr = expr;
  Step::env = env;
  Step::cont = cont;
  Step::val = NULL;
  
  expr->step_interp();
  
  bool ok;
  if (expected_mode == Step::continue_mode)
    ok = (Step::val != NULL
          && Step::val->equals(expected_val)
        && Step::cont->equals(expected_cont));
  else
    ok = (Step::expr->equals(expected_expr)
          && Step::env->equals(env)
        && Step::cont->equals(expected_cont));
  
  Step::val = NULL;
  Step::env = NULL;
  Step::cont = NULL;
  
  return ok;
}

TEST_CASE ("step_interp") {
  CHECK( step_ok(NEW(NumExpr)(0), Env::empty, Cont::done,
                 Step::continue_mode, NEW(NumVal)(0), NULL, Cont::done) );
  CHECK( step_ok(NEW(BoolExpr)(true), Env::empty, Cont::done,
                 Step::continue_mode, NEW(BoolVal)(true), NULL, Cont::done) );
  CHECK( step_ok(NEW(EqExpr)(NEW(NumExpr)(1), NEW(NumExpr)(2)), Env::empty, Cont::done,
                 Step::interp_mode, NULL, NEW(NumExpr)(1),
                 NEW(RightThenEqCont)(NEW(NumExpr)(2), Env::empty, Cont::done)) );
....
}

If you compare the test for EqExpr to its implementation, however, they will have almost the same information. The only difference is whether things are abstract, like rhs and env, or concrete, like NEW(NumExpr)(2) and Env::empty. As an example for understanding what EqExpr::step_interp should do, the concrete variant can be useful. But for testing, there's a lot of overhead required (such as implementing Cont::equals methods) to buy the privilege of writing everything twice. In some settings, there can be value in writing things twice as a cross-check, but this will not feel worthwhile for MSDscript.

A realistic way to exercise Expr::step_interp and Cont::step_continue methods is to interpret whole MSDscript programs and make sure they produce the right result:

CHECK( Step::interp_by_steps(parse_str("1"))->equals(NEW(NumVal)(1)) );

Construct enough such programs to complete coverage for your Expr::step_interp and Cont::step_continue implementations.

A stepping interpreter is difficult to test, because it's getting close to a low-level, assembly-like implementation. When something goes wrong with Step::interp_by_steps for a whole program, it may be more difficult to break it into smaller pieces that you can check with tests. Fortunately, having written Expr::interp in a more test-friendly style, you have developed a general understanding of how the interpreter should work, and you have developed many reliable helpers that feed into the stepping variant. If something goes wrong, though, you may find yourself relying more on a debugger than on new test cases.