From c149ec2500d0ad203bc511289b97d11dfc72721e Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Sat, 5 Sep 2026 14:59:42 -0500 Subject: [PATCH 1/7] update documentation --- docs/aam-specification-1.1.adoc | 3463 +++++++++++++++++++++++++++++++ docs/current.adoc | 2 +- src/6502/Makefile | 2 +- src/Makefile | 4 +- src/js/engine.js | 2 +- src/js/nodefrontend.js | 2 +- src/js/webfrontend.html | 2 +- 7 files changed, 3470 insertions(+), 7 deletions(-) create mode 100644 docs/aam-specification-1.1.adoc diff --git a/docs/aam-specification-1.1.adoc b/docs/aam-specification-1.1.adoc new file mode 100644 index 0000000..f5c12b4 --- /dev/null +++ b/docs/aam-specification-1.1.adoc @@ -0,0 +1,3463 @@ += Å-machine 1.0 Specification + +This file contains a detailed description of the Å-machine (Aa-machine) +version 1.0, including the two file formats (story and saved game) and the +bytecode semantics. Be warned that the description is technically +comprehensive and rather condensed. + +Note that 1.0 is not any more official or stable than its predecessor, 0.5. +The major version change marks where the community forked the project and +took over development and maintenance. + +:toc: + +== Changes in v1.1 + +<> behavior changed to match other arithmetic instructions + +== Changes in v1.0 + +New opcodes: + +* <> +* <> +* <> + +Changed opcodes: + +* <> + +New <>: + +* Div width +* Div height +* Interpreter supports styling +* Interpreter supports color +* Interpreter supports text alignment +* Currently transcripting + +Removed some previously-deprecated opcodes: + +* <> 0 +* <> + +Expanded key-value options for resources to include ARIA roles + +Specified behavior of unrecognized <> options + +Specified format of resource option strings + +Rewrote some portions of the spec for clarity + +These are still early days, and everything is subject to change. When only the +minor version number is incremented, efforts are made to keep all changes +backwards-compatible. + +== Conventions + +The notation `a..b` means the range of integers from a to b, inclusive. + +The notation `a-b` means the range of characters from a to b, inclusive. + +`BYTE` always means eight bits, `SHORT` +always means 16 bits (two bytes), and `LONG` always means 32 bits (four +bytes). `WORD` means either `SHORT` or `LONG`, depending on the word size +specified in <>. + +The notation: + +* Number of whatsits +** Whatsit data + +Means that the "whatsit data" appears "number of whatsits" times in sequence. + +Throughout this document, numbers used to count things (like "16 bits" or +"``WORD[64]``") are decimal (base 10), while numbers used as values (like "80" or +"7fff") are hexadecimal (base 16). In ambiguous cases, hexadecimal numbers are +marked with a `$` symbol. In pseudocode, hexadecimal numbers start with `0x`; +all other numbers are decimal. + +== Runtime data + +=== Words + +Most data is organized into words. A word is currently always 16 bits. +When words are stored in a save file, the byte order is big-endian. + +The internal format of a 16-bit word could be: + +*A raw 16-bit value*, such as an index into one of the heap +areas. + +*16 independent flags*, numbered 0..f starting with the most +significant bit. + +The unconventional bit order works together with +big-endian byte ordering to allow interpreters to work +with word-addressing or byte-addressing when dealing +with flag numbers. + +[[literal,literal]] +*A literal value*, as follows: + +* ``0001..1fff`` Object +* ``2000..3dff`` Dict word (index into dictionary) +* ``3e00..3eff`` Character +* ``3f00      `` `[]` (empty list) +* ``3f01..3fff`` reserved +* ``4000..7fff`` Integer in the range 0..16383 + +[[live]] +*A live value (tagged reference)*, as follows: + +* ``0000      `` undefined +* ``0001..7fff`` <> +* ``8000..9fff`` Indirect reference (word index into heap) +* ``a000..bfff`` reserved +* ``c000..dffe`` Pair (word index into heap) +* ``e000..fffe`` Extended dict word (word index into heap) + +Live values only exist in registers or on the heap, and +can be invalidated when the heap shrinks. + +In this document, tagged references are described as if +they had two independent fields, "tag" and "value". But +the tag is actually determined from a variable number +of upper bits of the word, as follows (v is the value): + +* `00000000 00000000` null_tag +* `000vvvvv vvvvvvvv` object (v > 0) +* `001vvvvv vvvvvvvv` word (v < 1e00) +* `00111110 vvvvvvvv` char +* `00111111 00000000` empty_tag +* `01vvvvvv vvvvvvvv` number +* `100vvvvv vvvvvvvv` reference +* `101vvvvv vvvvvvvv` (reserved) +* `110vvvvv vvvvvvvv` pair +* `111vvvvv vvvvvvvv` extdict + +The tag `001vvvvv vvvvvvvv` may also be referred to as +a "dict" to avoid ambiguity with the other meaning of +"word" (any two-byte value). + +A pair is a pointer to a two-word cell on the heap. The +first word of that cell is the head of the list (can be +any value). The second word of that cell is the rest of +the list (either a pair or `[]`). + +An extended dict (extdict) is a pointer to a two-word +cell on the heap. If the first word of that cell is a +pair, it represents an unrecognized dict (not found in +the dictionary); the first word is a list of characters +and the second word is `[]`. Otherwise, it represents a +dict with an optional ending; the first word is a dict +(the essential part) and the second word is a list of +characters (the optional part). + +*A stored value*, as follows: + +* ``0000      `` Unset (Null) +* ``0001..7fff`` <> +* ``8000..fffe`` Word index into long-term storage area + +Stored values exist in global and object variables (the +lower parts of RAM) and may have to be updated when +long-term storage is rearranged. See the pseudocode for +<>. + +Part of a *serialized data stream*: + +* ``0000      `` End of stream marker +* ``0001..7fff`` <> +* ``8000      `` Unbound variable +* ``8001..80ff`` reserved +* ``8100      `` Extdict (two words) follows +* ``8101..bfff`` reserved +* ``c000..dfff`` Proper list with (x & 1fff) elements follows +* ``e000..fffe`` Improper list with (x & 1fff) elements follows + +Values are serialized into this format if they need to be +stored on the aux heap or in long-term storage. See the +pseudocode for <> and +<>. + +The special value `3f3f` marks unused words in the main heap, +aux, and random access areas. This is handy for measuring peak +memory usage. + +=== Text + +The game-specific *character set* is a single-byte encoding. + +* ``00-1f`` are reserved. Some represent special keypresses: +** `08` backspace/delete +** `0d` return +** `10` up +** `11` down +** `12` left +** `13` right +* ``20-7e`` correspond to the ASCII characters +* ``7f   `` is reserved +* ``80-ff`` are game-specific + +Characters 80-ff are mapped to Unicode codepoints by a game-specific +*character mapping table*. + +Characters from the *character set* may appear in input, dictionary +words, and output. As part of input processing, characters are +converted to lowercase. The character mapping table contains +information about case conversion for letters other than A-Z. + +Strings are sequences of printable characters (20-7e, 80-ff) and +dictionary words, packed into bit streams according to a +Huffman-inspired encoding. + +* Characters in the ranges 20-7e and 80-9f (ASCII and 32 other + codepoints as per the character mapping table) have a compact, + variable-length encoding. + +* Characters in the range 9f-ff and dictionary words are + represented by a less efficient (but simpler) encoding. + +There is a game-specific bitstream decoding tree with up to 128 nodes +(see <>). + +=== Game state + +==== Unsaved data + +* Most output state (style, screen contents, ...) +* Runtime flags (transcript, trace, uppercase) +* TMP (global register used during some recursive operations) + +With the exception of the random access area (see below), the following +data is not directly addressable by runtime code, so interpreters are +free to work with a different internal representation at runtime. + +==== Special-purpose registers + +**Byte pointers into <> chunk:** + +* `LONG`: `INST`, Instruction pointer +* `LONG`: `CONT`, Continuation pointer + +**Word pointers into main heap:** + +* `WORD`: `TOP`, top of main heap (grows upwards) +* `WORD`: `ENV`, env frame pointer (grows downwards) +* `WORD`: `CHO`, choice frame pointer (grows downwards) +* `WORD`: `SIM`, simple cut pointer or null + +**Word pointers into aux heap:** + +* `WORD`: `AUX`, aux stack pointer (grows upwards) +* `WORD`: `TRL`, trail stack pointer (grows downwards) +* `WORD`: `STA`, stoppable aux pointer +* `WORD`: `STC`, stoppable choice pointer + +**Other:** + +* `BYTE`: `CWL`, collect-words nesting level +* `BYTE`: `SPC`, whitespace level + +Values for SPC: + +* `0` auto +* `1` nospace +* `2` nbsp +* `3` pendingspace +* `4` space +* `5` line +* `6` par + +==== General-purpose registers + +* `WORD[64]`: R00-R3f, general registers (args, temps, constants, globals) + +R3f is also known as `IDX` and is treated specially by certain instructions. + +This is how dialogc uses these 64 registers: + +* ``R00..R0c`` Arguments (with optional just-pointer) +* ``R0d..R3c`` Temporary values +* ``R3d     `` Quick temporary register (always available) +* ``R3e     `` Constant `[]` (3f00) +* ``R3f `` Only used as `IDX` + +Since registers take only one byte to encode and a large +immediate value takes two, storing frequently-needed constants in +registers can lead to smaller code. + +With a few exceptions, all of the registers (special-purpose and +general-purpose) are initialized to zero when the virtual machine +starts or restarts. The exceptions are: + +* `INST` = 1 +* `ENV` = <>.heapsz +* `CHO` = <>.heapsz +* `TRL` = <>.auxsz +* `SIM` = ffff +* `SPC` = line + +In case of a runtime error, the machine is restarted with R00 +initialized to a tagged integer according to the following table: + +* `1`: Heap space exhausted +* `2`: Auxiliary heap space exhausted +* `3`: Type error: Expected object +* `4`: Type error: Expected bound value +* `6`: Long-term heap space exhausted +* `7`: Invalid output state. + +Error code 5 is not used by the Å-machine. + +==== Saved output state + +* `WORD`: Number of active divs +* `WORD[n]`: Active divs (class ids), outermost first + +==== Initialized registers + +The main chunk of saved data follows. The game state must conform to +this format during restart, save, and restore operations (including the +big-endian byte ordering). + +* `WORD`: `NOB`, Number of objects +* `WORD`: `LTB`, Long-term heap bottom (index into random access area) +* `WORD`: `LTT`, Long-term heap top (index into random access area) + +==== Random access area + +This memory is word-addressable, and it may contain pointers in +the form of word offsets into itself. + +* `WORD`: Word offset of global data +* `WORD[<>]`: Word offsets of per-object data +* `WORD[<>.ramsz - <> - 1]`: Data + +The word offset of a data field is given by the following +pseudo-code: + +---- +def field_addr(field, obj): + if(obj > NOB) { + runtime_error(3) + } else { + return ram[obj] + field + } +---- + +When reading, non-null non-objects should return zero: + +---- +def read_field(field, obj): + if(obj > NOB) { + return 0 + } else { + return ram[ram[obj] + field] + } +---- + +For both `field_addr` and `read_field`, "obj" may be 0 to access global +variables (not affiliated with any object). + +==== Heaps + +* `WORD[<>.auxsz]`: Auxiliary heap (aux, trail) +* `WORD[<>.heapsz]`: Main heap (heap, env, choice) + +Saved games contain a RAM area that matches the layout described here +(starting with 'Initialized registers'). The on-disk representation +is a run-length encoded xor difference with the contents of the INIT +chunk (padded by copies of the 'unused' word, `3f3f`). + +It is possible to track the game state in RAM in the same format at +runtime, although it may be more efficient to represent words in the +native byte order. + +=== Long-term storage + +The long-term storage area is part of the random access area, +specifically all the words from LTB (inclusive) to LTT +(exclusive). + +Data in the long-term storage area is organized into chunks: + +* `WORD`: "n", Size in words of this chunk +* `WORD`: Pointer to owner reference (index into random access area) +* `WORD[n - 2]`: Serialized data + +These chunks can move around at runtime (see pseudocode for <> below). When this happens, pointers are +updated in the random access area. + +=== Env frame format + +From lower to higher heap index: + +* `WORD`: Saved ENV +* `WORD`: Saved SIM +* `LONG`: Saved CONT +* `WORD[]`: Local variables + +=== Choice frame format + +From lower to higher heap index: + +* `WORD`: Saved ENV +* `WORD`: Saved SIM +* `LONG`: Saved CONT +* `LONG`: Failure handler +* `WORD`: Saved CHO +* `WORD`: Saved TOP +* `WORD`: Saved TRL +* `WORD[]`: Saved arguments + +=== Stop frame format + +From lower to higher aux index: + +* `WORD`: Saved STA +* `WORD`: Saved STC + +== Savefile + +Saved games are stored in an IFF format, with form identifier "AASV" and the +following chunks. HEAD must be the first chunk inside the form. + +* <>: Story file header +* <>: Compressed game state +* <>: Registers and active divs + +=== HEAD chunk + +The contents of this chunk must exactly match the <> chunk of the +story file. + +=== DATA chunk + +Take the game state (including the full extent of the heaps), then +exclusive-or with the contents of the <> chunk padded with the +'unused' word (`3f3f`). This stream of bytes is run-length encoded in +the following way: A non-null byte is represented by itself. A stretch +of N (1..256) null bytes is represented by a null byte, followed by a +byte of value N - 1. + +=== REGS chunk + +* `WORD[64]`: State of the <> +* `LONG[2]`: State of the <> +* `WORD[8]`: (continued) +* `BYTE[2]`: (continued) +* `WORD`: Number of active divs +* `WORD[n]`: Active divs (class ids), outermost first + +The game state is stored as part of the <> opcode, and the saved value of +<> is taken from one of the operands. +In addition, interpreters may save during any ongoing <> or +<> operation. In that case, the saved <> +must point to the <> or <> instruction, so that it will +execute again when the game is restored. + +Interpreters may create additional chunks describing the current output state. +These can be in any format, and should probably include the name and version of +the interpreter. Unrecognized chunks must be ignored on loading. + +== Story file + +The story file is in IFF format, with form identifier "AAVM" and the following +chunks. <> must be the first chunk inside the form. In the following table, +(*) marks the chunks that participate in the story checksum. + +* <>: File header +* <> (*): Bytecode instructions +* <> (*): Game dictionary +* <>: Embedded resource file (may appear more than once) +* <> (*): Initial game state, also needed for save/restore +* <> (*): Character set, bitstream decoder, word endings +* <> (*): Style sheet +* <> (*): Word-to-object maps +* <> (optional): Story metadata +* <> (optional): Internal object names +* <>: Table of resources +* <> (*): Compressed text + +An interpreter may regard the entire story file as being mapped into a single, +contiguous read-only address space, and add the corresponding file offset to +addresses within the chunks. Or it could regard each chunk as a separate +address space. + +[[HEAD]] +=== HEAD + +* `BYTE[2]`: "version", File format version (major, minor) +* `BYTE`: "wordsz", Word size in bytes (currently always 2) +* `BYTE`: "shift", Shift amount for short/long <> + +* `SHORT`: "release", Story release number +* `BYTE[6]`: "serial", Story serial number (ASCII) + +* `LONG`: "crc", Running CRC-32 of the contents of <>, <>, <>, <>, <>, <>, and <>, in that specific order + +* `WORD`: "heapsz", Size (in words) of heap/env/choice area +* `WORD`: "auxsz", Size (in words) of aux/trail area +* `WORD`: "ramsz", Size (in words) of random access area (including long-term heap) + +* `BYTE[46]`: "ifid" (optional), "UUID://...//" + null (ASCII) + +[[INIT]] +=== INIT + +This chunk corresponds to the format of the game state detailed above, +starting with the <>. Words +are stored in big-endian byte order. The chunk can be smaller than the +RAM area. Remaining words are assumed to be filled with the 'unused' value +3f3f. + +[[LANG]] +=== LANG + +* `BYTE[2]`: Offset in chunk of <> +* `BYTE[2]`: Offset in chunk of <> +* `BYTE[2]`: Offset in chunk of <> +* `BYTE[2]`: Offset in chunk of <> + +[tbl-decode] +==== Bitstream decoding table + +* `BYTE[][2]`: Up to 128 2-byte entries. Entry 0 is the root. + +Each entry: + +* `xx yy` If the bit was 0, perform x, otherwise perform y. + +An individual byte in the table is decoded as follows: + +* ``00..5e`` Character $20 + x +* ``60..7f`` Character $20 + x +* ``5f    `` Escape code (see below) +* ``80    `` End of string +* ``81..ff`` Go to table entry x - $80 + +Escape code handling for story file version 0.3 or earlier: + +* Read 7 bits, msb first, from the stream. Call this X. +* Produce character $80 + X. Note that X must be greater than or equal to $20. + +Escape code handling for story file version 0.4 or later: + +* Let B = `max(0, n_ext - 32)` where `n_ext` is the number of extended characters, as defined in the <> chunk. +* Let K = `ceil(log2(B + n_dict))` where `n_dict` is the number of dictionary words, as defined in the <> chunk. +* Read K bits, msb first, from the stream. Call this X. +* If X < B, produce character $a0 + X. +* Otherwise, produce a space character ($20) followed by dictionary word X - B. + +[tbl-extchar] +==== Extended character table + +* `BYTE`: Number of extended characters +** `BYTE`: Corresponding lowercase character (can be the same) +** `BYTE`: Corresponding uppercase character (can be the same) +** `BYTE[3]`: Unicode codepoint + +[tbl-ending] +==== Word endings decoder + +An array of instruction bytes: + +* `00`: Fail +* `01`: Check +* `xx yy`: Shift xx, jump yy + +*Fail*: The word is not valid. Shift all letters and create an +unrecognized word. + +*Check*: If the word is in the dictionary, succeed (with the +current ending). + +*Shift xx, jump yy*: If the last character is xx, then move it to +the ending and resume execution at byte offset yy in the +decoder. + +[tbl-stop] +==== Special characters table + +* `BYTE[]`: Stop characters, a null-terminated set of characters that will be treated as separate words when parsing player input. + +The following fields are present if the storyfile version is +0.4 or more. + +* `BYTE[]`: Null-terminated subset of stop characters that will inhibit whitespace before them, such as ".". [spec 0.4] +* `BYTE[]`: Null-terminated subset of stop characters that will inhibit whitespace after them, such as "(". [spec 0.4] + +[[DICT]] +=== DICT + +* `WORD`: Number of dictionary words +** `BYTE`: Length of word (2..ff) +** `BYTE[2]`: Start of characters (offset in chunk) + +Strings of characters (arrays of bytes in the game-specific character +set) referenced by the dictionary. No terminator. Can overlap. + +[[MAPS]] +=== MAPS + +* `BYTE[2]`: Number of word-to-object maps +** `BYTE[2]`: Offset in chunk of word-to-object map + +Each word-to-object map: + +* `BYTE[2]`: Number of entries (sorted by index value) +** `WORD`: Index value (dictionary word or character) +** `WORD`: Single object id + e000, offset in chunk of payload, or null (for wildcard words such as 'the') + +Payload data: + +* ``00       `` End +* ``01..df   `` Short object id (0001..00df) +* ``e0..ff xx`` Long object id (0001..1fff) + +[[LOOK]] +=== LOOK + +* `BYTE[2]`: Number of style classes +** `BYTE[2]`: Offset in chunk of style definition + +Each style definition consists of zero or more null-terminated ASCII +strings. Each is a CSS key-value pair without the final semicolon, e.g. +"width: 100%". The original case from the Dialog source code is +preserved, so interpreters must perform case-insensitive comparison. +Leading whitespace has been removed from the entries. A blank entry +(just the terminating null byte) marks the end. + +These pairs are typically CSS properties, but may include other +information of use to the interpreter, such as ARIA roles. Thus, +interpreters must ignore any unrecognized keys or values, rather than +raising an error. + +[[TAGS]] +=== TAGS (optional) + +* `WORD`: Number of named objects +** `BYTE[2]`: Offset in chunk of null-terminated object name + +Strings of characters for the object names. + +[[META]] +=== META (optional) + +* `BYTE`: Number of entries +** `BYTE`: Identifier +** `BYTE[]`: Null-terminated string (variable length) + +Identifiers: + +* `01`: Story title +* `02`: Story author +* `03`: Story noun +* `04`: Story blurb (double linefeed is a paragraph break) +* `05`: Release date in YYYY-MM-DD format +* `06`: Compiler version string + +Strings use the game-specific character set, except that $10 (normally +undefined for output) is a line feed. + +[[WRIT]] +=== WRIT + +A byte-addressable read-only chunk containing packed bitstreams. +Streams always begin on a byte boundary. Bits are packed into bytes +starting with the MSB. Extra null bytes can be inserted to make better +use of packed addressing modes. + +[[URLS]] +=== URLS + +* `BYTE[2]`: Number of resources +** `BYTE[2]`: Offset in chunk of descriptor + +Each descriptor is: + +* `BYTE[3]`: String pointer to alt-text (shift according to header) + +followed by a null-terminated URL, followed by a null-terminated option +string. This option string is currently taken as a comma-delimited list +of options, each of which may be a simple string, or a key and value +separated by a colon. + +It is up to the interpreter to decide how to embed (or link to) a +resource. The filename ending can be significant, in addition to the +file contents. + +The URL scheme "file" refers to data inside <> chunks, e.g. +"file:title.png". + +[[FILE]] +=== FILE + +A null-terminated filename, e.g. "title.png", immediately followed by +the file contents. + +[[CODE-chunk,CODE]] +=== CODE + +A byte-addressable read-only chunk, up to 8 MB in size, containing +bytecode instructions. + +Address 0 must always contain the <> instruction. Interpreters may +thus intercept branches to address 0 and trigger a failure condition +immediately, which may be faster. + +Address 1 is the program/error entry point. The error code is passed as +a tagged integer in register R00. For regular program entry, R00 is +null. + +An opcode is always one byte, and it determines the number and format +of subsequent operands. The MSB of the opcode often distinguishes +between two variants of the same operation. This gives interpreter +authors a choice between dispatching on the entire byte (256 table +entries), or dispatching on the lower seven bits (128 table entries), +and then branching on the MSB at a later stage while processing the +instruction. + +[[op]] +==== Operands + +Operand types (always big-endian byte ordering): + +[[ZERO,0]] +**0**: implicit constant zero, not encoded + +[[BYTE,BYTE]] +*BYTE*: small constant number, e.g. number of slots to allocate + +* `xxxxxxxx`: Number 00..ff + +[[VBYTE,VBYTE]] +*VBYTE*: small constant <> (null or object number 1..ff) + +* `xxxxxxxx`: Literal 0000..00ff + +[[WORD,WORD]] +*WORD*: constant number, e.g. line number for tracing + +* `xxxxxxxx xxxxxxxx`: Number 0000..ffff + +[[VWORD,VWORD]] +*VWORD*: constant <> + +* `0xxxxxxx xxxxxxxx`: Literal 0000..7fff + +[[RAW,RAW]] +*RAW*: unsigned 16-bit number + +* `0xxxxxxx xxxxxxxx`: Number 0000..7fff +* `10xxxxxx`: Value of register 00..3f +* `11xxxxxx`: Value of env slot 00..3f + +[[VALUE,VALUE]] +*VALUE*: <> (tagged reference) + +* `0xxxxxxx xxxxxxxx`: Literal 0000..7fff +* `10xxxxxx`: Value of register 00..3f +* `11xxxxxx`: Value of env slot 00..3f + +[[DEST,DEST]] +*DEST*: destination for the result + +* `00xxxxxx`: Store into register 00..3f +* `01xxxxxx`: Store into env slot 00..3f +* `10xxxxxx`: Unify with register 00..3f +* `11xxxxxx`: Unify with env slot 00..3f + +[[INDEX,INDEX]] +*INDEX*: medium raw constant, e.g. index of variable, style class + +* `0xxxxxxx`: Index 00..7f +* `10xxxxxx`: Index 80..bf +* `11xxxxxx xxxxxxxx`: Index 0000..3fff + +[[CODE,CODE]] +*CODE*: byte address in <> chunk + +* `00000000`: Absolute address 0 (known to contain the <> opcode) +* `00xxxxxx`: Close relative pointer (+ 1..3f bytes) +* `01xxxxxx xxxxxxxx`: Relative pointer (± 8 kB), 14-bit signed integer relative to end of operand +* `1xxxxxxx xxxxxxxx xxxxxxxx`: Absolute pointer (full 8 MB range) + +[[STRING,STRING]] +*STRING*: shifted byte address in <> chunk: tiny pointers are shifted + right by 1 bit, short and long pointers are shifted right by + a number of bits stored in the header + +* `0xxxxxxx`: Tiny pointer (shift by 1) +* `10xxxxxx xxxxxxxx`: Short pointer (shift per header) +* `11xxxxxx xxxxxxxx xxxxxxxx`: Long pointer (shift per header) + +Decoding of a CODE operand can sometimes can be postponed until after +the execute stage, when it may be possible to decode straight into INST +or skip the operand altogether. Both close and relative pointers are +decoded relative to INST when encountering the CODE operand. + +RAW and VALUE only differ when the value read from a register or env +variable has its high bit set. For a RAW operand, 8002 is interpreted +as the number 8002. For a VALUE operand, it is treated as a pointer, +and the word at heap address 2 will be fetched and used instead. + +== Instructions + +<> + +* 0x: Execution flow +* 1x: Live data manipulation, stoppable environments +* 2x: Random access data manipulation +* 3x: Conditional branches +* 4x: Conditional branches (negated) +* 5x: Arithmetic +* 6x: Output +* 7x: Input, system control, miscellaneous + +[%header,cols="1,2,2,1",format=tsv,stripes=even] +|=== +Opcode Mnemonic Args Version + +00 <> {empty} {empty} +{empty} {empty} {empty} {empty} + +01 <> {empty} {empty} +{empty} {empty} {empty} {empty} + +02 <> <> {empty} +03 <> {empty} {empty} +{empty} {empty} {empty} {empty} + +04 <> <> {empty} +05 <> <> {empty} +85 <> <> {empty} +06 <> <> {empty} +86 <> <> {empty} +07 <> <> {empty} +87 <> {empty} {empty} +{empty} {empty} {empty} {empty} + +08/88 <> <>/<> {empty} +09 <> {empty} {empty} +89 <> {empty} {empty} +{empty} {empty} {empty} {empty} + +0a/8a <> <>/<> <> {empty} +0b/8b <> <>/<> {empty} +0c/8c <> <>/<> <> {empty} +{empty} {empty} {empty} {empty} + +0d <> {empty} {empty} +0e <> <> {empty} +0f <> <> {empty} +{empty} {empty} {empty} {empty} + +10 <> <> <> {empty} +90 <> <> <> ≥ 0.4 +{empty} {empty} {empty} {empty} + +11 <> <> {empty} +{empty} {empty} {empty} {empty} + +12 <> <> <> <> {empty} +13/93 <> <>/<> <> <> {empty} +{empty} {empty} {empty} {empty} + +14 <> <> {empty} +94 <> <> ≥ 0.5 +15/95 <> <>/<> {empty} +16 AUX_POP_VAL <> < 1.0 +17 <> <> {empty} +18 <> <> {empty} +19 <> <> {empty} +{empty} {empty} {empty} {empty} + +1b <> <> <> <> {empty} +{empty} {empty} {empty} {empty} + +1c <> {empty} {empty} +1d <> <> {empty} +1e <> {empty} {empty} +{empty} {empty} {empty} {empty} + +1f <> <> <> ≥ 0.4 +9f <> <> <> ≥ 0.4 +{empty} {empty} {empty} {empty} + +20/a0 <> <>/<> <> <> {empty} +21/a1 <> <>/<> <> <> {empty} +22/a2 <> <>/<> <> <> {empty} +{empty} {empty} {empty} {empty} + +24/a4 <> <>/<> <> <> {empty} +25/a5 <> <>/<> <> <> {empty} +26/a6 <> <>/<> <> <> {empty} +{empty} {empty} {empty} {empty} + +28/a8 <> <>/<> <> {empty} +29/a9 <> <>/<> <> {empty} +{empty} {empty} {empty} {empty} + +2d/ad <> <>/<> <> <> <> {empty} +{empty} {empty} {empty} {empty} + +2e/ae <> <>/<> <> {empty} +2f/af <> <>/<> <> {empty} +{empty} {empty} {empty} {empty} + +30/b0 <> <>/<> <> <> {empty} +31 <> <> <> {empty} +32 <> <> <> {empty} +33 <> <> <> {empty} +34 <> <> <> {empty} +35 <> <> <> {empty} +36 <> <> <> {empty} +b6 <> <> <> ≥ 0.4 +37 <> <> <> <> {empty} +38 <> <> <> <> {empty} +39/b9 <> <>/<> <> <> {empty} +3a/ba <> <>/<> <> <> <> {empty} +3b/bb <> <>/<> <> <> {empty} +3c <> <> {empty} +3d/bd <> <>/<> <> <> <> ≥ 0.4 +{empty} {empty} {empty} {empty} + +40/c0 <> <>/<> <> <> {empty} +41 <> <> <> {empty} +42 <> <> <> {empty} +43 <> <> <> {empty} +44 <> <> <> {empty} +45 <> <> <> {empty} +46 <> <> <> {empty} +c6 <> <> <> ≥ 0.4 +47 <> <> <> <> {empty} +48 <> <> <> <> {empty} +49/c9 <> <>/<> <> <> {empty} +4a/ca <> <>/<> <> <> <> {empty} +4b/cb <> <>/<> <> <> {empty} +4c <> <> {empty} +4d/cd <> <>/<> <> <> <> ≥ 0.4 +{empty} {empty} {empty} {empty} + +50 <> <> <> <> {empty} +d0 <> <> <> {empty} +51 <> <> <> <> {empty} +d1 <> <> <> {empty} +52 <> <> <> {empty} +{empty} {empty} {empty} {empty} + +58 <> <> <> <> {empty} +d8 <> <> <> {empty} +59 <> <> <> <> {empty} +d9 <> <> <> {empty} +5a <> <> <> <> {empty} +5b <> <> <> <> {empty} +5c <> <> <> <> {empty} +5d <> <> <> <> {empty} +{empty} {empty} {empty} {empty} + +60 <> <> {empty} +e0 <> <> {empty} +61 <> <> {empty} +e1 <> <> {empty} +{empty} {empty} {empty} {empty} + +62 <> {empty} {empty} +e2 <> {empty} {empty} +63 <> {empty} {empty} +e3 <> {empty} {empty} +{empty} {empty} {empty} {empty} + +64 <> <> {empty} +{empty} {empty} {empty} {empty} + +65 <> <> {empty} +{empty} {empty} {empty} {empty} + +66 <> <> {empty} +e6 <> {empty} {empty} +{empty} {empty} {empty} {empty} + +67 <> <> <> < 1.0 +e7 <> {empty} < 1.0 +{empty} {empty} {empty} {empty} + +67 <> <> ≥ 1.0 +{empty} {empty} {empty} {empty} + +68 <> <> ≥ 0.2 +e8 <> {empty} ≥ 0.2 +{empty} {empty} {empty} {empty} + +69 <> <> {empty} +e9 <> {empty} {empty} +{empty} {empty} {empty} {empty} + +6a <> {empty} ≥ 0.4 +ea <> {empty} ≥ 0.4 +{empty} {empty} {empty} {empty} + +6b <> <> deprecated +eb <> <> deprecated +{empty} {empty} {empty} {empty} + +6c <> <> ≥ 0.2 +ec <> <> <> ≥ 0.2 +{empty} {empty} {empty} {empty} + +6d <> <> <> {empty} +{empty} {empty} {empty} {empty} + +6e <> <> ≥ 0.4 +ee <> {empty} ≥ 0.4 +{empty} {empty} {empty} {empty} + +6f <> <> <> ≥ 0.5 +ef <> {empty} ≥ 1.0 +{empty} {empty} {empty} {empty} + +70 <> <> {empty} +{empty} {empty} {empty} {empty} + +72 <> <> {empty} +f2 <> <> {empty} +{empty} {empty} {empty} {empty} + +73 <> <> {empty} +f3 <> <> {empty} +{empty} {empty} {empty} {empty} + +74 <> <> <> {empty} +{empty} {empty} {empty} {empty} + +78 <> <> {empty} +79/f9 <> <>/<> <> {empty} +7a/fa <> <>/<> <> <> {empty} +7b/fb <> <>/<> <> {empty} +7c <> <> <> {empty} +{empty} {empty} {empty} {empty} + +7d <> <> <> <> ≥ 0.4 +fd <> <> <> <> ≥ 0.4 +{empty} {empty} {empty} {empty} + +7f <> <> <> <> <> {empty} +|=== + +[[status-values]] +\* <> byte values: + +* 00: top status area +* 01: inline status area + +[[EXT0,EXT0]] +† <> byte values: + +* 00: <> +* 01: <> +* 02: <> +* 03: <> +* 04: <> +* 05: <> +* 06: <> +* 07: <> +* 08: <> +* 09: <> +* 0a: <> +* 0b: <> +* 0c: <> +* 0d: <> +* 0e: <> +* 0f: <> (≥ 0.4) +* 10: <> (≥ 0.5) +* 11: <> (≥ 0.5) +* 12: <> (≥ 1.0) +* 13: <> (≥ 1.0) + +[[vminfo-values]] +‡ <> byte values: + +These return tagged numeric values: + +* 00: peak heap +* 01: peak aux +* 02: peak longterm +* 20: current div width +* 21: current div height + +These return raw 0/1 values: + +* 40: interpreter supports undo +* 41: interpreter supports save/restore +* 42: interpreter supports hyperlinks +* 43: interpreter supports quit +* 44: interpreter supports styling (≥ 1.0) +* 45: interpreter supports color (≥ 1.0) +* 46: interpreter supports text alignment (≥ 1.0) +* 50: currently transcripting (≥ 1.0) +* 60: interpreter supports top status (≥ 0.5) +* 61: interpreter supports inline status (≥ 0.5) + +Unrecognized options should return zero: \ for selectors +in the range 00 to 3f, the raw value 0 for selectors in the range +40 to 7f. Selectors above 7f should raise an error; their behavior +is not yet defined. + +"Styling" means that bold and italic text can be distinguished in +some way from the default, though they need not actually be bold +and italic: bright and underlined, or two different screen reader +voices, both qualify. Bold italic text must also be distinct from +the default, but may be the same as either bold or italic. + +== Output model + +In this and the following chapter, we will assume that an Å-machine +interpreter contains a virtual machine "engine" that decodes and +executes opcodes, and a device-specific output subsystem coupled to the +engine. The present chapter describes this coupling, as an API, while +the next chapter provides pseudo-code for the engine. + +Output flows from the Å-machine into a set of areas: The main area and +one or more status areas. Interpreters may freely ignore all or a +subset of the status areas, which is handy when the output device is +e.g. a screen reader or a dumb terminal. + +The interpreter engine selects a target area with the API calls: + + output_enterstatus(area, style_class) + output_leavestatus() + +The former call takes a parameter indicating the desired status area +(see <> above for a list of supported values), and another +indicating the desired style class for the status area (see the <> +chunk). Each time a status area is entered, it is also implicitly +cleared. + +Within each area, output is organized as a stream of characters, +linefeeds, and paragraph breaks. This stream is further subdivided into +nested elements called divs and spans. A div has implicit linefeeds at +its beginning and end. A link is a special kind of span. + +The engine transmits this output stream using a series of API calls: + + output_string(string) + output_char(charcode) + output_nbsp() + output_space() + output_space_n(number) + output_newline() + output_endpar() + + output_uppercase() // convert next character to uppercase + + output_enterdiv(style_class) + output_leavediv(style_class) + + output_enterspan(style_class) + output_leavespan() + + output_setbody(style_class) + + output_enter_selflink() + output_leave_selflink() + output_enterlink(string) + output_leavelink() + output_enterlink_res(resource_id) + output_leavelink_res() + + output_embed_res(resource_id) + output_progress_bar(amount, full_amount) + +Many of these calls are documented under the relevant instructions +in <>. + +In addition, it is possible to insert changes of text-style into the +stream, to e.g. turn italics on or off. Such changes are valid until +the next beginning or end of a subdivision, or until the end of the +current paragraph. + + output_setstyle(bits) // see <> for bit values + output_resetstyle(bits) + output_unstyle() + +This capability is deprecated and may be removed in a future version +of the Å-machine. + +Additional API calls perform high-level operations on the output state: + + output_clear() // keep current stack of divs, but clear them + output_clear_status() // hide all status areas + output_clear_all() // do both of the above + output_clear_links() // transform old links into static text + output_clear_div() // clear current div + output_clear_old() // clear old text + output_leave_all() + output_sync() + bool output_script_on() // returns true to indicate success + output_script_off() + bool output_can_embed_res(resource_id) // can it be embedded? + +The exact meaning of each of the above API calls is not specified here. +It is up to the output subsystem of the interpreter to react to them in +some sensible way. + +Paragraph breaks can legally appear inside spans, but the output +subsystem may choose to ignore them. + +The engine must ensure that the following rules are followed: + +* All calls to the enter- and leave-functions are perfectly balanced, with one exception: It is possible to call output_leave_all() at any point to return to the initial state (i.e. to clear the stack of spans and divs and leave any status area). + +* A div will not be nested inside a span (but a span may be nested inside a div or a span, and a div may be nested inside a div). +* A link will not be nested inside another link (at any level). +* No attempt will be made to enter a status area from inside a span or a status area. +* No attempt will be made to clear, clear_status, or clear_all from inside a span or a status area. +* No attempt will be made to save the game (to a file or an undo buffer) from within a span or a status area. + +The story file (and by extension the compiler) is responsible for +balancing each enter-call with a matching leave-call. Depending on the +story authoring language, this may include cutting choice points to +ensure return-once semantics, and setting up temporary stop-frames to +catch non-local returns from inner code. + +If the story file attempts to enter a link from within a link, the inner +link will behave like static text (forming a part of the outer link). + +For the remaining restrictions, the engine will detect violations and +throw a runtime error (invalid output state). Details are provided in +the next chapter. All runtime errors involve a call to +`output_leave_all()`. + +In order to detect such violations (and to provide the correct argument +to `output_leavediv`), the engine tracks output state using four global +variables (in addition to SPC and CWL): + +* nSpan: integer, initially 0 + +* nLink: integer, initially 0 +* inStatus: boolean, initially false +* divList: list/array of style classes, initially empty + +Of these, only divList is stored as part of a saved game (see <>). Interpreters may impose a limit on the size of divList, but it +must have room for at least eight items. + +All runtime errors invoke `output_leave_all()`, reset nSpan, nLink, +inStatus, and divList to their initial values, and restart the virtual +machine. + +Interpreters that do not support clickable links at all should treat +them as static text. Such interpreters do not need the nLink variable. + +== Opcode semantics + +There could be errors. Recommendation is to double-check with the +javascript interpreter. + +[[NOP]] +=== NOP + +Do nothing. + +[[FAIL]] +=== FAIL + + fail + + def fail: + INST = (heap[CHO + 4] << 16) | heap[CHO + 5] + leave recursive unify/push/pop/(de)serialize operation + +[[SET_CONT]] +=== SET_CONT + +*CODE* + + CONT = arg + +[[PROCEED]] +=== PROCEED + + if(SIM < 0x8000) CHO = SIM + INST = CONT + +Proceed from the continuation point (CONT). +When the simple cutting choice point (SIM) is set, proceed with +that as the current choice frame on the choice stack. SIM (and +by extension all choice frames) aren't expected to naturally dip +below $8000, as it would conflict with existing memory ranges. + +[[JMP]] +=== JMP + +*CODE* + + INST = arg + +Perform a straightforward jump. The JMPL_* variants are akin to +function calls, as they provide a continuation to resume from +later. + +[[JMP_MULTI]] +=== JMP_MULTI + +*CODE* + + SIM = 0xffff + INST = arg + +Perform a multi-jump chain starting at the CODE address provided. +SIM is invalidated as a cutting choice point. + +[[JMPL_MULTI]] +=== JMPL_MULTI + +*CODE* + + CONT = next_instruction + SIM = 0xffff + INST = arg + +[[JMP_SIMPLE]] +=== JMP_SIMPLE + +*CODE* + + SIM = CHO + INST = arg + +[[JMPL_SIMPLE]] +=== JMPL_SIMPLE + +*CODE* + + CONT = next_instruction + SIM = CHO + INST = arg + +[[JMP_TAIL]] +=== JMP_TAIL + +*CODE* + + if(SIM >= 0x8000) SIM = CHO + INST = arg + +Perform a jump to the CODE address, and save the current choice +frame to SIM if there is no existing cutting choice point. + +[[TAIL]] +=== TAIL + + if(SIM >= 0x8000) SIM = CHO + +[[PUSH_ENV]] +=== PUSH_ENV + +*BYTE/0* + + local variable addr = min(ENV, CHO) - 4 - arg + if(addr < TOP) runtime_error(1) + + heap[addr + 0] = ENV + heap[addr + 1] = SIM + heap[addr + 2] = CONT >> 16 + heap[addr + 3] = CONT & 0xffff + + ENV = addr + +[[POP_ENV]] +=== POP_ENV + + CONT = (heap[ENV + 2] << 16) | heap[ENV + 3] + SIM = heap[ENV + 1] + ENV = heap[ENV + 0] + +[[POP_ENV_PROCEED]] +=== POP_ENV_PROCEED + + INST = (heap[ENV + 2] << 16) | heap[ENV + 3] + if(heap[ENV + 1] < 0x8000) CHO = heap[ENV + 1] + ENV = heap[ENV + 0] + +[[PUSH_CHOICE]] +=== PUSH_CHOICE + +*BYTE/0 CODE* + + push_choice(arg1, arg2) + + def push_choice(narg, next): + local variable addr = min(ENV, CHO) - 9 - narg + if(addr < TOP) runtime_error(1) + heap[addr + 0] = ENV + heap[addr + 1] = SIM + heap[addr + 2] = CONT >> 16 + heap[addr + 3] = CONT & 0xffff + heap[addr + 4] = next >> 16 + heap[addr + 5] = next & 0xffff + heap[addr + 6] = CHO + heap[addr + 7] = TOP + heap[addr + 8] = TRL + for(local variable i = 0; i < narg; i++) { + heap[addr + 9 + i] = R[i] // register R00.. + } + CHO = addr + +[[POP_CHOICE]] +=== POP_CHOICE + +*BYTE/0* + + for(local variable i = 0; i < arg; i++) { + R[i] = heap[CHO + 9 + i] + } + while(TRL < heap[CHO + 8]) { + heap[aux[TRL++]] = 0 + } + TOP = heap[CHO + 7] + CONT = (heap[CHO + 2] << 16) | heap[CHO + 3] + SIM = heap[CHO + 1] + ENV = heap[CHO + 0] + CHO = heap[CHO + 6] + +[[POP_PUSH_CHOICE]] +=== POP_PUSH_CHOICE + +*BYTE/0 CODE* + + heap[CHO + 4] = arg2 >> 16 + heap[CHO + 5] = arg2 & 0xffff + for(local variable i = 0; i < arg1; i++) { + R[i] = heap[CHO + 9 + i] + } + while(TRL < heap[CHO + 8]) { + heap[aux[TRL++]] = 0 + } + TOP = heap[CHO + 7] + CONT = (heap[CHO + 2] << 16) | heap[CHO + 3] + SIM = heap[CHO + 1] + ENV = heap[CHO + 0] + +[[CUT_CHOICE]] +=== CUT_CHOICE + + CHO = heap[CHO + 6] + +[[GET_CHO]] +=== GET_CHO + +*DEST* + + arg <- CHO + +[[SET_CHO]] +=== SET_CHO + +*VALUE* + + CHO = arg + +[[ASSIGN]] +=== ASSIGN + +*VALUE/VBYTE DEST* + + arg2 <- arg1 + + def unify(a, b): + while(true) { + a = deref(a) + b = deref(b) + if(a.tag == reference && b.tag == reference) { + if(TRL <= AUX) runtime_error(2) + if(a.value < b.value) { + aux[--TRL] = b.value + heap[b.value] = a + } else { + aux[--TRL] = a.value + heap[a.value] = b + } + break + } else if(a.tag == reference) { + if(TRL <= AUX) runtime_error(2) + aux[--TRL] = a.value + heap[a.value] = b + break + } else if(b.tag == reference) { + if(TRL <= AUX) runtime_error(2) + aux[--TRL] = b.value + heap[b.value] = a + break + } else if(a.tag == extdict + && b.tag == extdict) + { + a = heap[a.value] + b = heap[b.value] + } else if(a.tag == extdict) { + a = heap[a.value] + } else if(b.tag == extdict) { + b = heap[b.value] + } else if(a == b) { + break + } else if(a.tag == pair && b.tag == pair) { + unify( + , + ) + a = + b = + } else { + fail + } + } + + def deref(a): + while(a.tag == reference && heap[a.value] != 0) { + a = heap[a.value] + } + return a + + def runtime_error(error_code): + R00 = error_code + reinitialize special-purpose registers + leave recursive unify/push/pop/(de)serialize operation + +[[MAKE_VAR]] +=== MAKE_VAR + +*DEST* + + local variable addr = TOP++ + if(TOP > min(ENV, CHO)) runtime_error(1) + heap[addr] = 0 + arg <- + +[[MAKE_PAIR]] +=== MAKE_PAIR + +*VWORD/VBYTE/DEST DEST DEST* + + if(arg3 is a store variant) { + local variable addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) runtime_error(1) + if(arg1 is a store variant) { + heap[addr + 0] = 0 + arg1 <- + } else { + heap[addr + 0] = arg1 + } + if(arg2 is a store variant) { + heap[addr + 1] = 0 + arg2 <- + } else { + heap[addr + 1] = arg2 + } + arg3 <- + } else { + local variable a3 = deref(arg3) + if(a3.tag == reference) { + local variable addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) runtime_error(1) + if(arg1 is a store variant) { + heap[addr + 0] = 0 + arg1 <- + } else { + heap[addr + 0] = arg1 + } + if(arg2 is a store variant) { + heap[addr + 1] = 0 + arg2 <- + } else { + heap[addr + 1] = arg2 + } + unify(a3, ) + } else if(a3.tag == pair) { + arg1 <- + arg2 <- + } else { + fail + } + } + +[[AUX_PUSH_VAL]] +=== AUX_PUSH_VAL + +*VALUE* + + push_serialized(arg) + +[[push_serialized]] + def push_serialized(v): + v = deref(v) + if(v.tag == pair) { + local variable count = 0 + while(true) { + push_serialized() + count++ + v = deref() + if(v == empty_list) { + v = 0xc000 + count + break + } else if(v.tag != pair) { + push_serialized(v) + v = 0xe000 + count + break + } + } + } else if(v.tag == extdict) { + push_serialized(heap[v.value + 1]) + push_serialized(heap[v.value + 0]) + v = 0x8100 + } else if(v.tag == reference) { + v = 0x8000 + } + if(AUX >= TRL) runtime_error(2) + aux[AUX++] = v + +[[AUX_PUSH_RAW]] +=== AUX_PUSH_RAW + +*VWORD/VBYTE/0* + + if(AUX >= TRL) runtime_error(2) + aux[AUX++] = arg + +[[AUX_POP_VAL]] +=== AUX_POP_VAL + +*DEST* + +This instruction was removed in version 1.0. No version of +the Dialog compiler generates it. The `pop_serialized` subroutine +is nevertheless used by <>. + + arg <- pop_serialized() + + def pop_serialized: + local variable v = aux[--AUX] + if(v == 0x8000) { + local variable addr = TOP++ + if(TOP > min(ENV, CHO)) runtime_error(1) + heap[addr] = 0 + v = + } else if(v == 0x8100) { + local variable addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) runtime_error(1) + heap[addr + 0] = pop_serialized() + heap[addr + 1] = pop_serialized() + v = + } else if((v & 0xc000) == 0xc000) { + local variable count = v & 0x1fff + if(v & 0x2000) { + v = pop_serialized() + } else { + v = empty_list + } + while(count--) { + local variable addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) runtime_error(1) + heap[addr + 0] = pop_serialized() + heap[addr + 1] = v + v = + } + } + return v + +[[AUX_POP_LIST]] +=== AUX_POP_LIST + +*DEST* + + arg <- pop_serialized_list() + + def pop_serialized_list(): + local variable list = empty_list + while(local variable v = pop_serialized()) { + local variable addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) runtime_error(1) + heap[addr + 0] = v + heap[addr + 1] = list + list = + } + return list + +[[AUX_POP_LIST_CHK]] +=== AUX_POP_LIST_CHK + +*VALUE* + + local variable flag = 0 + local variable key = deref(arg) + + while(local variable v = aux[--AUX]) { + if(v == key) { + flag = 1 + } + } + + if(!flag) { + fail + } + +[[AUX_POP_LIST_MATCH]] +=== AUX_POP_LIST_MATCH + +*VALUE* + + local variable old_top = TOP + + local variable key = deref(arg) + local variable list = pop_serialized_list() + // no need to deref the elements inside list + + while(key.tag == pair) { + local variable iter = list + local variable match = 0 + while(iter.tag == pair && !match) { + if(would_unify( + , + )) + { + match = 1 + } + iter = heap[iter.value + 1] + } + if(!match) { + fail + } + key = heap[key.value + 1] + } + + TOP = old_top // return the heap memory that we used + +[[SPLIT_LIST]] +=== SPLIT_LIST + +*VALUE VALUE DEST* + + arg3 <- split_list(arg1, arg2) + + def split_list(list, end): + list = deref(list) + end = deref(end) + if(list == end || list.tag != pair) { + return empty_list + } else { + local variable first, curr + first = curr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) runtime_error(1) + while(true) { + heap[curr + 0] = heap[list.value + 0] + list = deref(heap[list.value + 1]) + if(list == end || list.tag != pair) { + break + } else { + heap[curr + 1] = + curr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) { + runtime_error(1) + } + } + } + heap[curr + 1] = empty_list + return + } + +[[STOP]] +=== STOP + + CHO = STC + fail + +[[PUSH_STOP]] +=== PUSH_STOP + +*CODE* + + if(AUX + 2 > TRL) runtime_error(2) + aux[AUX++] = STC + aux[AUX++] = STA + STA = AUX + push_choice(0, arg) + STC = CHO + +[[POP_STOP]] +=== POP_STOP + + AUX = STA + STA = aux[--AUX] + STC = aux[--AUX] + +[[SPLIT_WORD]] +=== SPLIT_WORD + +*VALUE DEST* + + arg1 = deref(arg1) + if(arg1.tag == char) { + arg2 <- create_pair(arg1, empty_list) + } else if(arg1.tag == word) { + arg2 <- prepend_chars(arg1, empty_list) + } else if(arg1.tag == extdict) { + local variable part1 = heap[arg1.value + 0] + if(part1.tag == pair) { + arg2 <- part1 + } else { + local variable part2 = heap[arg1.value + 1] + arg2 <- prepend_chars(part1, part2) + } + } else if(arg1.tag == number) { + local variable i = arg1.value + local variable list = empty_list + do { + list = create_pair(, list) + i /= 10 + } while(i) + arg2 <- list + } else { + fail + } + + def prepend_chars(word, list): + local variable str = word_to_string(word) + local variable i + for(i = str.length - 1; i >= 0; i--) { + list = create_pair(, list) + } + return list + +[[JOIN_WORDS]] +=== JOIN_WORDS + +*VALUE DEST* + + arg1 = deref(arg1) + if(arg1.tag != pair) { + fail + } + local variable v1 = deref(heap[arg1.value + 0]) + local variable v2 = deref(heap[arg1.value + 1]) + if(v1.tag == char && v2 == empty_list) { + // Allow e.g. stop characters on their own. + arg2 <- v1 + } else { + arg2 <- parse_word(wordlist_to_string(arg1)) + } + + def wordlist_to_string(list): + // The string buffer "str" may have a limited size, + // but should be at least as large as the input buffer + // used during GET_INPUT. + // If the buffer space is exhausted, the instruction + // fails. + local variable str = "" + do { + local variable v = deref(heap[list.value + 0]) + if(v.tag == char) { + local variable ch = v.value + // Stop characters are defined + // in the LANG chunk. + if(ch <= 0x20 + || ch is a stop character) { + fail + } + str += ch + } else if(v.tag == word) { + str += dictionary[v.value] + } else if(v.tag == extdict) { + local variable part1 = heap[v.value + 0] + if(part1.tag == pair) { + str += wordlist_to_string(part1) + } else { + // Reinterpret the extdict cell + // as a list node: + str += wordlist_to_string(v) + } + } else if(v.tag == number) { + str += number_to_decimal_string(v.value) + } else { + fail + } + list = deref(heap[list.value + 1]) + } while(list.tag == pair) + if(list != empty_list) { + fail + } + return str + +[[LOAD_WORD]] +=== LOAD_WORD + +*VALUE/0 INDEX DEST* + + arg3 <- read_field(arg2, deref(arg1)) + +[[LOAD_BYTE]] +=== LOAD_BYTE + +*VALUE/0 INDEX DEST* + + arg3 <- (read_field(arg2 / 2, deref(arg1)) + >> ((arg2 & 1)? 0 : 8)) & 0xff + +[[LOAD_VAL]] +=== LOAD_VAL + +*VALUE/0 INDEX DEST* + + local variable val = get_longterm(read_field(arg2, deref(arg1))) + if(!val) { + fail + } + arg3 <- val + + def get_longterm(v): + if(v & 0x8000) { + TMP = v & 0x7fff + TMP += ram[TMP] + v = pop_longterm() + } + return v + + def pop_longterm(): + local variable v = ram[--TMP] + if(v == 0x8000) { + local variable addr = TOP + TOP++ + if(TOP > min(ENV, CHO)) runtime_error(1) + heap[addr] = 0 + v = + } else if(v == 0x8100) { + local variable addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) runtime_error(1) + heap[addr + 0] = pop_longterm() + heap[addr + 1] = pop_longterm() + v = + } else if((v & 0xc000) == 0xc000) { + local variable count = v & 0x1fff + if(v & 0x2000) { + v = pop_longterm() + } else { + v = empty_list + } + while(count--) { + local variable addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) runtime_error(1) + heap[addr + 0] = pop_longterm() + heap[addr + 1] = v + v = + } + } + return v + +[[STORE_WORD]] +=== STORE_WORD + +*VALUE/0 INDEX VALUE* + + ram[field_addr(arg2, deref(arg1))] = arg3 + +[[STORE_BYTE]] +=== STORE_BYTE + +*VALUE/0 INDEX VALUE* + + local variable addr = field_addr(arg2 / 2, deref(arg1)) + if(arg2 & 1) { + ram[addr] = (ram[addr] & 0xff00) | (arg3 & 0xff) + } else { + ram[addr] = (ram[addr] & 0x00ff) | (arg3 << 8) + } + +[[STORE_VAL]] +=== STORE_VAL + +*VALUE/0 INDEX VALUE* + + arg1 = deref(arg1) + if(arg1 == null || arg1.tag == object || arg3 != null) { + store_longterm(field_addr(arg2, arg1), arg3) + } + + def store_longterm(addr, v): + clear_longterm(addr) + v = deref(v) + if(v.tag == pair + || v.tag == extdict + || v.tag == reference) { + TMP = LTT + 2 + if(TMP > HEAD.ramsz) runtime_error(6) + push_longterm(v) + ram[addr] = 0x8000 + LTT + ram[LTT + 0] = TMP - LTT + ram[LTT + 1] = addr + LTT = TMP + } else { + ram[addr] = v + } + +[[push_longterm]] + def push_longterm(v): + v = deref(v) + if(v.tag == pair) { + local variable count = 0 + while(true) { + push_longterm(heap[v.value + 0]) + count++ + v = deref(heap[v.value + 1]) + if(v == empty_list) { + v = 0xc000 | count + break + } else if(v.tag != pair) { + push_longterm(v) + v = 0xe000 | count + break + } + } + } else if(v.tag == extdict) { + push_longterm(heap[v.value + 1]) + push_longterm(heap[v.value + 0]) + v = 0x8100 + } else if(v.tag == reference) { + runtime_error(4) + } + if(TMP > HEAD.ramsz) runtime_error(6) + ram[TMP++] = v + +[[clear_longterm]] + def clear_longterm(addr): + local variable v = ram[addr] + if(v & 0x8000) { + ram[addr] = 0 + v &= 0x7fff + local variable size = ram[v] + for(local variable i = v; i < LTT - size; i++) + { + ram[i] = ram[i + size] + } + LTT -= size + while(v < LTT) { + ram[ram[v + 1]] -= size + v += ram[v] + } + } + +[[SET_FLAG]] +=== SET_FLAG + +*VALUE/0 INDEX* + + ram[field_addr(arg2 / BITS_PER_WORD, deref(arg1))] |= + (1 << (BITS_PER_WORD - 1)) >> (arg2 % BITS_PER_WORD) + +[[RESET_FLAG]] +=== RESET_FLAG + +*VALUE/0 INDEX* + + arg1 = deref(arg1) + if(arg1 == null || arg1.tag == object) { + ram[field_addr(arg2 / BITS_PER_WORD, arg1)] &= + ~((1 << (BITS_PER_WORD - 1)) >> (arg2 % BITS_PER_WORD)) + } + +[[UNLINK]] +=== UNLINK + +*VALUE/0 INDEX INDEX VALUE* + + unlink(field_addr(arg2, deref(arg1)), arg3, arg4) + + def unlink(root_addr, field, key): + key = deref(key) + if(key.tag != object) { + return + } + local variable tail = + ram[field_addr(field, key.value)] + local variable addr = root_addr + while(ram[addr] != 0) { + if(ram[addr] == key) { + ram[addr] = tail + return + } + addr = field_addr(field, ram[addr].value) + } + +[[SET_PARENT]] +=== SET_PARENT + +*VBYTE/VALUE VBYTE/VALUE* + + constant FIELD_PARENT = 0 + constant FIELD_CHILD = 1 + constant FIELD_SIBLING = 2 + + arg1 = deref(arg1) + arg2 = deref(arg2) + if(arg2 != null) { + if(arg1.tag != object) runtime_error(3) + if(arg2.tag != object) runtime_error(3) + } + if(arg1.tag == object) { + local variable old_parent = + ram[field_addr(FIELD_PARENT, arg1.value)] + if(old_parent != 0) { + unlink( + field_addr(FIELD_CHILD, old_parent), + FIELD_SIBLING, + arg1) + } + ram[field_addr(FIELD_PARENT, arg1.value)] = arg2 + if(arg2.tag == object) { + ram[field_addr(FIELD_SIBLING, arg1.value)] = + ram[field_addr(FIELD_CHILD, arg2.value)] + ram[field_addr(FIELD_CHILD, arg2.value)] = arg1 + } + } + +[[IF_RAW_EQ]] +=== IF_RAW_EQ + +*VWORD/0 RAW CODE* + + if(arg1 == arg2) INST = arg3 + +[[IFN_RAW_EQ]] +=== IFN_RAW_EQ + +*VWORD/0 RAW CODE* + + if(arg1 != arg2) INST = arg3 + +[[IF_BOUND]] +=== IF_BOUND + +*VALUE CODE* + + if(deref(arg1).tag != reference) INST = arg2 + +[[IFN_BOUND]] +=== IFN_BOUND + +*VALUE CODE* + + if(deref(arg1).tag == reference) INST = arg2 + +[[IF_EMPTY]] +=== IF_EMPTY + +*VALUE CODE* + + if(deref(arg1) == empty_list) INST = arg2 + +[[IFN_EMPTY]] +=== IFN_EMPTY + +*VALUE CODE* + + if(deref(arg1) != empty_list) INST = arg2 + +[[IF_NUM]] +=== IF_NUM + +*VALUE CODE* + + if(deref(arg1).tag == number) INST = arg2 + +[[IFN_NUM]] +=== IFN_NUM + +*VALUE CODE* + + if(deref(arg1).tag != number) INST = arg2 + +[[IF_PAIR]] +=== IF_PAIR + +*VALUE CODE* + + if(deref(arg1).tag == pair) INST = arg2 + +[[IFN_PAIR]] +=== IFN_PAIR + +*VALUE CODE* + + if(deref(arg1).tag != pair) INST = arg2 + +[[IF_OBJ]] +=== IF_OBJ + +*VALUE CODE* + + // arg1 is guaranteed not to be null + if(deref(arg1).tag == object) INST = arg2 + +[[IFN_OBJ]] +=== IFN_OBJ + +*VALUE CODE* + + // arg1 is guaranteed not to be null + if(deref(arg1).tag != object) INST = arg2 + +[[IF_WORD]] +=== IF_WORD + +*VALUE CODE* + + if(deref(arg1).tag == word + || deref(arg1).tag == char + || deref(arg1).tag == extdict) INST = arg2 + +[[IFN_WORD]] +=== IFN_WORD + +*VALUE CODE* + + if(deref(arg1).tag != word + && deref(arg1).tag != char + && deref(arg1).tag != extdict) INST = arg2 + +[[IF_UWORD]] +=== IF_UWORD + +*VALUE CODE* + + arg1 = deref(arg1) + if(arg1.tag == extdict && heap[arg1.value + 0].tag == pair) { + INST = arg2 + } + +[[IFN_UWORD]] +=== IFN_UWORD + +*VALUE CODE* + + arg1 = deref(arg1) + if(arg1.tag != extdict || heap[arg1.value + 0].tag != pair) { + INST = arg2 + } + +[[IF_UNIFY]] +=== IF_UNIFY + +*VALUE VALUE CODE* + + if(would_unify(arg1, arg2)) INST = arg3 + + def would_unify(a, b): + while(true) { + a = deref(a) + b = deref(b) + if(a.tag == reference || b.tag == reference) { + return true + } else if(a.tag == extdict + && b.tag == extdict) + { + a = heap[a.value + 0] + b = heap[b.value + 0] + } else if(a.tag == extdict) { + a = heap[a.value + 0] + } else if(b.tag == extdict) { + b = heap[b.value + 0] + } else if(a == b) { + return true + } else if(a.tag == pair && b.tag == pair) { + if(!would_unify( + , + )) + { + return false + } + a = + b = + } else { + return false + } + } + +[[IFN_UNIFY]] +=== IFN_UNIFY + +*VALUE VALUE CODE* + + if(!would_unify(arg1, arg2)) INST = arg3 + +[[IF_GT]] +=== IF_GT + +*VALUE VALUE CODE* + + arg1 = deref(arg1) + arg2 = deref(arg2) + if(arg1.tag == number + && arg2.tag == number + && arg1.value > arg2.value) { + INST = arg3 + } + +[[IFN_GT]] +=== IFN_GT + +*VALUE VALUE CODE* + + arg1 = deref(arg1) + arg2 = deref(arg2) + if(!(arg1.tag == number + && arg2.tag == number + && arg1.value > arg2.value)) { + INST = arg3 + } + +[[IF_EQ]] +=== IF_EQ + +*VWORD/VBYTE VALUE CODE* + + if(arg1 == deref(arg2)) INST = arg3 + +[[IFN_EQ]] +=== IFN_EQ + +*VWORD/VBYTE VALUE CODE* + + if(arg1 != deref(arg2)) INST = arg3 + +[[IF_MEM_EQ]] +=== IF_MEM_EQ + +*VALUE/0 INDEX RAW/VBYTE CODE* + + if(read_field(arg2, deref(arg1)) == arg3) INST = arg4 + +[[IFN_MEM_EQ]] +=== IFN_MEM_EQ + +*VALUE/0 INDEX RAW/VBYTE CODE* + + if(read_field(arg2, deref(arg1)) != arg3) INST = arg4 + +[[IF_FLAG]] +=== IF_FLAG + +*VALUE/0 INDEX CODE* + + if(read_field(arg2 / BITS_PER_WORD, deref(arg1)) & + ((1 << (BITS_PER_WORD - 1)) >> (arg2 % BITS_PER_WORD))) + { + INST = arg3 + } + +[[IFN_FLAG]] +=== IFN_FLAG + +*VALUE/0 INDEX CODE* + + if(!(read_field(arg2 / BITS_PER_WORD, deref(arg1)) & + ((1 << (BITS_PER_WORD - 1)) >> (arg2 % BITS_PER_WORD)))) + { + INST = arg3 + } + +[[IF_CWL]] +=== IF_CWL + +*CODE* + + if(CWL != 0) INST = arg1 + +[[IFN_CWL]] +=== IFN_CWL + +*CODE* + + if(CWL == 0) INST = arg1 + +[[ADD_RAW]] +=== ADD_RAW + +*RAW RAW DEST* + + arg3 <- (arg1 + arg2) & 0xffff + +[[ADD_NUM]] +=== ADD_NUM + +*VALUE VALUE DEST* + + local variable result = unbox_int(arg1) + unbox_int(arg2) + arg3 <- box_int(result) + + def unbox_int(v): + v = deref(v) + if(v.tag != number) fail + return v.value + + def box_int(v): + if(v < 0 || v > 0x3fff) fail + return + +[[SUB_RAW]] +=== SUB_RAW + +*RAW RAW DEST* + + arg3 <- (arg1 - arg2) & 0xffff + +[[SUB_NUM]] +=== SUB_NUM + +*VALUE VALUE DEST* + + local variable result = unbox_int(arg1) - unbox_int(arg2) + arg3 <- box_int(result) + +[[MUL_NUM]] +=== MUL_NUM + +*VALUE VALUE DEST* + + local variable result = unbox_int(arg1) * unbox_int(arg2) + arg3 <- box_int(result) + +Prior to version 1.1, the pseudocode included the line +``result = result & 0x3fff``, which meant a result that exceeded the +Å-machine's number range would wrap around instead of failing. In version +1.1, this was changed to match <>, <>, and <>. + +An interpreter may use the old behavior for pre-1.1 story files. However, +since no known story files make use of this behavior, this is not required. + +[[DIV_NUM]] +=== DIV_NUM + +*VALUE VALUE DEST* + + if(unbox_int(arg2) == 0) fail + local variable result = unbox_int(arg1) / unbox_int(arg2) + arg3 <- box_int(result) + +[[MOD_NUM]] +=== MOD_NUM + +*VALUE VALUE DEST* + + if(unbox_int(arg2) == 0) fail + local variable result = unbox_int(arg1) % unbox_int(arg2) + arg3 <- box_int(result) + +[[RAND_NUM]] +=== RAND_NUM + +*VALUE VALUE DEST* + + local variable start = unbox_int(arg1) + local variable range = unbox_int(arg2) - start + 1 + if(range < 1) fail + // any timer-seeded PRNG can be used: + local variable result = start + (rand() % range) + arg3 <- box_int(result) + +[[RAND_RAW]] +=== RAND_RAW + +*BYTE DEST* + + // any timer-seeded PRNG can be used: + arg2 <- (rand() % (arg1 + 1)) & 0xffff + +[[INC_NUM]] +=== INC_NUM + +*VALUE DEST* + + local variable result = unbox_int(arg1) + 1 + arg2 <- box_int(result) + +[[DEC_NUM]] +=== DEC_NUM + +*VALUE DEST* + + local variable result = unbox_int(arg1) - 1 + arg2 <- box_int(result) + +[[INC_RAW]] +=== INC_RAW + +*RAW DEST* + + arg2 <- (arg1 + 1) & 0xffff + +[[DEC_RAW]] +=== DEC_RAW + +*RAW DEST* + + arg2 <- (arg1 - 1) & 0xffff + +[[PRINT_A_STR_A]] +=== PRINT_A_STR_A + +*STRING* + + if(SPC == auto || SPC == pendingspace) output_space() + elseif(SPC == nbsp) output_nbsp() + output_string(arg) + SPC = auto + +[[PRINT_N_STR_A]] +=== PRINT_N_STR_A + +*STRING* + + if(SPC == pendingspace) output_space() + elseif(SPC == nbsp) output_nbsp() + output_string(arg) + SPC = auto + +[[PRINT_A_STR_N]] +=== PRINT_A_STR_N + +*STRING* + + if(SPC == auto || SPC == pendingspace) output_space() + elseif(SPC == nbsp) output_nbsp() + output_string(arg) + SPC = nospace + +[[PRINT_N_STR_N]] +=== PRINT_N_STR_N + +*STRING* + + if(SPC == pendingspace) output_space() + elseif(SPC == nbsp) output_nbsp() + output_string(arg) + SPC = nospace + +[[NOSPACE]] +=== NOSPACE + + if(CWL == 0) { + if(SPC < nospace) { + SPC = nospace + } + } + +[[NBSP]] +=== NBSP + + if(CWL == 0) { + if(SPC < nbsp) { + SPC = nbsp + } + } + +[[SPACE]] +=== SPACE + + if(CWL == 0) { + if(SPC < pendingspace) { + SPC = pendingspace + } + } + +[[LINE]] +=== LINE + + if(CWL == 0) { + if(SPC < line) { + output_newline() + SPC = line + } + } + +[[PAR]] +=== PAR + + if(CWL == 0) { + if(SPC < par) { + output_endpar() + SPC = par + } + } + +[[SPACE_N]] +=== SPACE_N + +*VALUE* + + if(CWL == 0) { + arg = deref(arg) + if(arg.tag == number) { + output_space_n(arg.value) + SPC = space + } + } + +[[PRINT_VAL]] +=== PRINT_VAL + +*VALUE* + + if(CWL == 0) { + print_value(deref(arg)) + } else { + push_serialized(deref(arg)) + } + + def print_value(v): + if(v.tag == char) { + local variable ch = v.value + if(ch is listed as no-space-before in LANG chunk) { + if(SPC < nospace) { + SPC = nospace + } + } + if(SPC == auto || SPC == pendingspace) output_space() + output_char(ch) + if(ch is listed as no-space-after in LANG chunk) { + SPC = nospace + } else { + SPC = auto + } + } else { + if(SPC == auto || SPC == pendingspace) output_space() + if(v.tag == object) { + output_string("#") + if(TAGS chunk is present) { + output_string(get_object_name(v.value)) + } + } else if(v.tag == number) { + output_string(number_to_decimal_string(v.value)) + } else if(v.tag == word || v.tag == extdict) { + output_string(word_to_string(v)) + } else if(v.tag == reference) { + output_string("$") + } else if(v == empty_list) { + output_string("[]") + } else if(v.tag == pair) { + output_string("[") + print_value(deref(heap[v.value + 0])) + local variable tail = deref(heap[v.value + 1]) + while(tail != empty_list) { + if(tail.tag == pair) { + SPC = pendingspace + print_value(deref(heap[tail.value + 0])) + tail = deref(heap[tail.value + 1]) + } else { + output_space() + output_string("|") + SPC = pendingspace + print_value(tail) + tail = empty_list + } + } + output_string("]") + } + SPC = auto + } + + def word_to_string(v): + local variable str = "" + if(v.tag == word) { + str += dictionary[v.value] + } else if(v.tag == extdict) { + local variable part1 = heap[v.value + 0] + if(part1.tag == pair) { + while(part1.tag == pair) { + str += char_to_string(heap[part1.value + 0].value) + part1 = heap[part1.value + 1] + } + } else { + str += word_to_string(part1) + local variable part2 = heap[v.value + 1] + while(part2.tag == pair) { + str += char_to_string(heap[part2.value + 0].value) + part2 = heap[part2.value + 1] + } + } + } + return str + +[[ENTER_DIV]] +=== ENTER_DIV + +*INDEX* + + if(CWL == 0) { + if(nSpan > 0) runtime_error(7) + output_enterdiv(arg) + append arg to divList + SPC = par + } + +[[LEAVE_DIV]] +=== LEAVE_DIV + + if(CWL == 0) { + local variable style_class = last element of divList + remove last element from divList + output_leavediv(style_class) + SPC = line + } + +Previous versions of this specification erroneously said SPC +should be set to "par" instead of "line". This is not meant to +be a change between Aa-machine versions; ideally SPC should be +set to "line" even when running version 0.5 story files. + +[[ENTER_SPAN]] +=== ENTER_SPAN + +*INDEX* + + if(CWL == 0) { + if(SPC == auto || SPC == pendingspace) output_space() + output_enterspan(arg) + SPC = nospace + nSpan++ + } + +[[LEAVE_SPAN]] +=== LEAVE_SPAN + + if(CWL == 0) { + output_leavespan() + SPC = auto + nSpan-- + } + +[[ENTER_STATUS]] +=== ENTER_STATUS + +*BYTE INDEX* + + if(inStatus || nSpan > 0) runtime_error(7) + if(CWL == 0) { + output_enterstatus(arg1, arg2) + SPC = par + inStatus = true + } + +[[LEAVE_STATUS]] +=== LEAVE_STATUS + + if(CWL == 0) { + output_leavestatus() + SPC = par + inStatus = false + } + +Before version 1.0, this was opcode e7. From 1.0 onwards, it has +been moved to ef. The semantics are unchanged. + +[[ENTER_LINK_RES]] +=== ENTER_LINK_RES + +*VALUE* + + if(CWL == 0) { + if(nLink == 0) { + if(SPC == auto || SPC == pendingspace) output_space() + output_enterlink_res(deref(arg)) + SPC = nospace + } + nLink++ + nSpan++ + } + +[[LEAVE_LINK_RES]] +=== LEAVE_LINK_RES + + if(CWL == 0) { + nSpan-- + nLink-- + if(nLink == 0) output_leavelink_res() + } + +[[ENTER_LINK]] +=== ENTER_LINK + +*VALUE* + + if(CWL == 0) { + if(nLink == 0) { + if(SPC == auto || SPC == pendingspace) output_space() + local variable input = "" + arg = deref(arg) + while(arg.tag == pair) { + local variable w = deref(heap[arg.value + 0]) + if(w.tag == word + || w.tag == char + || w.tag == extdict + || w.tag == number) { + * if input is non-empty, then append + space to end of input + * append text of w to end of input + } + arg = deref(heap[arg.value + 1]) + } + output_enterlink(input) + SPC = nospace + } + nLink++ + nSpan++ + } + +[[LEAVE_LINK]] +=== LEAVE_LINK + + if(CWL == 0) { + nSpan-- + nLink-- + if(nLink == 0) output_leavelink() + } + +[[ENTER_SELF_LINK]] +=== ENTER_SELF_LINK + + if(CWL == 0) { + if(nLink == 0) { + if(SPC == auto || SPC == pendingspace) output_space() + output_enter_selflink() + SPC = space + } + nLink++ + nSpan++ + } + +[[LEAVE_SELF_LINK]] +=== LEAVE_SELF_LINK + + if(CWL == 0) { + nSpan-- + nLink-- + if(nLink == 0) output_leave_selflink() + } + +[[SET_STYLE]] +=== SET_STYLE + +*BYTE* + + // 1 = reverse, 2 = bold, 4 = italic, 8 = fixed + if(CWL == 0) { + if(SPC == auto || SPC == pendingspace) output_space() + output_setstyle(arg) + SPC = space + } + +This opcode is deprecated, but still supported for now. + +[[RESET_STYLE]] +=== RESET_STYLE + +*BYTE* + + // 1 = reverse, 2 = bold, 4 = italic, 8 = fixed + if(CWL == 0) { + output_resetstyle(arg) + } + +This opcode is deprecated, but still supported for now. + +[[SET_BODY]] +=== SET_BODY + +*INDEX* + + if(inStatus || nSpan > 0) runtime_error(7) + output_setbody(arg) + +`output_setbody` applies a style class to the main body of the +document, if possible. Whether this affects existing text is +left to the implementation. + +[[UNSTYLE]] +=== UNSTYLE + + if(CWL == 0) { + output_unstyle() + } + +[[PRINT_SERIAL]] +=== PRINT_SERIAL + + if(CWL == 0) { + if(SPC == auto || SPC == pendingspace) output_space() + output_string(HEAD.serial) + SPC = auto + } + +[[CLEAR]] +=== CLEAR + + if(CWL == 0) { + if(inStatus || nSpan > 0) runtime_error(7) + output_clear() + } + +[[CLEAR_STATUS]] +=== CLEAR_STATUS + + if(CWL == 0) { + if(inStatus || nSpan > 0) runtime_error(7) + output_clear_status() + } + +[[CLEAR_ALL]] +=== CLEAR_ALL + + if(CWL == 0) { + if(inStatus || nSpan > 0) runtime_error(7) + output_clear_all() + } + +[[EMBED_RES]] +=== EMBED_RES + +*VALUE* + + if(CWL == 0) { + output_embed_res(deref(arg1)) + } + +[[CAN_EMBED_RES]] +=== CAN_EMBED_RES + +*VALUE DEST* + + if(output_can_embed_res(deref(arg1))) { + arg2 <- 1 // raw, unboxed integer + } else { + arg2 <- null + } + +[[PROGRESS]] +=== PROGRESS + +*VALUE VALUE* + + if(CWL == 0) { + arg1 = deref(arg1) + arg2 = deref(arg2) + if(arg1.tag == number && arg2.tag == number) { + output_progress_bar(arg1.value, arg2.value) + } + } + +Prints a progress bar with `arg1/arg2` filled. + +[[QUIT]] +=== QUIT + + output_sync() + Either return, or exit the engine. + +[[RESTART]] +=== RESTART + + output_leave_all() + inStatus = false + nSpan = 0 + nLink = 0 + divList = empty array/list + // May rewrite all game state, including INST. + vm_restart() + +[[RESTORE]] +=== RESTORE + + if(vm_restore()) { + // By now, the game state has changed drastically, + // including INST and divList. + output_leave_all() + inStatus = false + nSpan = 0 + nLink = 0 + for(i = 0; i < divList.length; i++) { + output_enterdiv(divList[i]) + } + } + +[[UNDO]] +=== UNDO + + if(vm_undo()) { + // By now, the game state has changed drastically, + // including INST and divList. + output_leave_all() + inStatus = false + nSpan = 0 + nLink = 0 + for(i = 0; i < divList.length; i++) { + output_enterdiv(divList[i]) + } + } + +[[SAVE]] +=== SAVE + +*CODE* + + // fails on error/cancel, continues on success, jumps on restore + if(inStatus || nSpan > 0) runtime_error(7) + if(!vm_save(arg)) { + fail + } + +[[SAVE_UNDO]] +=== SAVE_UNDO + +*CODE* + + // fails on error/cancel, continues on success, jumps on restore + if(inStatus || nSpan > 0) runtime_error(7) + if(!vm_save_undo(arg)) { + fail + } + +[[GET_INPUT]] +=== GET_INPUT + +*DEST* + + if(SPC == auto || SPC == pendingspace) output_space() + elseif(SPC == nbsp) output_nbsp() + output_sync() + arg <- input_get_line() + SPC = line + +The string of characters typed by the player is converted into +lowercase, and split into substrings as follows: + +* Every stop character (as declared in the <> chunk) becomes a single substring. +* Remaining characters are separated by whitespace. + +Thus, the string `drop ball.north` is split into the +substrings `drop`, `ball`, `.`, and `north`. + +The list of substrings is converted into a list of Å-machine +values as follows: + + local variable list = empty_list + local variable i + for(i = list_of_substrings.length - 1; i >= 0; i--) { + local variable val = parse_word(list_of_substrings[i]) + list = create_pair(val, list) + } + return list // this gets stored into arg + + def create_pair(head, tail) { + local variable addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) runtime_error(1) + heap[addr + 0] = head + heap[addr + 1] = tail + return + } + + def parse_word(input) { + if(input.length == 1) { + if(input[0] >= '0' && input[0] <= '9') { + return + } else { + return + } + } + + // Look for an exact match in the DICT chunk: + local variable d = dict_lookup(input) + if(d) { + return + } + + // Check for a decimal number in the range 0-16383: + local variable num = parse_decimal(input) + if(num && num >= 0 && num <= 16383) { + return + } + + // Otherwise, run the word endings decoder: + local variable state = 0 + local variable ending = empty_list + local variable pos = input.length + while(true) { + local variable addr + if(decoder[state] == 0x00) { + while(pos > 0) { + addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) { + runtime_error(1) + } + local variable c = input[--pos] + local variable v + if(c >= '0' && c <= '9') { + v = + } else { + v = + } + heap[addr + 0] = v + heap[addr + 1] = ending + ending = + } + addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) { + runtime_error(1) + } + heap[addr + 0] = ending + heap[addr + 1] = empty_list + return + } else if(decoder[state] == 0x01) { + d = dict_lookup(input.first_n(pos)) + if(d) { + // The first pos characters + // exactly match a dictionary + // word. + addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) { + runtime_error(1) + } + heap[addr + 0] = + heap[addr + 1] = ending + return + } else { + state++ + } + } else { + local variable ch = input[pos - 1] + if(ch == decoder[state]) { + addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) { + runtime_error(1) + } + heap[addr + 0] = + heap[addr + 1] = ending + ending = + state = decoder[state + 1] + pos-- + } else { + state += 2 + } + } + } + } + +[[GET_KEY]] +=== GET_KEY + +*DEST* + + if(SPC == auto || SPC == pendingspace) output_space() + elseif(SPC == nbsp) output_nbsp() + output_sync() + arg <- input_get_key() // digits are numbers, others are chars + +If a link is clicked during a <> instruction, and the link +does not point to a single character or digit, the result is +undefined. It may be the first character of the link text, or +0d (RETURN), or a special sentinel value. It is not, however, an +error. Some value must be returned. + +(The web frontend currently returns the first character of the +link text, but this is an implementation detail, not required +by the specification.) + +[[SCRIPT_ON]] +=== SCRIPT_ON + + if(!output_script_on()) { + fail + } + +[[SCRIPT_OFF]] +=== SCRIPT_OFF + + output_script_off() + +[[TRACE_ON]] +=== TRACE_ON + + vm_trace_on() + +[[TRACE_OFF]] +=== TRACE_OFF + + vm_trace_off() + +[[INC_CWL]] +=== INC_CWL + + CWL++ + +[[DEC_CWL]] +=== DEC_CWL + + CWL-- + +[[UPPERCASE]] +=== UPPERCASE + + if(CWL == 0) { + output_uppercase() + } + +[[CLEAR_LINKS]] +=== CLEAR_LINKS + + // Turn all links in the main text area into non-clickable, + // ordinary text. The status areas are not affected. + + output_clear_links() + +[[CLEAR_OLD]] +=== CLEAR_OLD + + // Clear any text that the player has had a chance to read. + // This means everything up to and including the last input. + // The interpreter may ignore this operation. + + if(nSpan > 0) runtime_error(7) + output_clear_old() + +[[CLEAR_DIV]] +=== CLEAR_DIV + + // Clear or fold away the current div. + // The interpreter may ignore this operation. + + output_clear_div(); + +[[VM_INFO]] +=== VM_INFO + +*BYTE DEST* + + if(arg1 & 0x40) { + arg2 <- vm_terp_feature(arg1 & 0x3f) + } else if(arg1 & 0x20) { + local variable val = vm_output_state(arg1 & 0x1f) + arg2 <- box_int(val) + } else { + local variable count = 0 + if(arg1 == 0) { + for(local variable i = 0; i < HEAD.heapsz; i++) + { + if(heap[i] != unused) count++ + } + } else if(arg1 == 1) { + for(local variable i = 0; i < HEAD.auxsz; i++) + { + if(aux[i] != unused) count++ + } + } else if(arg1 == 2) { + for(local variable i = LTB; i < HEAD.ramsz; i++) + { + if(ram[i] != unused) count++ + } + } + arg2 <- box_int(count) + } + +[[SET_IDX]] +=== SET_IDX + +*VALUE* + + IDX = deref(arg) // IDX is another name for R3f + if(IDX.tag == extdict) { + IDX = heap[IDX.value + 0] + } + +[[CHECK_EQ]] +=== CHECK_EQ + +*VWORD/VBYTE CODE* + + if(IDX == arg1) INST = arg2 + +[[CHECK_GT_EQ]] +=== CHECK_GT_EQ + +*VWORD/VBYTE CODE CODE* + + if(IDX > arg1) { + INST = arg2 + } else if(IDX == arg1) { + INST = arg3 + } + +[[CHECK_GT]] +=== CHECK_GT + +*RAW/BYTE CODE* + + if(IDX > arg1) INST = arg2 + +[[CHECK_WORDMAP]] +=== CHECK_WORDMAP + +*INDEX CODE* + + local variable objs[] = find_in_wordmap(wordmaps[arg1], IDX) + if(objs == null) { + // The word was not in the table, and should not + // match anything. + // Nothing was pushed, so jump to enforce that. + INST = arg2 + } else if(objs.length == 0) { + // The word is a wildcard, so don't jump. + } else { + // The word matches a limited number of objects. + // Push them and jump to enforce that. + if(AUX + objs.length > TRL) runtime_error(2) + for(local variable i = 0; i < objs.length; i++) { + aux[AUX++] = objs[i] + } + INST = arg2 + } + +The 'find_in_wordmap' procedure reads from one of the wordmaps +(`wordmaps[arg1]`), looking up the word of interest (IDX) to see +if it's there. + +[[CHECK_EQ_2]] +=== CHECK_EQ_2 + +*VWORD/VBYTE VWORD/VBYTE CODE* + + if(IDX == arg1 || IDX == arg2) INST = arg3 + +[[TRACEPOINT]] +=== TRACEPOINT + +*STRING STRING STRING WORD* + + // event, predicate, file, line + vm_tracepoint(arg1, arg2, arg3, arg4) + + +== Examples + +=== To traverse children + +A1 is the incoming parent. + + LOAD_VAL A1.'child', >T1 + JUMP entry + loop + POP_CHOICE #2 + ASSIGN A1, >T1 + entry + LOAD_WORD T1.'sibling', >A1 + IF_RAW_EQ 0, A1, last + PUSH_CHOICE #2, loop + last + ASSIGN T1, =A0 + ... + +=== To traverse a list of flagged objects + + LOAD_VAL 0.'root', >T1 + JUMP entry + loop + POP_CHOICE #2 + ASSIGN A1, >T1 + entry + LOAD_WORD T1.'next', >A1 + IF_RAW_EQ 0, A1, last + PUSH_CHOICE #2, loop + last + ASSIGN T1, =A0 + ... + +=== To flag object A0 with list + + IF_FLAG A0.'flag', skip + SET_FLAG A0.'flag' + LOAD_WORD 0.'root', >T0 + STORE_WORD A0.'next', T0 + STORE_VAL 0.'root', A0 + skip + +=== To clear object A0 with list + + IFN_FLAG A0.'flag', skip + RESET_FLAG A0.'flag' + UNLINK 0.'root', 'next', A0 + skip + +=== To clear list of flagged objects + + LOAD_WORD 0.'root', >T1 + IF_RAW_EQ 0, T1, skip + loop + RESET_FLAG T1.'flag' + LOAD_WORD T1.'next', >T1 + IFN_NULL T1, loop + STORE_WORD 0.'root', #0 + skip + +=== To clear a property for every object + + ASSIGN max_obj_id, >T1 + loop + STORE_VAL T1.'prop', #0 + DEC_RAW T1, >T1 + IFN_NULL T1, loop + +=== To select 'then at random' with four choices + + LOAD_BYTE 0.'state', >R3f + CHECK_GT_EQ #3, random, last + INC_RAW R3f, >T0 + JMP done + last + ASSIGN #7, >T0 + JMP done + random + SUB_RAW R3f, #4, >R3f + RAND_RAW #2, >T0 + CHECK_GT T0, noadjust + INC_RAW T0, >T0 + noadjust + ASSIGN T0, >R3f + ADD_RAW T0, #4, T0 + done + STORE_BYTE 0.'state', T0 + + CHECK_EQ #1, case1 + CHECK_EQ #2, case2 + CHECK_EQ #3, case3 + case0 + ... + case1 + ... + case2 + ... + case3 + ... + +== Colophon + +This document was originally written by Linus Åkesson, and is now +maintained by the Dialog Project contributors. diff --git a/docs/current.adoc b/docs/current.adoc index 97547c4..b6caa85 120000 --- a/docs/current.adoc +++ b/docs/current.adoc @@ -1 +1 @@ -aam-specification-1.0.adoc \ No newline at end of file +aam-specification-1.1.adoc \ No newline at end of file diff --git a/src/6502/Makefile b/src/6502/Makefile index 9678c58..d676d5e 100644 --- a/src/6502/Makefile +++ b/src/6502/Makefile @@ -1,4 +1,4 @@ -VERSION=1.0.3 +VERSION=1.1.0 CFLAGS=-Wall -O2 -DVERSION=\"$(VERSION)\" all: aambox6502 aambox_frontend.bin c64_crunched.bin c64_drivecode.bin c64_loader.prg a2.system a2_0boot.bin a2_sboot.bin diff --git a/src/Makefile b/src/Makefile index 3838221..ca636e9 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,6 +1,6 @@ -VERSION=1.0.3 +VERSION=1.1.0 VER_MAJOR=1 -VER_MINOR=0 +VER_MINOR=1 CC = gcc CFLAGS += -Wall -O3 -DVERSION=\"$(VERSION)\" -DVER_MAJOR=$(VER_MAJOR) -DVER_MINOR=$(VER_MINOR) ifeq ($(shell uname -s),Darwin) diff --git a/src/js/engine.js b/src/js/engine.js index f8d18a7..b274147 100644 --- a/src/js/engine.js +++ b/src/js/engine.js @@ -423,7 +423,7 @@ function prepare_story(file_array, io, seed, quit, toparea, inlinearea) { e = { VER_MAJOR: 1, - VER_MINOR: 0, + VER_MINOR: 1, SP_AUTO: 0, SP_NOSPACE: 1, diff --git a/src/js/nodefrontend.js b/src/js/nodefrontend.js index bfafbe3..ba0f454 100644 --- a/src/js/nodefrontend.js +++ b/src/js/nodefrontend.js @@ -1,6 +1,6 @@ (function(){"use strict"; -const VERSION = "1.0.3"; +const VERSION = "1.1.0"; const fs = require('fs'); const readline = require('readline'); diff --git a/src/js/webfrontend.html b/src/js/webfrontend.html index 12e773e..2e45dce 100644 --- a/src/js/webfrontend.html +++ b/src/js/webfrontend.html @@ -50,7 +50,7 @@


