From a8cb3a58045a0abad1cb71597338b8e156a93528 Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Fri, 26 Jun 2026 20:20:35 -0500 Subject: [PATCH 1/5] copy data-* into the HTML along with aria-* --- src/js/webfrontend.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/webfrontend.js b/src/js/webfrontend.js index ca46fd0..6b251cd 100644 --- a/src/js/webfrontend.js +++ b/src/js/webfrontend.js @@ -207,14 +207,14 @@ function prepare_styles(styles, style_data) { for(i = 0; i < styles.length; i++) { mono = false; let name = "aa-" + (styles[i]["style-name"] || i); - name = name.replace(/[^a-z0-9-]/g, '-'); // There shouldn't be spaces and such, but sanitize just in case + name = name.replace(/[^a-zA-Z0-9-]/g, '-'); // There shouldn't be spaces and such, but sanitize just in case if(name in style_data) name = "aax-" + i; // Emergency fallback, guaranteed not to conflict style_data[i] = { name:name, attrs:{} }; html += "." + name + " { "; for(j in styles[i]) { - if(j.startsWith("aria-")) { // Copy aria-* declarations to a special array, since we want to assign these to the HTML tag, not just leave them in the CSS - if(j == "aria-role") { // The HTML name is simply "role" + if(j.startsWith("aria-") || j.startsWith("data-") { // Copy aria-* and data-* declarations to a special array, since we want to assign these to the HTML tag, not just leave them in the CSS + if(j == "aria-role") { // The HTML name is simply "role", not "aria-role", though it is an ARIA attribute style_data[i].attrs["role"] = styles[i][j]; } else { style_data[i].attrs[j] = styles[i][j]; From 024162ac5cbb038f36d2e92a26c87937b26d2412 Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Fri, 3 Jul 2026 19:41:05 -0500 Subject: [PATCH 2/5] first draft of 32-bit spec --- docs/aam-specification-1.0.adoc | 623 ++++++++++++++++++++++++++++---- 1 file changed, 543 insertions(+), 80 deletions(-) diff --git a/docs/aam-specification-1.0.adoc b/docs/aam-specification-1.0.adoc index 2184733..0e1e7b2 100644 --- a/docs/aam-specification-1.0.adoc +++ b/docs/aam-specification-1.0.adoc @@ -1,16 +1,22 @@ = Å-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 +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. -Note that 1.0 is not any more official or stable than its predecessor, 0.5. +Note that 1.x 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 + +32-bit story files are now supported + +New opcode: EXT32 + == Changes in v1.0 New opcodes: @@ -77,9 +83,12 @@ all other numbers are decimal. === Words -Most data is organized into words. A word is currently always 16 bits. +Most data is organized into words. The size of a word is stored in the <>: +either 2 bytes (16 bits) or 4 bytes (32 bits). When words are stored in a save file, the byte order is big-endian. +==== 16-bit words + The internal format of a 16-bit word could be: *A raw 16-bit value*, such as an index into one of the heap @@ -181,8 +190,120 @@ The special value `3f3f` marks unused words in the main heap, aux, and random access areas. This is handy for measuring peak memory usage. +==== 32-bit words + +The internal format of a 32-bit word could be: + +*A raw 32-bit value*, such as an index into one of the heap +areas. + +*32 independent flags*, numbered 00..1f 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. + +[[literal32,literal]] +*A literal value*, as follows: + +* ``0000 0001..0fff ffff`` Object +* ``1000 0000..1fff ffff`` Dict word (index into dictionary) +* ``2000 0000..2010 ffff`` Character (Unicode codepoint) +* ``2011 0000..2fff ffff`` reserved +* ``3000 0000 `` `[]` (empty list) +* ``3000 0001..3fff ffff`` reserved +* ``4000 0000..4fff ffff`` 28-bit signed integer +* ``5000 0000..7fff ffff`` reserved + +[[live32]] +*A live value (tagged reference)*, as follows: + +* ``0000 0000 `` undefined +* ``0000 0001..7fff ffff`` <> +* ``8000 0000..8fff ffff`` Indirect reference (word index into heap) +* ``9000 0000..9fff ffff`` reserved +* ``a000 0000..afff ffff`` Pair (word index into heap) +* ``b000 0000..bfff ffff`` Dict word, extended (word index into heap) +* ``c000 0000..ffff ffff`` reserved + +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" (the top four bits) +and "value" (the bottom 28 bits). + +* `0` null_tag (v = 0) +* `0` object (v > 0) +* `1` word +* `2` char +* `3` empty_tag (v = 0) +* `4` number +* `8` reference +* `a` pair +* `b` extdict + +Tag 3 (v > 0) and 5-7 are reserved for future simple values. +Tags 9 and c-f are reserved for future reference types. + +Tag `1` may also be referred to as a "dict" to avoid ambiguity with the +other meaning of "word" (a two-byte or four-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 0000 `` Unset (Null) +* ``0000 0001..7fff ffff`` <> +* ``8000 0000..bfff ffff`` Word index into long-term storage area +* ``c000 0000..ffff ffff`` reserved + +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 0000 `` End of stream marker +* ``0000 0001..7fff ffff`` <> +* ``8000 0000 `` Unbound variable +* ``8000 0001..80ff ffff`` reserved +* ``8100 0000 `` Extdict (two words) follows +* ``8100 0001..9fff ffff`` reserved +* ``a000 0000..afff ffff`` Proper list with (x & 0fff ffff) elements follows +* ``b000 0000..bfff ffff`` Improper list with (x & 0fff ffff) elements follows +* ``c000 0000..ffff ffff`` reserved + +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 3f3f` marks unused words in the main heap, +aux, and random access areas. This is handy for measuring peak +memory usage. + === Text +==== 16-bit text + +The following applies only when the word size is 16 bits. + The game-specific *character set* is a single-byte encoding. * ``00-1f`` are reserved. Some represent special keypresses: @@ -218,6 +339,31 @@ Huffman-inspired encoding. There is a game-specific bitstream decoding tree with up to 128 nodes (see <>). +==== 32-bit text + +The following applies only when the word size is 32 bits. + +The *character set* is a three-byte (24-bit) encoding. + +``00-1f`` are reserved. Some represent special keypresses: + +`08` backspace/delete +`0d` return +`10` up +`11` down +`12` left +`13` right + +Characters from `20` to `10ffff` are identical to Unicode. +Characters above `10ffff` are undefined. + +Characters from the *character set* may appear in input, dictionary +words, and output. As part of input processing, characters must be +converted to lowercase. + +Strings are sequences of characters encoded in UTF-8, a standard +variable-length encoding for Unicode text. + === Game state ==== Unsaved data @@ -291,7 +437,7 @@ starts or restarts. The exceptions are: * `ENV = <>.heapsz` * `CHO = <>.heapsz` * `TRL = <>.auxsz` -* `SIM = ffff` +* `SIM = ffff` (or `ffff ffff` depending on word size) * `SPC = line` In case of a runtime error, the machine is restarted with R00 @@ -364,7 +510,7 @@ variables (not affiliated with any object). 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`). +chunk (padded by copies of the 'unused' word, `3f3f` or `3f3f 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 @@ -432,9 +578,9 @@ story file. 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 +'unused' word (`3f3f` or `3f3f 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 @@ -485,7 +631,7 @@ address space. === HEAD * `BYTE[2]`: "version", File format version (major, minor) -* `BYTE`: "wordsz", Word size (currently always 2) +* `BYTE`: "wordsz", Word size (either 2 or 4) * `BYTE`: "shift", Shift amount for short/long string pointers * `SHORT`: "release", Story release number @@ -506,16 +652,24 @@ 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. +3f3f or 3f3f 3f3f. [[LANG]] === LANG +In 16-bit story files, this chunk begins with: + * `BYTE[2]`: Offset in chunk of <> * `BYTE[2]`: Offset in chunk of <> * `BYTE[2]`: Offset in chunk of <> * `BYTE[2]`: Offset in chunk of <> +In 32-bit story files, the bitstream decoding table and extended character table +are not present. The chunk instead begins with: + +* `BYTE[2]`: Offset in chunk of <> +* `BYTE[2]`: Offset in chunk of <> + [tbl-decode] ==== Bitstream decoding table @@ -546,6 +700,8 @@ Escape code handling for story file version 0.4 or later: * If X < B, produce character $a0 + X. * Otherwise, produce a space character ($20) followed by dictionary word X - B. +This table is not present in 32-bit files. + [tbl-extchar] ==== Extended character table @@ -554,9 +710,13 @@ Escape code handling for story file version 0.4 or later: ** `BYTE`: Corresponding uppercase character (can be the same) ** `BYTE[3]`: Unicode codepoint +This table is not present in 32-bit files. + [tbl-ending] ==== Word endings decoder +===== 16-bit + An array of instruction bytes: * `00`: Fail @@ -573,9 +733,29 @@ current ending). the ending and resume execution at byte offset yy in the decoder. +===== 32-bit + +An array of instruction words: + +* `00 00 00 00`: Fail +* `00 00 00 01`: Check +* `xx xx 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 *word* (not byte!) offset yy in the +decoder. + [tbl-stop] ==== Special characters table +===== 16-bit + * `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 @@ -584,39 +764,72 @@ The following fields are present if the storyfile version is * `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] +===== 32-bit + +* `WORD[]`: 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. + +* `WORD[]`: Null-terminated subset of stop characters that will inhibit whitespace before them, such as ".". [spec 0.4] +* `WORD[]`: 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) +** `BYTE`: Length of word in bytes (2..ff) +** `WORD`: Start of characters (byte offset in chunk) -Strings of characters (arrays of bytes in the game-specific character -set) referenced by the dictionary. No terminator. Can overlap. +Strings of characters referenced by the dictionary. No terminator. Can overlap. +In 16-bit story files, each word is an array of bytes in the game-specific character set. +In 32-bit story files, each word is an array of characters encoded in UTF-8. +In both cases, the length of each string is given in bytes, not characters. [[MAPS]] === MAPS -* `BYTE[2]`: Number of word-to-object maps -** `BYTE[2]`: Offset in chunk of word-to-object map +==== 16-bit + +* `SHORT`: Number of word-to-object maps +** `SHORT`: Byte offset in chunk of word-to-object map Each word-to-object map: -* `BYTE[2]`: Number of entries (sorted by index value) +* `SHORT`: 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') +** `WORD`: Single object id + e000, byte 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) +* ``e0..ff xx`` Long object id plus e000 (0001..1fff) + +==== 32-bit + +* `SHORT`: Number of word-to-object maps +** `SHORT`: *Word* (not byte!) offset in chunk of word-to-object map + +Each word-to-object map: + +* `SHORT`: Number of entries (sorted by index value) +** `WORD`: Index value (dictionary word or character) +** `WORD`: Single object id, byte offset in chunk of payload + 8000 0000, or null (for wildcard words such as 'the') + +Payload data: + +* ``00 `` End +* ``01..7f `` Short object id (0001..007f) +* ``80..bf xx `` Long object id plus 8000 (0001..3fff) +* ``c0..cf xx xx xx`` Full object id plus c000 0000 (0000 0001..0fff ffff) +* ``d0..ff `` reserved [[LOOK]] === LOOK -* `BYTE[2]`: Number of style classes -** `BYTE[2]`: Offset in chunk of style definition +* `SHORT`: Number of style classes +** `SHORT`: Byte 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. @@ -634,7 +847,7 @@ raising an error. === TAGS (optional) * `WORD`: Number of named objects -** `BYTE[2]`: Offset in chunk of null-terminated object name +** `WORD`: Offset in chunk of null-terminated object name Strings of characters for the object names. @@ -654,22 +867,35 @@ Identifiers: * `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. +In 16-bit story files, these strings use the game-specific character set, +except that $10 (normally undefined for output) is a line feed. + +In 32-bit story files, these strings use UTF-8. Line feeds should always +be $10, not $13. [[WRIT]] === WRIT +==== 16-bit + 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. +==== 32-bit + +A byte-addressable read-only chunk containing null-terminated strings +encoded in UTF-8. 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 +==== 16-bit + +* `SHORT`: Number of resources +** `SHORT`: Byte offset in chunk of descriptor Each descriptor is: @@ -687,6 +913,12 @@ file contents. The URL scheme "file" refers to data inside <> chunks, e.g. "file:title.png". +==== 32-bit + +As above, except that each descriptor instead begins with: + +* `BYTE[4]`: String pointer to alt-text (shift according to header) + [[FILE]] === FILE @@ -715,6 +947,11 @@ 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. +The one exception is the <> opcode (ff). Its number and format +of operands are not currently defined, but may vary depending on the +following bytes. This opcode is always illegal in 16-bit story files, +and thus 16-bit interpreters need not try to decode it. + [[op]] ==== Operands @@ -738,18 +975,32 @@ Operand types (always big-endian byte ordering): * `xxxxxxxx xxxxxxxx`: Number 0000..ffff +In 32-bit story files: + +* `xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx`: Number 0000 0000..ffff ffff + [[VWORD,VWORD]] *VWORD*: constant <> * `0xxxxxxx xxxxxxxx`: Literal 0000..7fff +In 32-bit story files: + +* `0xxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx`: Literal 0000 0000..7fff ffff + [[RAW,RAW]] -*RAW*: unsigned 16-bit number +*RAW*: unsigned word-sized number * `0xxxxxxx xxxxxxxx`: Number 0000..7fff * `10xxxxxx`: Value of register 00..3f * `11xxxxxx`: Value of env slot 00..3f +In 32-bit story files: + +* `0xxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx`: Number 0000 0000..7fff ffff +* `10xxxxxx`: Value of register 00..3f +* `11xxxxxx`: Value of env slot 00..3f + [[VALUE,VALUE]] *VALUE*: <> (tagged reference) @@ -757,6 +1008,12 @@ Operand types (always big-endian byte ordering): * `10xxxxxx`: Value of register 00..3f * `11xxxxxx`: Value of env slot 00..3f +In 32-bit story files: + +* `0xxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx`: Literal 0000 0000..7fff ffff +* `10xxxxxx`: Value of register 00..3f +* `11xxxxxx`: Value of env slot 00..3f + [[DEST,DEST]] *DEST*: destination for the result @@ -772,6 +1029,12 @@ Operand types (always big-endian byte ordering): * `10xxxxxx`: Index 80..bf * `11xxxxxx xxxxxxxx`: Index 0000..3fff +In 32-bit story files: + +* `0xxxxxxx`: Index 00..7f +* `10xxxxxx`: Index 80..bf +* `11xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx`: Index 0000 0000..3fff ffff + [[CODE]] *CODE*: byte address in <> chunk @@ -780,15 +1043,28 @@ Operand types (always big-endian byte ordering): * `01xxxxxx xxxxxxxx`: Relative pointer (± 8 kB), 14-bit signed integer relative to end of operand * `1xxxxxxx xxxxxxxx xxxxxxxx`: Absolute pointer (full 8 MB range) +In 32-bit story files: + +* `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 xxxxxxxx`: Absolute pointer (full 1 GB 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 + right by log2(wordsz) bits, short and long pointers are shifted + right by a number of bits stored in the header -* `0xxxxxxx`: Tiny pointer (shift by 1) +* `0xxxxxxx`: Tiny pointer (shift by log2(wordsz) = 1) * `10xxxxxx xxxxxxxx`: Short pointer (shift per header) * `11xxxxxx xxxxxxxx xxxxxxxx`: Long pointer (shift per header) +In 32-bit story files: + +* `0xxxxxxx`: Tiny pointer (shift by log2(wordsz) = 2) +* `10xxxxxx xxxxxxxx`: Short pointer (shift per header) +* `11xxxxxx xxxxxxxx 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 @@ -1041,6 +1317,9 @@ fd <> <> <> <> ≥ 0.4 {empty} {empty} {empty} {empty} 7f <> <> <> <> <> {empty} +{empty} {empty} {empty} {empty} + +ff <> TBD ≥ 1.1 |=== [[status-values]] @@ -1252,6 +1531,9 @@ them as static text. Such interpreters do not need the nLink variable. There could be errors. Recommendation is to double-check with the javascript interpreter. +In the following pseudocode, 0x1234(5678) means 0x1234 for a 16-bit story +file (word size 2) and 0x12345678 for a 32-bit story file (word size 4). + [[NOP]] === NOP @@ -1263,7 +1545,7 @@ Do nothing. fail def fail: - INST = (heap[CHO + 4] << 16) | heap[CHO + 5] + INST = (heap[CHO + 4] << BITS_PER_WORD) | heap[CHO + 5] leave recursive unify/push/pop/(de)serialize operation [[SET_CONT]] @@ -1276,14 +1558,14 @@ Do nothing. [[PROCEED]] === PROCEED - if(SIM < 0x8000) CHO = SIM + if(SIM < 0x8000(0000)) 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. +below $8000(0000), as it would conflict with existing memory ranges. [[JMP]] === JMP @@ -1301,7 +1583,7 @@ later. *CODE* - SIM = 0xffff + SIM = 0xffff(ffff) INST = arg Perform a multi-jump chain starting at the CODE address provided. @@ -1313,7 +1595,7 @@ SIM is invalidated as a cutting choice point. *CODE* CONT = next_instruction - SIM = 0xffff + SIM = 0xffff(ffff) INST = arg [[JMP_SIMPLE]] @@ -1338,7 +1620,7 @@ SIM is invalidated as a cutting choice point. *CODE* - if(SIM >= 0x8000) SIM = CHO + if(SIM >= 0x8000(0000)) SIM = CHO INST = arg Perform a jump to the CODE address, and save the current choice @@ -1347,7 +1629,7 @@ frame to SIM if there is no existing cutting choice point. [[TAIL]] === TAIL - if(SIM >= 0x8000) SIM = CHO + if(SIM >= 0x8000(0000)) SIM = CHO [[PUSH_ENV]] === PUSH_ENV @@ -1359,23 +1641,23 @@ frame to SIM if there is no existing cutting choice point. heap[addr + 0] = ENV heap[addr + 1] = SIM - heap[addr + 2] = CONT >> 16 - heap[addr + 3] = CONT & 0xffff + heap[addr + 2] = CONT >> BITS_PER_WORD + heap[addr + 3] = CONT & 0xffff(ffff) ENV = addr [[POP_ENV]] === POP_ENV - CONT = (heap[ENV + 2] << 16) | heap[ENV + 3] + CONT = (heap[ENV + 2] << BITS_PER_WORD) | 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] + INST = (heap[ENV + 2] << BITS_PER_WORD) | heap[ENV + 3] + if(heap[ENV + 1] < 0x8000(0000)) CHO = heap[ENV + 1] ENV = heap[ENV + 0] [[PUSH_CHOICE]] @@ -1390,10 +1672,10 @@ frame to SIM if there is no existing cutting choice point. 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 + 2] = CONT >> BITS_PER_WORD + heap[addr + 3] = CONT & 0xffff(ffff) + heap[addr + 4] = next >> BITS_PER_WORD + heap[addr + 5] = next & 0xffff(ffff) heap[addr + 6] = CHO heap[addr + 7] = TOP heap[addr + 8] = TRL @@ -1414,7 +1696,7 @@ frame to SIM if there is no existing cutting choice point. heap[aux[TRL++]] = 0 } TOP = heap[CHO + 7] - CONT = (heap[CHO + 2] << 16) | heap[CHO + 3] + CONT = (heap[CHO + 2] << BITS_PER_WORD) | heap[CHO + 3] SIM = heap[CHO + 1] ENV = heap[CHO + 0] CHO = heap[CHO + 6] @@ -1424,8 +1706,8 @@ frame to SIM if there is no existing cutting choice point. *BYTE/0 CODE* - heap[CHO + 4] = arg2 >> 16 - heap[CHO + 5] = arg2 & 0xffff + heap[CHO + 4] = arg2 >> BITS_PER_WORD + heap[CHO + 5] = arg2 & 0xffff(ffff) for(local variable i = 0; i < arg1; i++) { R[i] = heap[CHO + 9 + i] } @@ -1433,7 +1715,7 @@ frame to SIM if there is no existing cutting choice point. heap[aux[TRL++]] = 0 } TOP = heap[CHO + 7] - CONT = (heap[CHO + 2] << 16) | heap[CHO + 3] + CONT = (heap[CHO + 2] << BITS_PER_WORD) | heap[CHO + 3] SIM = heap[CHO + 1] ENV = heap[CHO + 0] @@ -1596,20 +1878,28 @@ frame to SIM if there is no existing cutting choice point. count++ v = deref() if(v == empty_list) { - v = 0xc000 + count + if(wordsz == 2) { + v = 0xc000 + count + } else { + v = 0xa000_0000 + count + } break } else if(v.tag != pair) { push_serialized(v) - v = 0xe000 + count + if(wordsz == 2) { + v = 0xe000 + count + } else { + v = 0xb000_0000 + count + } break } } } else if(v.tag == extdict) { push_serialized(heap[v.value + 1]) push_serialized(heap[v.value + 0]) - v = 0x8100 + v = 0x8100(0000) } else if(v.tag == reference) { - v = 0x8000 + v = 0x8000(0000) } if(AUX >= TRL) runtime_error(2) aux[AUX++] = v @@ -1635,19 +1925,19 @@ is nevertheless used by <>. def pop_serialized: local variable v = aux[--AUX] - if(v == 0x8000) { + if(v == 0x8000(0000)) { local variable addr = TOP++ if(TOP > min(ENV, CHO)) runtime_error(1) heap[addr] = 0 v = - } else if(v == 0x8100) { + } else if(v == 0x8100(0000)) { 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) { + } else if(wordsz == 2 && (v & 0xc000) == 0xc000) { local variable count = v & 0x1fff if(v & 0x2000) { v = pop_serialized() @@ -1662,6 +1952,21 @@ is nevertheless used by <>. heap[addr + 1] = v v = } + } else if(wordsz == 4 && (v & 0xa000_0000) == 0xa000_0000) { + local variable count = v & 0x0fff_ffff + if(v & 0x1000_0000) { + 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 @@ -1902,8 +2207,16 @@ is nevertheless used by <>. *VALUE/0 INDEX DEST* - arg3 <- (read_field(arg2 / 2, deref(arg1)) - >> ((arg2 & 1)? 0 : 8)) & 0xff + local variable offset = BITS_PER_WORD - 8 * ((arg2 % wordsz) + 1) + arg3 <- (read_field(arg2 / wordsz, deref(arg1)) + >> offset) & 0xff + +In other words, in a 16-bit file, index 2 has an offset of 8 (to +access the high byte of the word), while index 3 has an offset of 0 +(to access the low byte). In a 32-bit file, index 4 has an offset +of 24 (highest byte), index 5 has an offset of 16 (second highest), +index 6 has an offset of 8 (second lowest), and index 7 has an offset +of 0 (lowest byte). [[LOAD_VAL]] === LOAD_VAL @@ -1917,8 +2230,8 @@ is nevertheless used by <>. arg3 <- val def get_longterm(v): - if(v & 0x8000) { - TMP = v & 0x7fff + if(v & 0x8000(0000)) { + TMP = v & 0x7fff(ffff) TMP += ram[TMP] v = pop_longterm() } @@ -1926,20 +2239,20 @@ is nevertheless used by <>. def pop_longterm(): local variable v = ram[--TMP] - if(v == 0x8000) { + if(v == 0x8000(0000)) { local variable addr = TOP TOP++ if(TOP > min(ENV, CHO)) runtime_error(1) heap[addr] = 0 v = - } else if(v == 0x8100) { + } else if(v == 0x8100(0000)) { 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) { + } else if(wordsz == 2 && (v & 0xc000) == 0xc000) { local variable count = v & 0x1fff if(v & 0x2000) { v = pop_longterm() @@ -1954,6 +2267,21 @@ is nevertheless used by <>. heap[addr + 1] = v v = } + } else if(wordsz == 4 && (v & 0xa000_0000) == 0xa000_0000) { + local variable count = v & 0x0fff_ffff + if(v & 0x1000_0000) { + 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 @@ -1969,6 +2297,8 @@ is nevertheless used by <>. *VALUE/0 INDEX VALUE* +For 16-bit story files: + local variable addr = field_addr(arg2 / 2, deref(arg1)) if(arg2 & 1) { ram[addr] = (ram[addr] & 0xff00) | (arg3 & 0xff) @@ -1976,6 +2306,19 @@ is nevertheless used by <>. ram[addr] = (ram[addr] & 0x00ff) | (arg3 << 8) } +For 32-bit story files: + + local variable addr = field_addr(arg2 / 4, deref(arg1)) + if(arg2 % 4 == 0) { + ram[addr] = (ram[addr] & 0x00ff_ffff) | ((arg3 << 24) & 0xff) + } else if(arg2 % 4 == 1) { + ram[addr] = (ram[addr] & 0xff00_ffff) | ((arg3 << 16) & 0xff) + } else if(arg2 % 4 == 2) { + ram[addr] = (ram[addr] & 0xffff_00ff) | ((arg3 << 8) & 0xff) + } else { // arg2 % 4 == 3 + ram[addr] = (ram[addr] & 0xffff_ff00) | (arg3 & 0xff) + } + [[STORE_VAL]] === STORE_VAL @@ -1995,7 +2338,7 @@ is nevertheless used by <>. TMP = LTT + 2 if(TMP > HEAD.ramsz) runtime_error(6) push_longterm(v) - ram[addr] = 0x8000 + LTT + ram[addr] = 0x8000(0000) + LTT ram[LTT + 0] = TMP - LTT ram[LTT + 1] = addr LTT = TMP @@ -2013,18 +2356,26 @@ is nevertheless used by <>. count++ v = deref(heap[v.value + 1]) if(v == empty_list) { - v = 0xc000 | count + if(wordsz == 2) { + v = 0xc000 | count + } else { + v = 0xa000_0000 | count + } break } else if(v.tag != pair) { push_longterm(v) - v = 0xe000 | count + if(wordsz == 2) { + v = 0xe000 | count + } else { + v = 0xb000_0000 | count + } break } } } else if(v.tag == extdict) { push_longterm(heap[v.value + 1]) push_longterm(heap[v.value + 0]) - v = 0x8100 + v = 0x8100(0000) } else if(v.tag == reference) { runtime_error(4) } @@ -2034,9 +2385,9 @@ is nevertheless used by <>. [[clear_longterm]] def clear_longterm(addr): local variable v = ram[addr] - if(v & 0x8000) { + if(v & 0x8000(0000)) { ram[addr] = 0 - v &= 0x7fff + v &= 0x7fff(ffff) local variable size = ram[v] for(local variable i = v; i < LTT - size; i++) { @@ -2068,6 +2419,9 @@ is nevertheless used by <>. ~((1 << (BITS_PER_WORD - 1)) >> (arg2 % BITS_PER_WORD)) } +It's legal to reset a flag for an invalid object (but it will have +no effect). + [[UNLINK]] === UNLINK @@ -2387,7 +2741,7 @@ is nevertheless used by <>. *RAW RAW DEST* - arg3 <- (arg1 + arg2) & 0xffff + arg3 <- (arg1 + arg2) & 0xffff(ffff) [[ADD_NUM]] === ADD_NUM @@ -2397,6 +2751,8 @@ is nevertheless used by <>. local variable result = unbox_int(arg1) + unbox_int(arg2) arg3 <- box_int(result) +For 16-bit story files: + def unbox_int(v): v = deref(v) if(v.tag != number) fail @@ -2406,12 +2762,30 @@ is nevertheless used by <>. if(v < 0 || v > 0x3fff) fail return +For 32-bit story files, where sign extension is needed: + + def unbox_int(v): + v = deref(v) + if(v.tag != number) fail + return (v.value << 4) >> 4 + + def box_int(v): + if(v & 0xf000_0000 != 0 && + v & 0xf000_0000 != 0xf000_0000) { + fail + } + return + +In a 32-bit story file, a number is out of range if its top four +bits are not 0000 or 1111; or, equivalently, if it is less than +-2^27^ or more than 2^27^ - 1. + [[SUB_RAW]] === SUB_RAW *RAW RAW DEST* - arg3 <- (arg1 - arg2) & 0xffff + arg3 <- (arg1 - arg2) & 0xffff(ffff) [[SUB_NUM]] === SUB_NUM @@ -2427,7 +2801,11 @@ is nevertheless used by <>. *VALUE VALUE DEST* local variable result = unbox_int(arg1) * unbox_int(arg2) - result = result & 0x3fff + if(wordsz == 2) { + result = result & 0x3fff + } else { + result = result & 0x0fff_ffff + } arg3 <- box_int(result) [[DIV_NUM]] @@ -2448,6 +2826,9 @@ is nevertheless used by <>. local variable result = unbox_int(arg1) % unbox_int(arg2) arg3 <- box_int(result) +The result should have the sign of arg1. In other words, this is a +floored modulo, not a truncated remainder. + [[RAND_NUM]] === RAND_NUM @@ -2466,7 +2847,7 @@ is nevertheless used by <>. *BYTE DEST* // any timer-seeded PRNG can be used: - arg2 <- (rand() % (arg1 + 1)) & 0xffff + arg2 <- (rand() % (arg1 + 1)) & 0xffff(ffff) [[INC_NUM]] === INC_NUM @@ -2489,14 +2870,14 @@ is nevertheless used by <>. *RAW DEST* - arg2 <- (arg1 + 1) & 0xffff + arg2 <- (arg1 + 1) & 0xffff(ffff) [[DEC_RAW]] === DEC_RAW *RAW DEST* - arg2 <- (arg1 - 1) & 0xffff + arg2 <- (arg1 - 1) & 0xffff(ffff) [[PRINT_A_STR_A]] === PRINT_A_STR_A @@ -2705,7 +3086,7 @@ is nevertheless used by <>. SPC = line } -Previous versions of this specification erroneously said SPC +Pre-1.0 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. @@ -2945,13 +3326,16 @@ left to the implementation. if(CWL == 0) { arg1 = deref(arg1) arg2 = deref(arg2) - if(arg1.tag == number && arg2.tag == number) { + if(arg1.tag == number && arg2.tag == number + && arg1.value >= 0 && arg2.value >= 0) { output_progress_bar(arg1.value, arg2.value) } } Prints a progress bar with `arg1/arg2` filled. +TODO: What should happen if arg2.value == 0? + [[QUIT]] === QUIT @@ -3078,11 +3462,20 @@ values as follows: // Check for a decimal number in the range 0-16383: local variable num = parse_decimal(input) - if(num && num >= 0 && num <= 16383) { + if(wordsz == 2 && num && num >= 0 && num <= 16383) { + return + } + // Or, for 32-bit files, -134217728 - 134217727: + if(wordsz == 4 && num && num >= -2**27 && num <= 2**27-1) { return } - // Otherwise, run the word endings decoder: + // Otherwise, run the word endings decoder + return endings_decoder(input); + } + + // For 16-bit story files: + def endings_decoder(input) { local variable state = 0 local variable ending = empty_list local variable pos = input.length @@ -3150,6 +3543,76 @@ values as follows: } } } + + // For 32-bit story files: + def endings_decoder(input) { + local variable state = 0 + local variable ending = empty_list + local variable pos = input.length + while(true) { + local variable addr + if(decoder[state] == 0x0000_0000) { + 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] == 0x0000_0001) { + 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] >> 8) { + addr = TOP + TOP += 2 + if(TOP > min(ENV, CHO)) { + runtime_error(1) + } + heap[addr + 0] = + heap[addr + 1] = ending + ending = + state = decoder[state] & 0x0000_00ff + pos-- + } else { + state++ + } + } + } + } [[GET_KEY]] === GET_KEY From 2c110641fe1fd431d054b6c24f4819df228df6dd Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Fri, 3 Jul 2026 19:47:13 -0500 Subject: [PATCH 3/5] add EXT32 entry --- docs/aam-specification-1.0.adoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/aam-specification-1.0.adoc b/docs/aam-specification-1.0.adoc index 0e1e7b2..9220273 100644 --- a/docs/aam-specification-1.0.adoc +++ b/docs/aam-specification-1.0.adoc @@ -3807,6 +3807,12 @@ if it's there. // event, predicate, file, line vm_tracepoint(arg1, arg2, arg3, arg4) +[[EXT32]] +=== EXT32 + +The operands and semantics of this opcode are not yet defined. +It is guaranteed to always be illegal in 16-bit story files. + == Examples From 3358434269d54eaabebbc08fe5312ca1b7ca53d2 Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Fri, 3 Jul 2026 19:49:11 -0500 Subject: [PATCH 4/5] don't make MAPS use word pointers if the result isn't word-aligned --- docs/aam-specification-1.0.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/aam-specification-1.0.adoc b/docs/aam-specification-1.0.adoc index 9220273..04b6b26 100644 --- a/docs/aam-specification-1.0.adoc +++ b/docs/aam-specification-1.0.adoc @@ -809,7 +809,7 @@ Payload data: ==== 32-bit * `SHORT`: Number of word-to-object maps -** `SHORT`: *Word* (not byte!) offset in chunk of word-to-object map +** `SHORT`: Byte offset in chunk of word-to-object map Each word-to-object map: From dbb9b0e69fe0d6286026c2d6b588defe2cfe5461 Mon Sep 17 00:00:00 2001 From: Daniel Stelzer Date: Sun, 16 Aug 2026 22:09:28 -0500 Subject: [PATCH 5/5] update LONGs --- docs/aam-specification-1.0.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/aam-specification-1.0.adoc b/docs/aam-specification-1.0.adoc index 04b6b26..d46b90f 100644 --- a/docs/aam-specification-1.0.adoc +++ b/docs/aam-specification-1.0.adoc @@ -847,7 +847,7 @@ raising an error. === TAGS (optional) * `WORD`: Number of named objects -** `WORD`: Offset in chunk of null-terminated object name +** `WORD`: Byte offset in chunk of null-terminated object name Strings of characters for the object names. @@ -917,7 +917,7 @@ The URL scheme "file" refers to data inside <> chunks, e.g. As above, except that each descriptor instead begins with: -* `BYTE[4]`: String pointer to alt-text (shift according to header) +* `LONG`: String pointer to alt-text (shift according to header) [[FILE]] === FILE