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.
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.