ok (virtual) computer
Written 2026-01-26
ok is a virtual machine that I've been working on since around March of
2025. It ended up becoming my undergraduate thesis, where (alongside the virtual
machine's implementation) I created an assembly language and compiler in order
to aid in creating software that runs on the machine, while also writing a
100-page "white paper" describing its design.
The remainder of this article is essentially a very brief summary of this white paper. The original white paper is not only more in-depth, but also a lot more approachable for beginners to the subject of virtual machines and programming in general. You can read the white paper here.
Feel free to check out ok's reference implementation on GitHub- I may move
this repository over to something like Codeberg or sourcehut in the future, but
this GitHub link should at least remain as a mirror for the repository for
now. The reference implementation could be a lot simpler, and there's probably
some bugs that I haven't found yet, but it exists and that's what matters!
Origins
ok was originally inspired by Uxn, another small stack-based virtual
machine, that was created to be easy to understand and not very demanding of
computer resources, with a lot of retro-computing inspirations. I really liked
Uxn when I first encountered it, but as I learned more about it, I couldn't help
but think:
If I had made this, I'd have done it a little differently.
I didn't think there was anything wrong with Uxn- frankly I'd say it's a
nearly perfect piece of software. There were just some little things about it
where, if it worked a bit differently, it'd be easier for me personally to use
and wrap my head around. For example, with ok, I wanted to:
- Provide a little more RAM and ROM space (I decided upon 16 MiB rather than 64 KiB)
- Provide stack operations on 1, 2, 3, and 4 byte values (rather than just 1 and 2 byte ones)
- Use memory-mapped I/O rather than port-mapped (and event-driven) I/O
- Only use 16 opcodes rather than 32
The idea of using fewer opcodes was where the idea for the project started out.
I noticed that a lot of Uxn's operations could be implemented using 2 or 3
operations that were already supported- for instance, why use INC when you
could just do something like #01 ADD, or NIP when you could do SWP POP?
Obviously, it's nice that these commonly used operations require less bytes to
represent them, but I wanted a kind of "MISC" version of what Uxn had. So, I
initially attempted to reduce Uxn's instruction set as much as physically
possible.
Otherwise, ok is incredibly similar to Uxn, in that it has:
- 2 circular stacks (currently 256 bytes each)
- Opcodes that primarily operate on these stacks
- Separate RAM and ROM (i.e. instruction memory, following the Harvard architecture)
- Big-endian stacks and memory
- A reference implementation in C
Instruction set
In binary, ok's instructions come in the following form:
1 s w w o o o o
The 1 at the beginning is used to verify that this is a valid instruction- if an instruction is executed that doesn't have this 1 at the start, the virtual machine is halted. This way, uninitialized values in the instruction memory (which should be zeroes) will halt execution.
The o field is the opcode, taking values from 0 to 15 (0000 to 1111 in
binary). These opcodes correspond to the following operations:
| opcode | mnemonic | stack effect/description |
|--------|----------|-----------------------------------------------------|
| 0000 | ADD | ( a b -- a+b ) |
| 0001 | AND | ( a b -- a&b ) |
| 0010 | XOR | ( a b -- a^b ) |
| 0011 | SHF | ( a byte -- result ), bit-shifting* |
| 0100 | SWP | ( a b -- b a ) |
| 0101 | CMP | ( a b -- byte ), compare a and b* |
| 0110 | STR | ( a addr -- ), store a in RAM[addr] |
| 0111 | LOD | ( addr -- RAM[addr] ), fetch bytes at RAM[addr] |
| 1000 | DUP | ( a -- a a ) |
| 1001 | DRP | ( a -- ) |
| 1010 | PSH | ( a -- ) ( R: -- a ), push a onto return stack |
| 1011 | POP | ( -- a ) ( R: a -- ), pop a off of return stack |
| 1100 | JMP | ( n -- ), jump to nth instruction |
| 1101 | LIT | ( -- a ), push a constant number |
| 1110 | FET | ( addr -- flash[addr] ), fetch bytes at flash[addr] |
| 1111 | NOP | ( -- ), do nothing |
If you're already familiar with stack effect notation and the Uxn opcodes, these should be fairly self-explanatory. Each operation pops its arguments off of the corresponding stack (data or return) and then pushes the result onto the appropriate stack, manipulating memory or control flow when necessary.
There are some potentially less self-explanatory things to note here, however:
okuses the term flash rather than ROM, although conceptually it's basically the same thing (think of it like EEPROM)- The
SHFopcode works identically to Uxn'sSHFopcode CMPis an unsigned comparison, and will push 0 if the top two values are equal, 1 ifais greater thanb, and 255 ifais less thanbADDis a wrapping add- An
addris always a 3-byte value, as there are 16 MiB (i.e. 2^24 bytes) of RAM and flash, and abyteis always a 1-byte value
The sizes of the other stack arguments are determined by the w or width
field. A value of 00 means the opcode uses 1-byte arguments, 01 for 2-byte
arguments, 10 for 3-byte arguments, and 11 for 4-byte arguments. This is
akin to Uxn's short mode, only there's, well, more modes than just the one.
Finally, there's the s field, a.k.a. the skip flag. If this bit is 1, then
after the instruction pops its arguments off of the stack, a single-byte value
is also popped. If this value is 0, then the instruction's arguments (but not
the one-byte value) are restored to the stacks and the instruction is skipped,
otherwise the instruction is executed. This allows for some interesting
conditional execution patterns, and prevents the need for a separate
"conditional branch" instruction.
The header-only library implementation
ok's reference implementation exists as a header-only library. Since there
isn't really a standard device ecosystem, I wanted people to be able to
implement their own "devices" in C that connect to the virtual machine, such as
an emulated screen. The header-only library implementation makes this easy- you
just download the file and use it in your C program, kinda like this:
#define OK_IMPLEMENTATION
#include "ok.h"
#include <stdio.h>
#include <stdlib.h>
// defining the buffers for the VM to use
static uint8_t* ram;
static uint8_t* flash;
uint8_t ok_mem_read(size_t address) {
if (address == 1) return (uint8_t) getchar();
return ram[address];
}
void ok_mem_write(size_t address, uint8_t val) {
if (address == 2) putchar(val);
ram[address] = val;
}
uint8_t ok_fetch(size_t address) {
return flash[address];
}
int main(int argc, char* argv[]) {
if (argc != 2) {
printf("usage: okemu file.rom\n");
return 1;
}
// allocate the ram and flash buffers
ram = calloc(OK_MEM_SIZE, 1);
flash = calloc(OK_MEM_SIZE, 1);
if (!ram || !flash) {
free(ram);
free(flash);
return 1;
};
// load program file into the program buffer
if (!ok_load_file(flash, 0, argv[1])) {
free(ram);
free(flash);
return 1;
}
// start up and run the virtual machine
OkState vm;
ok_init(&vm);
while (vm.status == OK_RUNNING) ok_tick(&vm);
free(ram);
free(flash);
return vm.status != OK_HALTED;
}