Programming Languages

Running Ruby On The Apple II

An 8-bit CPU running at 1 megahertz. Kilobytes of RAM. The Apple II was released in the 1970's and many people first learned to program on it using the built in BASIC. Surely it is impossible to fit a language as complicated as Ruby on such a limited machine… right?

Come see Ruby running where it has never run before, and learn how such a rich language can be squeezed down to fit on the humble Apple II.

RubyKaigi 2019 https://rubykaigi.org/2019/presentations/PeterQuines.html#apr20

RubyKaigi 2019

00:00:00.269 Hi everyone, my name is Colin Fulton, and I'm a front-end developer at Duo Security, a division of Cisco. Sitting right here in front of me is a vintage Apple II computer, which is actually older than I am.
00:00:07.680 Just to give you a full demonstration of what's going on here, I'll turn the computer off. On this five and a quarter floppy disk is a custom written assembly slide presentation software, along with all my slides. The total code for this is probably smaller than the GIFs in everyone else's slides, so I'll go ahead and pop that in. Let's turn it on.
00:00:26.130 That knocking sound you hear is because, to save money, they didn't actually put a sensor in the Apple II to read where the disk head was. So when the computer first turns on, it doesn't know what sector it's reading, so it bangs the head against the far side a couple of times just to make sure it's definitely in sector one. I'm going to go ahead and run my slides. It'll take a second to load from the disk. Alright, here we go.
00:01:05.519 First, I need to make an apology. In the description of this talk, it said to come see Ruby running where it's never run before. Unfortunately, before I gave this talk, I found a couple of bugs and there were a number of implementation issues. Nothing that made it impossible and nothing that couldn't be fixed, but it meant that I wasn't able to get everything running so I could actually show you the code today. Unless, of course, you wanted to look at a bunch of assembly running, which is not very interesting. For that, I'm sorry. However, I think you've all seen Ruby running before.
00:01:28.170 I also mentioned in the description that I was going to show you how such a rich language can be squeezed onto such a small computer. That's what the majority of the talk will be about anyway. So you'll see how we actually managed to squeeze Ruby onto this little tiny thing. But first, before I can show you how we're going to put Ruby on there, it's important to know how programming used to be done, specifically in 1977, the way Steve Wozniak originally wrote software for the Apple II. I'm going to exit out of my slide software and go into what's known as the monitor.
00:02:23.010 When Steve Wozniak was originally developing the Apple II, he wanted to have a BASIC programming language on there. However, he didn't have the money for all the debugging tools and assemblers that were normally required. So he actually wrote out this particular program, the Mini Monitor, in machine code, which lets you see what's going on in memory and disassemble code. This Mini Monitor was all written out by hand in machine code, so he could then write assembly code to implement his version of the BASIC programming language.
00:03:22.859 I wanted to show you a little bit of what that's actually like. In the Mini Monitor, we can look at memory. At byte 800 in memory, it's 0. If I say 800 dot F, that will show you all memory between 808 and 8F. I’ll go ahead and hit enter. There’s a bunch of data there. We're going to write a program in machine code the old-fashioned way. I’ll say 800 colon, which says to insert the following hexadecimal into memory. What we're going to type in is 00EB90B0, and I assume you can all see where this is going with 996000, followed by 88D0F700.
00:04:24.510 There we have my program. However, I need a little bit of data to make this work because I think you all can see this program isn't going to do anything interesting on its own. So I’m going to type in a little bit of data. This is literally how Steve Wozniak implemented the program we're seeing right now. I'm going to do 48, 49, 60, and now I think you're all going to get really excited for the next part. I’m going to do 50 to 55, 40 to 59, and then the obvious bit is where we do 4B4149 then 47, 49. Of course, to finish it off, we want to be exciting with 61. We write that to memory and there we have our program.
00:05:14.430 Machine language may not look very readable, but it was a long time before we actually had the assembly programming languages to make it easier to write up machine code. There was a graduate student named Don Geils whose job was to take in the programs that professors and others would write and hand translate them into machine code. He thought this was very time-consuming, so in his spare time, he started writing an assembler, a way to input nice human-readable commands that a computer would translate. One of the professors at the university, John von Neumann—the creator of the von Neumann architecture—saw that Don Geils was doing this and stopped him, thinking it was a frivolous waste of time because he believed you should be typing all this out by hand.
00:06:45.240 This sounds silly now, but computers were very rare back then. There may have only been a handful of them in the world, and what Don Geils was doing was equivalent to one of us today using an entire supercomputer just to run a text editor. It was considered overkill. I’m going to take my program and, hopefully, if I wrote everything right, I’ll write 800, and then if it executes correctly, we'll see our program in action. I don’t know if you’ve done live coding before, but I may have made a typo; I have about a 50% accuracy rate writing this program. So if we run it, we see it flash on the screen with 'Hi, Ruby Kaigi.'
00:09:12.240 Now, that '00' in the middle there is a break command, which brought us back into the monitor. We see here that M0AX equals those values, which represent what all the registers had in them after executing our program. If we disassemble the program, we can see a little bit more of what’s going on. If I do 800L, that will disassemble the program. In the middle, we see the actual original machine code, and then we see the assembly. I have this nice 1980s pointing device here. If I switch on, I’ll put an arrow on the screen.
00:10:02.810 First, what our program does is it loads the Y register (LD Y) with a literal value. The hash symbol ($) indicates hexadecimal. We’re putting the value 'E' inside the Y register. Next, we're going to load A (LD A) with whatever value happens to be in the memory address 80, offset by what’s in the Y register. We’re essentially doing an indirect memory lookup, where we're looking at a specific part of memory and loading that into the A register, offsetting it by the Y register. Afterward, we store whatever is in the A register into a different memory address, again offset by the Y register. We then decrement the Y register and branch if not equal. If the previous decrement does not return zero, we loop back to 802 and continue this until the Y register counts down to zero.
00:11:44.400 Once it reaches zero, we hit the break instruction, and everything after that is just data, which in this case is 'Hi, Ruby,' displayed in flashing text. The older Apple IIs didn’t have lowercase letters, which is why they created this character encoding with all uppercase letters, all uppercase with inverted colors, and then all uppercase that would flash different colors. So while there may be no lowercase, we at least get flashing text. The assembly program simply writes this to the screen. It’s important to note that we're not doing something special to write to the screen; we are directly writing to memory. The Apple II has a very large address space that anyone can write to.
00:12:50.990 This programming in assembly is really painful. While it might not be as bad as you'd think, it's still not the most pleasant thing. Recently, I've been doing a lot of it. Steve Wozniak implemented a version of the BASIC programming language that ran on the Apple II, which later got dubbed Integer BASIC because it did not handle floating-point numbers. Integer BASIC was very fast due to its design, as Wozniak wanted people to be able to write games. However, it had limitations, so eventually Apple reached out to Microsoft, which provided Microsoft BASIC, later renamed Applesoft BASIC. This is commonly found on most newer Apple IIs today.
00:13:26.600 To start programming in BASIC, I’ll do command B and hit enter. BASIC is a far more pleasant programming language to work in compared to assembly. This is how many people first learned to program on computers. However, BASIC still has its quirks. Let's consider a variable; we can say, 'Our variable is Ruby'. If we set Ruby equals 'great,' there’s a type mismatch error. Strangely, you can't have string variables unless you denote them properly.
00:13:55.250 If we write Ruby followed by a dollar sign, it stores it as a string variable with the dollar sign indicating that it should recognize it as a string. If we create another string variable called 'rust' followed by a dollar sign, it will store another string variable. Now, let's print what's in that 'rust' variable, and it returns 'also great.' This seems normal. Now, if we print 'Ruby' followed by a dollar sign, it incorrectly prints 'also great' because Applesoft BASIC only looks at the first two characters for variable names. So since both 'Ruby' and 'rust' start with 'ru', they collide. This necessitates the creative naming of variables.
00:15:31.610 Switching between programs like this is not easy since they all share the same memory, yet it appears that BASIC programming itself has its amusements. It's a little silly, but it would be significantly better if we could run Ruby on the Apple II, and that’s been my goal for a long time. Last year at RubyKaigi, I had a conversation with someone, and we started talking about old computers. I mentioned the concept of running Ruby on the Apple II. He had done assembly programming on the Apple II and suggested that it wouldn't be possible.
00:16:13.800 However, I had zero experience with assembly programming or coding for the Apple II, so I decided to see if I could actually do it, which turned out to be not a great idea. However, I've had a lot of fun over the past couple of months trying to make this work. So here’s the plan: Ruby is written in C, so if we just take CRuby and compile it into the 6502 processor that’s on the Apple II, everything should be great, right? However, there are several problems with that.
00:17:23.170 CRuby expects certain functionalities, like a file system. The Apple II did not have a viable file system. There were primitive operating systems built on it, but they were not aligned with what CRuby expects. CRuby also anticipates Unicode support, which the Apple II lacks. Although this model can run in a mode that might yield ASCII characters, it doesn’t provide comprehensive ASCII support. There are even bigger problems at play. You see, the binary for CRuby, the interpreter, exceeds three megabytes in size. While you could get late-model Apple II systems with a megabyte of memory, that was exorbitantly large for the original Apple II, which had the lowest model available with only eight kilobytes of memory.
00:18:34.670 Moreover, the original design also limited memory sharing with video displays and other running code. This adds to the complications. It worsens since the 6502 processor cannot handle nearly as many operations as modern processors, meaning additional operations would inflate the code size significantly beyond those three megabytes. Thankfully, we have M Ruby, which was designed to be a very small Ruby for small devices like this. If we compile M Ruby, we can put it on the Apple II and everything should be alright. However, it is still too big—M Ruby is much smaller.
00:19:56.050 Currently, there are many great talks focusing on optimizing M Ruby for small amounts of RAM but storing it in larger amounts of ROM. This does not work on the Apple II, because ROM cannot be written to, and consequently, we must fit Ruby in RAM. The small size is imperative. Yet, M Ruby isn't optimized well for 8-bit CPUs. Ruby assumes larger pointers for moving data around. However, given this is an 8-bit CPU operating solely on values between 0 and 256, the design proves challenging.
00:20:47.100 Another daunting challenge arises: the C programming language is too high-level for our needs. As Ruby programmers, I’m sure you'll chuckle at the idea of C being a high-level language, but, by definition, C qualifies as one. A low-level programming language allows you to look at its code and translate it directly into assembly without ambiguity, while a high-level one may not yield that clarity. That reflects how optimizing compilers work with languages like C.
00:21:32.740 Lacking direct control over the machine code we viewed earlier limits our ability to optimize and compress as desired. Both M Ruby and CRuby are written in C, which isn’t ideal. But is all hope lost for running Ruby on the Apple II? No, we will do it the hard way—we’ll create n Ruby! What is n Ruby? It retains all the joys of Ruby, but it'll be exceptionally small. We will have to eliminate some features, but that’s acceptable.
00:23:06.590 At RubyKaigi, I feel fantastic presenting amidst talks on performance optimizations while discussing running Ruby on computers that fewer people use. We must bear in mind that n Ruby is going to be slower than most Ruby versions, but it will be notably smaller, entirely written in assembly. Before working on this, I had zero experience coding in assembly. My background is in Art and Design and Theater. So step one was to learn assembly programming, which isn't as bad as you might think, though it was an interesting adventure.
00:24:12.430 So what does the 'n' in n Ruby represent? It could mean nano Ruby, but the real reason is that an 'n' is just half of an 'M.' M Ruby was too big, so we’ll simply chop off half the 'M' and call it an version. How do we design n Ruby? We need that flexible Ruby syntax, which should include method calls, parentheses, and more so that everything remains an expression, allowing nesting and other fun implementations. Additionally, we need to be purely object-oriented, meaning that even integers will be objects, which does make our code somewhat slower. But if you desire fast execution on the Apple II, you're likely stuck with assembly or a more efficient language.
00:26:12.320 We also need to include core features that users love: addition, classes, modules, composition of code, and more. Implementation of modules is relatively straightforward because all a module essentially is, is a collection of methods, which we can easily accommodate. Blocks may be trickier, but the core content of a block is static code, so we can store it like any other method while tracking local variable metadata during instantiation. It's essential to help create an interactive way to program, which we can achieve, albeit in a somewhat convoluted manner.
00:27:55.750 We also want enumerables, so we can avoid for loops; we want each, map, and reduce. Furthermore, employing eval will be critical so that we can input CLI commands, as it’s not truly Ruby programming unless you can write commands. Making the language dynamically flexible is also a necessity. You must be able to redefine methods at any time. Although that might slow some things down, it’s still an important feature. For instance, redefinition of the plus operator on integers could lead to some hilariously poor Ruby code; yet, the functionality should be retained as we develop.
00:29:48.460 Another design consideration is dynamic memory allocation and garbage collection. We must strive for a minimal garbage collector and very efficient memory allocation code. You should be able to create many objects—almost carelessly—but I added my own restriction: you can create as many objects as you want as long as you maintain a total of 256 objects or fewer. How many here have written a Ruby program that utilizes more than 256 objects? If you're not raising your hand, you likely are not telling the truth. Ruby bootup typically generates a quantity far greater than that, but I believe 256 objects is an adequate number to facilitate concise coding without crowding our limited memory.
00:30:59.780 Everything becomes manageable if we or you can identify each object in memory with a single byte. By limiting our object IDs and pointers to one byte in size, we can make the allocation process and garbage collection significantly more compact. That way, we can deftly fit our operations into a cramped space. While assembly programming is daunting—often among the top disliked programming languages—it’s not as scary as it might seem, and we can perform various tasks solely through assembly code.
00:33:07.470 We will increment registers, add values, and sheathe them with syntactically simple but effective commands that nonetheless perform complex tasks. You can manipulate bytes in memory directly. Yet, I must clarify, we don't need an inordinate amount. Continuing this, we’re able to jump to another part of code, utilizing conditional jumps. Furthermore, the syntax itself may seem intimidating, but it's quite simple when broken down. Basic commands consist of a label for easy reference, a brief mnemonic for instruction, and an argument for context—whether it be a literal value or a memory address.
00:34:51.670 As previously mentioned, the commands that you’ll find in 6502 assembly are brief and highly efficient. Instructions primarily occupy one to three bytes long. This is minuscule compared to current processors that obsessively utilize excessive bytes for instructions. By mentioning this limited instruction set, we note that there are 56 available mnemonics—quite modest compared to the present-day processors. We retain a 16-bit addressing scheme but must also weigh the limitations. Fueled by flexible addressing, we can simultaneously address 64K of memory as needed. To expedite things, we might utilize the zero-page feature for addressing.
00:36:44.290 To be concise in our operations, garbage collection can prove a challenge, and we won't afford complexity through the traditional mark-and-sweep method found in CRuby. Instead, we can implement reference counting: every time a reference is added to an object, we increment a count. Conversely, we decrement upon removal. If that count hits zero, the object can be freed from memory. However, be warned; reference loops will remain a pitfall! There are solutions, but time isn’t on our side to explore them thoroughly. Our slots will store data via object references, with IDs pointing to class references.
00:38:37.500 We’ve arranged slots measuring 16 bytes to accommodate our minimal object size, supplying us with memory space. If we loop through and fail to fit additional objects, an error must occur, indicating the memory limit of 256 objects—a minor span when acknowledging the grand scale of traditional Ruby coding. However, the capacity for 4 kilobytes for object storage can be efficiently utilized, simplified by how we've outlined operations.
00:39:41.920 Thus, how do we transition from using a single byte to calculating an object ID? In our case, let's illustrate object allocation by assigning identifiers to respective locations within memory. Our 'n' in n Ruby conveys a versatile assembly approach to utilize our programming via traditional stack methodologies, allocating and deallocating seamlessly for maximum efficiency. Streamlining assembly code will readily translate to an XML-based infrastructure for operation within our program.
00:40:39.690 Next, understand how we generate operations and parse Ruby syntax. Although initially daunting, we’ll build symbol parsing mechanics through our Ruby code performance, condensing longer identifiers into efficient references. We further extend to handle assignment statements seamlessly—assuring blocks of code remain concise. Furthermore, as Ruby syntax becomes prevalent, our associative operations must transfer seamlessly from traditional notation into dynamic evaluations.
00:41:57.000 Ultimately, we achieve coherent source to object translation employing concise assembly code while accommodating Ruby’s flexibility. Our aims converge towards ensuring 'n' truly embodies the spirit of Ruby on the 8-bit architecture, all while we streamline user interactivity facilitated through modern conventions. Perhaps over time, we could visualize n Ruby functioning on various processors, providing an engaging Ruby environment on modern tiny circuits. We're keen to explore developing n Ruby across different limited platforms for unparalleled versatility and engagement.