On The Rusalka Virtual Machine

To get this out of the way real quick, Rusalka is a register-based virtual machine. In short, it loads bytecode that contains instructions that conform to its instruction set and runs that. Currently, there are no high-level-ish languages that compile to Rusalka bytecode, but Rusalka assembly is available to make playing with it relatively easy. As assembly languages go, it’s probably more usable than most by virtue of being high-level compared to x86 assembly.

Rusalka doesn’t attempt to emulate existing hardware or any existing instruction set or CPU. It has its own instruction set. I feel it’s important to point this out because a lot of programmers I’ve spoken with tend to confuse virtual machines with only those they use to run one OS under another (e.g., VirtualBox). Instead, Rusalka leans closer in nature to Lua’s VM and possibly the Parrot VM, among other similar virtual machines. This is particularly true of Lua, since Rusalka draws a lot of inspiration from it and the various publications on Lua’s internals.12 These fall more under the category of process virtual machines, where each VM is not an entire system but a process in the host operating system.

At the moment, Rusalka is open source on GitHub and licensed under the Boost Software License, version 1.0. I previously made it available under the GNU GPL 3, but decided this didn’t really fit with how I viewed Rusalka, nor was it really conducive to letting people play with it if they want to. This also makes it easier to reuse bits of Rusalka as need be, since, although the VM will likely never see use, some of its code may prove useful later.


As with a lot of projects I work on for fun, people tend to ask why I decided to start working on it. Since it’s a virtual machine, it’s maybe a little more curious than most personal projects because it’s more like a CS student’s homework and less like a random serialization library or one of the many thousands of Javascript animation/special-button libraries out there. Unfortunately, nobody accepts my usual explanation of “I felt like it,” so let’s try to break down why I felt like it.

First off, I want to eventually build my own programming language(s), compilers, and other tools. It’s probably on the list of weird romantic dreams programmers have. I just need you to either accept this is normal or smile and nod. I’ve toyed with various projects of this nature over the years, but the issue it’s always come down to is what to target. I already have C++, so I don’t really need something compiling to machine code and that seems pretty unpleasant anyway. JVM bytecode is mostly covered by Kotlin or Scala, and I don’t really like using the JVM if I can help it anyway.

What I’d really like is a scripting language I can embed in my own projects. Lua fits that role, but it seems less permanent and more like a haven’t-found-anything-better situation. I doubt I’ll do better than Lua — I haven’t got the years of experience and I don’t consider myself smarter than the folks working on Lua — but it seems doable to build a scripting language. I don’t want to pull a web dev and do something like CoffeeScript, though, so I wanted a VM. That’s one path to Rusalka.

Second, ever since I used the Cipher engine’s virtual machine3 and saw Quake 3 had a virtual machine,4 I’ve wanted to try my hand at implementing one of my own. These are VMs built to be embedded in the software they’re made for, and, although likely fairly impractical to do these days when we’ve got Lua and later perhaps mruby, it seems like a fun thing to try doing this as well, even if the end result isn’t perfect or hugely successful or whatever “web scale” means, it’s something to learn from and I enjoy the work involved.

Third, register-based virtual machines are seemingly uncommon — you’ve got Parrot, Lua, Dalvik, and likely a handful of others, but it seems like the majority go stack-based. Building a register-based VM instead then seemed interesting as well, so I wanted to try my hand at building one. This is especially true after past attempts to build stack-based virtual machines.5

Fourth, I felt like it.

You Get One Type

The Rusalka VM only supports a single type: doubles (i.e., 64-bit floating point numbers). This is a design choice mostly borrowed from Lua, albeit that Lua technically supports a few types (numbers, sometimes integers, strings, and tables). This wasn’t always the case for Rusalka, and initially the VM supported three types: floats and signed and unsigned 32-bit integers.

I liked the original three types, but they proved fairly annoying because it resulted in significant instruction bloat without introducing type tags into values and branching into most of the instructions. The former was more of a concern than the latter, realistically, since branching isn’t a huge issue. Really, though, it just seemed like a poor use of additional bytes since if I was going to end up increasing the size of a value in the VM, I wanted it to use 64 bits. There’re no 56-bit types in C++, much less standardized 56-bit floating point types supported by C++. Considering that, after some thinking, it seemed easier to use doubles as the universal type in Rusalka.

