Bat room

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

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: store, inc, dec, inc_chk, dec_chk, pull and load. Mini-Zork does not use any indirection for store; every 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 ( 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 ( operands) in
  var ^ rest 

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

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s