This is a crooked corridor from the north, with forks to the southwest and south.
> go north
If you think the winding passage is deliberately designed to be easily confused with the twisting passage, you’re right!
Code for this episode can be found in https://github.com/ericlippert/flathead/tree/blog14.
We’ve managed to make it through two instructions without crashing, which is great. The next one to implement would be “add”, but I’d like to implement returns first, because we’ve still got calls fresh in our minds. What needs to happen on a return?
All routines return a value – or, at least, the ones that return at all do! Note that this is true even if we’re using a version of the Z-machine where the particular call instruction discards the value. Our helper will take the value to be returned.
We need to pop off the topmost frame, reset the program counter to point to the instruction after the call that got us here and store the result in the variable indicated by the call. All in all much less work than doing the call, but still, we’ve got stuff to do here.
Suppose we already have computed the value. This helper function does the rest:
let interpret_return interpreter value = let frame = current_frame interpreter in let next_pc = Frame.resume_at frame in let store = Frame.store frame in let pop_frame_interpreter = remove_frame interpreter in let result_interpreter = set_program_counter pop_frame_interpreter next_pc in interpret_store result_interpreter store value
Just to be clear: this thing takes an interpreter and a value, and produces the interpreter which is the result of returning that value.
We can now implement the “ret” instruction in our interpreter. The ret instruction is very straightforward: it has a single operand which is evaluated to produce the returned value.
I’m going to follow a pattern whereby almost every instruction has an associated “handle” function, but I’m going to have other code talk directly to
interpret_return later in this episode, so we’ll keep these logically separated even though doing so is not strictly speaking necessary:
let handle_ret result interpreter = interpret_return interpreter result
And in step_instruction we add:
match (opcode, arguments) with | (OP1_139, [result]) -> handle_ret result interpreter ...
And that is actually it for returns; pretty straightforward.
Now that we have returns, we can interpret the “branch” portion of any instruction that has one. Recall that instructions which have a conditional branch may choose from “return true” “return false”, and “branch to a relative address” if the branch condition is met.
Of course we will not use this helper method for calls and returns, as they have special logic for determining the next instruction. But any “normal” instruction either has a branch or does not; if it has no branch then control moves to the following instruction. If it has a branch then the result is evaluated as a Boolean — zero is false, everything else is true — and checked to see if it matches the “sense” of the branch. That is, we saved away whether this branch was “branch on false” or “branch on true”. If the condition is met then we take the branch, otherwise we move on to the following instruction:
let interpret_branch interpreter instruction result = let result = not (result = 0) in let following = Instruction.following instruction in match Instruction.branch instruction with | None -> set_program_counter interpreter following | Some (sense, Return_false) -> if result = sense then interpret_return interpreter 0 else set_program_counter interpreter following | Some (sense, Return_true) -> if result = sense then interpret_return interpreter 1 else set_program_counter interpreter following | Some (sense, Branch_address branch_target) -> if result = sense then set_program_counter interpreter branch_target else set_program_counter interpreter following
Next time on FAIC: We’ll make some helper methods, and then implement the “add” instruction.