Article 199XF Frigid River, in the magic boat

Frigid River, in the magic boat

by
ericlippert
from Fabulous adventures in coding on (#199XF)

You are on the Frigid River just below the dam. The river flows quietly here. There is a landing on the west shore.

> wait

Time passes"
The current carries you downstream.
The river descends here into a valley. The are no landings on either shore. In the distance a faint rumbling can be heard.

> wait

Time passes"
The current carries you downstream.
The sound of rushing water is nearly unbearable here. On both the east and west shores there are beaches.
There is a red buoy here (probably a warning).

> take the buoy

As you take the buoy, you notice something odd about the feel of it.

> go west

The magic boat comes to rest on the shore.

The bug here - "The are no landings" - appears to be just a typo in the string literal.

Code for this episode is at https://github.com/ericlippert/flathead/tree/blog11

We're continuing to look at this routine; last time we got the addresses of the routines in the call instructions correct.

37d9: call 3b36 3e88 ffff ->sp37e2: storew sp 00 0137e7: call 3b36 4e50 28 ->sp37ef: call 3b36 4792 96 ->sp37f7: store 10 2e37fa: store 8a a737fd: store 36 013800: store 83 1e3803: insert_obj g73 g003806: call 5862 ->sp380b: new_line380c: call 61f4 ->sp3811: call 381a ->sp3816: jump ffc2

There are two more problems with how we're displaying the code here. The first is that "store" is a very unusual instruction in that its first argument evaluates to the number of the variable to which the result will be stored. I'll talk more about the power of this instruction, and how to best render it, in a later episode.

The much easier problem is that the jump address is displayed as an unsigned relative address rather than an absolute address. It's hard to look at this code and see what it really does, but it is easily fixed:

 match (instr.opcode, instr.operands) with | (OP1_140, [Large offset]) -> let offset = signed_word offset in let (Instruction addr) = jump_address instr offset in Printf.sprintf "%04x " addr | _ -> match call_address instr story with | Some (Routine addr) -> (Printf.sprintf "%04x " addr) ^ display_remainder (List.tl instr.operands) | _ -> display_remainder instr.operands

And now when we display the routine the jump comes out to

3816: jump 37d9

The jump unconditionally loops back to the beginning of the routine. There are no returns here; this is an infinite loop.

Since this is the "main" routine of the story - this is where execution begins - it is not really surprising that there's an infinite loop here. In fact this is a very special routine; the main routine has no local variables and may not return. In the Z-machine, a game ends not when main returns, but rather when the quit instruction is executed.

When I first ran my decompiler on this routine I was pleased that it seemed to come out to something sensible, but I quickly grew concerned. Do you see why? Read the code more carefully and you'll see it.

We have an infinite loop; every time through the loop we do six pushes but only one pop on the evaluation stack. I thought at first that I must have decoded something wrong, but no, this disassembly is correct. This infinite loop appears to consume an unbounded quantity of stack, which I assure you is very bad. What is up with that?

My first thought was that perhaps the called routines were taking values off the stack, but no, that's not legal. Each routine has its own evaluation stack; it cannot touch the evaluation stack of an earlier frame.

The conclusion I came to was that the last call must also be an infinite loop, and therefore the final jump is not reachable! And indeed, if we disassemble the routine at 381a we see that it is:

381d: call 3826 ->local03822: jump 381d

Aha! So that jump at the end of the main routine is never reached, and the stack is not pushed arbitrarily much.

Why does the main routine push so much on the stack in the first place? Because remember, I said (1) the main routine has no locals, and (2) the sole call instruction in version 3 has a store. That store has to store somewhere. It can't store to a local, and so that leaves either modifying global state for no reason, or pushing the stack. And why write unnecessary pop instructions if we're never coming back?

Later versions of the Z-machine had call instructions without stores, so a call which wishes to discard its result can do so without having to store to a local or push the stack.

Next time on FAIC: we'll start to think about the parts we need for a proper interpreter.


4168 b.gif?host=ericlippert.com&blog=67759120
External Content
Source RSS or Atom Feed
Feed Location http://ericlippert.com/feed
Feed Title Fabulous adventures in coding
Feed Link https://ericlippert.com/
Reply 0 comments