2: Dictionary of Opcodes

Opcodes are written here in the format:

opname L1 L2 S1

...where "L1" and "L2" are operands using the load addressing modes, and "S1" is an operand using the store addressing modes. (See section 1.5, "Instruction Format".)

The table of opcodes:

Opcodes 0x1000 to 0x10FF are reserved for use by FyreVM. Opcodes 0x1100 to 0x11FF are reserved for extension projects by Dannii Willis. Opcodes 0x1200 to 0x12FF are reserved for iOS extension features by Andrew Plotkin. These are not documented here. Opcodes 0x7900 to 0x79FF are (apparently) reserved for experimental features in the Git interpreter. See section 0.2, "Glulx and Other IF Systems".

2.1: Integer Math

add L1 L2 S1

Add L1 and L2, using standard 32-bit addition. Truncate the result to 32 bits if necessary. Store the result in S1.

sub L1 L2 S1

Compute (L1 - L2), and store the result in S1.

mul L1 L2 S1

Compute (L1 * L2), and store the result in S1. Truncate the result to 32 bits if necessary.

div L1 L2 S1

Compute (L1 / L2), and store the result in S1. This is signed integer division.

mod L1 L2 S1

Compute (L1 % L2), and store the result in S1. This is the remainder from signed integer division.

In division and remainer, signs are annoying. Rounding is towards zero. The sign of a remainder equals the sign of the dividend. It is always true that (A / B) * B + (A % B) == A. Some examples (in decimal):

 11 /  2 =  5
-11 /  2 = -5
 11 / -2 = -5
-11 / -2 =  5
 13 %  5 =  3
-13 %  5 = -3
 13 % -5 =  3
-13 % -5 = -3

neg L1 S1

Compute the negative of L1.

bitand L1 L2 S1

Compute the bitwise AND of L1 and L2.

bitor L1 L2 S1

Compute the bitwise OR of L1 and L2.

bitxor L1 L2 S1

Compute the bitwise XOR of L1 and L2.

bitnot L1 S1

Compute the bitwise negation of L1.

shiftl L1 L2 S1

Shift the bits of L1 to the left (towards more significant bits) by L2 places. The bottom L2 bits are filled in with zeroes. If L2 is 32 or more, the result is always zero.

ushiftr L1 L2 S1

Shift the bits of L1 to the right by L2 places. The top L2 bits are filled in with zeroes. If L2 is 32 or more, the result is always zero.

sshiftr L1 L2 S1

Shift the bits of L1 to the right by L2 places. The top L2 bits are filled in with copies of the top bit of L1. If L2 is 32 or more, the result is always zero or FFFFFFFF, depending on the top bit of L1.

Notes on the shift opcodes: If L2 is zero, the result is always equal to L1. L2 is considered unsigned, so 80000000 or greater is "more than 32".

2.2: Branches

All branches (except jumpabs) specify their destinations with an offset value. The actual destination address of the branch is computed as (Addr + Offset - 2), where Addr is the address of the instruction after the branch opcode, and offset is the branch's operand. The special offset values 0 and 1 are interpreted as "return 0" and "return 1" respectively. [This odd hiccup is inherited from the Z-machine. Inform uses it heavily for code optimization.]

It is legal to branch to code that is in another function. [Indeed, there is no well-defined notion of where a function ends.] However, this does not affect the current stack frame; that remains set up according to the same function call as before the branch. Similarly, it is legal to branch to code which is not associated with any function -- e.g., code compiled on the fly in RAM.

jump L1

Branch unconditionally to offset L1.

jz L1 L2

If L1 is equal to zero, branch to L2.

jnz L1 L2

If L1 is not equal to zero, branch to L2.

jeq L1 L2 L3

If L1 is equal to L2, branch to L3.

jne L1 L2 L3

If L1 is not equal to L2, branch to L3.

jlt L1 L2 L3
jle L1 L2 L3
jgt L1 L2 L3
jge L1 L2 L3

Branch is L1 is less than, less than or equal to, greater than, greater than or equal to L2. The values are compared as signed 32-bit values.

jltu L1 L2 L3
jleu L1 L2 L3
jgtu L1 L2 L3
jgeu L1 L2 L3

The same, except that the values are compared as unsigned 32-bit values.

[Since the address space can span the full 32-bit range, it is wiser to compare addresses with the unsigned comparison operators.]

jumpabs L1

Branch unconditionally to address L1. Unlike the other branch opcodes, this takes an absolute address, not an offset. The special cases 0 and 1 (for returning) do not apply; jumpabs 0 would branch to memory address 0, if that were ever a good idea, which it isn't.

2.3: Moving Data

copy L1 S1

Read L1 and store it at S1, without change.

copys L1 S1

Read a 16-bit value from L1 and store it at S1.

copyb L1 S1

Read an 8-bit value from L1 and store it at S1.

Since copys and copyb can access chunks smaller than the usual four bytes, they require some comment. When reading from main memory or the call-frame locals, they access two or one bytes, instead of four. However, when popping or pushing values on the stack, these opcodes pull or push a full 32-bit value.

Therefore, if copyb (for example) copies a byte from main memory to the stack, a 32-bit value will be pushed, whose value will be from 0 to 255. Sign-extension does not occur. Conversely, if copyb copies a byte from the stack to memory, a 32-bit value is popped, and the bottom 8 bits are written at the given address. The upper 24 bits are lost. Constant values are truncated as well.

If copys or copyb are used with both L1 and S1 in pop/push mode, the 32-bit value is popped, truncated, and pushed.

[NOTE: Since a call frame has no specified endianness, it is unwise to use these opcodes to pull out one or two bytes from a four-byte local variable. The result will be implementation-dependent. Therefore, use of the copyb and copys opcodes with a local-variable operand of different size is deprecated. Since locals of less than four bytes are also deprecated, you should not use copyb or copys with local-variable operands at all.]

sexs L1 S1

Sign-extend a value, considered as a 16-bit value. If the value's 8000 bit is set, the upper 16 bits are all set; otherwise, the upper 16 bits are all cleared.

sexb L1 S1

Sign-extend a value, considered as an 8-bit value. If the value's 80 bit is set, the upper 24 bits are all set; otherwise, the upper 24 bits are all cleared.

Note that these opcodes, like most, work on 32-bit values. Although (for example) sexb is commonly used in conjunction with copyb, it does not share copyb's behavior of reading a single byte from memory or the locals.

Also note that the upper bits, 16 or 24 of them, are entirely ignored and overwritten with ones or zeroes.

2.4: Array Data

astore L1 L2 L3

Store L3 into the 32-bit field at main memory address (L1+4*L2).

aload L1 L2 S1

Load a 32-bit value from main memory address (L1+4*L2), and store it in S1.

astores L1 L2 L3

Store L3 into the 16-bit field at main memory address (L1+2*L2).

