Typically, you need to distinguish between what it means for a statement or expression to "complete normally" or "complete abruptly" (borrowing terminology from the Java Language Specification). An expression which evalutes to a value "completes normally", as does a statement which executes in a normal sequence. On the other hand, a statement or expression which has some effect on the surrounding control-flow "completes abruptly".
A statement or expression could complete abruptly if it:
- Returns a value from the enclosing function.
- Throws an exception.
- Breaks from or continues an enclosing loop.
So generally speaking, these are all handled the same way. A typical solution is to represent the result of a statement or expression as a discriminated union of these possibilities, for example in Rust you might write:
enum EvalResult {
Normal(Value),
Return(Value),
Throw(Value),
Break,
Continue,
}
(If you have labelled breaks and continues, or you allow constructs like let foo = loop { break 5; }
, then your enum will need to account for those, too.)
Your "eval" function, which evaluates an AST node, returns a result like this. Then when evaluating a loop, you would check for these results and handle them appropriately:
match ast_node {
// ...
WhileLoop(cond, body) => {
loop {
let cond_result = eval(cond);
match cond_result {
Normal(v) => if !v.is_truthy() { break; },
_ => return cond_result,
}
let body_result = eval(body);
match body_result {
Normal(_) | Continue => continue,
Break => break,
Return(_) | Throw(_) => return body_result,
}
}
return Normal(VOID_UNIT);
},
// ...
}
Here I'm assuming is_truthy()
converts a Value
to a native Boolean in the host language, and VOID_UNIT
is the value resulting from a statement which doesn't produce a "proper" value.
Note how we return cond_result
or return body_result
directly when it is necessary to propagate a control-flow effect that this loop isn't supposed to handle itself, for example if the loop body contains a return
or throw
statement. The same propagation will have to be done elsewhere, e.g. when you add two numbers together, you'll have to propagate control-flow effects from left_result
and right_result
, in order for those control-flow effects to end up being handled by the eval
function for the correct AST node.