Live data from Hacker News

Revisiting "Let's Build a Compiler"

eli.thegreenplace.net

11–20 of 53 posts

Re: Revisiting "Let's Build a Compiler"

#11

For modern compiler and a more direct approach I recommend https://www.cs.cornell.edu/~asampson/blog/llvm.html

LLVM makes it so much easier to build a compiler - it's not even funny. Whenever I use it, I feel like I'm just arranging some rocks on a top of a pyramid.

Re: Revisiting "Let's Build a Compiler"

#12
post #10
post #9

Earlier quoted context omitted.

When I need to parse something nowadays I always end up with parser combinators. They just make so much sense.

What language do you use parser combinators in, and what kind of grammar do you parse usually? Nom was terribly verbose and unergonomic even by Rust's standards. Haskell's Megaparsec/Parsec is good but yeah, it's Haskell, you need to handle multiple monads (Parser itself is monadic, then your AST state, and maybe some error handling) at once and that's where I got confused. But I appreciated the elegance. I experimen…

[deleted]

Re: Revisiting "Let's Build a Compiler"

#13
post #10
post #9

Earlier quoted context omitted.

When I need to parse something nowadays I always end up with parser combinators. They just make so much sense.

What language do you use parser combinators in, and what kind of grammar do you parse usually? Nom was terribly verbose and unergonomic even by Rust's standards. Haskell's Megaparsec/Parsec is good but yeah, it's Haskell, you need to handle multiple monads (Parser itself is monadic, then your AST state, and maybe some error handling) at once and that's where I got confused. But I appreciated the elegance. I experimen…

Parser combinators is more of a concept than a library. You could make your own supporting the stuff you need. I like writing programs in languages I don't know or I barely know. I usually just take one of the popular libraries in any given language.

For Rust I used Nom and I didn't mind it all that much although I noticed it's quite baroque. If I had more to write I'd probably make some wrappers or macros of my own for most commonly used Nom snippets.

Re: Revisiting "Let's Build a Compiler"

#14
> Jack Crenshaw's tutorial takes the syntax-directed translation approach, where code is emitted while parsing, without having to divide the compiler into explicit phases with IRs.

Is "syntax-directed translation" just another term for a single-pass compiler, e.g. as used by Lua (albeit to generate bytecode instead of assembly / machine code)? Or is it something more specific?

> in the latter parts of the tutorial it starts showing its limitations. Especially once we get to types [...] it's easy to generate working code; it's just not easy to generate optimal code

So, using a single-pass compiler for a statically-typed language makes it difficult to apply type-based optimizations. (Of course, Lua sidesteps this problem because the language is dynamically typed.)

Are there any other downsides? Does single-pass compilation also restrict the level of type checking that can be performed?

Re: Revisiting "Let's Build a Compiler"

#15
post #7

> Rather than getting stuck in front-end minutiae, the tutorial goes straight to generating working assembly code, from very early on Good summary. I had no background in compilers or related theory but read Jack Crenshaw's Let's Build a Compiler tutorials some time ago. My main take away from reading half a dozen or so of these tutorials was that building a simple compiler for a toy language was a small project that…

I also enjoyed working with BF for toy compiler projects; here's a series of JIT compilers for BF in increasing level of sophistication: https://eli.thegreenplace.net/2017/adventures-in-jit-compila...

Re: Revisiting "Let's Build a Compiler"

#16
post #14

> Jack Crenshaw's tutorial takes the syntax-directed translation approach, where code is emitted while parsing, without having to divide the compiler into explicit phases with IRs. Is "syntax-directed translation" just another term for a single-pass compiler, e.g. as used by Lua (albeit to generate bytecode instead of assembly / machine code)? Or is it something more specific? > in the latter parts of the tutorial it…

It is more specific, it means emiting code as you go along throught the source file.

A sigle pass compiler can still split the various phases, and only do the code generation on the last phase.

Re: Revisiting "Let's Build a Compiler"

#17
post #11

For modern compiler and a more direct approach I recommend https://www.cs.cornell.edu/~asampson/blog/llvm.html

LLVM makes it so much easier to build a compiler - it's not even funny. Whenever I use it, I feel like I'm just arranging some rocks on a top of a pyramid.

A trend started with tools like the Amsterdam Compiler Toolkit, LLVM happens to be the more famous one.

https://en.wikipedia.org/wiki/Amsterdam_Compiler_Kit

Re: Revisiting "Let's Build a Compiler"

#18
post #14

> Jack Crenshaw's tutorial takes the syntax-directed translation approach, where code is emitted while parsing, without having to divide the compiler into explicit phases with IRs. Is "syntax-directed translation" just another term for a single-pass compiler, e.g. as used by Lua (albeit to generate bytecode instead of assembly / machine code)? Or is it something more specific? > in the latter parts of the tutorial it…

As long as your target language has a strict define-before-use rule and no advanced inference is required you will know the types of expressions, and can perform type-based optimizations. You can also do constant folding and (very rudimentary) inlining. But the best optimizations are done on IRs, which you don't have access to in an old-school single pass design. LICM, CSE, GVN, DCE, and all the countless loop opts are not available to you. You'll also spill to memory a lot, because you can't run a decent regalloc in a single pass.

I'm actually a big fan a function-by-function dual-pass compilation. You generate IR from the parser in one pass, and do codegen right after. Most intermediate state is thrown out (including the AST, for non-polymorphic functions) and you move on to the next function. This give you an extremely fast data-oriented baseline compiler with reasonable codegen (much better than something like tcc).

Re: Revisiting "Let's Build a Compiler"

#19
> Rather than getting stuck in front-end minutiae, the tutorial goes straight to generating working assembly code, from very early on.

I think this is important and for a more sophisticated compiler design I find Ghuloum approach very appealing [1]. I.e. build a very simple subset of the language from top to bottom and then grow the meat gradually.

The really great book following this approach I've discovered recently was [2]. Although I find both C and x86 not the best targets for your first compiler, still a very good book for writing your first compiler.

[1] http://scheme2006.cs.uchicago.edu/11-ghuloum.pdf

[2] https://norasandler.com/2024/08/20/The-Book-Is-Here.html

Re: Revisiting "Let's Build a Compiler"

#20
post #10
post #9

Earlier quoted context omitted.

When I need to parse something nowadays I always end up with parser combinators. They just make so much sense.

What language do you use parser combinators in, and what kind of grammar do you parse usually? Nom was terribly verbose and unergonomic even by Rust's standards. Haskell's Megaparsec/Parsec is good but yeah, it's Haskell, you need to handle multiple monads (Parser itself is monadic, then your AST state, and maybe some error handling) at once and that's where I got confused. But I appreciated the elegance. I experimen…

I've used tree-sitter for generating my parsers in Rust, and just working with the untyped syntax tree it generates, and gives you error-tolerance for free. It's a bit of a setup at first tho, requiring an extra crate for the generated parser, but editing it from there saves so much time.
Post reply on HN