First, doubles can perfectly represent all signed and unsigned integers up to ±2^53 (or 52 bits explicitly stored via the mantissa plus one), which for the most part is sufficient to me. I’m sure there are some potential uses for the remaining 12 explicit bits you lose to the double’s sign and exponent, but I see this as a fair trade-off. As a way of not encouraging people to try not to treat it as a 52-bit integer, however, Rusalka’s bitwise instructions cast the value to an unsigned 32-bit integer.

Second, by using doubles, this automatically gains additional precision over the float that I previously used. This could end up being slower in some respects, but it’s a benefit in the long run. At least until someone hits floating point precision issues, but I can’t really help that.

The two questions I tend to get from friends after I talk about this are more or less the same: what about strings / other objects? The simple answer for Rusalka is, “They don’t exist.”

This is a partial lie: strictly, as far as Rusalka is concerned, there are no types other than doubles. This doesn’t mean you can’t return a double and use it as a handle, though. Rusalka does allow allocation of memory and access to it via handles that identify these blocks (these can also be embedded in the bytecode to be loaded as read-only memory blocks). There are a few instructions for dealing with memory, mainly:

  • REALLOC and FREE, both of which reallocate and free a memory block. A zero block handle is reserved for no block, so reallocating a zero block allocates a new block.

  • PEEKand POKE allow reading from and writing to memory. They’re two of the weirder instructions, seeing as they both take five operands in order to specify their inputs/outputs, the type they’re reading from the memory.6 Because memory access is intentionally limited by the VM, these instructions are both bounds-checked to ensure you’re not doing something bad.

  • MEMLEN, MEMDUP, and MEMMOVE query the length of blocks, duplicate blocks, and copy data from one block to another. MEMDUP can be effectively recreated using MEMLEN, REALLOC, PEEK, and POKE, but that’s unpleasant. MEMMOVE is the same, though it’s much easier to provide as an instruction since it uses std::memmove and is far safer.7

So although you only get doubles, you can allocate a string as a block of memory and pass around the block’s handle to functions that take strings (and arrays and dictionaries and likely any other conceivable type of object).

There’s one possible trick around this that can yield type-tagged object handles, provided one is okay with 32 bits for a handle and around 16 to 20 bits for a type tag. Unfortunately, it’s also an enormous, horrifying hack: it’s possible these bits into a quiet NaN’s (QNaN) mantissa. Because a QNaN only requires 12 bits to be set to generate one, where the remaining bits are all theoretically inconsequential, we’re left with 52 bits to pack data into if you count the sign bit.

There is the question of why one would bother doing this with a QNaN when you can represent 52 bits with a double anyway, and the answer is slightly simple: it’s a recognizable bit pattern. The VM can potentially identify QNaN values with certain bits set and give them special treatment. In addition, this should make this class of values somewhat immune to the existing arithmetic instructions, making error checking possible if operating on these. Still, though, this is one possible hack and it would be easier, in the end, to use a tagged union. It’s fun to think about, anyway.

In the end, the use of doubles over a wider range of types is more of a pragmatic choice that has its pros and cons. On the one hand, it greatly simplifies the instruction set and the code necessary to execute it, as well as guaranteeing that any language built for Rusalka won’t need to worry about multiple integral and floating point types. On the other, it inherently limits what the VM can directly provide access to through its value type. Pointers are off limits.8 You get no special array, string, or dictionary types as of yet. And lastly, without additional support from the host process you’re limited to what you can do with memory blocks and doubles. That last limit’s pretty loose, but it’s still a limit that means any code produced for the VM is going to be moderately lower-level than some other VMs.

I think this has the benefit of producing an instruction set and virtual machine that’s restricted to the bare essentials: arithmetic, logic, and moderately safe memory access. Further specialization can be done as the VM evolves as well, meaning it’s in a good position to grow out from where it is. Granted, that’s a nice way of saying it’s not very useful at the moment, but it beats throwing my computer out a window.


One of the most important points to keep in mind is this: Rusalka is a register-based virtual machine. I mean that in the sense that it has a (currently) finite number of registers that act as operands, inputs, outputs, and so on for instructions. The stack is also available, albeit only as a storage mechanism and as part of the VM’s argument passing mechanism for function calls (which exists mainly for calling host functions, as there’s no requirement to use the stack for in-VM functions). Only three or four instructions manipulate the stack directly, and otherwise all instructions operate on and with registers.

