
My First Fifteen Compilers - azhenley
https://blog.sigplan.org/2019/07/09/my-first-fifteen-compilers/
======
lewisjoe
To folks interested in writing compilers, here's a tiny list of resources I
put together for myself: [http://hexopress.com/@joe/blog/2019/04/14/awesome-
compiler-r...](http://hexopress.com/@joe/blog/2019/04/14/awesome-compiler-
resources/)

~~~
langitbiru
There is a typo. "List Compiler" -> "Lisp Compiler". Also, you may need to
update the list with some suggestions from this thread. :)

~~~
lewisjoe
Thanks! Fixed the typo and updated with links.

------
mojuba
As another extreme, Turbo Pascal's earlier implementations had only one pass.
Just straight from Pascal to binary code with some optimizations too. I wonder
what the minimum number of passes could be by today's optimization standards.
Two? Three?

~~~
sansnomme
Go, V etc. all have straightforward compiler systems that produce reasonably
fast code without calling into LLVM. The fact that a lot of modern day
compilers don't have a built-in assemblers etc. are probably more due to
laziness.

~~~
dorfsmay
Laziness? Or time constraints and specialization?

I see the last stage of compiling as a special skills which requires a lot of
time, especially if you want to support multiple platforms. If you're thing is
to create a great programming language, then your time is better spent on that
rather than create a bad or ok backend supporting very few platforms.

------
jonjacky
There was a FORTRAN compiler for the IBM 1401 that had 63 passes (they called
them phases). See this paper from 1965:

