You are in a square room with tall ceilings. A huge mirror fills the south wall. There are exits east and northeast.
> look in the mirror
An ugly person stares back at you.
> go northeast
Code for this and the next several episodes can be found in https://github.com/ericlippert/flathead/tree/blog13.
The very first instruction in our program is a call. Just our luck, that the first instruction should be among the hardest to implement!
First I want to go over the semantics of a call instruction in detail, and then we’ll see what parts we need to build.
A call instruction is variadic; it has one to four operands. The first operand is the packed address of a routine to call, or zero. The rest of the operands are arguments. A call always has a store.
Later versions added more call instructions that have other numbers of operands and have no store; we’ll design our call-handling function to be general-purpose enough to handle all of these, but for now will only test it on v3 calls.
When interpreting a call (or for that matter, any instruction) first all the operands must be evaluated; operands are evaluated left-to-right. This is important because remember, there are five kinds of operands: small constants, large constants, local variables, global variables and “pop the evaluation stack”. Popping the evaluation stack produces both a side effect and a value, so it is important that all the pops happen in the right order.
Note also that it is perfectly legal for the first operand to be a variable or stack pop. Imagine for instance that there was a table that mapped verbs (“take”, “look”, and so on) onto routines that implemented them; the packed routine address might well be pushed onto the stack by a previous instruction.
Also it is legal for the packed routine address to be zero; if it is then the call is treated as though the called method simply returned zero. Zero is stored in the location specified by the call instruction’s store, and control moves on to the next instruction. This does happen in practice.
Supposing that we have evaluated a valid routine address and arguments. What happens next?
A new stack frame must be created. Recall that a stack frame has:
- an evaluation stack, which will be empty
- a collection of local variables, which will be initialized to their default values
- the address that control will resume at when the routine returns
- the store of the call instruction, so that the return knows where to put the result
The arguments are then passed to the routine by writing their values to the corresponding locals. The leftmost argument is written into local 1, and so on. We’ll look at the details later.
All right. What parts do we need here that we have not already built?
- We need a general-purpose mechanism for reading a local variable, global variable, or stack pop, to evaluate the operands.
- We need a similar mechanism for writing / pushing, in the event that the call is simply a store because the routine is zero.
- We need code to evaluate operands and copy the resulting arguments onto locals
- And of course we need an immutable interpreter object to keep track of interpreter and story state.
We already have mechanisms for reading locals and popping the evaluation stack but we have not yet ever said how global variables work, so let’s start with that.
The global variables are just an array of 240 words, indexed by the numbers 16 through 255, starting at an address given in the header:
let global_variables_table_base story = let global_variables_table_base_offset = Word_address 12 in Global_table_base (read_word story global_variables_table_base_offset)
Reading and writing globals is trivial:
let first_global = 16 let last_global = 255 let global_addr story (Global global) = if global < first_global || global > last_global then failwith "global variable index out of range" else let (Global_table_base base) = Story.global_variables_table_base story in let base = Word_address base in let offset = global - first_global in inc_word_addr_by base offset let read story global = Story.read_word story (global_addr story global) let write story global value = Story.write_word story (global_addr story global) value let display story = let to_string g = Printf.sprintf "%02x %04x\n" (g - first_global) (read story (Global g)) in accumulate_strings_loop to_string first_global (last_global + 1)
Next time on FAIC: We’ll create an interpreter type and add methods to deal with variables.