The Z-Machine spec is not completely exhaustive, but it’s pretty good. You can actually implement a working interpreter from it, which is more than can be said for some other IF formats. But there are some ambiguities, and you can’t expect them to be all resolved. You can look through the forums to see if there are any past discussions on an issue, or ask a question if you think it’s an important issue, but most of the time you’re probably best just making a choice, implementing it, and then seeing it if causes issues later.
But in addition to the spec, make sure you’re also using:
Also version 6 is ignored by most people. It should only be implemented by masochists. If you have questions you’re mostly on your own.
Table locations
However, as far as I’m aware, there is no mention of what kind of memory the custom alphabet table (address in byte $34 of the header) or the Unicode translation table (word 3 in header extension table) can be stored in. Am I to assume that they can also be located in dynamic memory, and so it is theoretically not safe to deserialize them in advance? Unfortunately, none of my sample files feature any of these two tables, so I’m unable to confirm or deny that.
You’ll have to decide what kind of interpreter you want to write: one that makes minimal assumptions and allows for anything, or one that takes short cuts. Despite a lot of potential for dynamic compilation in the Z-Machine, almost no storyfiles do any of it. My interpreter (ZVM) just reads those arrays in at the start, and any changes to them are ignored later on. I don’t even account for the object property table address being moved, even though 12.4 explicitly says that is legal.
I have a vague memory that abbreviations might be altered in some cases, so I made those dynamic, but I’m not certain. Because the Z-Machine doesn’t provide any way to encode arbitrary length texts, a game would need to include its own encoding functions in order to write dynamic text back as encoded text. It would be easier to copy an encoded text around, but it would have to be the right length to fit in the destination. It might be safe to cache abbreviations and text in dynamic memory - something to consider next time I work on ZVM.
Undefined EXT opcodes
It’s best to throw a fatal error for any unrecognised opcodes.
scan_table
The spec for the scan_table
opcode says that an additional argument can be used to configure whether the table to be scanned contains words or bytes. Now, suppose the first operand (the value to search for) resolves to a word (i.e. it is anything but a “small constant”). In this case, should we immediately return 0 if the high byte of this word is non-zero, or should we truncate the word to a byte and perform the search as usual?
Don’t truncate a value without the spec telling you to. So yes you could implement that kind of optimisation here.
From the default value of the third argument ($82 = 10000010b), it appears that the field length of the table is always given in bytes, even if we’re searching for a word. Is it legal in this case to have a field length of 1, and if it is, what should be done in this scenario?
Don’t overthink it, a game is very unlikely to be doing something stupid. Do whatever makes most sense to you. I’d do whatever requires the least special-cased code. In that case it would be to make the loop increment the index by one byte but look for a matching word - which means it’s checking two fields at once.
store
opcode corner case
Now, suppose the first operand indicates that the value should be put into the top of the stack (NB: not pushed), and the second operand is of type “variable” with value 0, which indicates a value should be popped from the stack. There seems to be a conflict here, as the value we’re updating literally ceases to exist during the update process. If this is legal, what should happen? The logical course of action would be to replace the new value now on top of the stack (e.g. we essentialy “pop” a value from under the top of the stack with such an operation), but this is not adequately explained - clarification is needed.
While in general you’re meant to evaluate operands from left to right, the only sensible option here would be to pop first and then mutate the new top of stack. @store
and the other opcodes like it can be implemented as regular storers - if you put the complexity into their disassembly rather than into their run code.