To master your tools, make them
I admire the tools I use in everyday life: it’s great how vehicles can get us from one location to another swiftly, food is made, money trades hands, and society works well enough that a large amount of people can get what they need done without much coordination. It’s kind of a wonder how well society works – I assume that most people haven’t studied the law and yet we have a rough understanding of what is and isn’t legal. I assume most people don’t have precise knowledge of how the economy works, or how politics gets stuff done, or even how medicine works, and yet, here we are, still trucking along. Most of these show good design by those who designed the systems. Take money, for example: it’s very complicated to understand how and why money gets produced, how it’s valued, and so on, but we’ll buy or sell a lot of things and the rest of that doesn’t really matter. Same with medicine, we take for granted that most medicine does what it says, but it’s very hard to prove that medicine actually works in the way we want it to.
To learn more about anything, it’s important to get hands-on experience. In my case, as a programmer, we have many layers of abstraction between our code and the computer. Most of these exist for good reason (I would rather not run my code without an operating system or write my code in machine code without a compiler), and there are good implementations, so why bother learning more about them?
In my humble opinion, it improves our skills as craftspeople. You learn a lot about your tools in the process, and the reasons why they made the trade-offs they did. I personally seek to increase my appreciation of stuff so I can keep myself motivated to learn something. What better way to appreciate something than to learn how painful it is to implement?
I’ve tried implementing many things, but my favorite things have to be an OS, and compiler, and I’ll explain why I chose those projects.
A toy OS
The bridge between hardware and software is the operating system. The code that protects you from yourself. I started off by reading through Operating Systems: Three Easy Pieces, and implementing most of the projects throughout the book. The companion code and problems outline some basic programs to write, but you’re generally benchmarking your host OS to learn more about it. That wasn’t enough for me.
I moved onto reading Lion’s
commentary on Unix since it gave commentary on an actual system
(Unix V6), but I found the code hard to digest since it was using C from
the 70s. Finally, I stumbled upon Octox which is an
implementation of xv6-riscv in rust, and you can run the entire system
with a cargo run command. I took this code, tweaked it a
bunch, and learned a lot about operating systems in the process.
Operating systems have the tricky job of taking a scarce resource (the hardware of the computer) and doling them out to multiple users at the same time, while also not allowing the users to step on each other’s toes. The operating system also has to provide a stable interface that doesn’t change, since any user visible changes require changes in a lot of code that rely upon the OS, and I learned about providing a strict interface boundary. Data in, data out. Don’t break your promises. For a normal monolithic kernel, it gets very hard to keep this promise, because so much of the kernel is hidden from the user. For a microkernel, since most of the functionality of the OS is owned by various services in user and kernel space, it’s not as bad to break backwards compatibility, since that only affects the code that requires that particular service, rather than the whole world.
Boundaries are important for all of us.
A Compiler
I followed Crafting Interpreters to implement two interpreters, the first part in Rust and the second part in C. A compiler really is just another program – you give it a program, some knobs to tweak, and you get back an executable program.
Writing a compiler was useful in more areas than language design – I improved at parsing various data structures and stopped trying to hack a duct tape parser with regex, but started composing parsers off of smaller parts to make a better parser. My knowledge in interpreting code also helped for projects at work that required constructing languages that supported powerful features like macros, but I also learned that testing is invaluable to a program as complicated as a compiler.
You have to generate tests for any arbitrarily valid grammar and the compiler always needs to give the correct result. The way that most compiler writers do that these days? Fuzzing. I added it to my toolbox and started fuzzing my own projects to find bugs. I found a lot of them. Now I have a lot more bugs to fix.
Overall, the world became easier to parse.