This is in contrast to convention in some sense. Most VMs end up being implemented as stack machines, which I’ve been told is partly because it’s easier to reason about. That is, it’s easy to say you want to push X, push Y, execute an add instruction, then finally pop the result and store it somewhere. I have no single concrete reason for not going this route except that I tend to find it harder to reason about. So whereas someone might see this as clear:

// a = a * b
// We'll assume A and B are local variables or
// something and can be referenced by push and
// some sort of store local instruction.
push a
push b
mul             // push(pop() * pop())
store a         // a = pop()

It instead comes across as difficult for me to think about. I suspect this is because I’ve spent so long in high-level languages like C and C++ where I think in terms of functions and their parameters/inputs. As a result, it feels natural to me to treat each instruction as a function performing a small, specific task with its operands and returning a result, or in this case writing the result to a register. There’s no need to keep a mental image of how things on the stack are ordered, I just have to think about the input and output of a function instead of having indirect input and output on a stack.

So, for me, Rusalka assembly feels a bit simpler:

// let a = $0; b = $1
mul $0 $0 $1    // a = a * b

Which, in fairness to stack-machines, is a somewhat unfair comparison. A register-machine requires fewer instructions by virtue of not requiring a stack, so it should feel simple by comparison. Despite that, I prefer my little functions comparison for register-machine instructions.

This of course means instructions aren’t as light as those in stack-machines, but the benefit is that there’s overall fewer instructions per operation needed. Where a combined multiply and add might take about five instructions (three pushes, a multiply, and an add), the same takes only two in Rusalka. This also means I spend less time manipulating the stack, keeping things fairly simple. Depending on how many registers there are, this can be difficult to work with, but this isn’t too much of a concern for Rusalka with 256 registers on-hand (four reserved, but they’re still registers).

So let’s have a quick overview of the registers. Each register is just an index into an array of registers held by Rusalka, since they don’t map directly to hardware registers. These indices are broken up into three ranges:

  • Registers 0 — 3

    Reserved registers, of which there are four: IP, EBP, ESP, and RP. IP is the instruction pointer (or program counter) of the VM and for the most part isn’t directly touched by the bytecode. EBP and ESP are simply the stack base pointer and stack top pointer (in this case, the stack grows up). RP, finally, is just a dedicated volatile register used to store the return value of functions. These are also the only named registers.

    In the case of IP, EBP, ESP, the pointers are just offsets into the instruction data or stack. Why RP ends in a P is a mystery to me at this point, seeing as it’s not a pointer, but the name stuck. I suppose I could call it the return product to justify it now. At any rate, it’s an artifact of an earlier version of the VM and I kind of like having all the named registers ending in ‘P’ now.

  • Registers 4 — 11

    General purpose non-volatile registers, of which there are currently eight. These are automatically preserved and restored between function calls, meaning the VM itself is responsible for restoring their values between calls. As such, a function is free to stomp9 all over these as much as it pleases.

  • Registers 12 — 255

    General purpose volatile registers, which make up the remainder of the registers. There are currently 256 registers in total, so there are 244 remaining volatile registers. These are not preserved at all by any part of the VM. At the moment, there is no calling convention to determine who — the caller or callee — preserves these registers, though I suspect any language I build on top of Rusalka will put the responsibility on the caller to preserve those registers it needs preserved.

Again, in implementation, these are just members of an array,10 since all registers are in-memory. I have some plans to change this to allow functions to only allocate as many registers as they need, but this is ongoing and I’m undecided still if it’s entirely necessary. It should prove to be fun as an experimental branch of Rusalka, at least, particularly if I can allocate all registers in host stack memory (i.e., basically a VLA — given that Rusalka’s written using C++11, though could prove frustrating to pull off).

One particularly handy upside to having registers, especially considering the only type we care about is doubles, means that basic variables map very cleanly onto registers. By not worrying about emulating a specific hardware (as some register VMs might choose to do), there’s no realistic concern about whether a variable fits in a register — it does, always.

The Instruction Set

