The Glulx machine consists of main memory, the stack, and a few registers (the program counter, the stack pointer, and the call-frame pointer.)
Main memory is a simple array of bytes, numbered from zero up. When accessing multibyte values, the most significant byte is stored first (big-endian). Multibyte values are not necessarily aligned in memory.
The stack is an array of values. It is not a part of main memory; the terp maintains it separately. The format of the stack is technically up to the implementation. However, the needs of the machine (especially the game-save format) leave about one good option. (See section 1.8, "The Save-Game Format".) One important point: the stack can be kept in either byte ordering. The program should make no assumptions about endianness on the stack. (In fact, programs should never need to care.) Values on the stack always have their natural alignment (16-bit values at even addresses, 32-bit values at multiples of four).
The stack consists of a set of call frames, one for each function in the current chain. When a function is called, a new stack frame is pushed, containing the function's local variables. The function can then push or pull 32-bit values on top of that, to store intermediate computations.
All values are treated as unsigned integers, unless otherwise noted. Signed integers are handled with the usual two's-complement notation. Arithmetic overflows and underflows are truncated, also as usual.
No input/output facilities are built into the Glulx machine itself. Instead, the machine has one or more opcodes which dispatch calls to an I/O library.
At the moment, that means Glk. All Glulx interpreters support the Glk I/O facility (via the glk opcode), and no other I/O facilities exist. However, other I/O libraries may be adapted to Glk in the future. For best behavior, a program should test for the presence of an I/O facility before using it, using the IOSystem gestalt selector (see section 2.18, "Miscellaneous").
One I/O system is set as current at any given time. This does not mean that the others are unavailable. (If the interpreter supports Glk, for example, the glk opcode will always function.) However, the basic Glulx output opcodes -- streamchar, streamnum, and streamstr -- always print using the current I/O system.
Every Glulx interpreter supports at least one normal I/O facility (such as Glk), and also two special facilities.
The "null" I/O system does nothing. If this is selected, all Glulx output is simply discarded. [Silly, perhaps, but I like simple base cases.] When the Glulx machine starts up, the null system is the current system. You must select a different one before using the streamchar, streamnum, or streamstr opcodes.
The "filter" I/O system allows the Glulx program itself to handle output. The program specifies a function when selecting this I/O system. That function is then called for every single character of output that the machine generates (via streamchar, streamnum, or streamstr). The function can output its character directly via the glk opcode (or one of the other output opcodes).
[This may all seem rather baroque, but in practice most authors can ignore it. Most programs will want to test for the Glk facility, set it to be the current output system immediately, and then leave the I/O system alone for the rest of the game. All output will then automatically be handled through Glk.]
Memory is divided into several segments. The sizes of the segments are determined by constant values in the game-file header.
Segment Address (hex) +---------+ 00000000 | Header | | - - - - | 00000024 | | | ROM | | | +---------+ RAMSTART | | | RAM | | | | - - - - | EXTSTART | | | | +---------+ ENDMEM
As you might expect, the section marked ROM never changes during execution; it is illegal to write there. Executable code and constant data are usually (but not necessarily) kept in ROM. Note that unlike the Z-machine, the Glulx machine's ROM comes before RAM; the 36-byte header is part of ROM.
The boundary marked EXTSTART is a trivial gimmick for making game-files smaller. A Glulx game-file only stores the data from 0 to EXTSTART. When the terp loads it in, it allocates memory up to ENDMEM; everything above EXTSTART is initialized to zeroes. Once execution starts, there is no difference between the memory above and below EXTSTART.
For the convenience of paging interpreters, the three boundaries RAMSTART, EXTSTART, and ENDMEM must be aligned on 256-byte boundaries.
Any of the segments of memory can be zero-length, except that ROM must be at least 256 bytes long (so that the header fits in it).
The stack pointer starts at zero, and the stack grows upward. The maximum size of the stack is determined by a constant value in the game-file header. For convenience, this must be a multiple of 256.
The stack pointer counts in bytes. If you push a 32-bit value on the stack, the pointer increases by four.
A call frame looks like this:
+------------+ FramePtr | Frame Len | (4 bytes) | Locals Pos | (4 bytes) | | | Format of | (2*n bytes) | Locals | | | | Padding | (0 or 2 bytes) +------------+ FramePtr+LocalsPos | Locals | (1, 2, or 4 bytes each) | | | Padding | (0 to 3 bytes) +------------+ FramePtr+FrameLen | Values | (4 bytes each) | .... | +------------+ StackPtr
When a function begins executing, the last segment is empty (StackPtr equals FramePtr+FrameLen.) Computation can push and pull 32-bit values on the stack. It is illegal to pop back beyond the original FramePtr+FrameLen boundary.
The "locals" are a list of values which the function uses as local variables. These also include function arguments. (The first N locals can be used as the arguments to an N-argument function.) Locals can be 8, 16, or 32-bit values. They are not necessarily contiguous; padding is inserted wherever necessary to bring a value to its natural alignment (16-bit values at even addresses, 32-bit values at multiples of four).
The "format of locals" is a series of bytes, describing the arrangement of the "locals" section of the frame (from LocalsPos up to FrameLen). This information is copied directly from the header of the function being called. (See section 1.6.2, "Functions".)
Each field in this section is two bytes:
The section is terminated by a pair of zero bytes. Another pair of zeroes is added if necessary to reach a four-byte boundary.
(Example: if a function has three 8-bit locals followed by six 16-bit locals, the format segment would contain eight bytes: (1, 3, 2, 6, 0, 0, 0, 0). The locals segment would then be 16 bytes long, with a padding byte after the third local.)
The "format of locals" information is needed by the terp in two places: when calling a function (to write in function arguments), and when saving the game (to fix byte-ordering of the locals.) The formatting is not enforced by the terp while a function is executing. The program is not prevented from accessing locations whose size and position don't match the formatting, or locations that overlap, or even locations in the padding between locals. However, if a program does this, the results are undefined, because the byte-ordering of locals is up to the terp. The save-game algorithm will fail, if nothing else.
[In fact, the call frame may not exist as a byte sequence during function execution. The terp is free to maintain a more structured form, as long as it generates valid save-game files, and correctly handles accesses to valid (according to the format) locals.]
[NOTE: 8-bit and 16-bit locals have never been in common use, and this spec has not been unambiguous in describing their handling. (By which I mean, what I implemented in the reference interpreter didn't match the spec.) Therefore, 8-bit and 16-bit locals are deprecated. Use of the copyb and copys opcodes with a local-variable operand is also deprecated.]
Several different Glulx operations require the ability to jump back to a previously-saved execution state. (For example: function call/return, game-state save/restore, and exception catch/throw.)
For simplicity, all these operations store the execution state the same way -- as a "call stub" on the stack. This is a block of four 32-bit values. It encodes the PC and FramePtr, and also a location to store a single 32-bit value at jump-back time. (For example, the function return value, or the game-restore success flag.)
The values are pushed on the stack in the following order (FramePtr pushed last):
+-----------+ | DestType | (4 bytes) | DestAddr | (4 bytes) | PC | (4 bytes) | FramePtr | (4 bytes) +-----------+
FramePtr is the current value of FramePtr -- the stack position of the call frame of the function during which the call stub was generated.
PC is the current value of the program counter. This is the address of the instruction after the one which caused the call stub to be generated. (For example, for a function call, the call stub contains the address of the first instruction to execute after the function returns.)
DestType and DestAddr describe a location in which to store a result. This will occur after the operation is completed (function returned, game restored, etc). It happens after the PC and FramePtr are reloaded from the call stub, and the call stub is removed from the stack.
DestType is one of the following values:
The string-decoding mechanism complicates matters a little, since it is possible for a function to be called from inside a string, instead of another function. (See section 1.3.4, "Calling and Returning Within Strings".) The following DestType values allow this:
When a function is called, the terp pushes a four-value call stub. (This includes the return-value destination, the PC, and the FramePtr; see section 1.3.2, "Call Stubs".) The terp then sets the FramePtr to the StackPtr, and builds a new call frame. (See section 1.3.1, "The Call Frame".) The PC moves to the first instruction of the function, and execution continues.
Function arguments can be stored in the locals of the new call frame, or pushed on the stack above the new call frame. This is determined by the type of the function; see section 1.6.2, "Functions".
When a function returns, the process is reversed. First StackPtr is set back to FramePtr, throwing away the current call frame (and any pushed values). The FramePtr and PC are popped off the stack, and then the return-value destination. The function's return value is stored where the destination says it should be. Then execution continues at the restored PC.
(But note that a function can also return to a suspended string, as well as a suspended caller function. See section 1.3.4, "Calling and Returning Within Strings" and section 1.3.5, "Calling and Returning During Output Filtering".)
Glulx uses a Huffman string-compression scheme. This allows bit sequences in strings to decode to large strings, or even function invocations which generate output. This means the streamstr opcode can invoke function calls, and we must therefore be able to represent this situation on the stack.
When the terp begins printing a string, it pushes a type-11 call stub. (This includes only the current PC. The FramePtr is included, for consistency's sake, but it will be ignored when the call stub is read back off.) The terp then starts decoding the string data. The PC now indicates the position within the string data.
If, during string decoding, the terp encounters an indirect reference to a string or function, it pushes a type-10 call stub. This includes the string-decoding PC, and the bit number within that address. It also includes the current FramePtr, which has not changed since string-printing began.
If the indirect reference is to another string, the decoding continues at the new location after the type-10 stub is pushed. However, if the reference is to a function, the usual call frame is pushed on top of the type-10 stub, and the terp returns to normal function execution.
When a string completes printing, the terp pops a call stub. This will necessarily be either a type-10 or type-11. If the former, the terp resumes string decoding at the PC address/bit number in the stub. If the latter, the topmost string is finished, and the terp resumes function execution at the stub's PC.
When a function returns, it must check to see if it was called from within a string, instead of from another function. This is the case if the call stub it pops is type-10. (The call stub cannot be type-11.) If so, the FramePtr is taken from the stub as usual; but the stub's PC is taken to refer to a string data address, with the DestAddr value being the bit number within that address. (The function's return value is discarded.) String decoding resumes from there.
[It may seem wasteful for the terp to push and pop a call stub every time a string is printed. Fortunately, in the most common case -- printing a string with no indirect references at all -- this can easily be optimized out. (No VM code is executed between the push and pop, so it is safe to skip them.) Similarly, when printing an unencoded (E0) string, there can be no indirect references, so it is safe to optimize away the call stub push/pop.]
The "filter" I/O system allows the terp to call a Glulx function for each character that is printed via streamchar, streamnum, or streamstr. We must be able to represent this situation on the call stack as well.
If filtering is the current I/O system, then when the terp executes streamchar, it pushes a normal function call stub and begins executing the output function. Nothing else is required; when the function returns, execution will resume after the streamchar opcode. (A type-0 call stub is used, so the function's return value is discarded.)
The other output opcodes are more complex. When the terp executes streamnum, it pushes a type-11 call stub. As before, this records the current PC. The terp then pushes a type-12 call stub, which contains the integer being printed and the position of the next character to be printed (namely 1). It then executes the output function.
When the output function returns, the terp pops the type-12 stub and realizes that it should continue printing the integer contained therein. It pushes another type-12 stub back on the stack, indicating that the next position to print is 2, and calls the output function again.
This process continues until there are no more characters in the decimal representation of the integer. The terp then pops the type-11 stub, restores the PC, and resumes execution after the streamnum opcode.
The streamstr opcode works on the same principle, except that instead of type-12 stubs, the terp uses type-10 stubs (when interrupting an encoded string) and type-13/14 stubs (when interruping a C-style, null-terminated string of bytes/Unicode chars). Type-13 and type-14 stubs look like the others, except that they contain only the address of the next character to print; no other position or bit number is necessary.
The interaction between the filter I/O system and indirect string/function calls within encoded strings is left to the reader's imagination. [Because I couldn't explain it if I tried. Follow the rules; they work.]
The header is the first 36 bytes of memory. It is always in ROM, so its contents cannot change during execution. The header is organized as nine 32-bit values. (Recall that values in memory are always big-endian.)
+---------------+ address 0 | Magic Number | (4 bytes) | Glulx Version | (4 bytes) | RAMSTART | (4 bytes) | EXTSTART | (4 bytes) | ENDMEM | (4 bytes) | Stack Size | (4 bytes) | Start Func | (4 bytes) | Decoding Tbl | (4 bytes) | Checksum | (4 bytes) +---------------+
The interpreter should validate the magic number and the Glulx version number. An interpreter which is written to version X.Y.Z of this specification should accept game files whose Glulx version between X.0.0 and X.Y.*. (That is, the major version number should match; the minor version number should be less than or equal to Y; the subminor version number does not matter.)
EXCEPTION: A version 3.* interpreter should accept version 2.0 game files. The only difference between spec 2.0 and spec 3.0 is that 2.0 lacks Unicode functionality. Therefore, an interpreter written to this version of the spec (3.1.2) should accept game files whose version is between 2.0.0 and 3.1.* (0x00020000 and 0x000301FF inclusive).
[These rules mean, in the vernacular, that minor version changes are backwards compatible, and subminor version changes are backwards and forwards compatible. If I add a feature which I expect every terp to implement (e.g. mzero and mcopy), then I bump the minor version number, and your game can use that feature without worrying about availability. If I add a feature which not all terps will implement (e.g. floating point), then I bump the subminor version number, and your game should only use the feature after doing a gestalt test for availability.]
[The header is conventionally followed by a 32-bit word which describes the layout of data in the rest of the file. This value is not a part of the Glulx specification; it is the first ROM word after the header, not a part of the header. It is an option that compilers can insert, when generating Glulx files, to aid debuggers and decompilers.
For Inform-generated Glulx files, this descriptive value is 49 6E 66 6F, which is to say ASCII 'Info'. There then follow several more bytes of data relevant to the Inform compiler. See the Glulx chapter of the Inform Technical Manual.]
[Note that version 2.0 (pre-Unicode) has been obsolete since 2006. There are still 2.0 game files out there, so interpreters should still support them. However, there are no 2.0-only interpreters left; so compilers may freely target 3.*.]
There are 2^28 Glulx opcodes, numbered from 0 to 0FFFFFFF. If this proves insufficient, more may be added in the future.
An instruction is encoded as follows:
+--------------+ | Opcode Num | (1 to 4 bytes) | | | Operand | (two per byte) | Addr Modes | | | | Operand Data | (as defined by | .... | addr modes) +--------------+
The opcode number OP, which can be anything up to 0FFFFFFF, may be packed into fewer than four bytes:
Note that the length of this field can be decoded by looking at the top two bits of the first byte. Also note that, for example, 01 and 8001 and C0000001 all represent the same opcode.
The operand addressing modes are a list of fields which tell where opcode arguments are read from or written to. Each is four bits long, and they are packed two to a byte. (They occur in the same order as the arguments, low bits first. If there are an odd number, the high bits of the last byte are left zero.)
Since each addressing mode is a four-bit number, there are sixteen addressing modes. Each is associated with a fixed number of bytes in the "operand data" segment of the instruction. These bytes appear after the addressing modes, in the same order. (There is no alignment padding.)
Things to note:
The "constant" modes sign-extend their data into a 32-bit value; the other modes do not. This is just because negative constants occur more frequently than negative addresses.
The indirect modes (all except "constant") access 32-bit fields, either in the stack or in memory. This means four bytes starting at the given address. A few opcodes are exceptions: copyb and copys (copy byte and copy short) access 8-bit and 16-bit fields (one or two bytes starting at the given address.)
The "call frame local" modes access a field on the stack, starting at byte ((FramePtr+LocalsPos) + address). As described in section 1.3.1, "The Call Frame", this must be aligned with (and the same size as) one of the fields described in the function's locals format. It must not point outside the range of the current function's locals segment.
The "contents of address" modes access a field in main memory, starting at byte (addr). The "contents of RAM" modes access a field in main memory, starting at byte (RAMSTART + addr). Since the byte-ordering of main memory is well-defined, these need not have any particular alignment or position.
All address addition is truncated to 32 bits, and addresses are unsigned. So, for example, "contents of RAM" address FFFFFFFC (RAMSTART + FFFFFFFC) accesses the last 32-bit value in ROM, since it effectively subtracts 4 from RAMSTART. "Contents of address" FFFFFFFC would access the very last 32-bit value in main memory, assuming you can find a terp which handles four-gigabyte games. "Call frame local" FFFFFFFC is illegal; whether you interpret it as a negative number or a large positive number, it's outside the current call frame's locals segment.
Some opcodes store values as well as reading them in. Store operands use the same addressing modes, with a few exceptions:
Operands are evaluated from left to right. (This is important if there are several push/pop operands.)
It is convenient for a program to store object references as 32-bit pointers, and still determine the type of a reference at run-time.
To facilitate this, structured objects in Glulx main memory follow a simple convention: the first byte indicates the type of the object.
At the moment, there are only two kinds of Glulx objects: functions and strings. A program (or compiler, or library) may declare more, but the Glulx VM does not have to know about them.
Of course, not every byte in memory is the start of the legitimate object. It is the program's responsibility to keep track of which values validly refer to typable objects.
Strings have a type byte of E0 (for unencoded, C-style strings), E2 (for unencoded strings of Unicode values), or E1 (for compressed strings.) Types E3 to FF are reserved for future expansion of string types.
An unencoded string consists of an E0 byte, followed by all the bytes of the string, followed by a zero byte.
An unencoded Unicode string consists of an E2 byte, followed by three padding 0 bytes, followed by the Unicode character values (each one being a four-byte integer). Finally, there is a terminating value (four 0 bytes).
Unencoded Unicode string +----------------+ | Type: E2 | (1 byte) | Padding: 00 | (3 bytes) | Characters.... | (any length, multiple of 4) | NUL: 00000000 | (4 bytes) +----------------+
Note that the character data is not encoded in UTF-8, UTF-16, or any other peculiar encoding. It is treated as an array of 32-bit integers (which are, as always in Glulx, stored big-endian). Each integer is a Unicode code point.
A compressed string consists of an E1 byte, followed by a block of Huffman-encoded data. This should be read as a stream of bits, starting with the low bit (the 1 bit) of the first byte after the E1, proceeding through the high bit (the 128 bit), and so on with succeeding bytes.
Decoding compressed strings requires looking up data in a Huffman table. The address of this table is normally found in the header. However, the program can select a different decompression table at run-time; see section 2.11, "Output".
The Huffman table is logically a binary tree. Internal nodes are branch points; leaf nodes represent printable entities. To decode a string, begin at the root node. Read one bit from the bit stream, and go to the left or right child depending on its value. Continue reading bits and branching left or right, until you reach a leaf node. Print that entity. Then jump back to the root, and repeat the process. One particular leaf node indicates the end of the string (rather than any printable entity), and when the bit stream leads you to that node, you stop.
[This is a fairly slow process, with VM memory reads and a conditional test for every bit of the string. A terp can speed it up considerably by reading the Huffman table all at once, and caching it as native data structures. A binary tree is the obvious choice, but one can do even better (at the cost of some space) by looking up four-bit chunks at a time in a 16-branching tree.]
[Note that decompression tables are not necessarily in ROM. This is particularly important for tables that are generated and selected at run-time. Furthermore, it is technically legal for a table in RAM to be altered at runtime -- possibly even when it is the currently-selected table. Therefore, an interpreter that caches or preloads this decompression data must be careful. If it caches data from RAM, it must watch for writes to that RAM space, and invalidate its cache upon seeing such a write.]
The decoding table has the following format:
+-----------------+ | Table Length | (4 bytes) | Number of Nodes | (4 bytes) | Root Node Addr | (4 bytes) | Node Data .... | (table length - 12 bytes) +-----------------+
The table length is measured in bytes, from the beginning of the table to the end of the last node. The node count includes both branch and leaf nodes. [There will, of course, be an odd number of nodes, and (N+1)/2 of them will be leaves.] The root address indicates which node is the root of the tree; it is not necessarily the first node. This is an absolute address, not an offset from the beginning of the table.
There then follow all the nodes, with no extra data before, between, or after them. They need not be in any particular order. There are several possible types of nodes, distinguished by their first byte.
Branch (non-leaf node) +----------------+ | Type: 00 | (1 byte) | Left (0) Node | (4 bytes) | Right (1) Node | (4 bytes) +----------------+
The left and right node fields are addresses (again, absolute addresses) of the nodes to go to given a 0 or 1 bit from the bit stream.
String terminator +----------------+ | Type: 01 | (1 byte) +----------------+
This ends the string-decoding process.
Single character +----------------+ | Type: 02 | (1 byte) | Character | (1 byte) +----------------+
This prints a single character. [The encoding scheme is the business of the I/O system; in Glk, it will be the Latin-1 character set.]
C-style string +----------------+ | Type: 03 | (1 byte) | Characters.... | (any length) | NUL: 00 | (1 byte) +----------------+
This prints an array of characters. Note that the array cannot contain a zero byte, since that is reserved to terminate the array. [A zero byte can be printed using the single-character node type.]
Single Unicode character +----------------+ | Type: 04 | (1 byte) | Character | (4 bytes) +----------------+
This prints a single Unicode character. [To be precise, it prints a 32-bit character, which will be interpreted as Unicode if the I/O system is Glk.]
C-style Unicode string +----------------+ | Type: 05 | (1 byte) | Characters.... | (any length, multiple of 4) | NUL: 00000000 | (4 bytes) +----------------+
This prints an array of Unicode characters. Note that the array cannot contain a zero word, since that is reserved to terminate the array. Also note that, unlike an E2-encoded string object, there is no padding.
[If the Glk library is unable to handle Unicode, node types 04 and 05 are still legal. However, characters beyond FF will be printed as 3F ("?").]
Indirect reference +----------------+ | Type: 08 | (1 byte) | Address | (4 bytes) +----------------+
This prints a string or calls a function, which is not actually part of the decoding table. The address may refer to a location anywhere in memory (including RAM.) It must be a valid Glulx string (see section 1.6.1, "Strings") or function (see section 1.6.2, "Functions"). If it is a string, it is printed. If a function, it is called (with no arguments) and the result is discarded.
The management of the stack during an indirect string/function call is a bit tricky. See section 1.3.4, "Calling and Returning Within Strings".
Double-indirect reference +----------------+ | Type: 09 | (1 byte) | Address | (4 bytes) +----------------+
This is similar to the indirect-reference node, but the address refers to a four-byte field in memory, and that contains the address of a string or function. The extra layer of indirection can be useful. For example, if the four-byte field is in RAM, its contents can be changed during execution, pointing to a new typable object, without modifying the decoding table itself.
Indirect reference with arguments +----------------+ | Type: 0A | (1 byte) | Address | (4 bytes) | Argument Count | (4 bytes) | Arguments.... | (4*N bytes) +----------------+ Double-indirect reference with arguments +----------------+ | Type: 0B | (1 byte) | Address | (4 bytes) | Argument Count | (4 bytes) | Arguments.... | (4*N bytes) +----------------+
These work the same as the indirect and double-indirect nodes, but if the object found is a function, it will be called with the given argument list. If the object is a string, the arguments are ignored.
Functions have a type byte of C0 (for stack-argument functions) or C1 (for local-argument functions). Types C2 to DF are reserved for future expansion of function types.
A Glulx function always takes a list of 32-bit arguments, and returns exactly one 32-bit value. (If you want a function which returns no value, discard or ignore it. Store operand mode zero is convenient.)
If the type is C0, the arguments are passed on the stack, and are made available on the stack. After the function's call frame is constructed, all the argument values are pushed -- last argument pushed first, first argument topmost. Then the number of arguments is pushed on top of that. All locals in the call frame itself are initialized to zero.
If the type is C1, the arguments are passed on the stack, and are written into the locals according to the "format of locals" list of the function. Arguments passed into 8-bit or 16-bit locals are truncated. It is legitimate for there to be too many or too few arguments. Extras are discarded silently; any locals left unfilled are initialized to zero.
A function has the following structure:
+------------+ | C0 or C1 | Type (1 byte) +------------+ | Format of | (2*n bytes) | Locals | +------------+ | Opcodes | | .... | +------------+
The locals-format list is encoded the same way it is on the stack; see section 1.3.1, "The Call Frame". This is a list of LocalType/LocalCount byte pairs, terminated by a zero/zero pair. (There is, however, no extra padding to reach four-byte alignment.)
Note that although a LocalType/LocalCount pair can only describe up to 255 locals, there is no restriction on how many locals the function can have. It is legitimate to encode several pairs in a row with the same LocalType.
Immediately following the two zero bytes, the instructions start. There is no explicit terminator for the function.
There are no other Glulx objects at this time, but type 80 to BF are reserved for future expansion. Type 00 is also reserved; it indicates "no object", and should not be used by any typable object. A null reference's type would be considered 00. (Even though byte 00000000 of main memory is not in fact 00.)
Types 01 to 7F are available for use by the compiler, the library, or the program. Glulx will not use them.
[Inform uses 60 for dictionary words, and 70 for objects and classes. It reserves types 40 to 7F. Types 01 to 3F remain available for use by Inform programmers.]
Glulx values are 32-bit integers, big-endian when stored in memory. To handle floating-point math, we must be able to encode float values as 32-bit values. Unsurprisingly, Glulx uses the big-endian, single-precision IEEE-754 encoding. (See http://www.psc.edu/general/software/packages/ieee/ieee.php.) This allows floats to be stored in memory, on the stack, in local variables, and in any other place that a 32-bit value appears.
However, float values and integer values are not interchangable. You cannot pass floats to the normal arithmetic opcodes, or vice versa, and expect to get meaningful answers. Always pass floats to the float opcodes and integers to the int opcodes, with the appropriate conversion opcodes to convert back and forth. (See section 2.12, "Floating-Point Math".)
Floats have limited precision; they cannot represent all real values exactly. They can't even represent all integers exactly. (Integers between -1000000 and 1000000 (hex) have exact representations. Beyond that, the rounding error can be greater than 1. But when you get into fractions, errors are possible anywhere: 1/3 cannot be stored exactly.)
Therefore, you must be careful when comparing results. A series of float operations may produce a result fractionally different from what you expect. When comparing float values, you will most often want to use the jfeq opcode, which tests whether two values are near each other (within a specified range).
A float value has three fields in its 32 bits, from highest (the sign bit) to lowest:
+---------------+ | Sign Bit (S) | (1 bit) | Exponent (E) | (8 bits) | Mantissa (M) | (23 bits) +---------------+
The interpretation of the value depends on the exponent value:
[I'm using decimal exponents there amid all the hex constants. -149 is hex -95; -150 is hex -96. Sorry about that.]
The numeric formulas may look more familiar if you write them as 2^(-126)*(0.MMMM...) and 2^(E-127)*(1.MMMM...), where "0.MMMM..." is a fraction between zero and one (23 mantissa bits after the binal point) and "1.MMMM...." is a fraction beween one and two.
Some example values:
To give you an idea of the behavior of the special values:
NaN is sticky; almost any mathematical operation involving a NaN produces NaN. (There are a few exceptions.)
However, Glulx does not guarantee which NaN value you will get from such operations. The underlying platform may try to encode information about what operation failed in the mantissa field of the NaN. Or, contrariwise, it may return the same value for every NaN. The sign bit, similarly, is never guaranteed. (The sign may be preserved if that's meaningful for the failed operation, but it may not be.) You should not test for NaN by comparing to a fixed encoded value; instead, use the jisnan opcode.
(Or, if you like, "serializing the machine state".)
This is a variant of Quetzal, the standard Z-machine save file format. (See http://ifarchive.org/if-archive/infocom/interpreters/specification/savefile_14.txt.)
Everything in the Quetzal specification applies, with the following exceptions:
In both compressed and uncompressed form, the memory chunk ('CMem' or 'UMem') starts with a four-byte value, which is the current size of memory. The memory data then follows. During a restore, the size of memory is changed to this position.
The memory area to be saved does not start at address zero, but at RAMSTART. It continues to the current end of memory (which may not be the ENDMEM value in the header.) When generating or reading compressed data ('CMem' chunk), the data above EXTSTART is handled as if the game file were extended with as many zeroes as necessary.
Before the stack is written out, a four-value call stub is pushed on -- result destination, PC, and FramePtr. (See section 1.3.2, "Call Stubs".) Then the entire stack can be written out, with all of its values (of whatever size) transformed to big-endian. (Padding is not skipped; it's written out as the appropriate number of zero bytes.)
When the game-state is loaded back in -- or, for that matter, when continuing after a game-save -- the four values are read back off the stack, a result code for the operation is stored in the appropriate destination, and execution continues.
[Remember that in a call stub, the PC contains the address of the instruction after the one being executed.]
If the heap is active (see section 2.9, "Memory Allocation Heap"), an allocation heap chunk is written ('MAll'). This chunk contains two four-byte values, plus two more for each extant memory block:
The blocks need not be listed in any particular order.
If the heap is not active, the 'MAll' chunk can contain 0,0 or it may be omitted.
The contents of the game-file identifier ('IFhd' chunk) are simply the first 128 bytes of memory. This is within ROM (since RAMSTART is at least 256), so it does not vary during play. It includes the story file length and checksum, as well as any compiler-specific information that may be stored immediately after the header.
Some aspects of Glulx execution are not part of the save process, and therefore are not changed during a restart, restore, or restoreundo operation. The program is responsible for checking these values after a restore to see if they have (from the program's point of view) changed unexpectedly.
Examples of information which is not saved: