In Rainbows
Written 2026-10-06
rainbows is a specification for devices connected to the ok virtual machine, named after Radiohead's album In Rainbows. ok itself only provides instruction evaluation and the "data" and "return" stacks for basic computation, but if you wanted to output something to a screen or a console, or receive input from a mouse or keyboard, you'd have to "map" those devices into memory.
Memory Mapping
ok calls ok_mem_read(addr) whenever it needs to read a byte from RAM, and ok_mem_write(addr) whenever it needs to write a byte to RAM. These are functions that you define yourself, in order to make user-defined memory-mapping possible. For example, with no memory-mapping, your definition of ok_mem_read might look like this:
// assuming "ram" is defined as an array of bytes elsewhere
uint8_t ok_mem_read(size_t address) {
return ram[address];
}
But, let's say you want to be able to read data from a console's standard input (stdin). You could map a "magic" location in RAM so that reading a byte from this address reads data from stdin, like so:
#define STDIN_MAGIC (3)
uint8_t ok_mem_read(size_t address) {
if (address == STDIN_MAGIC) {
char ch;
read(STDIN_FILENO, &ch, 1);
return (uint8_t)ch;
}
return ram[address];
}
With this, reading a value from RAM at this address (in ok's assembly language, that might look like #000003 lod1) will read a byte from stdin.
The idea behind rainbows is that it defines these magic values for you, in order to provide a standard set of input/output devices for someone to use- turning ok into a proper "fantasy computer" rather than just a stack-based instruction evaluator.
Devices
rainbows is still actively being written, so these descriptions for the devices it supports may become outdated rather soon (but I'll try to update this page if I change something).
As of right now, I've either implemented or have plans to implement the following devices (in order of how confident I am in their current definition):
RbConsole, for interfacing with a POSIX-style CLIRbArith, for providing more convenient math operations thatokdoesn't support very well on its ownRbDisk, for providing disk I/O and persistent storageRbDatetime, for providing date/time informationRbScreen, for providing a screen/framebuffer
Now, we'll go over the memory layout I have for the devices I've implemented so far, as well as any thoughts I have for the devices that I'm still unsure about. Expect the memory addresses described below to change as I work on rainbows more!
RbConsole
RbConsole uses the following addresses- the numbers on the left side of the table are the addresses, and the text on the right side of the table are the "names" of those addresses that the device uses (think of them like struct fields):
|---------------------|
| RbConsole |
|--------+------------|
| 000000 | stdin (R) |
|--------+------------|
| 000001 | stdout (W) |
|--------+------------|
| 000002 | argc (R) |
|--------+------------|
| 000003 | arg_index |
|--------+------------|
| 000004 | arg_char_i |
|--------+------------|
| 000005 | arg_char |
|--------+------------|
Reading a byte from stdin, of course, reads from stdin, and writing a byte to stdout does exactly what you'd expect it to.
argc stores the number of command line arguments that were provided (like argc in C).
From here it gets interesting- if you want to read a specific command line argument, you first write a value to arg_index to determine which command line argument you're looking at (0-indexed, of course). Then, you write a value to arg_char_i to determine which character of the argument at arg_index you're looking at, and then reading a byte from arg_char reads that character, and will read 0 if you're at the end or out of bounds.
So, reading a byte from arg_char is a bit like doing this (in C):
char* argument = argv[arg_index];
uint8_t arg_char = argument[arg_char_i];
It's a bit clunky, but the alternative ideas that I had were either to be able to store an argument at a specific location in memory by writing that location into a place in the RbConsole device (which would mean RbConsole could manipulate any arbitrary place in memory, which I didn't like), or it would've meant that RbConsole would need a buffer with a predefined length to store an individual argument, which I didn't wanna do since I wasn't sure what a good predefined length would be (plus I didn't feel like having some kind of error handling for if an argument was too long to fit in the buffer).
This device, as of right now, is fully implemented!
RbArith
RbArith adds support for multiplication, division, and modulo, for 1, 2, 3, and 4-byte unsigned integers. Here is its memory layout:
|-----------------|
| RbArith |
|--------+--------|
| 000006 | op |
|--------+--------|
| 000007 | err |
|--------+--------|
| 000008 | width |
|--------+--------|
| 000009 | x |
| 00000a | |
| 00000b | |
| 00000c | |
|--------+--------|
| 00000d | y |
| 00000e | |
| 00000f | |
| 000010 | |
|--------+--------|
| 000011 | result |
| 000012 | |
| 000013 | |
| 000014 | |
|--------+--------|
When you write a magic value to op, the operation associated with that value is performed on x and y, with the result being stored in result. When this operation is performed, x, y, and result are treated as having a width of width - 1 (so 3 treats them as 4-byte values, 2 as 3-byte values, 1 as 2-byte values, and 0 as 1-byte values). The values that op can take on are as follows:
- 0 is a no-op, no operation is performed.
- 1 stores
x * yintoresult - 2 stores
x / yinto result, settingerrto 1 ifyis 0 - 3 stores
x % yinto result, settingerrto 2 ifyis 0
I may add other math operations (or perhaps random number generation?) in the future. If/when I do, I'll just add them as new values that op could take on (5, 6, etc).
RbDisk
RbDisk provides disk I/O using a library I wrote recently, which I talk more about in my "Playing with Blocks" article.
RbDisk's memory layout is as follows:
|----------------------------|
| RbDisk |
|--------+-------------------|
| 000015 | capacity |
| 000016 | |
| 000017 | |
| 000018 | |
|--------+-------------------|
| 000019 | update |
|--------+-------------------|
| 00001a | error |
|--------+-------------------|
| 00001b | current |
| 00001c | |
| 00001d | |
| 00001e | |
|--------+-------------------|
| 00001f | data (1024 bytes) |
| ... | |
| 00041e | |
|--------+-------------------|
When you run the emulator for rainbows, you provide a BlockFile (described in more detail in the article I mentioned earier) or can choose to create a new one. This file has a set capacity, i.e. a max number of 1024-byte "blocks" that are stored within it. This value is stored in capacity.
Note: if/when I ever decide to getrainbowsrunning on bare-metal (without an underlying OS), rather than using aBlockFile, it would just use the free space of the disk itself.
You can then read a value from update to load the contents of the block at index current into data. Writing a value to update will then store data to the block at index current.
RbDatetime
This device I'm still unsure about- the idea behind RbDatetime is that it provides clock/date info, but I'm not sure exactly how complex I want it to be. I thought about taking inspiration from (i.e. basically stealing) how Varvara's datetime device works, and just have addresses in memory that store the current year, day, day of the week, hours, minutes, seconds, whether we're in daylight savings time, etc:
But I've also considered just storing the 64-bit Unix time (i.e. the number of "non-leap" seconds since midnight of January 1st, 1970, UTC), and just writing software that determines the date using this information- although this wouldn't be super convenient, considering that ok only supports a maximum integer width of 32 bits. Some systems still store Unix time as a 32-bit value, but this would mean that the value would overflow in early 2038, which is a bit too soon for me to justify using it.
Plus, there's the issue of representing smaller units of time like milliseconds, which may be nice to have depending on what I want to make.
All in all, this device is still a work-in-progress. When I add it though, I have a feeling I'll put it before the RbDisk device, meaning that the addresses for the table above would need to be updated.
RbScreen
This is another device that I'm rather unsure about.
For one thing, I don't know how many colors I'd like to support- Varvara uses 2-bit color, allowing for a maximum of 4 different colors, but I've thought about adding a way to set the "color mode" for the screen. For smaller color depth values, like 1, 2, 4, and 5-bit color, they may be able to set a palette, whereas for larger values, I've considered adding support for 8-bit color (perhaps using RGB332, using 3 bits for "red", 3 bits for "green", and two for "blue"), as well as 16-bit "high" color (using RGB565), and maybe even 24-bit "true" color.
However, for larger color depth values and larger screen resolutions, the memory usage grows rather large. For example, a 1024x600 screen using 24-bit color would have a framebuffer that takes up 1800 kibibytes (where a kibibyte is 1024 bytes) of memory, i.e. around 11% of the total memory that ok can access.
Screen resolution itself is a whole other problem- do I want to allow the user to define the screen size themselves? This would allow for resizable windows, which would be nice for running rainbows as just a program on your system, but then I'd need to have something like a framebuffer_addr field in the section of memory for RbScreen, pointing to where the framebuffer starts, and then any software targeting rainbows would have to account for that (i.e. try not to accidentally overwrite that section of memory during computation).
This brings up other concerns too- for instance, possible support for multiple buffering.
If I didn't allow for a resizable screen/window, I'd then need to decide what resolution I'd want to have, as well as what aspect ratio I'd want. Right now, one of my main goals is to get rainbows running bare-metal on a little laptop I call my zen machine (which I haven't written about yet, but I will eventually). This laptop has a resolution of 1024x600 pixels, which is a bit nonstandard (it's not really 16:9 or 4:3). I've also debated using a 4:3 aspect ratio (perhaps 640x480), mostly just because I like 4:3 a lot, but then running rainbows full-screen may look weird on a 16:9 display.
As you can see, RbScreen is a device that I've thought a lot about, and still haven't really come to a specific solution that I like. At this point it's literally keeping me up at night, and has been since I was first writing about ok.
Other ideas for devices
In terms of other devices I'd like to define/implement eventually, I've thought a bit about an RbKeyboard device for reading keyboard input (perhaps using something like a circular character buffer to track multiple keypresses at once?). Ideally, RbKeyboard would also support some "controller" functionality, like how Varvara's "controller" device supports both keyboard keys as well as treating certain keys (like ctrl, alt, shift, and home) as though they were keys on a Nintendo device (i.e. A, B, Select, and Start). I've debated only using a certain subset of the keyboard, as I quite like using a 60% or 75% keyboard layout.
Another device I've strongly considered is RbFloat, a device for working with 32-bit floating point values, although ideally this device shouldn't be that "essential" for running programs on rainbows (as perhaps I may try to get rainbows software running on a system that doesn't exactly adhere to, or doesn't even use, the 32-bit floating point standard).
Something of note is that I haven't really thought about a "mouse" device- personally, I don't really like using a mouse or trackpad of any kind, so I haven't prioritized making a device for one. I'll definitely implement one eventually, but I want to (somehow) encourage software written for rainbows to be keyboard first, i.e. all software running on rainbows should be perfectly usable with no mouse or pointing device at all. This is especially important for working with the zen machine, since even though it does have a trackpad, it's very small, and the left-click button is broken beyond repair (and I can't find much info about replacement parts or how to repair it in general).
I also haven't really thought much about supporting audio, as I feel like this would be quite the rabbit hole (do I support audio synthesis? Audio samples? MIDI? Some combination of the three? Or maybe I could make my own audio format...).
I haven't considered a networking device for similar reasons- although I have thought about adding a device that basically acts as a very-barebones Gemini protocol client, since I like using Gemini a lot.
Conclusions
As you can see, even though I've been actively working on rainbows for a while, it's still got a long way to go. I first started sketching ideas for it back when I wrote my undergraduate thesis on ok, where originally I had considered implementing rainbows for the latter-half of the thesis rather than attempting to implement a compiler. I quickly realized that just writing a specification for it was a hell of an undertaking, let alone implementing it.
As of right now, my goal is to at least get RbConsole, RbDisk, and RbArith fully implemented, and then from there I may work on a simple compiled language (POP-2000, anyone?) to test their functionality and start writing some "useful" software for rainbows before moving on.
In the meantime, I'll keep this article up-to-date whenever some new developments occur, and I hope you enjoyed listening to me ramble about the rabbit hole that this project has taken me down.