As an example of the Rusalka instruction set and its current assembly language, this is a ROT13 cipher I quickly implemented in my assembly language. This doesn’t display the entire instruction set, but at least shows how a smallish function might be implemented using it. The following also contains instruction numbers, though these aren’t part of the assembly itself.

 0   function rot13(mem_in, mem_out;
..                  index, char, base, length, length_out) {
 2    memlen length     mem_in
 3    memlen length_out mem_out
 4    if length_out < length
 5        load length length_out
 6    load index 0
 7    for index < length {
 9         peek char mem_in index MEMOP_UINT8
10         le 'a' char 0 ; jump @__rot13_test_upper
12         le char 'z' 0 ; jump @__rot13_test_upper
14         load base 'a'
15         jump @__rot13__apply_rotate
..         @__rot13_test_upper:
16         le 'A' char 0 ; jump @__rot13__continue
18         le char 'Z' 0 ; jump @__rot13__continue
20         load base 'A'
..         @__rot13__apply_rotate:
21         sub  char char base
22         add  char char 13
23         imod char char 26
24         add  char char base
..         @__rot13__continue:
25         poke mem_out char index MEMOP_UINT8
26         add  index index 1
27     }
28     load rp length
29     return
.. }

It’s fairly ugly, but it should be easy enough to understand. As a quick primer: both a function declaration, or a let statement, tells the assembler to alias unused registers with the given names. Any identifier prefixed with a @, ., or ^ followed by a colon declares a label. if and for conditionals are syntactic sugar for compare-and-jump instruction pairs (because it’s easy enough to generate instructions for those patterns). If an instruction has an output/destination parameter, it is always the first parameter, as with Intel’s x86 assembly, though in practice it’s a mix of AT&T and Intel syntax.

Function declarations are broken up into three parts: the function name, the argument list (these are optional, but the arguments are automatically aliased to registers and their values are popped off the stack per the current calling convention), and a local register list. The locals function the same as arguments, except their values are not automatically popped off the stack. So, in the case of the above function, it has two mem_in and mem_out arguments and requests five additional named registers. Named registers are also syntactic sugar in the assembly and their info isn’t retained in the bytecode (it could be as part of a metadata chunk, but the VM would ignore it). In any case, this ultimately yields the following assembly:

    pop $1 // pop mem_out to general register 1
    pop $0 // pop mem_in to general register 0
    /* ... */

Similarly, an if statement, like if x < y { /* ... body ... */ }, would yield something like the following:

    // let x and y be two arbitrary registers
    // if ((x < y) != 0) ++ip
    lt x y 0
    jump @end_conditional
    /* ... body ... */
    /* ... */

A for statement behaves mostly the same, except it’s a looping conditional statement, so it’ll have an additional jump at the end of the loop back to the initial test. Aside from that, it’s fundamentally the same as an if statement.

Now that we’ve both got an idea of what I’m working with here, let’s talk about the actual instruction set. Rusalka currently has the following 36 instructions, as of this writing (these are re-ordered for organization here):

R = Register; C = Constant; M = Mixed; F = Flag
          U V W X Y Z   (Operand names)
====( Arithmetic )========================================
ADD       R M M F       U := V + W
SUB       R M M F       U := V - W
DIV       R M M F       U := V / W
IDIV      R M M F       U := i64(V) / i64(W)
MUL       R M M F       U := V * W
POW       R M M F       U := pow(V, W)
MOD       R M M F       U := fmod(V, W)
IMOD      R M M F       U := i64(V) % i64(W)
NEG       R R           U := -V
====( Bitwise ops )=======================================
NOT       R R           U := ~u32(V)
OR        R M M F       U := u32(V) | u32(W)
AND       R M M F       U := u32(V) & u32(W)
XOR       R M M F       U := u32(V) ^ u32(W)
ASHIFT    R M M F       U := i32(V) <<+->> i32(W)
BSHIFT    R M M F       U := u32(V) <<+->> i32(W)
====( Rounding )========================================
FLOOR     R R           U := floor(V)
CEIL      R R           U := ceil(V)
ROUND     R R           U := nearbyint(FE_TONEAREST, V)
RINT      R R           U := nearbyint(FE_TOWARDZERO, V)
====( Branching )=========================================
EQ        M M C F       if ((U = V) != (W != 0)) IP++
LE        M M C F          ... <= ...
LT        M M C F          ... <  ...
JUMP      M F           IP := U
====( Stack ops )=========================================
PUSH      R             push(U)
POP       R             U := pop()
====( Register assignment )===============================
LOAD      R M F         U := V
(Function calls)
CALL      M M F         exec_call(U, V)
RETURN                  vm.sequence--  (End frame)
====( Memory manipulation )===============================
REALLOC   R M M F       U := realloc(&V, W)
FREE      R             free(&U)
MEMMOVE   R M M M M F   memmove(&U + V, &W + X, Y)
MEMDUP    R M F         U := memdup(&V)
MEMLEN    R M F         U := memlen(&V)
PEEK      R M M M F     U := *type_ptr<X>(&V + W)
POKE      R M M M F     *type_ptr<X>(&U + W) = V
====( Internal )==========================================
TRAP                    vm.trap++  (Set kill-switch)

