or, “How I put too much time into making an 8-bit ISA and accompanying virtual machine”
It all started with my colleague Cassie having fun designing a toy 8-bit ISA (“instruction set architecture”). I love encoding tables (I helped out a little with the one for Swift’s
String struct representation), and I did assignments in college involving simplified CPUs. So I started thinking about what it would be like to write a program in Cassie’s ISA…and decided its four registers were too limited for me. How could I get up to 8 registers while still keeping most of the instructions in a single byte?
That was the start of the project I named ROSE-8: a toy instruction set for a non-existent CPU with 8-bit registers and 32KiB of memory. Over the last several days I’ve been coming up with the pieces you need for such a little computer, deciding on the best way to encode them as “ROSE-8 machine code”, and then actually implementing it as a toy VM. You can feed code into the ROSE-8 and it will do things!
let printFromLastSegment: [Instruction] = [ .geti(-1), // GET the Immediate value -1 (255) .set1, // SET the data1 segment to 'it' .zero, // get the value 0 (special encoding) .setr(.r0) // SET Register r0 to 'it' // LoaD the byte at data1[r0], then Update r0 by incrementing .ld1u(.r0, update: true), .bezi(5), // Branch if 'it' (the byte) is Equal to Zero, // to the Immediate offset 5 bytes forward // (This will be the STOP) .prnt, // PRiNT 'it' (otherwise) .jofi(-4), // Jump to the OFfset (Immediate) 4 bytes backward // (This will be the LD1U) .stop // self-explanatory :-) ] var machine = Machine() machine.load(printFromLastSegment) machine.load("Hello World!\n".utf8, atSegment: 255) machine.run()
Sorry for not making this blog post accessible to a general programming audience. If anyone has a good recommendation for an intro to CPU architecture, I’ll link it here!
If you want to play around with what I have, you can download it as a Swift package. There’s a bit more documentation in there, as well as a version of the discussion below. I also previously uploaded a near-final version of the “spec” if you just want to see the instructions and their encodings.
EDIT: I forgot to explicitly thank Cassie for their input, suggestions, and conversation, not to mention the idea in the first place. Thank you, Cassie!
A Detour: On GitHub
Normally with a project like this, I’d be posting it on GitHub, both to make it easy for people to browse around the source and to make it easy to track changes (and possibly even take pull requests). But unfortunately, GitHub’s taken a contract from ICE, which is a pretty heinous thing to do these days. They’re hardly the only tech company involved with ICE (as if that makes it better, “there is no ethical consumption under capitalism” and all that), but back in December I signed an open letter to GitHub asking them to “commit to a higher ethical standard” in this and in the future. Until they cancel their ICE contract, I’m not putting any new projects on GitHub.
(Why not GitLab or Bitbucket? Well, GitLab’s not immediately a problem, but I don’t have much faith. Bitbucket is owned by Atlassian, an Australian company, so I probably wouldn’t have this problem with them, but…at this point I’m feeling burned, and not inclined to make my project’s canonical home be somewhere else at all. It’s certainly a testament to GitHub’s success though that its competitors don’t seem nearly as compelling.)
Anyway, you get archive drops from me instead, at least for now. I did still leave the git repository inside the archive, but I didn’t start tracking revisions till partway into the project (bad me), so the history isn’t as interesting as it might be.
So, how do you get up to 8 registers while still keeping most of the instructions in a single (8-bit) byte? The answer I chose has actually been around for a long time: make a special register called an accumulator. In some architectures, that means giving one regular register special privileges, including the Intel 8080 that’s an ancestor of most modern desktops. But I chose to make the accumulator be its own thing, colloquially called “it”. This name comes from programming environments that use this to reference the last thing you accessed; the oldest one I’ve used is HyperTalk and I’ve seen from LOLCODE that it’s reasonable to have this be part of how you program.
With the accumulator as the implicit target of most operations, it was “easy” to have eight registers available and still have room for a large number of operations. I took some other hints from Cassie’s ISA, like the compact encoding for bitstring immediate operations based on the intuition that adding or subtracting large numbers known at compile-time isn’t very common.
The next big challenge was memory. (Cassie realized this too.) With 8-bit registers, you’d only be able to address 256 bytes of memory, which…isn’t a ton. I took another hint from old Intel machines by using segment registers: accessing address 0x55 means something different based on which segment you’re accessing it in. Around this point I also realized this felt pretty familiar…
…and realized that the last 8-bit machine I heard about was the Game Boy, via Eevee’s series about writing a Game Boy Color game, which I very much enjoyed / am enjoying. With that consciously realized, I got to check what I was doing against the list of Game Boy opcodes (provided by Randy Mongenel) to make sure I wasn’t
making any stupid mistakes making any mistakes that Nintendo hadn’t, at least not accidentally.
The last “clever bit” of this architecture is how to do function calls. If you want more than 256 bytes of code, some of it is going to be too far away to refer to with an 8-bit register. It’s the segment problem again! So there’s a “code” segment for “normal” function calls…but since that means a fair amount of overhead getting too and from a far-away function, there’s also support for “offset” function calls. Encoding jumps by offset (from the current instruction) is pretty standard practice for jumping around in a function, but unusual for calls, because the thing you’re calling has to know how to get back. I’m still not sure if I think this is worth it, but I haven’t really written enough big or even medium-sized programs to know yet.
An assembler/interpreter, i.e. running from a text file (and outputting to a binary file, I guess). Writing arrays of instructions by hand (as shown above) isn’t so bad except for manually computing addresses and offsets, so I still want to get to this at some point.
Memory-mapped I/O, so that it’s possible to implement something Game-Boy-like with buttons and maybe a “screen”.
I’m not building a compiler that targets ROSE-8 but someone could, in theory.