Everyone tells you to write a compiler. The advice is correct but the reasons given are usually wrong.
It’s not about understanding “how computers work.” It’s not about impressing interviewers. It’s about what happens to your model of programming languages when you have to make them real.
What actually changes
When you write a parser, you stop thinking of syntax as arbitrary aesthetic choices and start seeing it as a grammar — a set of rules that creates an unambiguous tree from a flat string. When those rules conflict, you understand why languages make the choices they do.
When you implement closures, you understand what an environment actually is. Not as a metaphor — as a data structure you have to allocate and carry around.
When you do register allocation, you understand why every optimization pass exists.
Where to start
Start with a small language. Lox from Crafting Interpreters is excellent. Or design your own — the constraint of having to implement every feature you add is a good design filter.
Tree-walking interpreters first. Bytecode compilers second. Native codegen last.
The part nobody tells you
The hardest part isn’t any particular algorithm. It’s holding the whole pipeline in your head at once — knowing that a decision in the parser will create pain in the type checker, which will create pain in codegen.
That’s the actual skill: managing complexity across transformation stages. Every large software system has this property.