[http://ibm-1401.info/1401-IBM-Systems-Journal-
FORTRAN.html](http://ibm-1401.info/1401-IBM-Systems-Journal-FORTRAN.html)

~~~
mntmoss
I came into the thread looking for this comment. The motivation in the 63-pass
compiler is to get the job done in a very memory-frugal form. That it's
relatively easy to follow is an additional bonus - a good lesson in software
design.

------
kazinator
ROT13 in 39 nano-passes!

Week 1: A-hoisting: free occurrences of A are renamed to the symbol @ which
doesn't occur anywhere in the input.

Week 2: N-substitution: every top-level as well as lexically nested N is
transformed to A.

Week 3: @-lowering: every @ (denoting a previously hoisted A from pass 1) is
reified as an instance of N.

------
panda_4
Every single time I try to do an online Compilers class, my brain gets stuck
at grammars. I understand the concept of grammars but for whatever reason
can't go from taking rules and writing a grammar out with it.

Probably will have to do an in classroom course for it?

~~~
aidenn0
Assuming you are talking about using a grammar to parser tool like yacc, you
probably should at some point figure out what your hangup is there, but
shouldn't let that stop you from writing a compiler. I very rarely use such a
tool for writing a compiler, and find that real-world compilers similarly
rarely use a tool.

Regardless of whether you are writing a grammar or a recursive descent parser,
start with a really simple language say:

    
    
        Expr = "A" | "(" Expr* ")"
    

Which is balanced parentheses with the token that is the exact character "A"
possibly appearing. All of these are valid matches:

    
    
        (())
        ((A(A)A)A)
        (((((AAA)))))
    

The parse tree should be such that each node is a list where each element is
either an A token or another parse tree node. Once you can parse that, then
you can start moving on to more complicated grammars; a possible complication
is to add "B" as a valid expression, with the caveat that B cannot be the
sibling of a parenthesized expression. That is:

    
    
        (ABA)
    

is valid, but

    
    
        (B(A))
    

is not valid.

I used single characters as the primitive here, so perhaps the next step would
be to add a tokenizer. "B" has special rules, so it will be the keyword
"bananas". A will be any other alphabetic token.

If all of the above is already doable, then perhaps I've misunderstood where
your hangup is. Let me know, perhaps I can help.

~~~
panda_4
Should that grammar be:

    
    
        Expr = "A" | "(" Expr* ")" | ""
    

so that (()) is s a valid match? Because then Expr can be an empty string
also?

~~~
aidenn0
Typically Star (*) means zero or more while Plus (+) means one or more.

------
deepaksurti
Is Kent Dybvig‘s compilers course, known as P523 referred to in the article,
still available online? Thanks!

~~~
quazar
I can't find the course online, but there are two github repos with students'
code:

    
    
      https://github.com/pavenvivek/Compiler-for-Scheme
      https://github.com/hyln9/P523
    

I did find a book ("Essentials of Compilation. An Incremental Approach"), also
from Indiana University, but not authored by Dybvig, on compiling Racket to
x86-64 assembly and it seems to take similar approach:

    
    
      https://jeapostrophe.github.io/courses/2017/spring/406/notes/book.pdf

~~~
asdfman123
Saving this textbook to hypothetically read in the future.

------
wglb
This is a marvelous article.

I first encountered a many-many pass compiler when working on the IBM 1800
(same architecture as the IBM 1130) where the Fortran II compiler had 29
passes. It was challenging, as the machines often had only 4k and the
removable cartridge disks were 5 megs.

------
cosarara
How does this approach play with error handling? If there is an error in my
code and it's only caught at step 10, won't the error message be completely
inscrutable? Will the line numbers make sense?

~~~
thedufer
In the only compiler I'm somewhat familiar with (OCaml), a decent amount of
the work is done in AST->AST transformations. In the AST, every node is
annotated with a location, which is a tuple of filename, line number, char
start, char end. As long as these are propagated in the right way, you get
good location information in errors.

------
oh_teh_meows
This might be a little off-topic, but I wonder if the idea of nano-passes can
be applied to other areas. For example, can we predict the future of
technology by gradually evolving their capabilities? Or say start from some
point in the future (interstellar travel!), and gradually work backwards what
we think is needed?

Or how about predicting geopolitics/economics?

To use this method effectively, I think we'll need to fully specify, to the
extent possible, the condition of each stage in time. That way, at any stage,
we can see what constituents might possibly interact with each other and
evolve something new.

~~~
derjames
A common practice in organic chemistry is to start from the target molecule
and work backwards trying to determine simpler precursor molecules that would
produce the target. 'Retrosynthesis'

~~~
oh_teh_meows
Really really cool! I wonder if we can do something similar with programming.
Suppose we have a conceptual framework where each module/system is a widget
made up of slightly simpler and well-defined widgets. If we can specify what
we want, perhaps a system can be devised that would try to build it from a
database of widgets. If some of the constituent widgets are not found, the
system could recursively specify each one and find/construct.

In this recursive process, the language used to specify the requirements of a
widget can be a changing DSL whose grammar and basic constructs co-evolve with
the complexity/abstraction level of the widgets.

This seems feasible because we (software engineers) are one such system.
Starting from basic transistors, we build ever higher abstraction layers along
with the language used to specify them (circuit diagrams -> microcodes ->
assembly -> C -> DSLs). I believe multi-layer neural networks are also a prime
example of such a system.

------
mhh__
I think all compilers should follow the many-pass, lowering, based approach.

It greatly reduces the surface area that needs to be tested while also keeping
the compiler's code from being spaghetti crap which can often happen in non-
toy compilers.

~~~
Gibbon1
I saw your comment and decided to look up how C#'s compiler does things.

It does two passes of the program text. And then does three dozen passes after
that. Each pass does what you suggest, only one thing.

[https://blogs.msdn.microsoft.com/ericlippert/2010/02/04/how-...](https://blogs.msdn.microsoft.com/ericlippert/2010/02/04/how-
many-passes/)

I think it's still fast because the passes are all or mostly O(n).

~~~
mhh__
There's no reason for the approach to be slow anyway because as long as there
is a reasonable way for information to be retained inter-pass then they should
be doing approximately the same amount of work as a less discrete alternative.
AST lowering can be very fruitful for languages with lots of metaprogramming
etc., i.e. DRY principle. Error propagation could be a bit lacklustre if not
handled with care, however.

On a more local basis, if you program in a language that encourages it,
composing work at compile time can save a huge amount of cruft that one might
imagine comes with using a many-pass compiler.

[https://d.godbolt.org/z/w9At24](https://d.godbolt.org/z/w9At24) (Already
posted on this thread), I wrote this little example to show how you can
compose series of discrete manipulations in D into what is indistinguishable
from a normal function as if it were monolithically written by hand.

That example (in D, in particular) could be made as pure-functional and
discrete as is desired but I used pointers to keep it small.

Compilers are extremely good at optimizing interprocedural code, or at least
better than a human, so a certain level of lazyness can be bestowed upon the
programmer at the expense of trusting a compiler.

------
moopling
Does anyone have recommendations for good resources on writing your first
compiler?

~~~
amelius
Also curious, what is the modern equivalent of the "Dragon Book"? A lot has
changed since the old times, e.g. JIT compilers, and garbage collected VMs and
such. Also CPUs have changed a lot, e.g. speculative execution, deep
pipelining, large memory cache hierarchies, GPUs, etc. Is there a compiler
book that addresses it all?

~~~
ernst_klim
Modern Compiler in ML/Java. Dragon book is nearly useless and could be useful
only for lexer/parser implementation.

~~~
MaxBarraclough
> Dragon book is nearly useless

Why's that?

~~~
ernst_klim
You can't take seriously a book which spends half of its pages talking about
lexing and parsing.

It's the book from the times when single-pass compilers were a thing, and
that's what it's about, a primitive single pass compiler. The field has
advanced too much since then.

Things you wont find in dragon book: type inference and modern type systems,
modern GC implementations, exceptions and error handling, modern register
allocations and optimizations, modules and parametric modules. Seriously, a
book spends 300 pages on parsers and 4 pages on type inference and
unification.

~~~
MaxBarraclough
Interesting, thanks.

------
rurban
Note that you do that kind of mega multipass compilers only when compiling to
static binaries, not when targetting dynamic languages. It's way too slow, the
compilation steps would last longer than the expected runtime. You can easily
combine all these passes into one tree traversal and optimize on demand and
benefit. 400 passes is beyond sanity.

~~~
rickbutton
the "nanopass" approach mentioned in the article is used in Dybvig's chez
scheme (not the nanopass library, just the style of compiler construction),
and chez scheme is as close to an industrial grade scheme compiler as it gets,
and is pretty fast.

edit: I missed that chez scheme is already mentioned in the article.

------
narrowtux
Please don't mess with the scroll event.

~~~
slimsag
God, this is so annoying -- I literally cannot bear to read articles when they
do this. I truly don't understand why people think this is a good idea.

I also don't understand why there isn't a browser extension to prevent scroll-
jacking effectively? "No more scroll jacking" on the Chrome store seems to,
but annoyingly you have to hold a meta key for it to work.

~~~
adrianN
NoScript works very well for me.

~~~
elderK
Me too!

