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:

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:

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:

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;
}