You are in a small room with exits to the east and south.
There is an exquisite jade figurine here.
In the corner of the ceiling, a large vampire bat is holding his nose.
> go east
Code for this episode can be found in https://github.com/ericlippert/flathead/tree/blog16.
Last time we managed to get as far as the instruction
37f7: store 10 2e
This is a bit of an odd instruction. It takes two operands; the first identifies a variable, the second identifies a value. The value is stored into the variable. So in this case, variable 10 is a global, and value 0x002e is stored.
Now you might think that this is a bit silly. We already have a mechanism for storing a value into a variable: make the instruction have a store portion. The store portion of an instruction identifies a variable, and we’re done, right? Why isn’t the form of the store instead
store 2e ->g00
The reason is that the store portion of an instruction must resolve to a specific variable number baked in to the instruction. Suppose we had the instruction:
store g11 2e
That does not mean “store 2e in variable g11”; rather, it means “store 2e in the variable whose number is stored in g11“. This instruction allows you to make a reference to a variable and store it in another variable! Put another way, this enables a level of indirection that would not otherwise be available.
The convention that the Inform assembly language uses is: when printing out the
store instruction, if the first operand is a constant then it is written as a variable; if it is a variable then it is written as the variable in square braces. So in this convention our instruction that we display
37f7: store 10 2e
should be displayed as
37f7: store g00 2e
and the instruction we would now display as
37f7: store g11 2e
would be notated
37f7: store [g11] 2e
This oddity applies to seven instructions:
load. Mini-Zork does not use any indirection for
store in Mini-Zork has a small constant as its first operand. It does however use it for
load. For example, there is a
load instruction of the form:
load [sp] ->sp
That is: first pop the stack. The popped value contains a number from 0 to 255. Turn that into a variable, fetch the contents of that variable, and push that value onto the stack.
Here’s an interesting question: suppose the stack contained 0 and 10 when we executed this instruction. We first pop the stack, producing 0. Now we must look up the value of variable 0 – the stack pointer – which is 10. Does this second access to the stack also pop the stack?
No. These seven instructions have the interesting property that when the value of the first operand is zero, they peek the top of the stack when reading, rather than popping. And similarly, they write in place to the top of the stack, rather than pushing.
This is all a bit of a mess, but we can deal with it. I want to do three things.
- First, I want to refactor the code that displays operands, because it is getting to be a mess of special cases.
- Second, I want to implement methods to read and write variables that do not change the state of the stack.
- Third, I want to implement these seven instructions.
We’ll do the first two in this episode and the third next time.
We need to know if we dealing with a special instruction? Bizarrely, the
pull instruction does not have this special variable decoding behaviour only in version 6:
let has_indirection instruction ver = match (instruction.opcode, ver) with | (VAR_233, V6) -> false (* pull *) | (OP2_4, _) (* dec_chk *) | (OP2_5, _) (* inc_chk *) | (OP2_13, _) (* store *) | (OP1_133, _) (* inc *) | (OP1_134, _) (* dec *) | (OP1_142, _) (* load *) | (VAR_233, _) (* pull *) -> true | _ -> false
We can pull all the different cases for operand display out into helper methods:
let display_indirect_operand operand = match operand with | Large large -> (display_variable (decode_variable large)) ^ " " | Small small -> (display_variable (decode_variable small)) ^ " " | Variable variable -> "[" ^ (display_variable variable) ^ "] " let display_operand operand = match operand with | Large large -> Printf.sprintf "%04x " large | Small small -> Printf.sprintf "%02x " small | Variable variable -> (display_variable variable) ^ " " let display_jump instr = match instr.operands with | [Large offset] -> let offset = signed_word offset in let (Instruction target) = jump_address instr offset in Printf.sprintf "%04x " target | _ -> accumulate_strings display_operand instr.operands let display_call instr story = match call_address instr story with | Some (Routine addr) -> let routine = (Printf.sprintf "%04x " addr) in let args = accumulate_strings display_operand (List.tl instr.operands) in routine ^ args | _ -> accumulate_strings display_operand instr.operands let display_indirect_operands operands = let var = display_indirect_operand (List.hd operands) in let rest = accumulate_strings display_operand (List.tl operands) in var ^ rest
display_operands becomes simply:
if instr.opcode = OP1_140 then display_jump instr else if is_call ver instr.opcode then display_call instr story else if has_indirection instr ver then display_indirect_operands instr.operands else accumulate_strings display_operand instr.operands in
OK, that was pretty easy. And if we run our little program again we see that now we crash on
37f7: store g00 2e
Now we have to make helper methods that know how to read and write variables without popping the stack. Reading is easy; we already have a peek method. And writing without popping the stack is logically the same thing as popping the old value and pushing the new value:
let read_variable_in_place interpreter variable = match variable with | Stack -> peek_stack interpreter | Local_variable local -> read_local interpreter local | Global_variable global -> read_global interpreter global let write_variable_in_place interpreter variable value = match variable with | Stack -> push_stack (pop_stack interpreter) value | Local_variable local -> write_local interpreter local value | Global_variable global -> write_global interpreter global value
Note that the reader can now simply return a value, as the interpreter does not change. The writer of course continues to return an interpreter.
Next time on FAIC: we’ll implement those seven instructions.