aloads L1 L2 S1

Load an 16-bit value from main memory address (L1+2*L2), and store it in S1.

astoreb L1 L2 L3

Store L3 into the 8-bit field at main memory address (L1+L2).

aloadb L1 L2 S1

Load an 8-bit value from main memory address (L1+L2), and store it in S1.

Note that these opcodes cannot access call-frame locals, or the stack. (Not with the L1 and L2 opcodes, that is.) L1 and L2 provide a main-memory address. Be not confused by the fact that L1 and L2 can be any addressing mode, including call-frame or stack-pop modes. That controls where the values come from which are used to compute the main-memory address.

The other end of the transfer (S1 or L3) is always a 32-bit value. The "store" opcodes truncate L3 to 8 or 16 bits if necessary. The "load" opcodes expand 8-bit or 16-bit values without sign extension. (If signed values are appropriate, you can follow aloads/aloadb with sexs/sexb.)

L2 is considered signed, so you can access addresses before L1 as well as after.

astorebit L1 L2 L3

Set or clear a single bit. This is bit number (L2 mod 8) of memory address (L1+L2/8). It is cleared if L3 is zero, set if nonzero.

aloadbit L1 L2 S1

Test a single bit, similarly. If it is set, 1 is stored at S1; if clear, 0 is stored.

For these two opcodes, bits are effectively numbered sequentially, starting with the least significant bit of address L1. L2 is considered signed, so this numbering extends both positively and negatively. For example:

