Why Learn Compilers (2021)
amasad.me
amasad.me
There is a book-length expansion of this paper that goes into more detail: https://github.com/IUCompilerCourse/Essentials-of-Compilatio...
Like other sibling comments mention about Crafting Interpreters, I recommend following the book but implementing in another language you're familiar with. I ran through "Writing an Interpreter" in Go once, but felt like it stuck a lot harder when I went back through and did it again in Swift. Though you're still following code with the hard parts figured out, the simple act of translating the syntax will mean you're looking harder at what's actually happening.
Someone on here a few weeks ago also mentioned chapter 8 of Rob Pike and Brian Kernighan's "the UNIX Programming Environment". I've had an old eBay recovered copy of this book on my shelve for a few years, it's a neat intro to shell as well, but I've been working though chapter 8 this Memorial Day weekend to dip my toes back into the vintage C waters. The book builds up a simple calculator adding variables, builtins, complex control flow, and functions. It starts as a pure yacc parser, and then shifts to a strategy reminiscent of bytecode. This time I'm using a process I'm familiar with (language implementation) to sharpen my C skills.
By doing either of these projects, you also then have a testbed language where you can start developing more advanced concepts. GC, a type system, externalizeable bytecode, linking, the list goes on forever. About four years ago I started working on the Apex language at Salesforce, and the problems encountered in these projects are absolutely still relevant at the scale we're compiling and executing code across our datacenter.
EDIT: I'll also throw in... it gets a lot more fun when the parser's done. If you're finding recursive descent to be a bit of a brain twister, fight through!
YMMV, but it's hard to genuinely learn from Crafting Interpreters. All the code is already written for you.
Another strategy I used was to look at the title of the chapter, then work ahead as much as possible to implement that. It really helped my learning process to naturally explore the problem space myself, then read through and see how my naive attempts compared to a more seasoned implementation. But as the parent said, ymmv!
I got rid of the code gen and visitor pattern stuff by using records in C# with pattern matching which felt a lot simpler to me.
https://www.cs.princeton.edu/~appel/modern/
Depending on where you're coming from, you can pick C, Java or Standard ML as implementation language to go along and implement the Tiger language in its variants across the book.
It isn't perfect and it might not contain all the best practices out there, consider it notes from a student struggling to learn about the topic
https://returnzero.win/2023/01/28/lisp-my-second-attempt-at-...
Resources I came to know about from HN comments were very valuable in my life and career.
It’s exciting. Especially the first time, it’s magic. It’s a lot of fun.
Once I wrote a DSL for a rule system, compiled down into Java source. Later, I wrote another higher level rule system that generated the DSL.
At one point I think there was a USENET .sig line that read “Will write code that writes code that writes code for food.” Indeed.
Right now, if I want to mess with some transformation passes for an existing language, I would just look for LLVM and Clang for C for example (readily made front end, and backend with hackable middle end).
P.S. I am a hobbyist at this stage and you can consider me knowing nothing about LLVM. My advice is based on my friends' (same colleague) who works for nVidia for their CUDA compiler using LLVM.
Another reason for me to be interested in LLVM is that I used JVM languages (Scala, Java) for the past 20+ years and with GraalVM and its polyglot support for LLVM based languages; whose support was added using LLVM.
my rusty two cents (Rust is awesome too ;)).
Not even close! Optimization and code generation is a fantastically hard problem full of heuristics and tradeoffs.
There is definitely more than one way to compile C source code to x86 binary code. There is even more than one way to compile TypeScript to JavaScript if you consider things like whitespace, indentation, and constant folding.
The rest of the article is alright.
It's bit of a stretch, sure, but I don't expect my performance problems to go away by recompiling stuff over and over again without changing either the code or flags and expecting the optimizer to make a better decision next time. It doesn't work that way.
This is true, but not what they actually said.
> you shouldn't get something radically different because it's Tuesday, midnight [...] I don't expect my performance problems to go away by recompiling stuff over and over again without changing either the code or flags and expecting the optimizer to make a better decision next time
Maybe we should, though. I shouldn't have to be forever locked into the performance profile that matches the -O level that my distribution's package maintainers used at compile time.
I'd love to see a shift in programming systems towards a place where the language is designed with particular attention to how fast it can be processed by the toolchain to get _something_ on disk as quickly as possible and further optimization is deferred to a later stage to be fulfilled by a separate, asynchronous process. Imagine if there were no tradeoff between time spent waiting on the compiler to finish vs runtime performance, because your program no matter how large would never take more than 20 seconds* to compile. For more modestly sized programs, the effect would be the ability to test it almost immediately, but the compiler continues optimizing away all the while—to the point that you could even go to sleep on Wednesday and wake up on Thursday morning with a program that's even snappier. The expected outcome should be faster compile times and faster binaries.
* or choose your own adventure
But in practice it does work exactly that way because of PGO and if we're precluding PGO then the observation is as banal as "programs that have no side-effects are pure". Like I get that the post is trying to paint some beautiful picture about how compiler passes are abstract beautiful transformations of representations of programs but it's not a useful picture at all.
It is pretty useful picture in my eyes, because that's what I've been observing since like forever. Non-deterministic compiler would be pretty hard to test or reason about.
It might be a banal observation, but an important one if you want to contrast a compiler with something like an OS kernel or most modern programming projects that interact heavily with outside world. When you throw hardware, network traffic, or users into the mix, it gets crazy.
It is actually a very useful picture especially to a beginner.
PGO is an edge case and not used a whole lot in practice. Many compilers do not support it at all, even production compilers. Someone learning to write a compiler does not need to think about PGO. And besides, a compiler with PGO is still a pure function of source + flags + profile.
Both clang and gcc support `-fprofile-generate`. Beyond that generic infrastructure, in my area (DL compilers) if you're not doing PGO/autotuning you're not serious.
>Someone learning to write a compiler does not need to think about PGO.
That's like saying someone learning to write a compiler doesn't need to think about optimization at all - sure maybe the first time. But then every time after that it should be at the forefront of your mind.
How many binaries in a typical Linux distro are built with PGO for example? The answer is approximately zero.
Congratulations on your recent PhD in quantum machine learning or whatever -- I hope you find some way to speed up FB/G's ad serving algorithms by 1% and land that promotion. I'm sure you're very smart.
Your point in bringing up Linux was to reiterate your original point? Interesting.
> I hope you find some way to speed up FB/G's ad serving algorithms by 1% and land that promotion. I'm sure you're very smart.
lol
- Bug in your program? Could be a dependency of a dependency built wrong, better do a full clean and rebuild.
- All of your pre-release tests passed? The code you shipped doesn't work, because you hit that unlucky 1%. Imagine how bad this would be back when software was released on CDs...
- Have 70 dependencies which are like this (not hard in the webscale era)? Now your CI needs to retry every build up to 10 times, because every build has a ~63.4% chance of failing. And there's still ~1% chance your CI fails anyways because you got 10 unlucky builds in a row.
I'm sure there are more stories of these kinds of issues...
That's not the only kind of non-determinism, you know?
Lots of algorithms benefits from access to random bits, too.
> for each input, there is one and only output
A randomized algorithm which uses non-determinism but produces the same output is OK.
Also, psuedo-randomness is OK as long as the same program compiled in the same version of the compiler always gets the same seed.
But if the algorithm can produce different output causing the same program to compile to different assembly, that's a problem. Unless you can formally prove that every possible variation will have the same behavior, but formal methods + non-determinism is hard, so it usually isn't worth it.
Even random hash order in hashmaps can create problems: see https://github.com/rust-lang/rust/issues/84447 and https://github.com/rust-lang/rust/pull/82272#issuecomment-82..., though this is a very small issue in the diagnostic reporter and not a case of 2 valid programs with different semantics.
Or anything that compiles a corpus into a model, though we typically call that training rather than compilation :).
Having said that, most people are not careful, and most languages make it very hard to be careful.
The keyword is 'deterministic parallelism', if you want to look into the research. See eg https://www.well-typed.com/blog/2012/12/deterministic-parall...
Nondeterministic doesn’t mean random. Consider how NFAs relate to DFAs and then realize the same applies to nondeterministic TMs.
I support deterministic compilers due to deterministic builds. For a given input source code, compiler executable, and command-line flags, it should always produce the same binary, regardless of the time of day, random number generator, I/O, threading, environment, etc.
My refutation of the article is that he made it sound like a compiler is like computing Fibonacci numbers - here are all the valid inputs, here is how each one maps to its one and only correct output answer, so fill in the code to accomplish that.
Also in the previous paragraph he wrote:
> Because the most basic compiler architecture is standard, you have limited boundaries and degrees of freedom when designing one. This might sound restricting, but actually, it's freeing because you get to play in a predictable sandbox.
Though, of course, in practice you can use pseudo-random bits and fix the seed.
Ya that's a hilariously naive take (the quoted line not your response) given PGO, to say nothing of MLGO.