My First Fifteen Compilers
blog.sigplan.org
blog.sigplan.org
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.
Let's replace "laziness" with "carefully considered trade-offs" in this context.
i.e. "The fact that a lot of modern day compilers don't have built in assemblers etc. are probably more due to carefully considered trade-offs".
I feel like it may have been possible to know the full spec of everything you were working on.
It's still perfectly possible to design languages [0] for efficient single pass compilation, but they won't look like C#, Swift or Rust.
We often forget this alternative way (with its pros and cons).
But anyway, what you describe is maybe not entirely unlike equality saturation: https://www.cs.cornell.edu/~ross/publications/eqsat/
The idea is to apply a whole bunch of optimizations in parallel, but in a non-destructive fashion. So instead of changing "x * 5" to "x << 2 + x" or whatever, you just note that both are equivalent ways of expressing the same. Then you go on applying optimizations to both variants. At some point you stop and end up with a soup that contains many many different computations that all do the same thing. Then you apply a solver once to pick out an optimal variant.
The trick is to make this scale.
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.
Probably will have to do an in classroom course for it?
Or, alternatively you can dive into it and get a book or a course focused on parsers. It's not something that you can easily learn by just thinking about it, so being stuck on it is completely natural.
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.
Expr = "A" | "(" Expr* ")" | ""
so that (()) is s a valid match? Because then Expr can be an empty string also?PL grammars also tend to have a lot of degrees of freedom in the “production” side of them; because it’s basically up to you (as the author of the compiler) what the resulting AST output by the generated parser will “need” to look like, as you also control the next stage that consumes the AST, and there’s nothing forcing that API between the generated parser and your “code DOM builder” to take any particular shape. This is bad, for https://en.m.wikipedia.org/wiki/Analysis_paralysis reasons. Things are a lot easier when you lock down an AST “shape” (i.e. a known data structure) that your generated parser should aim to output. (Once you have this, you can write a conformance test suite as a set of {text input, data-structure output} pairs!)
So, my suggestion would be to 1. try writing a grammar for something simpler than a complete language syntax, 2. where it’s obvious what the resulting AST “should” look like.
Personally, I threw myself into the deep end, and my first experience with grammars was in writing a PR for a library that parsed just function-signatures of a particular language, previously with a hand-rolled parser. I wrote a grammar for a parser-generator to replace this hand-rolled parser. This was pretty good as an exercise, as I just had to match the output of the existing parser.
But even then, it was a bit of a struggle trying to understand what this thing was I was parsing at the same time that I was trying to write the grammar for it.
I’d instead suggest, a learning exercise, taking a text format you know well (for example, URLs, or email addresses) which is specified rigidly by an RFC (including a BNF grammar!) and attempting to recreate the BNF grammar (or an equivalent for your parser-generator of choice) by reading the RFC but avoiding the BNF part until you’re ready to “check your work.”
For what it is worth, I think you can emulate this if you really want to (and have enough time); it may make you feel more confident in the topic. But to be clear, we did not do anything fancy. The project was done in C and wasn't done "cleverly". What I mean by that is that it (at least our compiler) was very straightforward. Read in sentences based on seperators; remove whitespace; identify keywords; set/identify variables; and a few steps later execute the commands (in C I think, we only did assembly after this).
Consider postponing the deep-dive into theory until you've tried solving the problem using what you already know. Once you know your way around a problem, digesting theory becomes much easier.
On one level, there's nothing magical about compilers and interpreters. It's the same old regular code [0] solving the same kinds of problems.
What makes it tricky is working on a meta-level in parallel, visualizing what's happening in two dimensions. Like writing Lisp macros if that makes any sense.
I think it's a shame that so many get stuck on parsing and type theory. The top priority should be to get a feedback loop up and running.
[0] https://github.com/codr7/cidk/blob/master/src/cidk/read.cpp
Good intro: http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-e...
Project I made using a Pratt parser, effectively 0 dependencies (for the interesting bits at least): https://github.com/JacksonKearl/RollDice-Discord
Check the “parse.ts” for how simple the Pratt implementation is, “calculator-bot.ts” for how easy it is to define a simple language with infix operators and precedence and etc, and “dice-bot.ts” and “dice-expression.ts” for a complete implementation of an actually “interesting” language. The “tokenize.ts” file has the tokenizer, but it’s incredibly sketchy. Enter at your own risk. ;)
Edit: the Readme is a bit outdated. Proper order of operations is implemented
http://instaparse-live.matt.is/
With the syntax from Instaparse:
https://github.com/Engelberg/instaparse/blob/master/README.m...
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.pdfMaybe it would be possible to start a fund raiser and pay him to record videos and release the materials used in the course?
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.
However, as you descend into deeper optimization layers of the compiler, errors are less and less likely to occur. Optimizations are either possible or not, though sometimes these layers can warn you about possible inefficiencies at the source code level (unused local, etc.)
using (<span decl>var x = 3</span>) { <span inner>Console.WriteLine(x);</span>}
Parser 0 transforms the using:
<span stmt1 parent=decl>var x = 3;</span> try { <span stmt2 parent=inner> Console.WriteLine(x);</span> } finally { <span stmt3=generated>x.Dispose();</span> }
Parser 1 (or a later parser, inductively) notices x doesn't have Dispose() defined, and throws MissingMethod(Dispose, span=generated).
Parser 0 catches this error, knows that this is because x isn't disosable, and emits its own error: NotDisposable(span=decl).
If a parser doesn't know to catch the error, it still passes it up the chain, transforming spans where appropriate. When it hits the IDE, the error is printed and the red squiggly lines go where they should.
(Disclaimer: I've never written a compiler)
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.
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.
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.
That's called optimization, baby!
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-...
I think it's still fast because the passes are all or mostly O(n).
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 (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.
https://github.com/melling/ComputerLanguages/blob/master/com...
Why's that?
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.
Have a look at the books and compilers at http://t3x.org. For a first glance, I would recommend https://t3x.org/t3x/book.html. If you are only interested in the source code, it is in the public domain and can be downloaded on that page.
I took Alex Aiken's course in my first year when I started to learn writing code, was feasible back then and pretty interesting.
"The compiler we construct accepts a large subset of the Scheme programming language and produces assembly code for the Intel x86 architecture, the dominant architecture of personal computing. The development of the compiler is broken into many small incremental steps. Every step yields a fully working compiler for a progressively expanding subset of Scheme. Every compiler step produces real assembly code that can be assembled then executed directly by the hardware."
"We do not assume that the reader knows anything about assembly language beyond the knowledge of the computer organization, memory, and data structures. The reader is assumed to have very limited or no experience in writing compilers."
As for compiler resources, I've put a respectable dent in [3] and so far I've found it to be pretty accessible.
[0] https://craftinginterpreters.com [1] https://interpreterbook.com [2] http://www.buildyourownlisp.com [3] https://holub.com/compiler/
edit: I missed that chez scheme is already mentioned in the article.
https://d.godbolt.org/z/w9At24 I hacked together this to show what I mean, i.e. Composing working functions with templates, which the compiler can then optimize into just a function call like any other (i.e. This can be used to minimize runtime scheduling cost)
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.
They're usually trying to create smooth scrolling. The problem is, it never works in every browser.
> I also don't understand why there isn't a browser extension to prevent scroll-jacking effectively?
It should really just be an option in the browser, even if it's in a fancy hidden advanced settings page.