astorebit  1002  0  1:  Set bit 0 of address 1002. (The 1's place.)
astorebit  1002  7  1:  Set bit 7 of address 1002. (The 128's place.)
astorebit  1002  8  1:  Set bit 0 of address 1003.
astorebit  1002  9  1:  Set bit 1 of address 1003.
astorebit  1002 -1  1:  Set bit 7 of address 1001.
astorebit  1002 -3  1:  Set bit 5 of address 1001.
astorebit  1002 -8  1:  Set bit 0 of address 1001.
astorebit  1002 -9  1:  Set bit 7 of address 1000.

Like the other aload and astore opcodes, these opcodes cannot access call-frame locals, or the stack.

2.5: The Stack

stkcount S1

Store a count of the number of values on the stack. This counts only values above the current call-frame. In other words, it is always zero when a C1 function starts executing, and (numargs+1) when a C0 function starts executing. It then increases and decreases thereafter as values are pushed and popped; it is always the number of values that can be popped legally. (If S1 uses the stack push mode, the count is done before the result is pushed.)

stkpeek L1 S1

Peek at the L1'th value on the stack, without actually popping anything. If L1 is zero, this is the top value; if one, it's the value below that; etc. L1 must be less than the current stack-count. (If L1 or S1 use the stack pop/push modes, the peek is counted after L1 is popped, but before the result is pushed.)

stkswap

Swap the top two values on the stack. The current stack-count must be at least two.

stkcopy L1

Peek at the top L1 values in the stack, and push duplicates onto the stack in the same order. If L1 is zero, nothing happens. L1 must not be greater than the current stack-count. (If L1 uses the stack pop mode, the stkcopy is counted after L1 is popped.)

An example of stkcopy, starting with six values on the stack:

5 4 3 2 1 0 <top>
stkcopy 3
5 4 3 2 1 0 2 1 0 <top>

stkroll L1 L2

Rotate the top L1 values on the stack. They are rotated up or down L2 places, with positive values meaning up and negative meaning down. The current stack-count must be at least L1. If either L1 or L2 is zero, nothing happens. (If L1 and/or L2 use the stack pop mode, the roll occurs after they are popped.)

An example of two stkrolls, starting with nine values on the stack:

8 7 6 5 4 3 2 1 0 <top>
stkroll 5 1
8 7 6 5 0 4 3 2 1 <top>
stkroll 9 -3
5 0 4 3 2 1 8 7 6 <top>

Note that stkswap is equivalent to stkroll 2 1, or for that matter stkroll 2 -1. Also, stkcopy 1 is equivalent to stkpeek 0 sp.

These opcodes can only access the values pushed on the stack above the current call-frame. It is illegal to stkswap, stkpeek, stkcopy, or stkroll values below that -- i.e, the locals segment or any previous function call frames.

2.6: Functions

call L1 L2 S1

Call function whose address is L1, passing in L2 arguments, and store the return result at S1.

The arguments are taken from the stack. Before you execute the call opcode, you must push the arguments on, in backward order (last argument pushed first, first argument topmost on the stack.) The L2 arguments are removed before the new function's call frame is constructed. (If L1, L2, or S1 use the stack pop/push modes, the arguments are taken after L1 or L2 is popped, but before the result is pushed.)

Recall that all functions in Glulx have a single 32-bit return value. If you do not care about the return value, you can use operand mode 0 ("discard value") for operand S1.

callf L1 S1
callfi L1 L2 S1
callfii L1 L2 L3 S1
callfiii L1 L2 L3 L4 S1

Call function whose address is L1, passing zero, one, two, or three arguments. Store the return result at S1.

These opcodes behave the same as call, except that the arguments are given in the usual opcode format instead of being found on the stack. (If L2, L3, etc. all use the stack pop mode, then the behavior is exactly the same as call.)

return L1

Return from the current function, with the given return value. If this is the top-level function, Glulx execution is over.

Note that all the branch opcodes (jump, jz, jeq, and so on) have an option to return 0 or 1 instead of branching. These behave exactly as if the return opcode had been executed.

tailcall L1 L2

Call function whose address is L1, passing in L2 arguments, and pass the return result out to whoever called the current function.

This destroys the current call-frame, as if a return had been executed, but does not touch the call stub below that. It then immediately calls L1, creating a new call-frame. The effect is the same as a call immediately followed by a return, but takes less stack space.

It is legal to use tailcall from the top-level function. L1 becomes the top-level function.

[This opcode can be used to implement tail recursion, without forcing the stack to grow with every call.]

2.7: Continuations

catch S1 L1

Generates a "catch token", which can be used to jump back to this execution point from a throw opcode. The token is stored in S1, and then execution branches to offset L1. If execution is proceeding from this point because of a throw, the thrown value is stored instead, and the branch is ignored.

Remember if the branch value is not 0 or 1, the branch is to to (Addr + L1 - 2), where Addr is the address of the instruction after the catch. If the value is 0 or 1, the function returns immediately, invalidating the catch token.

If S1 or L1 uses the stack push/pop modes, note that the precise order of execution is: evaluate L1 (popping if appropriate); generate a call stub and compute the token; store S1 (pushing if appropriate).

throw L1 L2

Jump back to a previously-executed catch opcode, and store the value L1. L2 must be a valid catch token.

The exact catch/throw procedure is as follows:

When catch is executed, a four-value call stub is pushed on the stack -- result destination, PC, and FramePtr. (See section 1.3.2, "Call Stubs". The PC is the address of the next instruction after the catch.) The catch token is the value of the stack pointer after these are pushed. The token value is stored in the result destination, and execution proceeds, branching to L1.

When throw is executed, the stack is popped down until the stack pointer equals the given token. Then the four values are read back off the stack, the thrown value is stored in the destination, and execution proceeds with the instruction after the catch.

If the call stub (or any part of it) is removed from the stack, the catch token becomes invalid, and must not be used. This will certainly occur when you return from the function containing the catch opcode. It will also occur if you pop too many values from the stack after executing the catch. (You may wish to do this to "cancel" the catch; if you pop and discard those four values, the token is invalidated, and it is as if you had never executed the catch at all.) The catch token is also invalidated if any part of the call stub is overwritten (e.g. with stkswap or stkroll).

[Why is the catch branch taken at catch time, and ignored after a throw? Because it's easier to write the interpreter that way, that's why. If it had to branch after a throw, either the call stub would have to contain the branch offset, or the terp would have to re-parse the catch instruction. Both are ugly.]

2.8: Memory Map

getmemsize S1

Store the current size of the memory map. This is originally the ENDMEM value from the header, but you can change it with the setmemsize opcode. (The malloc and mfree opcodes may also cause this value to change; see section 2.9, "Memory Allocation Heap".) It will always be greater than or equal to ENDMEM, and will always be a multiple of 256.

setmemsize L1 S1

Set the current size of the memory map. The new value must be a multiple of 256, like all memory boundaries in Glulx. It must be greater than or equal to ENDMEM (the initial memory-size value which is stored in the header.) It does not have to be greater than the previous memory size. The memory size may grow and shrink over time, as long as it never gets smaller than the initial size.

When the memory size grows, the new space is filled with zeroes. When it shrinks, the contents of the old space are lost.

If the allocation heap is active (see section 2.9, "Memory Allocation Heap") you may not use setmemsize -- the memory map is under the control of the heap system. If you free all heap objects, the heap will then no longer be active, and you can use setmemsize.

Since memory allocation is never guaranteed, you must be prepared for the possibility that setmemsize will fail. The opcode stores the value zero if it succeeded, and 1 if it failed. If it failed, the memory size is unchanged.

Some interpreters do not have the capability to resize memory at all. On such interpreters, setmemsize will always fail. You can check this in advance with the ResizeMem gestalt selector.

Note that the memory size is considered part of the game state. If you restore a saved game, the current memory size is changed to the size that was in effect when the game was saved. If you restart, the current memory size is reset to its initial value.

2.9: Memory Allocation Heap

Manage the memory allocation heap.

Glulx is able to maintain a list of dynamically-allocated memory objects. These objects exist in the memory map, above ENDMEM. The malloc and mfree opcodes allow the game to request the allocation and destruction of these objects.

Some interpreters do not have the capability to manage an allocation heap. On such interpreters, malloc will always fail. You can check this in advance with the MAlloc gestalt selector.

When you first allocate a block of memory, the heap becomes active. The current end of memory -- that is, the current getmemsize value -- becomes the beginning address of the heap. The memory map is then extended to accomodate the memory block.

Subsequent memory allocations and deallocations are done within the heap. The interpreter may extend or reduce the memory map, as needed, when allocations and deallocations occur. While the heap is active, you may not manually resize the memory map with setmemsize; the heap system is responsible for doing that.

When you free the last extant memory block, the heap becomes inactive. The interpreter will reduce the memory map size down to the heap-start address. (That is, the getmemsize value returns to what it was before you allocated the first block.) Thereafter, it is legal to call setmemsize again.

It is legitimate to read or write any memory address in the heap range (from ENDMEM to the end of the memory map). You are not restricted to extant blocks. [The VM's heap state is not stored in its own memory map. So, unlike the familiar C heap, you cannot damage it by writing outside valid blocks.]

The heap state (whether it is active, its starting address, and the addresses and sizes of all extant blocks) is part of the saved game state.

These opcodes were added in Glulx version 3.1.

malloc L1 S1

Allocate a memory block of L1 bytes. (L1 must be positive.) This stores the address of the new memory block, which will be within the heap and will not overlap any other extant block. The interpreter may have to extend the memory map (see section 2.8, "Memory Map") to accomodate the new block.

This operation does not change the contents of the memory block (or, indeed, the contents of the memory map at all). If you want the memory block to be initialized, you must do it yourself.

If the allocation fails, this stores zero.

mfree L1

Free the memory block at address L1. This must be the address of an extant block -- that is, a value returned by malloc and not previously freed.

This operation does not change the contents of the memory block (or, indeed, the contents of the memory map at all).

2.10: Game State

quit

Shut down the terp and exit. This is equivalent to returning from the top-level function, or for that matter calling glk_exit().

Note that (in the Glk I/O system) Glk is responsible for any "hit any key to exit" prompt. It is safe for you to print a bunch of final text and then exit immediately.

restart

Restore the VM to its initial state (memory, stack, and registers). Note that the current memory size is reset, as well as the contents of memory.

save L1 S1

Save the VM state to the output stream L1. It is your responsibility to prompt the player for a filespec, open the stream, and then destroy these objects afterward. S1 is set to zero if the operation succeeded, 1 if it failed, and -1 if the VM has just been restored and is continuing from this instruction.

(In the Glk I/O system, L1 should be the ID of a writable Glk stream. In other I/O systems, it will mean something different. In the "filter" and "null" I/O systems, the save opcode is illegal, as the interpreter has nowhere to write the state.)

restore L1 S1

Restore the VM state from the input stream L1. S1 is set to 1 if the operation failed. If it succeeded, of course, this instruction never returns a value.

saveundo S1

Save the VM state in a temporary location. The terp will choose a location appropriate for rapid access, so this may be called once per turn. S1 is set to zero if the operation succeeded, 1 if it failed, and -1 if the VM state has just been restored.

restoreundo S1

Restore the VM state from temporary storage. S1 is set to 1 if the operation failed.

protect L1 L2

Protect a range of memory from restart, restore, restoreundo. The protected range starts at address L1 and has a length of L2 bytes. This memory is silently unaffected by the state-restoring operations. (However, if the result-storage S1 is directed into the protected range, that is not blocked.)

When the VM starts up, there is no protection range. Only one range can be protected at a time. Calling protect cancels any previous range. To turn off protection, call protect with L1 and L2 set to zero.

It is important to note that the protection range itself (its existence, location, and length) is not part of the saved game state! If you save a game, move the protection range to a new location, and then restore that game, it is the new range that will be protected, and the range will remain there afterwards.

verify S1

Perform sanity checks on the game file, using its length and checksum. S1 is set to zero if everything looks good, 1 if there seems to be a problem. (Many interpreters will do this automatically, before the game starts executing. This opcode is provided mostly for slower interpreters, where auto-verify might cause an unacceptable delay.)

Notes:

All the save and restore opcodes can generate diagnostic information on the current output stream.

A terp may support several levels of temporary storage. You should not make any assumptions about how many times restoreundo can be called. If the player so requests, you should keep calling it until it fails.

Glk opaque objects (windows, streams, filespecs) are not part of the saved game state. Therefore, when you restore a game, all the object IDs you have in Glulx memory must be considered invalid. (This includes both IDs in main memory and on the stack.) You must use the Glk iteration calls to go through all the opaque objects in existence, and recognize them by their rocks.

The same applies after restoreundo, to a lesser extent. Since saveundo/restoreundo only operate within a single play session, you can rely on the IDs of objects created before the first saveundo. However, if you have created any objects since then, you must iterate and recognize them.

The restart opcode is a similar case. You must do an iteration as soon as your program starts, to find objects created in an earlier incarnation. Alternatively, you can be careful to close all opaque objects before invoking restart.

[Another approach is to use the protect opcode, to preserve global variables containing your object IDs. This will work within a play session -- that is, with saveundo, restoreundo, and restart. You must still deal with save and restore.]

2.11: Output

getiosys S1 S2

Return the current I/O system mode and rock.

Due to a long-standing bug in the reference interpreter, the two store operands must be of the same general type: both main-memory/global stores, both local variable stores, or both stack pushes.

setiosys L1 L2

Set the I/O system mode and rock. If the system L1 is not supported by the interpreter, it will default to the "null" system (0).

These systems are currently defined:

It is important to recall that when Glulx starts up, the Glk I/O system is not set. And when Glk starts up, there are no windows and no current output stream. To make anything appear to the user, you must first do three things: select the Glk I/O system, open a Glk window, and set its stream as the current one. (It is illegal in Glk to send output when there is no stream set. Sending output to Glulx's "null" I/O system is legal, but pointless.)

streamchar L1

Send L1 to the current stream. This sends a single character; the value L1 is truncated to eight bits.

streamunichar L1

Send L1 to the current stream. This sends a single (32-bit) character.

This opcode was added in Glulx version 3.0.

streamnum L1

Send L1 to the current stream, represented as a signed decimal number in ASCII.

streamstr L1

Send a string object to the current stream. L1 must be the address of a Glulx string object (type E0, E1, or E2.) The string is decoded and sent as a sequence of characters.

When the Glk I/O system is set, these opcodes are implemented using the Glk API. You can bypass them and directly call glk_put_char(), glk_put_buffer(), and so on. Remember, however, that glk_put_string() only accepts unencoded string (E0) objects; glk_put_string_uni() only accepts unencoded Unicode (E2) objects.

Note that it is illegal to decode a compressed string (E1) if there is no string-decoding table set.

getstringtbl S1

Return the address the terp is currently using for its string-decoding table. If there is no table, set, this returns zero.

setstringtbl L1

Change the address the terp is using for its string-decoding table. This may be zero, indicating that there is no table (in which case it is illegal to print any compressed string). Otherwise, it must be the address of a valid string-decoding table.

[This does not change the value in the header field at address 001C. The header is in ROM, and never changes. To determine the current table address, use the getstringtbl opcode.]

A string-decoding table may be in RAM or ROM, but there may be speed penalties if it is in RAM. See section 1.6.1.4, "The String-Decoding Table".

2.12: Floating-Point Math

Recall that floating-point values are encoded as single-precision (32-bit) IEEE-754 values (see section 1.7, "Floating-Point Numbers"). The interpreter must convert values (from memory or the stack) before performing a floating-point operation, and unconvert them afterwards.

[In other words, passing a float value to an integer arithmetic opcode will operate on the IEEE-754-encoded 32-bit value. Such an operation would be deterministic, albeit mathematically meaningless. The same is true for passing an integer to a float opcode.]

Float operations which produce inexact results are not guaranteed to be identical on every platform. That is, 1.0 plus 1.0 will always be 2.0, because that can be represented exactly. But acos(-1.0), which should be pi, may generate either 40490FDA (3.14159250...) or 40490FDB (3.14159274...). Both are approximations of the correct result, but which one you get depends on the interpreter's underlying math library.

If any argument to a float operation is a NaN ("not a number") value, the result will be a NaN value. (Except for the pow opcode, which has some special cases.)

[Speaking of special cases: I have tried to describe all the important ones for these operations. However, you should also consult the Glulxercise unit test (available on the Glulx web site). Consider it definitive if this document is unclear.]

These opcodes were added in Glulx version 3.1.2. However, not all interpreters may support them. You can test for their availability with the Float gestalt selector.

numtof L1 S1

Convert an integer value to the closest equivalent float. (That is, if L1 is 1, then 3F800000 -- the float encoding of 1.0 -- will be stored in S1.) Integer zero is converted to (positive) float zero.

If the value is less than -1000000 or greater than 1000000 (hex), the conversion may not be exact. (More specifically, it may round to a nearby multiple of a power of 2.)

ftonumz L1 S1

Convert a float value to an integer, rounding towards zero (i.e., truncating the fractional part). If the value is outside the 32-bit integer range, or is NaN or infinity, the result will be 7FFFFFFF (for positive values) or 80000000 (for negative values).

ftonumn L1 S1

Convert a float value to an integer, rounding towards the nearest integer. Again, overflows become 7FFFFFFF or 80000000.

fadd L1 L2 S1
fsub L1 L2 S1
fmul L1 L2 S1
fdiv L1 L2 S1

Perform floating-point arithmetic. Overflows produce infinite values (with the appropriate sign); underflows produce zero values (ditto). 0/0 is NaN. Inf/Inf, or Inf-Inf, is NaN. Any finite number added to infinity is infinity. Any nonzero number divided by an infinity, or multiplied by zero, is a zero. Any nonzero number multiplied by an infinity, or divided by zero, is an infinity.

fmod L1 L2 S1 S2

Perform a floating-point modulo operation. S1 is the remainder (or modulus); S2 is the quotient.

S2 is L1/L2, rounded (towards zero) to an integral value. S1 is L1-(S2*L2). Note that S1 always has the same sign as L1; S2 has the appropriate sign for L1/L2.

If L2 is 1, this gives you the fractional and integer parts of L1. If L1 is zero, both results are zero. If L2 is infinite, S1 is L1 and S2 is zero. If L1 is infinite or L2 is zero, both results are NaN.

ceil L1 S1
floor L1 S1

Round L1 up (towards +Inf) or down (towards -Inf) to the nearest integral value. (The result is still in float format, however.) These opcodes are idempotent.

The result keeps the sign of L1; in particular, floor(0.5) is 0 and ceil(-0.5) is -0. Rounding -0 up or down gives -0. Rounding an infinite value gives infinity.

sqrt L1 S1
exp L1 S1
log L1 S1

Compute the square root of L1, e^L1, and log of L1 (base e).

sqrt(-0) is -0. sqrt returns NaN for all other negative values. exp(+0) and exp(-0) are 1; exp(-Inf) is +0. log(+0) and log(-0) are -Inf. log returns NaN for all other negative values.

pow L1 L2 S1

Compute L1 raised to the L2 power.

The special cases are breathtaking. The following is quoted (almost) directly from the libc man page:

sin L1 S1
cos L1 S1
tan L1 S1
acos L1 S1
asin L1 S1
atan L1 S1

Compute the standard trigonometric functions.

sin and cos return values in the range -1 to 1. sin, cos, and tan of infinity are NaN.

asin is always in the range -pi/2 to pi/2; acos is always in the range 0 to pi. asin and acos of values greater than 1, or less than -1, are NaN. atan(±Inf) is ±pi/2.

atan2 L1 L2 S1

Computes the arctangent of L1/L2, using the signs of both arguments to determine the quadrant of the return value. (Note that the Y argument is first and the X argument is second.)

Again with the special cases:

2.13: Floating-Point Comparisons

All these branch opcodes specify their destinations with an offset value. See section 2.2, "Branches".

Most of these opcodes never branch if any argument is NaN. (Exceptions are jisnan and jfne.) In particular, NaN is neither less than, greater than, nor equal to NaN.

These opcodes were added in Glulx version 3.1.2. However, not all interpreters may support them. You can test for their availability with the Float gestalt selector.

jisnan L1 L2

Branch to L2 if the floating-point value L1 is a NaN value. (See section 1.7, "Floating-Point Numbers".)

jisinf L1 L2

Branch to L2 if the floating-point value L1 is an infinity (7F800000 or FF800000).

jfeq L1 L2 L3 L4

Branch to L4 if the difference between L1 and L2 is less than or equal to (plus or minus) L3. The sign of L3 is ignored.

If any of the arguments are NaN, this will not branch. If L3 is infinite, this will always branch -- unless L1 and L2 are opposite infinities. (Opposite infinities are never equal, regardless of L3. Infinities of the same sign are always equal.)

If L3 is (plus or minus) zero, this tests for exact equality. Note that +0 is considered exactly equal to -0.

jfne L1 L2 L3 L4

The reverse of jfeq. This will branch if any of the arguments is NaN.

jflt L1 L2 L3
jfle L1 L2 L3
jfgt L1 L2 L3
jfge L1 L2 L3

Branch to L3 if L1 is less than (less than or equal to, greater than, greater than or equal to) L2.

+0 and -0 behave identically in comparisons. In particular, +0 is considered equal to -0, not greater than -0.

2.14: Random Number Generator

random L1 S1

Return a random number in the range 0 to (L1-1); or, if L1 is negative, the range (L1+1) to 0. If L1 is zero, return a random number in the full 32-bit integer range. (Remember that this may be either positive or negative.)

setrandom L1

Seed the random-number generator with the value L1. If L1 is zero, subsequent random numbers will be as genuinely unpredictable as the terp can provide; it may include timing data or other random sources in its generation. If L1 is nonzero, subsequent random numbers will follow a deterministic sequence, always the same for a given nonzero seed.

The terp starts up in the "nondeterministic" mode (as if setrandom 0 had been invoked.)

The random-number generator is not part of the saved-game state.

2.15: Block Copy and Clear

mzero L1 L2

Write L1 zero bytes, starting at address L2. This is exactly equivalent to:

for (ix=0: ix<L1: ix++) L2->ix = 0;

mcopy L1 L2 L3

Copy L1 bytes from address L2 to address L3. It is safe to copy a block to an overlapping block. This is exactly equivalent to:

if (L3 < L2)
  for (ix=0: ix<L1: ix++) L3->ix = L2->ix;
else
  for (ix=L1-1: ix>=0: ix--) L3->ix = L2->ix;

For both of these opcodes, L1 may be zero, in which case the opcodes do nothing. The operands are considered unsigned, so a "negative" L1 is a very large number (and almost certainly a mistake).

These opcodes were added in Glulx version 3.1. You can test for their availability with the MemCopy gestalt selector.

2.16: Searching

Perform a generic linear, binary, or linked-list search.

[These are outrageously CISC for an hardware CPU, but easy enough to add to a software terp; and taking advantage of them can speed up a program considerably. Advent, under the Inform library, runs 15-20% faster when property-table lookup is handled with a binary-search opcode instead of Inform code. A similar change in the dictionary lookup trims another percent or so.]

All three of these opcodes operate on a collection of fixed-size data structures in memory. A key, which is a fixed-length array of bytes, is found at a known position within each data structure. The opcodes search the collection of structures, and find one whose key matches a given key.

The following flags may be set in the Options argument. Note that not all flags can be used with all types of searches.

linearsearch L1 L2 L3 L4 L5 L6 L7 S1

An array of data structures is stored in memory, beginning at Start, each structure being StructSize bytes. Within each struct, there is a key value KeySize bytes long, starting at position KeyOffset (from the start of the structure.) Search through these in order. If one is found whose key matches, return it. If NumStructs are searched with no result, the search fails.

NumStructs may be -1 (0xFFFFFFFF) to indicate no upper limit to the number of structures to search. The search will continue until a match is found, or (if ZeroKeyTerminates is used) a zero key.

The KeyIndirect, ZeroKeyTerminates, and ReturnIndex options may be used.

binarysearch L1 L2 L3 L4 L5 L6 L7 S1

An array of data structures is in memory, as above. However, the structs must be stored in forward order of their keys (taking each key to be a big-endian unsigned integer.) There can be no duplicate keys. NumStructs must indicate the exact length of the array; it cannot be -1.

The KeyIndirect and ReturnIndex options may be used.

linkedsearch L1 L2 L3 L4 L5 L6 S1

The structures need not be consecutive; they may be anywhere in memory, in any order. They are linked by a four-byte address field, which is found in each struct at position NextOffset. If this field contains zero, it indicates the end of the linked list.

The KeyIndirect and ZeroKeyTerminates options may be used.

2.17: Accelerated Functions

To improve performance, Glulx incorporates some complex functions which replicate code in the Inform library. [Yes, this is even more outrageously CISC than the search opcodes.]

Rather than allocating a new opcode for each function, Glulx offers an expandable function acceleration system. Two functions are defined below. The game may request that a particular address -- the address of a VM function -- be replaced by one of the available functions. This does not alter memory; but any subsequent call to that address might invoke the terp's built-in version of the function, instead of the VM code at that address.

(A "call" includes any function invocation of that address, including the call, tailcall, and callf (etc.) opcodes. It also includes invocation via the filter I/O system, and function nodes in the string-decoding table. Branches to the address are not affected; neither are returns, throws, or other ways the terp might reach it.)

A terp may implement any, all, or none of the functions on the list. If the game requests an accelerated function which is not available, the request is ignored. Therefore, the game must be sure that it only requests an accelerated function at an address which actually matches the requested function.

Some functions may require values (or addresses) which are compiled into the game file, or otherwise stored by the game. The interpreter maintains a table of these parameters -- whichever ones are needed by the functions it supports. All parameters in the table are initially zero; the game may supply values as needed.

The set of active acceleration requests, and the values in the parameter table, are not part of the saved-game state.

The behavior of an accelerated function is somewhat limited. The state of the VM during the function is not defined, so there is no way for an accelerated function to call a normal VM function. The normal printing mechanism (as in the streamchar opcode, etc) is not available, since that can call VM functions via the filter I/O system. [Not that I/O functions are likely to be worth accelerating in any case.]

Errors encountered during an accelerated function will be displayed to the user by some convenient means. For example, an interpreter may send the error message to the current Glk output stream. However, the terp may have no recourse but to invoke a fatal error. (For example, if there is no current Glk output stream.) Therefore, accelerated functions are defined with no error conditions that must be recoverable.

These opcodes were added in Glulx version 3.1.1. Since a 3.1.1 game file ought to run in a 3.1.0 interpreter, you may not use these opcodes without first testing the Acceleration gestalt selector. If it returns zero, your game is running on a 3.1.0 terp (or earlier), and it is your responsibility to avoid executing these opcodes. [Of course, the way the opcodes are defined should ensure that skipping them does not affect the behavior of your game.]

accelfunc L1 L2

Request that the VM function with address L2 be replaced by the accelerated function whose number is L1. If L1 is zero, the acceleration for address L2 is cancelled.

If the terp does not offer accelerated function L1, this does nothing.

If you request acceleration at an address which is already accelerated, the previous request is cancelled before the new one is considered. If you cancel at an unaccelerated address, nothing happens.

A given accelerated function L1 may replace several VM functions (at different addresses) at the same time. Each request is considered separate, and must be cancelled separately.

accelparam L1 L2

Store the value L2 in the parameter table at position L1. If the terp does not know about parameter L1, this does nothing.

The list of accelerated functions is as follows. They are defined as if in Inform 6 source code. (Consider Inform's "strict" mode to be off, for the purposes of operators such as .& and -->.) ERROR() represents code which displays an error, as described above.

(Functions may be added to this list in future versions of the Glulx spec. Existing functions will not be removed or altered. Functions and parameters numbered 0x1100 to 0x11FF are reserved for extension projects by Dannii Willis. These are not documented here. See section 0.2, "Glulx and Other IF Systems".)

Note that functions 2 through 7 are deprecated; they behave badly if the Inform 6 NUM_ATTR_BYTES option (parameter 7) is changed from its default value (7). They will not be removed, but new games should use functions 8 through 13 instead.

Constant PARAM_0_classes_table = #classes_table;
Constant PARAM_1_indiv_prop_start = INDIV_PROP_START;
Constant PARAM_2_class_metaclass = Class;
Constant PARAM_3_object_metaclass = Object;
Constant PARAM_4_routine_metaclass = Routine;
Constant PARAM_5_string_metaclass = String;
Constant PARAM_6_self = #globals_array + WORDSIZE * #g$self;
Constant PARAM_7_num_attr_bytes = NUM_ATTR_BYTES;
Constant PARAM_8_cpv__start = #cpv__start;

! OBJ_IN_CLASS: utility function; implements "obj in Class".
[ OBJ_IN_CLASS obj;
  return ((obj + 13 + PARAM_7_num_attr_bytes)-->0
    == PARAM_2_class_metaclass);
];

! FUNC_1_Z__Region: implements Z__Region() as of Inform 6.31.
[ FUNC_1_Z__Region addr
  tb endmem; ! locals
  if (addr<36) rfalse;
  @getmemsize endmem;
  @jgeu addr endmem?outrange;  ! branch if addr >= endmem (unsigned)
  tb=addr->0;
  if (tb >= $E0) return 3;
  if (tb >= $C0) return 2;
  if (tb >= $70 && tb <= $7F && addr >= (0-->2)) return 1;
.outrange;
  rfalse;
];

! FUNC_2_CP__Tab: implements CP__Tab() as of Inform 6.31.
[ FUNC_2_CP__Tab obj id
  otab max res; ! locals
  if (FUNC_1_Z__Region(obj)~=1) {
    ERROR("[** Programming error: tried to find the ~.~ of (something) **]");
    rfalse;
  }
  otab = obj-->4;
  if (otab == 0) return 0;
  max = otab-->0;
  otab = otab+4;
  @binarysearch id 2 otab 10 max 0 0 res;
  return res;
];

! FUNC_3_RA__Pr: implements RA__Pr() as of Inform 6.31.
[ FUNC_3_RA__Pr obj id
  cla prop ix; ! locals
  if (id & $FFFF0000) {
    cla = PARAM_0_classes_table-->(id & $FFFF);
    if (~~FUNC_5_OC__Cl(obj, cla)) return 0;
    @ushiftr id 16 id;
    obj = cla;
  }
  prop = FUNC_2_CP__Tab(obj, id);
  if (prop==0) return 0;
  if (OBJ_IN_CLASS(obj) && cla == 0) {
    if (id < PARAM_1_indiv_prop_start
        || id >= PARAM_1_indiv_prop_start+8)
      return 0;
  }
  if (PARAM_6_self-->0 ~= obj) {
    @aloadbit prop 72 ix;
    if (ix) return 0;
  }
  return prop-->1;
];

! FUNC_4_RL__Pr: implements RL__Pr() as of Inform 6.31.
[ FUNC_4_RL__Pr obj id
  cla prop ix; ! locals
  if (id & $FFFF0000) {
    cla = PARAM_0_classes_table-->(id & $FFFF);
    if (~~FUNC_5_OC__Cl(obj, cla)) return 0;
    @ushiftr id 16 id;
    obj = cla;
  }
  prop = FUNC_2_CP__Tab(obj, id);
  if (prop==0) return 0;
  if (OBJ_IN_CLASS(obj) && cla == 0) {
    if (id < PARAM_1_indiv_prop_start
        || id >= PARAM_1_indiv_prop_start+8)
      return 0;
  }
  if (PARAM_6_self-->0 ~= obj) {
    @aloadbit prop 72 ix;
    if (ix) return 0;
  }
  @aloads prop 1 ix;
  return WORDSIZE * ix;
];

! FUNC_5_OC__Cl: implements OC__Cl() as of Inform 6.31.
[ FUNC_5_OC__Cl obj cla
  zr jx inlist inlistlen; ! locals
  zr = FUNC_1_Z__Region(obj);
  if (zr == 3) {
    if (cla == PARAM_5_string_metaclass) rtrue;
    rfalse;
  }
  if (zr == 2) {
    if (cla == PARAM_4_routine_metaclass) rtrue;
    rfalse;
  }
  if (zr ~= 1) rfalse;
  if (cla == PARAM_2_class_metaclass) {
    if (OBJ_IN_CLASS(obj)
      || obj == PARAM_2_class_metaclass or PARAM_5_string_metaclass
         or PARAM_4_routine_metaclass or PARAM_3_object_metaclass)
      rtrue;
    rfalse;
  }
  if (cla == PARAM_3_object_metaclass) {
    if (OBJ_IN_CLASS(obj)
      || obj == PARAM_2_class_metaclass or PARAM_5_string_metaclass
         or PARAM_4_routine_metaclass or PARAM_3_object_metaclass)
      rfalse;
    rtrue;
  }
  if (cla == PARAM_5_string_metaclass or PARAM_4_routine_metaclass)
    rfalse;
  if (~~OBJ_IN_CLASS(cla)) {
    ERROR("[** Programming error: tried to apply 'ofclass' with non-class **]");
    rfalse;
  }
  inlist = FUNC_3_RA__Pr(obj, 2);
  if (inlist == 0) rfalse;
  inlistlen = FUNC_4_RL__Pr(obj, 2) / WORDSIZE;
  for (jx=0 : jx<inlistlen : jx++) {
    if (inlist-->jx == cla) rtrue;
  }
  rfalse;
];

! FUNC_6_RV__Pr: implements RV__Pr() as of Inform 6.31.
[ FUNC_6_RV__Pr obj id
  addr; ! locals
  addr = FUNC_3_RA__Pr(obj, id);
  if (addr == 0) {
    if (id > 0 && id < PARAM_1_indiv_prop_start) {
      return PARAM_8_cpv__start-->id;
    }
    ERROR("[** Programming error: tried to read (something) **]");
    return 0;
  }
  return addr-->0;
];

! FUNC_7_OP__Pr: implements OP__Pr() as of Inform 6.31.
[ FUNC_7_OP__Pr obj id
  zr; ! locals
  zr = FUNC_1_Z__Region(obj);
  if (zr == 3) {
    if (id == print or print_to_array) rtrue;
    rfalse;
  }
  if (zr == 2) {
    if (id == call) rtrue;
    rfalse;
  }
  if (zr ~= 1) rfalse;
  if (id >= PARAM_1_indiv_prop_start
      && id < PARAM_1_indiv_prop_start+8) {
    if (OBJ_IN_CLASS(obj)) rtrue;
  }
  if (FUNC_3_RA__Pr(obj, id) ~= 0)
    rtrue;
  rfalse;
];

! FUNC_8_CP__Tab: implements CP__Tab() as of Inform 6.33.
[ FUNC_8_CP__Tab obj id
  otab max res; ! locals
  if (FUNC_1_Z__Region(obj)~=1) {
    ERROR("[** Programming error: tried to find the ~.~ of (something) **]");
    rfalse;
  }
  otab = obj-->(3+(PARAM_7_num_attr_bytes/4));
  if (otab == 0) return 0;
  max = otab-->0;
  otab = otab+4;
  @binarysearch id 2 otab 10 max 0 0 res;
  return res;
];

! FUNC_9_RA__Pr: implements RA__Pr() as of Inform 6.33.
[ FUNC_9_RA__Pr obj id
  cla prop ix; ! locals
  if (id & $FFFF0000) {
    cla = PARAM_0_classes_table-->(id & $FFFF);
    if (~~FUNC_11_OC__Cl(obj, cla)) return 0;
    @ushiftr id 16 id;
    obj = cla;
  }
  prop = FUNC_8_CP__Tab(obj, id);
  if (prop==0) return 0;
  if (OBJ_IN_CLASS(obj) && cla == 0) {
    if (id < PARAM_1_indiv_prop_start
        || id >= PARAM_1_indiv_prop_start+8)
      return 0;
  }
  if (PARAM_6_self-->0 ~= obj) {
    @aloadbit prop 72 ix;
    if (ix) return 0;
  }
  return prop-->1;
];

! FUNC_10_RL__Pr: implements RL__Pr() as of Inform 6.33.
[ FUNC_10_RL__Pr obj id
  cla prop ix; ! locals
  if (id & $FFFF0000) {
    cla = PARAM_0_classes_table-->(id & $FFFF);
    if (~~FUNC_11_OC__Cl(obj, cla)) return 0;
    @ushiftr id 16 id;
    obj = cla;
  }
  prop = FUNC_8_CP__Tab(obj, id);
  if (prop==0) return 0;
  if (OBJ_IN_CLASS(obj) && cla == 0) {
    if (id < PARAM_1_indiv_prop_start
        || id >= PARAM_1_indiv_prop_start+8)
      return 0;
  }
  if (PARAM_6_self-->0 ~= obj) {
    @aloadbit prop 72 ix;
    if (ix) return 0;
  }
  @aloads prop 1 ix;
  return WORDSIZE * ix;
];

! FUNC_11_OC__Cl: implements OC__Cl() as of Inform 6.33.
[ FUNC_11_OC__Cl obj cla
  zr jx inlist inlistlen; ! locals
  zr = FUNC_1_Z__Region(obj);
  if (zr == 3) {
    if (cla == PARAM_5_string_metaclass) rtrue;
    rfalse;
  }
  if (zr == 2) {
    if (cla == PARAM_4_routine_metaclass) rtrue;
    rfalse;
  }
  if (zr ~= 1) rfalse;
  if (cla == PARAM_2_class_metaclass) {
    if (OBJ_IN_CLASS(obj)
      || obj == PARAM_2_class_metaclass or PARAM_5_string_metaclass
         or PARAM_4_routine_metaclass or PARAM_3_object_metaclass)
      rtrue;
    rfalse;
  }
  if (cla == PARAM_3_object_metaclass) {
    if (OBJ_IN_CLASS(obj)
      || obj == PARAM_2_class_metaclass or PARAM_5_string_metaclass
         or PARAM_4_routine_metaclass or PARAM_3_object_metaclass)
      rfalse;
    rtrue;
  }
  if (cla == PARAM_5_string_metaclass or PARAM_4_routine_metaclass)
    rfalse;
  if (~~OBJ_IN_CLASS(cla)) {
    ERROR("[** Programming error: tried to apply 'ofclass' with non-class **]");
    rfalse;
  }
  inlist = FUNC_9_RA__Pr(obj, 2);
  if (inlist == 0) rfalse;
  inlistlen = FUNC_10_RL__Pr(obj, 2) / WORDSIZE;
  for (jx=0 : jx<inlistlen : jx++) {
    if (inlist-->jx == cla) rtrue;
  }
  rfalse;
];

! FUNC_12_RV__Pr: implements RV__Pr() as of Inform 6.33.
[ FUNC_12_RV__Pr obj id
  addr; ! locals
  addr = FUNC_9_RA__Pr(obj, id);
  if (addr == 0) {
    if (id > 0 && id < PARAM_1_indiv_prop_start) {
      return PARAM_8_cpv__start-->id;
    }
    ERROR("[** Programming error: tried to read (something) **]");
    return 0;
  }
  return addr-->0;
];

! FUNC_13_OP__Pr: implements OP__Pr() as of Inform 6.33.
[ FUNC_13_OP__Pr obj id
  zr; ! locals
  zr = FUNC_1_Z__Region(obj);
  if (zr == 3) {
    if (id == print or print_to_array) rtrue;
    rfalse;
  }
  if (zr == 2) {
    if (id == call) rtrue;
    rfalse;
  }
  if (zr ~= 1) rfalse;
  if (id >= PARAM_1_indiv_prop_start
      && id < PARAM_1_indiv_prop_start+8) {
    if (OBJ_IN_CLASS(obj)) rtrue;
  }
  if (FUNC_9_RA__Pr(obj, id) ~= 0)
    rtrue;
  rfalse;
];

2.18: Miscellaneous

nop

Do nothing.

gestalt L1 L2 S1

Test the Gestalt selector number L1, with optional extra argument L2, and store the result in S1. If the selector is not known, store zero.

The reasoning behind the design of a Gestalt system is, I hope, too obvious to explain.

[This list of Gestalt selectors has nothing to do with the list in the Glk library.]

The list of L1 selectors is as follows. Note that if a selector does not mention L2, you should always set that argument to zero. [This will ensure future compatibility, in case the selector definition is extended.]

Selectors 0x1000 to 0x10FF are reserved for use by FyreVM. Selectors 0x1100 to 0x11FF are reserved for extension projects by Dannii Willis. Selectors 0x1200 to 0x12FF are reserved for iOS extension features by Andrew Plotkin. These are not documented here. See section 0.2, "Glulx and Other IF Systems".

[The Unicode selector is slightly redundant. Since the Unicode operations exist in Glulx spec 3.0 and higher, you can get the same information by testing GlulxVersion against 0x00030000. However, it's clearer to have a separate selector. Similarly, the MemCopy selector is true exactly when GlulxVersion is 0x00030100 or higher.]

[The Unicode selector does not guarantee that your Glk library supports Unicode. For that, you must check the Glk gestalt selector gestalt_Unicode. If the Glk library is non-Unicode, the Glulx Unicode operations are still legal; however, Unicode characters (beyond FF) will be printed as 3F ("?").]

debugtrap L1

Interrupt execution to do something interpreter-specific with L1. If the interpreter has nothing in mind, it should halt with a visible error message.

[This is intended for use by debugging interpreters. The program might be sprinkled with consistency tests, set to call debugtrap if an assertion failed. The interpreter could then be set to halt, display a warning, or ignore the debugtrap.]

This should not be used as an arbitrary interpreter trap-door in a finished (non-debugging) program. If you really want to add interpreter functionality to your program, and you're willing to support an alternate interpreter to run it, you should add an entirely new opcode. There are still 2^28 of them available, give or take.

glk L1 L2 S1

Call the Glk API function whose identifier is L1, passing in L2 arguments. The return value is stored at S1. (If the Glk function has no return value, zero is stored at S1.)

The arguments are passed on the stack, last argument pushed first, just as for the call opcode.

Arguments should be represented in the obvious way. Integers and character are passed as integers. Glk opaque objects are passed as integer identifiers, with zero representing NULL. Strings and Unicode strings are passed as the addresses of Glulx string objects (see section 1.6.1, "Strings".) References to values are passed by their addresses. Arrays are passed by their addresses; note that an array argument, unlike a string argument, is always followed by an array length argument.

Reference arguments require more explanation. A reference to an integer or opaque object is the address of a 32-bit value (which, being in main memory, does not have to be aligned, but must be big-endian.) Alternatively, the value -1 (FFFFFFFF) may be passed; this is a special case, which means that the value is read from or written to the stack. Arguments are always evaluated left to right, which means that input arguments are popped from the stack first-topmost, but output arguments are pushed on last-topmost.

A reference to a Glk structure is the address of an array of 32-bit values in main memory. Again, -1 means that all the values are written to the stack. Also again, an input structure is popped off first-topmost, and an output structure is pushed on last-topmost.

All stack input references (-1 addresses) are popped after the Glk argument list is popped. [This should be obvious, since the -1 occurs in the Glk argument list.] Stack output references are pushed after the Glk call, but before the S1 result value is stored.

[The difference between strings and character arrays is somewhat confusing. These are the same type in the C Glk API, but different in Glulx. Calls such as glk_put_buffer() and glk_request_line_event() take character arrays; this is the address of a byte array containing character values, followed by an integer array length. The byte array itself has neither a length field or a terminator. In contrast, calls such as glk_put_string() and glk_fileref_create_by_name() take string arguments, which must be unencoded Glulx string objects. An unencoded Glulx string object is nearly a byte array, but not quite; it has an E0 byte at the beginning and a zero byte at the end. Similarly, calls such as glk_put_string_uni() take unencoded (E2) Unicode objects.]

[Previous versions of this spec said that string arguments could be unencoded or encoded string objects. This use of encoded strings has never been supported, however, and it is withdrawn from the spec.]

[The convention that "address" -1 refers to the stack is a feature of the Glk invocation mechanism; it applies only to Glk arguments. It is not part of the general Glulx definition. When instruction operands are being evaluated, -1 has no special meaning. This includes the L1, L2, and S1 arguments of the glk opcode.]

2.19: Assembly Language

The format used by Inform is acceptable for now:

@opcode [ op op op ... ] ;

Where each "op" is a constant, the name of a local variable, the name of a global variable, or "sp" (for stack push/pop modes).

[It would be convenient to have a one-line form for the opcodes that pass arguments on the stack (call and glk).]

To make life a little easier for cross-platform I6 code, Inform accepts the macro "@push val" for "@copy val sp", and "@pull val" for "@copy sp val". Supporting these forms is recommended.

You can synthesize opcodes that the compiler does not know about:

@"FlagsCount:Code" [ op op op ... ] ;

The optional Flags can include "S" if the last operand is a store; "SS" if the last two operands are stores; "B" for branch format; "R" if execution never continues after the opcode. The Count is the number of arguments (0 to 9). The Code is a decimal integer representing the opcode number. So these two lines generate the same code:

@add x 1 y;
@"S3:16" x 1 y;

...because the @add opcode has number 16 (decimal), and has format "@add L1 L2 S1".


Up to top Previous chapter Next chapter