[CAssembly::] C Assembly. The problem of assembly language. @h General implementation. This section does just one thing: compiles invocations of assembly-language opcodes. = void CAssembly::initialise(code_generator *gtr) { METHOD_ADD(gtr, INVOKE_OPCODE_MTID, CAssembly::invoke_opcode); METHOD_ADD(gtr, ASSEMBLY_MARKER_MTID, CAssembly::assembly_marker); } typedef struct C_generation_assembly_data { struct dictionary *opcodes_used; } C_generation_assembly_data; void CAssembly::initialise_data(code_generation *gen) { C_GEN_DATA(asmdata.opcodes_used) = NULL; } void CAssembly::begin(code_generation *gen) { CAssembly::initialise_data(gen); } void CAssembly::end(code_generation *gen) { } @ Inter is for the most part fully specified and cross-platform, but assembly language is the big hole in that. It is legal for Inter code to contain almost anything which purports to be assembly language. For example, the following code will successfully build as part of an Inter kit: = (text as Inform 6) [ Peculiar x; @bandersnatch x; ]; = Kit code, and also material included in |(-| and |-)| brackets in I7 source text, can claim to use assembly language opcodes with any names it likes. No checking is done that these are "real" opcodes. (Spoilers: |@bandersnatch| is not.) The point of this is that different final targets support different sets of assembly language. This was always true for Inform 6 code (after around 2000, anyway), because the Z and Glulx virtual machines had different assembly languages: |@split_window| exists for Z but not Glulx, |@atan| exists for Glulx but not Z, for example. So each different final generator needs to make its own decision about what assembly language opcodes to provide, and what they will do. In theory, we could make an entirely new assembly language for C. But in practice that would just make the standard Inform kits, such as BasicInformKit, impossible to support on C, because those kits make quite heavy use of opcodes from Z/Glulx. We will instead: (1) Emulate exactly that subset of the Glulx assembly language which is used by the standard Inform kits, and (2) Allow any other opcodes to be externally defined by the user. In this way, we obtain both compatibility with the Inform kits, enabling us to compile works of IF to C, and also extensibility. @ Each different opcode we see will be matched up to a //C_supported_opcode// giving it some metadata: we will gather these into a dictionary so that names of opcodes can quickly be resolved to their metadata structures. That dictionary will begin with (1) about 60 standard supported opcodes, but then may pick up (2) a few others such as |@bandersnatch|, if kits do something non-standard. So now we define some very minimal metadata on our opcodes. Each opcode will, when used, be followed by a number of operands, which we number from 1: = (text as Inform 6) @fmod a b rem quot; ! 1 2 3 4 = This opcode, which performs floating-point division with remainder, reads in operands 1 and 2, and writes results out to operands 3 and 4. In the following, |store_this_operand[3]| and |store_this_operand[4]| would be |TRUE|, while |store_this_operand[1]| and |store_this_operand[2]| would be |FALSE|. (In fact, this is an outlier, because it is the only opcode we support which has more than one store operand. But in principle we could have many.) Glulx assembly language also allows variable numbers of arguments to some opcodes, or "varargs". For example: = (text as Inform 6) @glk 4 _vararg_count ret; ! 1 2 3 = Here operand 3 is a store, and operands 1 and 2 are read in. But operand 2 is special in that it is a count of additional operands which are found on the stack rather than in the body of the instruction. For example, = (text as Inform 6) @glk 4 6 ret; = would provide |@glk| with seven operands to read in: the one in the instruction itself, |4|, and then the top 6 items on the stack. Because of this, an operand holding a variable-argument count is special. There can be at most one for any opcode; |vararg_operand| is -1 if there isn't one, but for |@glk|, |vararg_operand| would be 2. = typedef struct C_supported_opcode { struct text_stream *name; /* including the opening |@| character */ int store_this_operand[MAX_OPERANDS_IN_INTER_ASSEMBLY]; int vararg_operand; /* position of |_vararg_count| operand, or -1 if none */ int speculative; /* i.e., not part of the standard supported set */ CLASS_DEFINITION } C_supported_opcode; @ On creation, a //C_supported_opcode// is automatically added to the dictionary: = C_supported_opcode *CAssembly::new_opcode(code_generation *gen, text_stream *name, int s1, int s2, int va) { C_supported_opcode *opc = CREATE(C_supported_opcode); opc->speculative = FALSE; opc->name = Str::duplicate(name); for (int i=0; istore_this_operand[i] = FALSE; if (s1 >= 1) opc->store_this_operand[s1] = TRUE; if (s2 >= 1) opc->store_this_operand[s2] = TRUE; opc->vararg_operand = va; Dictionaries::create(C_GEN_DATA(asmdata.opcodes_used), name); Dictionaries::write_value(C_GEN_DATA(asmdata.opcodes_used), name, opc); return opc; } @ When the generator encounters an opcode called |name| which seems to be used with |operand_count| operands, it calls the following function to find the corresponding metadata. Note that this always returns a valid //C_supported_opcode//, because even if a completely unexpected name is encountered, the above mechanism will just create a meaning for it. = C_supported_opcode *CAssembly::find_opcode(code_generation *gen, text_stream *name, int operand_count) { if (C_GEN_DATA(asmdata.opcodes_used) == NULL) { C_GEN_DATA(asmdata.opcodes_used) = Dictionaries::new(256, FALSE); @; } C_supported_opcode *opc; if (Dictionaries::find(C_GEN_DATA(asmdata.opcodes_used), name)) { opc = Dictionaries::read_value(C_GEN_DATA(asmdata.opcodes_used), name); } else { @; } return opc; } @ = CAssembly::new_opcode(gen, I"@acos", 2, -1, -1); CAssembly::new_opcode(gen, I"@add", 3, -1, -1); CAssembly::new_opcode(gen, I"@aload", 3, -1, -1); CAssembly::new_opcode(gen, I"@aloadb", 3, -1, -1); CAssembly::new_opcode(gen, I"@aloads", 3, -1, -1); CAssembly::new_opcode(gen, I"@asin", 2, -1, -1); CAssembly::new_opcode(gen, I"@atan", 2, -1, -1); CAssembly::new_opcode(gen, I"@binarysearch", 8, -1, -1); CAssembly::new_opcode(gen, I"@call", 3, -1, 2); CAssembly::new_opcode(gen, I"@ceil", 2, -1, -1); CAssembly::new_opcode(gen, I"@copy", 2, -1, -1); CAssembly::new_opcode(gen, I"@cos", 2, -1, -1); CAssembly::new_opcode(gen, I"@div", 3, -1, -1); CAssembly::new_opcode(gen, I"@exp", 2, -1, -1); CAssembly::new_opcode(gen, I"@fadd", 3, -1, -1); CAssembly::new_opcode(gen, I"@fdiv", 3, -1, -1); CAssembly::new_opcode(gen, I"@floor", 2, -1, -1); CAssembly::new_opcode(gen, I"@fmod", 3, 4, -1); CAssembly::new_opcode(gen, I"@fmul", 3, -1, -1); CAssembly::new_opcode(gen, I"@fsub", 3, -1, -1); CAssembly::new_opcode(gen, I"@ftonumn", 2, -1, -1); CAssembly::new_opcode(gen, I"@ftonumz", 2, -1, -1); CAssembly::new_opcode(gen, I"@gestalt", 3, -1, -1); CAssembly::new_opcode(gen, I"@glk", 3, -1, 2); CAssembly::new_opcode(gen, I"@hasundo", 1, -1, -1); CAssembly::new_opcode(gen, I"@jeq", -1, -1, -1); CAssembly::new_opcode(gen, I"@jfeq", -1, -1, -1); CAssembly::new_opcode(gen, I"@jfge", -1, -1, -1); CAssembly::new_opcode(gen, I"@jflt", -1, -1, -1); CAssembly::new_opcode(gen, I"@jisinf", -1, -1, -1); CAssembly::new_opcode(gen, I"@jisnan", -1, -1, -1); CAssembly::new_opcode(gen, I"@jleu", -1, -1, -1); CAssembly::new_opcode(gen, I"@jnz", -1, -1, -1); CAssembly::new_opcode(gen, I"@jz", -1, -1, -1); CAssembly::new_opcode(gen, I"@log", 2, -1, -1); CAssembly::new_opcode(gen, I"@malloc", -1, -1, -1); CAssembly::new_opcode(gen, I"@mcopy", -1, -1, -1); CAssembly::new_opcode(gen, I"@mzero", -1, -1, -1); CAssembly::new_opcode(gen, I"@mfree", -1, -1, -1); CAssembly::new_opcode(gen, I"@mod", 3, -1, -1); CAssembly::new_opcode(gen, I"@mul", 3, -1, -1); CAssembly::new_opcode(gen, I"@neg", 2, -1, -1); CAssembly::new_opcode(gen, I"@nop", -1, -1, -1); CAssembly::new_opcode(gen, I"@numtof", 2, -1, -1); CAssembly::new_opcode(gen, I"@pow", 3, -1, -1); CAssembly::new_opcode(gen, I"@quit", -1, -1, -1); CAssembly::new_opcode(gen, I"@random", 2, -1, -1); CAssembly::new_opcode(gen, I"@restart", -1, -1, -1); CAssembly::new_opcode(gen, I"@restore", 2, -1, -1); CAssembly::new_opcode(gen, I"@restoreundo", 1, -1, -1); CAssembly::new_opcode(gen, I"@return", -1, -1, -1); CAssembly::new_opcode(gen, I"@save", 2, -1, -1); CAssembly::new_opcode(gen, I"@saveundo", 1, -1, -1); CAssembly::new_opcode(gen, I"@setiosys", -1, -1, -1); CAssembly::new_opcode(gen, I"@setrandom", -1, -1, -1); CAssembly::new_opcode(gen, I"@shiftl", 3, -1, -1); CAssembly::new_opcode(gen, I"@sin", 2, -1, -1); CAssembly::new_opcode(gen, I"@sqrt", 2, -1, -1); CAssembly::new_opcode(gen, I"@streamchar", -1, -1, -1); CAssembly::new_opcode(gen, I"@streamnum", -1, -1, -1); CAssembly::new_opcode(gen, I"@streamunichar", -1, -1, -1); CAssembly::new_opcode(gen, I"@sub", 3, -1, -1); CAssembly::new_opcode(gen, I"@tan", 2, -1, -1); CAssembly::new_opcode(gen, I"@ushiftr", 3, -1, -1); CAssembly::new_opcode(gen, I"@verify", 1, -1, -1); @ Speculative opcodes cannot store and cannot have varargs. Also, since they are not part of our supported set, there's no code here to implement them. Instead we predeclare a function and simply assume that the user will have written this function somewhere and linked it to us. For example, we might predeclare this: = (text as C) void i7_opcode_bandersnatch(i7process_t *proc, i7word_t v1); = @ = opc = CAssembly::new_opcode(gen, name, -1, -1, -1); opc->speculative = TRUE; segmentation_pos saved = CodeGen::select(gen, c_predeclarations_I7CGS); text_stream *OUT = CodeGen::current(gen); WRITE("void "); CNamespace::mangle_opcode(OUT, name); WRITE("(i7process_t *proc"); for (int i=1; i<=operand_count; i++) WRITE(", i7word_t v%d", i); WRITE(");\n"); CodeGen::deselect(gen, saved); @ We finally have enough infrastructure to invoke a general assembly-language instruction found in our Inter. = void CAssembly::invoke_opcode(code_generator *gtr, code_generation *gen, text_stream *opcode, int operand_count, inter_tree_node **operands, inter_tree_node *label, int label_sense) { C_supported_opcode *opc = CAssembly::find_opcode(gen, opcode, operand_count); text_stream *OUT = CodeGen::current(gen); if (label_sense != NOT_APPLICABLE) @; int push_store[MAX_OPERANDS_IN_INTER_ASSEMBLY]; for (int i=0; i; if (label_sense != NOT_APPLICABLE) @; @; WRITE(";\n"); } @ = WRITE("if ("); @ = if (label_sense == FALSE) WRITE(" == FALSE"); WRITE(") goto "); if (label == NULL) internal_error("no branch label"); Vanilla::node(gen, label); @ Each instruction becomes a function call to the function implementing the opcode in question, except that |@return| becomes the C statement |return|. If the opcode has N operands then the function has N+1 arguments, since the first is always the process pointer. It may seem to compile slow code if we turn instructions into function calls, but (a) assembly is not used very much in Inter code, and then not for time-sensitive operations, and (b) the C compiler receiving the code we generate will almost certainly perform inline optimisation to remove most of these calls anyway. @ = if (Str::eq(opcode, I"@return")) { WRITE("return ("); } else { CNamespace::mangle_opcode(OUT, opcode); WRITE("(proc"); if (operand_count > 0) WRITE(", "); } for (int operand = 1; operand <= operand_count; operand++) { if (operand > 1) WRITE(", "); TEMPORARY_TEXT(O) CodeGen::select_temporary(gen, O); Vanilla::node(gen, operands[operand-1]); CodeGen::deselect_temporary(gen); if (opc->store_this_operand[operand]) @ else @; DISCARD_TEXT(O) } WRITE(")"); @ The argument for a regular operand will have type |i7word_t|, so we have to compile something of that type here. The special operand notation |sp| is a pseudo-variable meaning "the top of the stack", so if we see that then we compile that to a pull: note that |i7_pull| returns an |i7word_t|. @ = if (Str::eq(O, I"sp")) { WRITE("i7_pull(proc)"); } else { WRITE("%S", O); } @ The argument for a store operand will have type |i7word_t *|, so now we have to make a pointer. Again, |sp| is a pseudo-variable meaning "the top of the stack", but this time we have to push, not pull, and that's something we can't do until the function has returned -- the function will create the value we need to push. We get around this by compiling a pointer to some temporary memory. Finally, assembly also allows |0| as a special value for a store operand, and this means "throw the value away". We don't want to incur a C compiler warning by attempting to write |0| in a pointer context, so we pass it as |NULL| instead. @ = if (Str::eq(O, I"sp")) { WRITE("&(proc->state.tmp[%d])", operand); push_store[operand] = TRUE; } else if (Str::eq(O, I"0")) { WRITE("NULL"); } else { WRITE("&%S", O); } @ That may leave a few stored results stranded in temporary workspace, so: @ = for (int operand = 1; operand <= operand_count; operand++) if (push_store[operand]) WRITE("; i7_push(proc, proc->state.tmp[%d])", operand); @ And where does the special operand |sp| come from? From here: = void CAssembly::assembly_marker(code_generator *gtr, code_generation *gen, inter_ti marker) { text_stream *OUT = CodeGen::current(gen); switch (marker) { case ASM_SP_ASMMARKER: WRITE("sp"); break; default: WRITE_TO(STDERR, "Unsupported assembly marker is '%d'\n", marker); internal_error("unsupported assembly marker in C"); } } @h call. That does everything except to implement the standard set of opcodes, which must be done with about 60 functions in the C library. This is not the place to specify what Glulx opcodes do. See Andrew Plotkin's //documentation on the Glulx virtual machine -> https://www.eblong.com/zarf/glulx//. Most of the opcodes we support are defined below, but see also //C Input-Output Model// for |@glk|, and see //C Arithmetic// for the plethora of mathematical operations such as |@fmul|. To begin, here is a |@call|, which performs a function call to a perhaps computed address: = (text to inform7_clib.h) void i7_opcode_call(i7process_t *proc, i7word_t fn_ref, i7word_t varargc, i7word_t *z); = = (text to inform7_clib.c) void i7_opcode_call(i7process_t *proc, i7word_t fn_ref, i7word_t varargc, i7word_t *z) { i7word_t args[10]; for (int i=0; i<10; i++) args[i] = 0; for (int i=0; i= 0) && (y < 32)) value = (x << y); if (z) *z = value; } void i7_opcode_ushiftr(i7process_t *proc, i7word_t x, i7word_t y, i7word_t *z) { i7word_t value = 0; if ((y >= 0) && (y < 32)) value = (x >> y); if (z) *z = value; } = @h jeq, jleu, jnz, jz. These are branch opcodes and return an |int|. The implementation of |@jleu| here is modelled on the one from |dumb-glulxe|. Writing code like |*((i7word_t *) &ux) = x| really makes you proud to have chosen C as your programming language, but it works. = (text to inform7_clib.h) int i7_opcode_jeq(i7process_t *proc, i7word_t x, i7word_t y); int i7_opcode_jleu(i7process_t *proc, i7word_t x, i7word_t y); int i7_opcode_jnz(i7process_t *proc, i7word_t x); int i7_opcode_jz(i7process_t *proc, i7word_t x); = = (text to inform7_clib.c) int i7_opcode_jeq(i7process_t *proc, i7word_t x, i7word_t y) { if (x == y) return 1; return 0; } int i7_opcode_jleu(i7process_t *proc, i7word_t x, i7word_t y) { unsigned_i7word_t ux, uy; *((i7word_t *) &ux) = x; *((i7word_t *) &uy) = y; if (ux <= uy) return 1; return 0; } int i7_opcode_jnz(i7process_t *proc, i7word_t x) { if (x != 0) return 1; return 0; } int i7_opcode_jz(i7process_t *proc, i7word_t x) { if (x == 0) return 1; return 0; } = @h nop, quit, verify. There is no real meaning for |@verify| in this situation: it's supposed to check the checksum for the contents of a virtual machine, to protect against the (entirely likely) scenario of a floppy disk sector going bad in 1983. So we unconditionally store the "okay" result. = (text to inform7_clib.h) void i7_opcode_nop(i7process_t *proc); void i7_opcode_quit(i7process_t *proc); void i7_opcode_verify(i7process_t *proc, i7word_t *z); = = (text to inform7_clib.c) void i7_opcode_nop(i7process_t *proc) { } void i7_opcode_quit(i7process_t *proc) { i7_fatal_exit(proc); } void i7_opcode_verify(i7process_t *proc, i7word_t *z) { if (z) *z = 0; } = @h restoreundo, saveundo, hasundo, discardundo. This all works, but we do something pretty inelegant to support |@restoreundo|: we insert a call to a (presumably kit-based) function called |DealWithUndo|, provided this exists. This is done because we are unable safely to follow the proper Glulx specification. In principle, after a |@restoreundo| succeeds, execution immediately continues from the position in the program where the |@saveundo| occurred. For a while the implementation here imitated this by using |longjmp| and |setjmp|, but it all proved very fragile because of the difficulty of storing |setjmp| positions safely in memory. Correspondingly, our implementation of |@saveundo| always stores the result value 0. The result value 1 would indicate that execution had switched there from a successful |@restoreundo|: but, as noted, that never happens. = (text to inform7_clib.h) void i7_opcode_restoreundo(i7process_t *proc, i7word_t *x); void i7_opcode_saveundo(i7process_t *proc, i7word_t *x); void i7_opcode_hasundo(i7process_t *proc, i7word_t *x); void i7_opcode_discardundo(i7process_t *proc); = = (text to inform7_clib.c) #ifdef i7_mgl_DealWithUndo i7word_t i7_fn_DealWithUndo(i7process_t *proc); #endif void i7_opcode_restoreundo(i7process_t *proc, i7word_t *x) { if (i7_has_snapshot(proc)) { i7_restore_snapshot(proc); if (x) *x = 0; #ifdef i7_mgl_DealWithUndo i7_fn_DealWithUndo(proc); #endif } else { if (x) *x = 1; } } void i7_opcode_saveundo(i7process_t *proc, i7word_t *x) { i7_save_snapshot(proc); if (x) *x = 0; } void i7_opcode_hasundo(i7process_t *proc, i7word_t *x) { i7word_t rv = 0; if (i7_has_snapshot(proc)) rv = 1; if (x) *x = rv; } void i7_opcode_discardundo(i7process_t *proc) { i7_destroy_latest_snapshot(proc); } = @h restart, restore, save. For the moment, at least, we intentionally do not implement these. It seems likely that anyone using C to run interactive fiction is doing so in a wider framework where saved states will work differently from the traditional model of asking the user for a filename and then saving data out to a binary file of that name in the current working directory. Better to do nothing here, and let users handle this themselves. Similar considerations apply to |@restart|. The intention of this opcode is essentially to reboot the virtual machine and start over: here, though, we have a real machine. It's easy enough to reinitialise the process state, but not so simple to restart execution as if from a clean process start. = (text to inform7_clib.h) void i7_opcode_restart(i7process_t *proc); void i7_opcode_restore(i7process_t *proc, i7word_t x, i7word_t *y); void i7_opcode_save(i7process_t *proc, i7word_t x, i7word_t *y); = = (text to inform7_clib.c) void i7_opcode_restart(i7process_t *proc) { printf("(RESTART is not implemented on this C program.)\n"); } void i7_opcode_restore(i7process_t *proc, i7word_t x, i7word_t *y) { printf("(RESTORE is not implemented on this C program.)\n"); } void i7_opcode_save(i7process_t *proc, i7word_t x, i7word_t *y) { printf("(SAVE is not implemented on this C program.)\n"); } = @h streamchar, streamnum, streamunichar. = (text to inform7_clib.h) void i7_opcode_streamnum(i7process_t *proc, i7word_t x); void i7_opcode_streamchar(i7process_t *proc, i7word_t x); void i7_opcode_streamunichar(i7process_t *proc, i7word_t x); = = (text to inform7_clib.c) void i7_opcode_streamnum(i7process_t *proc, i7word_t x) { i7_print_decimal(proc, x); } void i7_opcode_streamchar(i7process_t *proc, i7word_t x) { i7_print_char(proc, x); } void i7_opcode_streamunichar(i7process_t *proc, i7word_t x) { i7_print_char(proc, x); } @h binarysearch. This is a Grand Imperial Hotel among Glulx opcodes, with 8 operands, only the last of which is a store. It performs a binary search on a block of structures known to be sorted already. It has a nice general-purpose look but was devised so that command verbs could be looked up quickly in dictionary tables when interactive fiction is being played: that's the only use which the standard Inform kits make of it. The elegant implementation here comes from Andrew Plotkin's reference code for |glulxe|, a Glulx interpreter. |options| is a bitmap of the bits defined below. In the only use the standard Inform kits make of this opcode, |options| will be just |serop_KeyIndirect|, but |keysize| will be more than 4, so that the elaborate speed optimisation for keys of size 1, 2 and 4, and thus |keybuf|, are never used. But we may as well have the full functionality here. = (text to inform7_clib.h) #define serop_KeyIndirect 1 #define serop_ZeroKeyTerminates 2 #define serop_ReturnIndex 4 void i7_opcode_binarysearch(i7process_t *proc, i7word_t key, i7word_t keysize, i7word_t start, i7word_t structsize, i7word_t numstructs, i7word_t keyoffset, i7word_t options, i7word_t *s1); = = (text to inform7_clib.c) void i7_opcode_binarysearch(i7process_t *proc, i7word_t key, i7word_t keysize, i7word_t start, i7word_t structsize, i7word_t numstructs, i7word_t keyoffset, i7word_t options, i7word_t *s1) { if (s1 == NULL) return; /* Do not spend any time if the result is to be ignored */ /* If the key size is 4 or fewer, copy it directly into the keybuf array */ unsigned char keybuf[4]; if (options & serop_KeyIndirect) { if (keysize <= 4) for (int ix=0; ix byte2) cmp = 1; } } else { for (int ix=0; (!cmp) && ix byte2) cmp = 1; } } if (cmp == 0) { /* Success! */ if (options & serop_ReturnIndex) *s1 = val; else *s1 = addr; return; } if (cmp < 0) bot = val+1; /* Chop search range to the second half */ else top = val; /* Chop search range to the first half */ } /* Failure! */ if (options & serop_ReturnIndex) *s1 = -1; else *s1 = 0; } = @h mcopy, mzero, malloc, mfree. A Glulx assembly opcode is provided for fast memory copies, which we must implement. We're choosing not to implement the Glulx |@malloc| or |@mfree| opcodes for now, but that will surely need to change in due course. (When that does change, we will need also to change |@gestalt|.) = (text to inform7_clib.h) void i7_opcode_mcopy(i7process_t *proc, i7word_t x, i7word_t y, i7word_t z); void i7_opcode_mzero(i7process_t *proc, i7word_t x, i7word_t y); void i7_opcode_malloc(i7process_t *proc, i7word_t x, i7word_t y); void i7_opcode_mfree(i7process_t *proc, i7word_t x); = = (text to inform7_clib.c) void i7_opcode_mcopy(i7process_t *proc, i7word_t x, i7word_t y, i7word_t z) { if (z < y) for (i7word_t i=0; i=0; i--) i7_write_byte(proc, z+i, i7_read_byte(proc, y+i)); } void i7_opcode_mzero(i7process_t *proc, i7word_t x, i7word_t y) { for (i7word_t i=0; istate.seed.interval != 0) { rawvalue = proc->state.seed.counter++; if (proc->state.seed.counter == proc->state.seed.interval) proc->state.seed.counter = 0; } else { proc->state.seed.A = 0x015a4e35L * proc->state.seed.A + 1; rawvalue = (proc->state.seed.A >> 16) & 0x7fff; } uint32_t value; if (x == 0) value = rawvalue; else if (x >= 1) value = rawvalue % (uint32_t) (x); else value = -(rawvalue % (uint32_t) (-x)); *y = (i7word_t) value; } void i7_opcode_setrandom(i7process_t *proc, i7word_t s) { if (s == 0) { proc->state.seed.A = (uint32_t) time(NULL); proc->state.seed.interval = 0; } else if (s < 1000) { proc->state.seed.interval = s; proc->state.seed.counter = 0; } else { proc->state.seed.A = s; proc->state.seed.interval = 0; } } i7word_t i7_random(i7process_t *proc, i7word_t x) { i7word_t r; i7_opcode_random(proc, x, &r); return r+1; } = @h setiosys. This opcode in principle allows a story file to select the input-output system it will use. But the Inform kits only use system 2, called Glk, and this is the only system we support, so we will simply ignore this. = (text to inform7_clib.h) void i7_opcode_setiosys(i7process_t *proc, i7word_t x, i7word_t y); = = (text to inform7_clib.c) void i7_opcode_setiosys(i7process_t *proc, i7word_t x, i7word_t y) { } = @h gestalt. This opcode allows a story file to ask the Glulx interpreter running it whether or not the interpreter can perform certain tasks. = (text to inform7_clib.h) void i7_opcode_gestalt(i7process_t *proc, i7word_t x, i7word_t y, i7word_t *z); = = (text to inform7_clib.c) void i7_opcode_gestalt(i7process_t *proc, i7word_t x, i7word_t y, i7word_t *z) { int r = 0; switch (x) { case 0: r = 0x00030103; break; /* Say that the Glulx version is v3.1.3 */ case 1: r = 1; break; /* Say that the interpreter version is 1 */ case 2: r = 0; break; /* We do not (yet) support @setmemsize */ case 3: r = 1; break; /* We do support UNDO */ case 4: if (y == 2) r = 1; /* We do support Glk */ else r = 0; /* But not any other I/O system */ break; case 5: r = 1; break; /* We do support Unicode operations */ case 6: r = 1; break; /* We do support @mzero and @mcopy */ case 7: r = 0; break; /* We do not (yet) support @malloc or @mfree */ case 8: r = 0; break; /* Since we do not support @malloc */ case 9: r = 0; break; /* We do not support @accelfunc pr @accelparam */ case 10: r = 0; break; /* And therefore provide none of their accelerants */ case 11: r = 1; break; /* We do support floating-point maths operations */ case 12: r = 1; break; /* We do support @hasundo and @discardundo */ } if (z) *z = r; } =