In the above list, mixed values are those that are either a constant or a register reference. This is determined by the flag operand, which I’ll get to in a moment. The instruction set above is intended to be sparse but functional enough that it can provide most of what’s necessary without going too far overboard. The actual opcode as it’s encoded in the bytecode is a 16-bit unsigned integer, so there’s plenty of room for more instructions, but it seems unlikely that I’ll need more than 8 bits worth of data for an opcode.11

A few, namely EQ, LE, and LT, are more or less carbon copies of how the same instructions from Lua’s VM — this is mainly because it makes sense and, as test instructions go, they’re simultaneously lightweight and should meet most needs. Unlike some instruction sets, however, the unconditional JUMP is absolute rather than relative, and is likely a completely useless instruction except for it making intent obvious. For relative jumps, there’s always the option to use any of the normal arithmetic instructions with the IP register. Besides that, the use of an absolute jump is more of an oversight in the initial design of the VM than anything else and may be corrected later. It would certainly be more useful, given that it would make jump relocation completely unnecessary (since otherwise the only things that require relocation are data references and function calls).

Most instructions have a final flag operand to determine whether a mixed input is either a register or a constant. Currently, this is a false operand: it is not part of an instruction’s operand list in the bytecode. Instead, it immediately follows the opcode as a 16-bit unsigned bitmask. While it’s possible to specify your own flags as operands to any instruction that takes it, it’s often better to leave it out of the assembly because asm2bc, my hacked-together bytecode assembler, will generate the flags for any instruction that doesn’t already have them. There are only two types of operands, constants and registers indices, so asm2bc does this well enough already. This saves a bit of time when hand-writing Rusalka assembly, since I have nothing better yet.

This instruction set is still rather large, but it’s only about a third the size of early Rusalka that required instructions for each type used. As a result, the past typed version of the instruction set had cast instructions, arithmetic for all three types (though only unsigned bitwise ops aside from shifts). In addition, because the flag operands to instructions weren’t yet present, each instruction that takes an optional constant required both a register- and constant-operand instruction. The cost of the flag operand is that it introduces branches into many of the instructions, but the upside is that the instruction set remains simple. Future iterations of the bytecode loader could convert instructions to more granular branch-less forms later as well, but for now I’m opting for an instruction set I can remember.

Unfortunately, I still need to add some instructions once I’ve decided how to handle global values in the VM. Storing them in read-write static data blocks seemed like one option, but also probably unnecessary. The other problem is how lookup is handled, but I imagine the end result will be based on hashed global names (to ensure that access to a global can be done by-name or by-hash). That’s one problem I’m still working through, since I’d like to come up with at least a few different ways to handle it and then spend some time just to think about them. Almost all further instruction additions and changes will probably be for the purpose of setting up features for whatever language I build on top of Rusalka.

Future Plans

What happens with Rusalka in the future is up in the air since it’s an experimental project, but there are a few things I’d like to work on implementing. Nothing I have planned is explicitly necessary for any one end result here, so they’re just ideas I think’d be neat.

  • Some sort of concurrent execution support. This can be done right now, albeit unsafely, by simply splitting up the existing vm_state type into a vm_process and vm_thread, wherein the process takes ownership of its threads and the threads have their own registers and can execute concurrently.

    I don’t mean to suggest that each thread consumes an actual host thread, as it’s entirely possible that the VM could schedule each thread for X instructions and yield as needed. However, it should be possible to spawn a host thread and have a VM thread running on that as needed. This would require some locks, of course, to ensure safe access to memory. Ideally, the locks should be granular enough that they don’t block VM threads too much (in other words, no one big global interpreter lock as in, say, some implementations of Python or Ruby).

  • Explicit support for classes or objects. This would require coming up with some way of identifying these objects at runtime, potentially increasing the size of the VM’s value type to include a type tag and any other necessary data. Beyond that, it also necessitates deciding how classes work, how method dispatch should work in the VM, and so on.

    This could be done at the bytecode level as well by storing everything in memory blocks (metaclasses and such could be stored in static memory blocks), but doing that could be potentially very ugly and slow. It could also be done through host functions, but this also necessitates some initialization done at the bytecode level (like an exported module init function).

    Considering performance, it’s probably a better idea to make this a part of the VM itself.

  • Global variables. These aren’t actually supported in Rusalka, which is an oversight on my part, but also comes down to me being undecided on how to reference a given global variable. How should a global variable be referenced by name? Using a string to identify a global variable at runtime has some reflection benefits, but it’d mean doing string comparisons for each lookup.

    My current idea is to use arbitrary numbers, though ideally these would be hashed and unique integers to refer to names. In that case, it should be easy enough to store globals in a hash table for quick access.12 After that, the only question is whether to make global variables viable operands for all instructions or if GETGLOBAL and SETGLOBAL instructions should be added.

  • Some sort of FFI support. Right now, access to host functions goes through Ruby-C-extension-like callbacks.13 This works well since it provides a generic interface to the function, but it could be better, since it’d be much nicer to write something like vm.bind_function<float, float, float>("fmod", &std::fmod) and have it just work. I already have some ideas as to how this could work, but it’s low on the priority list since the callback API already works and there are other issues to deal with — like the mysterious lack of global variables.

