diff --git a/docs/aam-specification-1.0.adoc b/docs/aam-specification-1.0.adoc index 2184733..d46b90f 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`` <> ≥ 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
@@ -3344,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
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];