From 1bac0f9ba90f350cd9e8df15e416b7f1b32cfdb8 Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Sat, 5 Sep 2026 15:02:51 -0500 Subject: [PATCH 2/7] JavaScript implementation --- src/js/engine.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/js/engine.js b/src/js/engine.js index b274147..b091407 100644 --- a/src/js/engine.js +++ b/src/js/engine.js @@ -2154,8 +2154,10 @@ function vm_run(e, param) { a1 = deref(fvalue()); a2 = deref(fvalue()); if(a1 >= 0x4000 && a1 < 0x8000 && a2 >= 0x4000 && a2 < 0x8000) { - v = ((a1 & 0x3fff) * (a2 & 0x3fff)) & 0x3fff; - store(e.code[e.inst++], v | 0x4000); + v = (a1 & 0x3fff) * (a2 & 0x3fff); + if(v < 0x4000) { + store(e.code[e.inst++], v | 0x4000); + } else fail(); } else fail(); break; case 0x5c: // div_num value value dest From a0d8041fd449c5fe71894ef78957456f6ed7d616 Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Wed, 9 Sep 2026 15:52:24 -0500 Subject: [PATCH 3/7] new 6502 implementation of mul16 that is more efficient and also flags overflow, allowing for fail --- src/6502/engine.s | 70 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/src/6502/engine.s b/src/6502/engine.s index d728bde..971295f 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -3642,7 +3642,11 @@ enter lda #0 sta denom+1 sta quot+1 + txa ; New mul16 routine clobbers x + pha jsr mul16 + pla + tax lda #100 sta denom lda #0 @@ -4261,7 +4265,11 @@ chknum sta denom+1 lda #10 sta denom + txa ; New mul16 clobbers x + pha jsr mul16 + pla + tax pla and #$0f @@ -5357,15 +5365,24 @@ op_mul_num sta denom jsr mul16 + ; If it overflowed, C is set (and numer, numer+1 are unaltered) + ; Otherwise, the result is in numer and numer+1 + bcs fail + ; If the top two bits of numer+1 are not zero, it's too big + lda #%11000000 + and numer+1 + bne fail + ; Otherwise, we know it fits in an int lda numer+1 - and #$3f ora #$40 sta result+0 lda numer sta result+1 jmp ldystorefetchnext +fail + jmp failure .) op_pop_choice @@ -9206,7 +9223,8 @@ nomatch jmp loop .) -mul16 +/* +oldmul16 ; input quot, denom ; output numer ; clobbers quot, denom, y @@ -9237,6 +9255,50 @@ nobit rts .) +*/ + +mul16 + ; omult16.a from https://github.com/TobyLobster/multiply_test + ; from BBC BASIC 2 ROM + ; input quot, denom + ; output numer, c (set on overflow) + ; clobbers quot, denom, a, x, y + .( + ldx #0 ; using x,y as 16-bit accumulator + ldy #0 ; x high y low +loop + ; Get lsb of denom + lsr denom+1 + ror denom + bcc skip ; Do not add quot if the bit is clear + + ; Add quot to accumulator + clc + tya + adc quot + tay + txa + adc quot+1 + tax + + ; Bail out on overflow + bcs overflow +skip + ; Multiply quot by two + asl quot + rol quot+1 + + ; Continue until denom is zero + lda denom + ora denom+1 + bne loop + + clc ; No overflow in this path + sty numer + stx numer+1 +overflow + rts + .) div16 ; input numer, denom @@ -10314,8 +10376,12 @@ digloop sta denom+1 lda #10 sta denom + txa ; The new mul16 routine clobbers x + pha ; So we need to stash it on the stack jsr mul16 pla + tax + pla clc adc numer sta quot From a29a649c1d633a9dd68902fba11cd16212a5c8c0 Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Wed, 9 Sep 2026 15:56:44 -0500 Subject: [PATCH 4/7] consistency of version numbers --- bin/bump-version.sh | 3 +++ docs/aam-specification-1.1.adoc | 2 +- src/aavm.h | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bin/bump-version.sh b/bin/bump-version.sh index 4f296e7..86b3eff 100755 --- a/bin/bump-version.sh +++ b/bin/bump-version.sh @@ -21,3 +21,6 @@ sed -Ei "s|VER_MINOR:.+|VER_MINOR: $MINOR_VERSION,|" src/js/engine.js sed -Ei "s|web interpreter v.+|web interpreter v$FULL_VERSION|" src/js/webfrontend.html sed -Ei "s|const VERSION = .+|const VERSION = \"$FULL_VERSION\";|" src/js/nodefrontend.js + +sed -Ei "s|#define AAVM_FORMAT_MAJOR .+|#define AAVM_FORMAT_MAJOR $MAJOR_VERSION|" src/aavm.h +sed -Ei "s|#define AAVM_FORMAT_MINOR .+|#define AAVM_FORMAT_MINOR $MINOR_VERSION|" src/aavm.h diff --git a/docs/aam-specification-1.1.adoc b/docs/aam-specification-1.1.adoc index f5c12b4..d2822fa 100644 --- a/docs/aam-specification-1.1.adoc +++ b/docs/aam-specification-1.1.adoc @@ -1,4 +1,4 @@ -= Å-machine 1.0 Specification += Å-machine 1.1 Specification This file contains a detailed description of the Å-machine (Aa-machine) version 1.0, including the two file formats (story and saved game) and the diff --git a/src/aavm.h b/src/aavm.h index 50d0002..e7eb48c 100644 --- a/src/aavm.h +++ b/src/aavm.h @@ -1,5 +1,5 @@ #define AAVM_FORMAT_MAJOR 1 -#define AAVM_FORMAT_MINOR 0 +#define AAVM_FORMAT_MINOR 1 #define AA_NOP 0x00 #define AA_FAIL 0x01 From ddb9f67ec8b931a537fb8505ed1ed7f63c8230eb Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Thu, 10 Sep 2026 19:45:56 -0500 Subject: [PATCH 5/7] replace 6502 multiplication algorithm again; the old one did not always detect overflow correctly --- src/6502/engine.s | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/6502/engine.s b/src/6502/engine.s index 971295f..d628f47 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -9257,7 +9257,8 @@ nobit .) */ -mul16 +/* +badmul16 ; omult16.a from https://github.com/TobyLobster/multiply_test ; from BBC BASIC 2 ROM ; input quot, denom @@ -9299,6 +9300,50 @@ skip overflow rts .) +*/ + +mul16 + ; omult6.a from https://github.com/TobyLobster/multiply_test + ; from Commodore 64 kernal ROM + ; also in Applesoft II BASIC + ; input quot, denom + ; output numer, c (set on overflow) + ; clobbers quot, denom, remain, a, x, y + ; note - this algorithm starts with the high bit of the result, not the low bit + .( + ; we repurpose remain here for the bit counter + lda #16 + sta remain + ldx #0 ; result low + ldy #0 ; result high +loop + txa ; get result low + asl ; mult by 2 + tax ; save result low + tya ; get result high + rol ; mult by 2 + tay ; save result high + bcs overflow + asl quot ; mult by 2 + rol quot+1 + bcc skip ; skip add if no carry + clc ; otherwise clear carry for adc + txa ; get result low + adc denom ; add denom low to it + tax + tya ; get result high + adc denom+1 ; add denom high to it + tay + bcs overflow +skip + dec remain + bne loop + stx numer ; save result + sty numer+1 +; clc ; must be clear because none of the instructions after the bcs affect it +overflow + rts ; return whatever result we have + .) div16 ; input numer, denom From d568afd8a816168f23c75be55891b831aa36454f Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Tue, 22 Sep 2026 15:02:55 -0500 Subject: [PATCH 6/7] cleanup --- docs/aam-specification-1.1.adoc | 5 +++-- src/6502/c64_frontend.s | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/aam-specification-1.1.adoc b/docs/aam-specification-1.1.adoc index d2822fa..c9b9787 100644 --- a/docs/aam-specification-1.1.adoc +++ b/docs/aam-specification-1.1.adoc @@ -1,9 +1,10 @@ = Å-machine 1.1 Specification This file contains a detailed description of the Å-machine (Aa-machine) -version 1.0, including the two file formats (story and saved game) and the +version 1.1, including the two file formats (story and saved game) and the bytecode semantics. Be warned that the description is technically -comprehensive and rather condensed. +comprehensive and rather condensed. But we are always working to make this +specification more readable, and feedback is welcome! Note that 1.0 is not any more official or stable than its predecessor, 0.5. The major version change marks where the community forked the project and diff --git a/src/6502/c64_frontend.s b/src/6502/c64_frontend.s index 333937f..02e0e73 100644 --- a/src/6502/c64_frontend.s +++ b/src/6502/c64_frontend.s @@ -509,7 +509,7 @@ io_mstyle ; input a = style bits ; 1 reverse, 2 bold, 4 italic ; Or on C64: - ; 1 warm, 2 light, 4 blue + ; 1 warm, 2 light, 4 saturated .( pha From c5e56940df05231ea11372a07849720f6e5247ce Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Tue, 22 Sep 2026 15:09:42 -0500 Subject: [PATCH 7/7] make sure binary codepoints.aastory file is checked in --- test/codepoints/codepoints.aastory | Bin 9204 -> 9202 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/codepoints/codepoints.aastory b/test/codepoints/codepoints.aastory index fbc190b436a48f039b60a99583d7322a598db0de..95ee5e97dd9e88127c9064a59c7dfc39a92fc658 100644 GIT binary patch delta 5682 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 MDh^v9Qar`|U-oXMjQ{`u delta 5659 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