Ultimately, Rusalka’s been fun to work on and I plan to continue working on it, though have taken a break recently for various personal reasons and to work on other projects (including learning Swift and slowly picking up development of Snow again). With any luck, I’ll also get around to building the language that should sit on top of Rusalka as well, but that’s still in the not-quite-sure-what-I-want-it-to-be phase.

In the meantime, back to coding.

  1. The Implementation of Lua 5.0 by Roberto Ierusalimschy, Luiz Henrique de Figueiredo, and Waldemar Celes. <http://www.lua.org/doc/jucs05.pdf>. [return]
  2. The Virtual Machine of Lua 5.0 by Roberto Ierusalimschy. <http://www.inf.puc-rio.br/~roberto/talks/lua-ll3.pdf>. [return]
  3. An old Quake 3-like engine I licensed. I never used it for much, but I spent a lot of time studying its source code. It was pretty well-written and featured a stack-based VM, like Quake 3’s. It’s a shame the engine is entirely defunct and hasn’t been open-sourced. [return]
  4. The QVM, which Phaethon’s put a specification together for over yonder: icculus.org/~phaethon/…/q3vm_specs.html. Cipher’s was likely heavily inspired by Quake 3’s, though I’ve never asked Rik Heywood about this so it’s speculation on my part. [return]
  5. It’s slightly funny to me that when I tried building my first virtual machine years ago, I didn’t think to call it one and didn’t consider that there was anything other than stack-based VMs. In retrospect, I may have actually given myself a harder time by building a stack-machine, since I’ll point out a little later that I find them hard to reason about. [return]
  6. Curiously, I allow reading 64-bit integers here, but they get casted to doubles, which makes their range unfortunately limited. I’ll probably reconsider allowing 64-bit integer reads or allow untyped 64-bit support (i.e., bitwise-only and anything else gets casted). [return]
  7. MEMMOVE takes six operands, which is mostly unavoidable (source and destination blocks, source and destination offsets, size, and flags indicating which operands are constants that don’t need to be loaded from registers). They’re all mostly utility instructions, but MEMMOVE might stand out as the weirdest of all instructions. [return]
  8. In theory — in practice, if you need more than about ~48 bits of a pointer, I’m not prepared to handle your awe-inspiring amount of memory in Rusalka anyway. [return]
  9. For some reason I’ve convinced myself that “stomp” is used to describe register usage, but I have no evidence for this. It probably came from memory stomping. As such, I’ll just define it as I use it: stomping a register is modifying it without preserving its value or being concerned for how the register is used elsewhere. In other words, your code went in and wrecked the place. For the non-volatile registers, the VM will clean up for you. For the volatile registers, some part of the code needs to clean up after itself. [return]
  10. value_t _registers[REGISTER_COUNT];

    Negative register numbers are also valid, but don’t reference registers. Negative register number accesses refer to values on the stack, specifically stack[ESP + RegNum] (where RegNum is a negative number). This is provided to ensure it’s possible to access data on the stack as an operand to an instruction, in the event that you want to quickly get or set a stack value without popping it. So, it’s a convenient way to use a value on the stack as an operand.

  11. High probability I’ll eat those words later. [return]
  12. Or, if the name integers can be guaranteed to be sequential, an array also works and could allow the VM to relocate global storage depending on access frequency, but that’s an optimization that could be done after loading the bytecode. [return]
  13. That is, using vm_callback_t = value_t(vm_state_t &vm, int32_t argc, value_t const *argv); [return]