You are at the base of the dam, which looms above you. The river Frigid begins here. Across it, to the east, cliffs form giant walls stretching north-south along the shore.
There is a folded pile of plastic here which has a small valve attached.
> blow up the pile of plastic with the air pump
The boat inflates and appears seaworthy. A tan label is lying inside the boat.
> read the label
Hello, Sailor! Instructions: To enter a body of water, say “Launch”. To get to shore, say “Land” or the direction you want to go. FROBOZZ MAGIC BOAT COMPANY Warranty: This boat is guaranteed for 9 seconds from date of purchase or until used, whichever comes first. Good Luck!
> drop the label and the pump
tan label: Dropped.
hand-held air pump: Dropped.
> get in the boat
You are now in the magic boat.
> launch the boat
Code for this episode is at https://github.com/ericlippert/flathead/tree/blog11
Last time we decoded a routine as follows:
37d9: call 1d9b 3e88 ffff ->sp 37e2: storew sp 00 01 37e7: call 1d9b 4e50 28 ->sp 37ef: call 1d9b 4792 96 ->sp 37f7: store 10 2e 37fa: store 8a a7 37fd: store 36 01 3800: store 83 1e 3803: insert_obj g73 g00 3806: call 2c31 ->sp 380b: new_line 380c: call 30fa ->sp 3811: call 1c0d ->sp 3816: jump ffc2
This is the first time we’ve seen a chunk of Z-machine instructions, so I want to go over this and address some deficiencies in our display logic.
The action of the routine is pretty straightforward, even if you don’t know the exact semantics of each instruction. It calls a routine, it stores something somewhere, it calls two more routines, does a bunch more stores, does something to the object tree, does another call, prints a new line, does two more calls, and then jumps unconditionally. Let’s look at each of those in a bit more detail.
First off, that call. There is only one call instruction in Z-machine version 3. It’s first operand is always the address of a routine, then it has zero to three operands which are the arguments to the call, and it always ends in a store. Routines always return a value in the Z-machine; there are no “void” routines.
I said that the first operand is the address of a routine; that could use a bit more explanation. First: the value of an operand is always a one or two byte integer. It’s either a short constant, long constant, local, global or stack pop; the first is a byte, and the rest produce a two-byte value. But routines can be in high memory, above the 64K barrier imposed by a two-byte address. Routine addresses are therefore “packed”, much the same way that string addresses are packed in the abbreviations table. A routine address is actually 17 or more bits, depending on the version of the Z-machine. So the routine really is not at 0x1d9b; it’s actually at twice that. It would be nice if our display logic could show the routine address rather than the packed address.
Later versions have multiple call instructions, so we’ll solve the problem for all of them:
let is_call ver opcode = match opcode with | OP1_143 (* call_1n in v5, logical not in v1-4 *) -> Story.v5_or_higher ver | VAR_224 (* call / call_vs *) | OP1_136 (* call_1s *) | OP2_26 (* call_2n *) | OP2_25 (* call_2s *) | VAR_249 (* call_vn *) | VAR_250 (* call_vn2 *) | VAR_236 (* call_vs2 *) -> true | _ -> false
There is no requirement that the routine be statically encoded in the instruction; the routine could be looked up out of a variable. But for our display purposes we only care about the most common situation, which is when a large operand is the routine address:
let call_address instr story = if is_call (Story.version story) instr.opcode then match instr.operands with | (Large packed_address) :: _ -> let packed_address = Packed_routine packed_address in let unpacked_address = Story.decode_routine_packed_address story packed_address in Some unpacked_address | _ -> None else None
We already have functions in story.ml for decoding packed addresses. Note that the value that comes back is an integer tagged with
Instruction. As we will discuss in a later episode, a routine actually has a short header before the first instruction.
Now we can re-jigger our operand display helper method to have this special case:
let display_operands () = let display_remainder operands = let to_string operand = match operand with | Large large -> Printf.sprintf "%04x " large | Small small -> Printf.sprintf "%02x " small | Variable variable -> (display_variable variable) ^ " " in accumulate_strings to_string operands in match call_address instr story with | Some (Routine addr) -> (Printf.sprintf "%04x " addr) ^ display_remainder (List.tl instr.operands) | _ -> display_remainder instr.operands
List.tl is the unfortunately named helper method that discards the head of a list.
Now the calls all have the right addresses:
37d9: call 3b36 3e88 ffff ->sp 37e2: storew sp 00 01 37e7: call 3b36 4e50 28 ->sp 37ef: call 3b36 4792 96 ->sp 37f7: store 10 2e 37fa: store 8a a7 37fd: store 36 01 3800: store 83 1e 3803: insert_obj g73 g00 3806: call 5862 ->sp 380b: new_line 380c: call 61f4 ->sp 3811: call 381a ->sp 3816: jump ffc2
Next time on FAIC: we’ll continue to examine this routine.