Skip to content
HN On Hacker News ↗

In Rainbows

▲ 39 points • 10 comments • by surprisetalk • 2w ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is human-written.

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 1 of 1
SEGMENTS · AI 0 of 1
WORD COUNT 1,598
PEAK AI % 0% · §1
Analyzed
Oct 25
backend: pangram/v3.3
Segments scanned
1 windows
avg 1598 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,598 words · 1 segments analyzed

Human AI-generated
§1 Human · 0%

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 CLI RbArith, for providing more convenient math operations that ok doesn't support very well on its own RbDisk, for providing disk I/O and persistent storage RbDatetime, for providing date/time information RbScreen, 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 * y into result 2 stores x / y into result, setting err to 1 if y is 0 3 stores x % y into result, setting err to 2 if y is 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 get rainbows running on bare-metal (without an underlying OS), rather than using a BlockFile, 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.