TinyCompiler: A compiler in a week-end
ssloy.github.io
ssloy.github.io
(Crenshaw's Let's Build a Compiler is an excellent source too if you want to go one step lower and go standard library free, focusing only on function call stacks: https://compilers.iecc.com/crenshaw/)
But once you’re ready to generate code, use llvm or something like that because you are going to hit very problematic roadblocks early in your effort that will kill your interest in compilers otherwise.
Some examples are: ensuring you can compare expressions for equality, ensuring that changed expressions don’t violate dominance, SSA conversion, avoiding infinite loops when analyzing cfgs. If you don’t start with knowledge of several things like this, you are looking at rewriting your compiler a dozen times. That is a great way to learn about compilers, but perhaps not the first compiler you make.
https://github.com/ymyzk/tinyc/blob/master/tinyc/generator/n...
in your blog, i would have liked to see the BNF grammar (without having to peek into the code).
now, heres your next assignment :) - do the same for functional programming (lambda calc) or logic programming (prolog). That would round out the education on languages and compilers for computation.
I would love a a longer course.
Every features on top of that has either leaky abstractions and/or nightmare scenarios.
That said I'm not claiming the world should run on this type of code. Or should it.
"Pascal was influenced by the ALGOL W efforts, with the explicit goals of teaching programming in a structured fashion and for the development of system software.[5] A generation of students used Pascal as an introductory language in undergraduate courses."
Its grammar made it relatively easy to write compilers for the language, which could be done as undergraduate exercises. Of course, this did not make it so popular with professional programmers - see http://eprg.org/computerphile/pascal.pdf by Brian Kernighan, although extended Pascals such as Delphi were very productive in the Windows environment.
Turbo Pascal's successor, Delphi, was a successful rapid application development environment, and still being used today. Pascal was used extensively at Apple for the Apple III and Lisa, and was also the original development environment for Mac apps.[1]
But he was probably right about Ada, which has survived and may even be enjoying something of a renaissance as a fix for the sins of C (and C++) that isn't as alienating as Rust.
[1] https://archive.org/details/bitsavers_applelisapriefHistoryo...
Modula-2, designed for memory safe systems programming, in 1978, succedding Pascal, had support for unbounded arrays and strings.
By the way, GCC nowadays has GNU Modula-2 integrated in the official set of supported languages.
Code is 10/10. Go for merge! :-)
- [1] https://github.com/ssloy/tinycompiler/blob/main/test-program...
The more real-life alternatives to those abominations of gcc and clang, the merrier.
I wish the linux kernel devs did care to keep the door _reasonably_ open for such compilers (with assembler source files as alternative to inline assembly, some extensions avoidance and niche expensive compiler features).
This is another case of why super complex syntax computer languages should be avoided like hell (c++, rust, etc).
I would recommend libfirm over QBE by at least 10km.
https://libfirm.github.io/ https://github.com/libfirm/libfirm
I had a wall of supporting tokei but decided against it, but the short of it is, LLVM is phenomenally massive, and libfirm at 135kloc and 35kloc of comments is but a raindrop that is in the 15Mloc+ that is LLVM.
Perhaps instead of looking towards libfirm, we should look at cranelift. It is of comparable size and actually includes 12kloc of documentation, not just comments.
https://github.com/bytecodealliance/wasmtime/tree/main/crane...
Cranelift has been looking interesting. Also, LuaJIT.
https://c9x.me/git/qbe.git/tree/rega.c
QBE is a one person performance art project.
As for QBE itself, many of the core transformations are fairly standard, which makes it somewhat more approachable for me (e.g. Cooper et al's dominance algorithm, Rideau et al's parallel moves algorithm, etc.). Of course, this doesn't negate the fact that it's not as "hackable" as they probably intended.
But it is written in plain and simple C99, so it is at least much less toxic than LLVM.
I wonder how cparser (did not check if it was plain and simple C)+libfirm compare in performance to cproc+QBE on my benchmarks. May have to take some time to check that.
Whatever the results, it is always good to have, again, a real life alternative for optimizing C toolchains.
The main issues are the heavy usage of gcc extensions by linux (and glibc, etc).
Definitely worth some benchmarks, until libfirm SDK is reasonable: there is a bad start with cmake (c++) and I have no idea if it requires expensive dependencies (even if those are written in plain and simple C).
It replaces clang, it is not a fork of it.
Maybe, "normal"/"average" devs would help you to get the point.
That doesn't diminish Einstein's achievement of course.
https://github.com/seanbaxter/circle
It is like if one tried claiming that Fabrice Bellard was an example of a median dev.
The number of C++ compilers written by one person is one. And because of this, it gives us no predictive power in what pjmlp was trying to assert.
I agree with you, Albert and Lorentz and Riemann got caught in the rhetorical crossfire.
https://pikuma.com/courses/create-a-programming-language-com...
Not to take away from this awesome sharing.
Production compilers are complicated because of the feature set of languages and performance requirements.
You can write a lexer + parser + treewalk interpreter for a simple language in a day if you know what you are doing.
Just getting the grammar straight on the naturally recursive structures can be a trick. They also touch a large portion of the code generation and run time.
Get:
(a + c/2) * sqrt(b)
working and you’re 80% there.This is why teaching material should be tailored towards teaching rather than implementing demented stuff from the real world just because it is "there." You can easily implement a parser that ignores precedence and tell the reader that we are using brackets to force precedence instead.
In a real-word parser, you might use something like this[1] or some other home-grown algorithm if you didn't know about it. Doesn't matter as long as the parser works.
[1] Top-Down operator precedence (Pratt) parsing https://eli.thegreenplace.net/2010/01/02/top-down-operator-p...
I never got much farther than that though, I've looked into making a compiler many times and got overwhelmed every time.
1. You dont recognise the recursive nature of the problem,
Or
2. You don't recognise the stack-based nature in the problem (shunting yard algo, etc).
IOW, you did it the hard way :-).
https://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-...
(a + c/2) * sqrt(b)
is not a simple language. (* (+ a (/ c 2)) (sqrt b))
would be a simple and precise to parse language, and the typical compiler for this is written in a day. Search for SIODThe alternative is going to an IR and doing full register allocation but then you need to implement Lengauer-Tarjan to get into SSA, all the same parallel load stuff for phi functions/block arguments, out-of-SSA, reconstruct in optimisation passes all the information you discard by going to a linear IR, etc.
If you are saying that constructing SSA following the classical approach is a premature optimisation, perhaps you would prefer Braun et al's retelling of Cliff Click's SSA construction algorithm - which works backwards from uses to place phis and requires no dominance information to be computed.
Why not fully maximal SSA + DCE? I mean, other than timings consideration. Fully maximal does not even need any graph traversal, it is entirely local to each block.
Braun et al's algorithm works backwards from uses (which generate liveness), so you get pruned SSA out. In the case of reducible control flow graphs, you also get minimal SSA. This is all without any liveness or dominance computation beforehand. Granted, you may want those things later, but it's nice that you can construct a decent quality SSA with a fairly intuitive algorithm. Also shown in the paper is that you can incorporate a few light optimisations during SSA construction (constant folding, for example).
I'm always happy to see more accessible resources for compiler writers.
---
As an aside: for displaying CFGs on the page, it would be very interesting to emit something somewhat dynamic. SVGs are always a good start, but there is a neat library for doing hierarchical graph layout (dagre, with d3-dagre handling rendering as well). In my own efforts at pedagogy, I've been interested in producing CFGs in-browser whose basic blocks comprise a "unified diff" view of the block (this being achieved in realtime by maintaining a subset of LLVM whose CFGs are persistent). Then it is more obvious what has changed: at least in the case of mem2reg which shouldn't introduce new blocks or move too much around (I forget if it hoists allocas to the entry block or not).
It'd also be cool to distil what underlying ideas you have found to be most useful in your efforts. The constrained scope of them may be useful to me, as I've wanted to create a kind of "advent of compilers" for years (advent of code but with a heavy slant towards compiler tasks/algorithms).
If all you do is construct an AST and interpret it it's not really a compiler is it. It doesn't compile from source to target. At most you're describing a compiler front end (arguably), or an interpreter.
I would expect:
lexer + parser + AST definition and construction + semantic analysis/type checking + X + codegen to ASM/WASM/C
where X includes
- definition of an intermediate representation (IR)
- lowering AST to IR
- static analysis
- improvers/optimisation passes (at least some simple stuff)
- code gen including register allocation
This can still be a simple "complicated" program, but there's more to a compiler than an AST interpreter.EDIT: I notice that the author of the original article has also started work on an optimizing compiler: https://ssloy.github.io/tinyoptimizer/
What if I dumped the AST to disk, called it "bytecode" and updated the "interpreter" to run this "bytecode"? Would you consider that to be a compiler? :-)
> where X includes
I don't consider any of these things to be essential. There are entire languages out there which run on interpreters of some kind. There are others which compile down to C/JS. Many compilers use LLVM as their backend. By your definition, none of them use (or are) compilers.
:) No. Under my definition there needs to be some non-trivial transformation into or out of an intermediate and/or target representation (either syntax-directed directly out of the parser, or from a materialized AST). Personally I would argue that even if you trivially "compiled" to sequential bytecode, if the bytecode is then interpreted it is hard to argue that you have created a compiler (the pre-JIT cpython interpreter is still an interpreter, even though it includes a bytecode representation). But I can see that this point can be argued, and historically a school exercise of writing a Pascal-to-pcode converter would be called a compiler, so sure, you can take that perspective if you like. Just don't get confused about whether you are learning to build a compiler (definition 1) or a compiler (definition 2).
Based on that experience, I would say your definition is more academic than realworldly as javac (or any compiler targeting the JVM) is not a real compiler according to it.
I think it depends on how you interpret my "non-trivial transformation into or out of an intermediate and/or target representation". For example if javac involves IR, SSA based analysis and transformation (which I assume it does) then that would be a non-trivial transformation and I'd call it a compiler. If on the other hand it was a direct syntax-directed transformation to java byte code then I'd call it a translator not a compiler. But I'm not claiming any authority over definitions here. Words aren't my strong suit.
https://github.com/openjdk/jdk/blob/master/src/jdk.compiler/...
I am fairly certain that it does not. The JVM is a stack-machine.
I wrote a compiler for a simple programming language that compiled down to JVM bytecode for a project in the early 2010s. Its output for test programs was as fast as the comparative Java ones because they were fairly similar at the bytecode level.
The HotSpot JVM is where all the optimization effort is applied.
A recursive-descent parser is a beautiful thing and can be implemented very quickly.
The problem, always, is people are given terrible reading recommendations which makes the subject more complicated than it is.
I'm curious what you'd recommend.
For what it's worth my goal was to compile to machine code. Anything less would have seemed insufficient. Later I got Appel's "Modern Compiler Implementation in Java" and Allen and Kennedy "Optimizing Compilers for Modern Architectures".
Cooper and Torczon "Engineering a Compiler" was recommended here recently. I haven't seen it.
Nils Holm's work https://t3x.org/
Teaching and Learning Compilers Incrementally - Jeremy Siek - RacketCon 2023 https://www.youtube.com/watch?v=43VA_QaTRT8
Nanopass https://nanopass.org/
original paper that was the basis for nanopass http://scheme2006.cs.uchicago.edu/11-ghuloum.pdf
https://www.reddit.com/r/ProgrammingLanguages/comments/gnzra...
The Crafting Interpreters and the Thorsten Ball books
https://craftinginterpreters.com/contents.html
"Engineering a Compiler" is quite approachable.
But, once you know the basics, you get the best bang-for-the-buck by looking at source code of various compilers and virtual machines.
This is exactly how my university's compiler course did it[1]. It was really nice to start with assembly and go upwards. We had all the lexing/parsing stuff being discussed around the middle of the semester rather than being saddled with theory-heavy regex, automata theory, etc right at the beginning.
The fun part are the magic algorithms you sometimes get to use
Sorry, having worked on language semantics topics, "well-defined" is not how I would describe either a programming language or the behavior of machine code.
> Personally, I get a twisted pleasure wearing a T-shirt with this print in the 2020s, even though I hate being a walking billboard. But the number of angry comments from confused people makes up for any moral discomfort of advertising on my belly!
It's possible the 'angry comments' are from people who may have mistaken it for an antisemitic, white supremacist symbol[1].