I recently stumbled across the source to a NES emulator I wrote when I was a teenager. Reading through the source code, I thought a lot about how much I had grown as a developer and engineer since that long-forgotten summer. For instance, I now understand things like (ha!) having a consistent style (some of my code was indented over 24 spaces so that it would be in the middle of the monitor), writing comments (there are basically no comments in this code), and tests (I didn't even know that testing was a thing). But, I also realized that writing that emulator, and all of the development I did at the time, laid a strange foundation that colored how I've thought about programming since.
Up to that point, I'd puttered around a little in BASIC and C, but I didn't fall in love with programming until I discovered assembly language. Assembly was the first thing that really clicked for me: it was so simple, its semantics were so clear, and I found some great resources to help me learn (e.g., a nice tutorial I found on a BBS). So, in my heart, assembly language is my first language.
When I tell this to people now, they give me strange looks. It's almost unfathomable to teach people assembly language first. But, in doing so, I learned an awful lot about how software works, how computers work, and what foundation programming languages are built on top of.
My masterpiece as a teen programmer was the NES emulator I wrote entirely in x86 assembly language: almost exactly 10,000 lines of it. This was my first taste of a real program: it was potentially useful, it had meaning, and it was a moderately large software project (my first). Programming this was what made me dig deep and learn how to program.
Here's some things that writing that emulator, and assembly language in general, taught me:
Data is just memory. I remember trying to understand pointers in C and being incredibly frustrated with the strange syntax and the weird semantics. On the other hand, dealing with memory in assembly is so clearcut: memory is just a giant flat array (more or less), and pointers just point to stuff in that giant array. Indirect references, arrays of pointers, etc., make so much more sense when boiled down like this.
Even today, when trying to visualize many problems, I think to myself: what does this look like in memory? What structures are being used and how are they laid out? This helps me understand how algorithms and data structures work together.
How to write everything yourself. There were some limited BIOS and DOS kernel calls, but for the most part, if I needed some capability, say, reading from a floppy drive or writing to the screen, I had to write it from scratch: calling I/O ports, reading and writing to RAM addresses, etc. If I wanted graphics, I had to draw every single pixel myself. These days, I know that this is just reinventing the wheel, something normally regarded as a bad habit; however, I also learned an awful lot about inventing wheels doing it. I still have the bad habit of writing too many things myself, and not leveraging existing libraries as often as I should, so this lesson wasn't a complete victory.
Sometimes you just have to buckle down and program all of the tedious parts. A big part of the NES emulator is writing a cycle-accurate CPU emulator. I'm sure that you can imagine how daunting it must be to start writing
CPU.ASMand have an empty jump table with 256 entries, knowing that you have to write all 256 functions to handle every single operation. Even worse, you have to get every single piece exactly right.
This taught me an extremely valuable skill: being able to focus on tedious, repetitive tasks. Programming is absolutely chock-full of tedious programming that is absolutely necessary. A great example is date and time functionality (especially time zones): incredibly important code to get correct, but so incredibly tedious and detail-oriented. Being able to dive into thousands of pages of specifications and turn them into working libraries and APIs has paid great dividends in my career as a software engineer, and I first learned that here. Being methodical and untiring is a great skill.
And really, sometimes there is just no elegant solution. Often I see programmers strive to come up with a beautiful, simple, clean way to solve a problem, only to realize that the solution doesn't stand up to reality. Sometimes reality is ugly, and sometimes you have to write ugly code to solve it: you just need to program your way out of it.
Assembly is useful. Learning assembly has come in handy countless times: reading GDB disassembly of my code for degugging, understanding and optimizing performance, writing compilers. Every time I see an assembly dump on a screen, it feels like home.
You have to fail a lot to succeed. When writing code as a teenager in assembly language, I spent a lot of time crashing my computer. I mean, assembly is really easy to get wrong. When you get it wrong, you often do so catastrophically.
But more so, in order to learn how to build things, you first have to learn to build them wrong, and learn to recognize your mistakes. And this code is just full of mistakes.
Overall, revisiting old code like this helped me reflect on my programming: where I've been, where I am, where I'm going. I encourage everyone to save as much of their code as they ever write, and come back to it in a few years, and to reflect.