Okay! Here’s my initial sketch. Please comment if you see any issues, have any questions, or otherwise want to give feedback; I don’t have a lot of experience designing VMs on my own. (I recommend highlighting sections and hitting “quote” as you go, because this is long.)
Literals
These are the values that don’t need heap storage: they fit in a single word. On the 16-bit Å-machine, they used variable-width tags to give each type a different amount of the available space: integers needed a full half of it, while characters only needed one byte.
000vvvvv vvvvvvvv object (v > 0)
001vvvvv vvvvvvvv dictionary word (v < $1E00)
00111110 vvvvvvvv character
00111111 00000000 [] (nil, the empty list)
00111111 00111111 sentinel for unused memory
00111111 vvvvvvvv (reserved) (all other v)
01vvvvvv vvvvvvvv unsigned integer
On a 32-bit machine, there’s plenty of space to go around. I propose switching to consistent four-bit tags: every literal type uses four bits for the tag. One of those bits marks it as a literal; the other three determine the type.
0000 object
0001 dictionary word
0010 character
0011 special constant
0100 signed integer
0101 (reserved)
0110 (reserved)
0111 (reserved)
0000vvvv vvvvvvvv vvvvvvvv vvvvvvvv object (v > 0)
0001vvvv vvvvvvvv vvvvvvvv vvvvvvvv dictionary word
00100000 000vvvvv vvvvvvvv vvvvvvvv character
0010vvvv vvvvvvvv vvvvvvvv vvvvvvvv (reserved) (v > $10FFFF)
00110000 00000000 00000000 00000000 [] (nil, the empty list)
00111111 00111111 00111111 00111111 sentinel for unused memory
0011vvvv vvvvvvvv vvvvvvvv vvvvvvvv (reserved) (all other v)
0100vvvv vvvvvvvv vvvvvvvv vvvvvvvv signed integer
0101vvvv vvvvvvvv vvvvvvvv vvvvvvvv (reserved)
0110vvvv vvvvvvvv vvvvvvvv vvvvvvvv (reserved)
0111vvvv vvvvvvvv vvvvvvvv vvvvvvvv (reserved)
Key points:
- This allows storing a full Unicode codepoint in the “character” type
- The sentinel value for unused memory ($3F3F) remains, and is still part of the “special constants” section
- On the 16-bit machine, the integer range (unsigned 14-bit) was chosen so it could represent all possible four-digit numbers. This new range (signed 28-bit) can represent all possible eight-digit numbers, while also handling signed math.
- There’s a big chunk of the literal space remaining for future expansion, in case there’s some new type of literal we want to implement (maybe for Dialog, maybe for some other language)
- There’s still no object 0, so “all zeroes” works as a null value that doesn’t represent anything
Live values
These are values living in registers or on the heap. Previously, these used three-bit tags:
0vvvvvvv vvvvvvvv literal (v > 0)
100vvvvv vvvvvvvv reference (word index into the heap)
101vvvvv vvvvvvvv (reserved)
110vvvvv vvvvvvvv pair (cons cell; word index into the heap)
111vvvvv vvvvvvvv extended dictionary word (word index into the heap)
Like with literals, I propose switching to four-bit tags. There are plenty of bits available, and using four-bit tags consistently across data types can make them easier to generalize about.
0vvv literal
1000 reference
1001 (reserved)
1010 pair
1011 extdict
11** (reserved)
0vvvvvvv vvvvvvvv vvvvvvvv vvvvvvvv literal (v > 0)
1000vvvv vvvvvvvv vvvvvvvv vvvvvvvv reference (word index into the heap)
1001vvvv vvvvvvvv vvvvvvvv vvvvvvvv (reserved)
1010vvvv vvvvvvvv vvvvvvvv vvvvvvvv pair (cons cell; word index into the heap)
1011vvvv vvvvvvvv vvvvvvvv vvvvvvvv extended dictionary word (word index into the heap)
11**vvvv vvvvvvvv vvvvvvvv vvvvvvvv (reserved)
I kept the one (reserved) tag in between “reference” and “pair” for consistency with the 16-bit machine. If some new heap type is implemented on the 16-bit machine, it can take that slot on both versions. If some new heap type is implemented on the 32-bit machine only (like floats), it can use one of the new tags at the end.
(See below in the serialized data section for some other options. We could also give pairs and extdicts the tags 1100 and 1110, so that their prefixes look the same as before in hex.)
Persistent variables
These are values living in global and object variables, which need to remain valid no matter how the heap grows and shrinks. Previously, these used one-bit tags:
00000000 00000000 unset/null
0vvvvvvv vvvvvvvv literal (v > 0)
1vvvvvvv vvvvvvvv word index into the long-term heap
And…in this case I don’t really see a need to change that. It’s either a literal or it’s not. We could switch to four-bit tags for consistency—we certainly don’t need 31 bits’ worth of long-term storage space—but I don’t know what would ever get stored in a variable beyond “literal” and “pointer to non-literal”.
00000000 00000000 00000000 00000000 unset/null
0vvvvvvv vvvvvvvv vvvvvvvv vvvvvvvv literal (v > 0)
1vvvvvvv vvvvvvvv vvvvvvvv vvvvvvvv word index into the long-term heap
Serialized data
When data is stored on the main heap, it uses the live value encoding above. But sometimes, arbitrary data needs to be serialized into a sequence of words that can be stashed on the aux heap or put into long-term storage. That’s what this is for.
Previously this could either be analyzed as variable-length tags or three-bit tags:
00000000 00000000 end of stream
0vvvvvvv vvvvvvvv literal (v > 0)
10000000 00000000 unbound variable
10000001 00000000 extdict (two elements) follows
10vvvvvv vvvvvvvv reserved (all other v)
110vvvvv vvvvvvvv proper list with v elements follows
111vvvvv vvvvvvvv improper list with v elements follows
And like with the other data types, I think switching to four-bit tags for consistency will make implementation easier without sacrificing anything important.
0vvv literal
1000 fixed-length data type
1001 (reserved)
1010 proper list
1011 improper list
11** (reserved)
00000000 00000000 00000000 00000000 end of stream
0vvvvvvv vvvvvvvv vvvvvvvv vvvvvvvv literal (v > 0)
10000000 00000000 00000000 00000000 unbound variable
10000001 00000000 00000000 00000000 extdict (two elements) follows
1000vvvv vvvvvvvv vvvvvvvv vvvvvvvv (reserved) (all other v)
1001vvvv vvvvvvvv vvvvvvvv vvvvvvvv (reserved)
1010vvvv vvvvvvvv vvvvvvvv vvvvvvvv proper list with v elements follows
1011vvvv vvvvvvvv vvvvvvvv vvvvvvvv improper list with v elements follows
11**vvvv vvvvvvvv vvvvvvvv vvvvvvvv (reserved)
The placement of the (reserved) sections is somewhat arbitrary. I wanted to be similar to the existing encoding, but we could also (e.g.) use 1100 and 1110 for proper and improper lists, which would make their prefixes look the same as before in hexadecimal. I don’t expect to ever run into a list whose length needs all 24 bits, so the details aren’t vitally important here.
Instruction operands
Many of these types don’t really need to change.
BYTE (small numeric constant)
No change.
VBYTE (small object number)
No change. Less important without the 16-bit space constraints, but the first 255 objects still get referenced a lot.
WORD (large numeric constant)
Increase from 16 bits to 32 bits. I considered changing this type to SHORT and making it always 16 bits, but the only place it’s ever used is for line numbers when tracing, and it sounds like those do exceed 16 bits sometimes.
VWORD (literal)
Increase from 16 bits to 32 bits, because the range of literals increased to 32 bits.
RAW (numeric constant, or register holding a numeric constant)
Currently, the Å-machine has $40 (64) global registers and $40 (64) env slots. We could increase this limit, but…is there any reason to? Unlike on Z-machine, the Å-machine’s registers aren’t generally used for global variables; they’re only used for routine arguments, temporaries (local variables), and a couple special purposes. Arguments are limited by the Dialog compiler, so temporaries are the only ones you can run out of—and I’ve never seen that happen without a compiler bug.
So I don’t see a real need to increase the number of registers. I’m happy to be persuaded, though!
Previously:
0vvvvvvv vvvvvvvv number 0000-7FFF
10vvvvvv value of register v (00-3F)
11vvvvvv value of env slot v (00-3F)
Now:
0vvvvvvv vvvvvvvv vvvvvvvv vvvvvvvv number 0000 0000 - 7FFF FFFF
10vvvvvv value of register v (00-3F)
11vvvvvv value of env slot v (00-3F)
VALUE (literal, or register holding a literal or heap reference)
Same as RAW. Same rationale applies. Whatever we decide for one should apply to the other.
Previously:
0vvvvvvv vvvvvvvv literal (v > 0)
10vvvvvv value of register v (00-3F)
11vvvvvv value of env slot v (00-3F)
Now:
0vvvvvvv vvvvvvvv vvvvvvvv vvvvvvvv literal (v > 0)
10vvvvvv value of register v (00-3F)
11vvvvvv value of env slot v (00-3F)
DEST (where to store a result)
As with the last two, this just depends on the number of registers. My current proposal is no change.
Previously and now:
00vvvvvv store into register v (00-3F)
01vvvvvv store into env slot v (00-3F)
10vvvvvv unify with register v (00-3F)
11vvvvvv unify with env slot v (00-3F)
INDEX (medium-sized numeric constant)
This type currently has a range of 0 to $3FFF, for a total of 16,384 possible values. It’s used for variable and property numbers (so this caps the number of global and per-object variables), style classes (so this caps the number of style classes for divs, spans, and body styles), and wordmaps (an optimization trick for looking up objects from dictionary words).
Are these caps worth increasing? It would cost us basically nothing to increase, because the first 192 values can be stored in one byte, so the vast majority of projects would see no change at all.
Previously:
0vvvvvvv Index v (00-7F)
10vvvvvv Index v+80 (80-BF)
11vvvvvv vvvvvvvv Index v (0000-3FFF)
Now, it could stay the same, or it could be:
0vvvvvvv Index v (00-7F)
10vvvvvv Index v+80 (80-BF)
11vvvvvv vvvvvvvv vvvvvvvv Index v (00 0000 - 3F FFFF)
Or:
0vvvvvvv Index v (00-7F)
10vvvvvv Index v+80 (80-BF)
11vvvvvv vvvvvvvv vvvvvvvv vvvvvvvv Index v (0000 0000 - 3FFF FFFF)
I’m ambivalent on this one. I don’t think the decision we make here will affect much of anything.
CODE (byte address in the CODE chunk to jump to)
This one’s easy. It can encode either absolute or relative jumps; we increase the range of absolute jumps but not relative ones.
Previously:
00000000 fail
00vvvvvv +v bytes (01 - 3F) from the beginning of the operand
01vvvvvv vvvvvvvv +v bytes (signed -2000 - 1FFF) from " " " " "
1vvvvvvv vvvvvvvv vvvvvvvv absolute address v (00 0000 - 7F FFFF)
Now:
00000000 fail
00vvvvvv +v bytes (01 - 3F) from the beginning of the operand
01vvvvvv vvvvvvvv +v bytes (signed -2000 - 1FFF) from " " " " "
1vvvvvvv vvvvvvvv vvvvvvvv vvvvvvvv absolute address v (0000 0000 - 7FFF FFFF)
The fact that we’ve increased the size of various other operands might mean that more jumps need to be encoded in absolute form instead of relative forms. But I don’t expect that to make any significant difference. Jumps within a routine will usually be in relative form, jumps between routines will usually be in absolute form (the Å-machine has no built-in call stack or routine call opcodes), and that won’t change.
STRING (shifted byte pointer in the WRIT chunk)
Like with CODEs, STRINGs can be encoded in three different ways. And like with CODEs, I propose increasing the size of the largest one while leaving the smaller two untouched. “S” is a byte value stored in the header; currently, Dialog always stores 0.
Previously:
0vvvvvvv address v << 1
10vvvvvv vvvvvvvv address v << S
11vvvvvv vvvvvvvv vvvvvvvv address v << S
Now:
0vvvvvvv address v << 1
10vvvvvv vvvvvvvv address v << S
11vvvvvv vvvvvvvv vvvvvvvv vvvvvvvv address v << S
This one I’m torn on, for a few reasons:
- Is it worth changing at all, when a larger S value would already allow for more string space?
- Is it worth shifting long pointers by S, when a 30-bit value on its own can already address so much space?
- Is it worth keeping the weird anomaly where tiny pointers use 1 instead of S as their shift value?
But I think doing it this way, keeping it as consistent as possible with the 16-bit version, will make it easier to implement. It probably won’t be useful to have an S-value higher than 1, but most strings already fit in a short pointer, even with an S-value of zero.