Live data from Hacker News

Revisiting "Let's Build a Compiler"

eli.thegreenplace.net

21–30 of 53 posts

Re: Revisiting "Let's Build a Compiler"

#21
post #20
post #10

Earlier quoted context omitted.

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.

What do you mean exactly by "error-tolerance"? Is it like, each node is wrapped into a result type, that you have to match against each time you visit it, even though you know for a fact, that it is not empty or something like that?

I suppose that one of the pros of using tree-sitter is its portability? For example, I could define my grammar to both parse my code and to do proper syntax highlighting in the browser with the same library and same grammar? Is that correct? Also it is used in neovim extensively to define syntax for a languages? Otherwise it would have taken to slightly modify the grammar.

Re: Revisiting "Let's Build a Compiler"

#23

> 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 recentl…

Yeah, I think this is one of the (few, rare) cases where the "official" academic way of teaching the subject is actually baggage and not really aligned with what's practically useful.

Compiler courses are structured like that because parsing really was the most important part, but I'd say in the "modern" world once you have a clear idea of how parsing actually works, it's more important to understand how compilers implement language features.

Even if you want to implement a compiler yourself, "Claude, please generate a recursive descent parser for this grammar" is close to working one-shot.

Re: Revisiting "Let's Build a Compiler"

#24
https://t3x.org has several examples on creating a C, Lisp and several more.

The T3X language it's very Pascal like and fun to use (and portable: DOS/Win/Unix/CPM...).

Also, as an intro, with Zenlisp you can get a mini CS-101 a la SICP or CACS but simpler and explained in a much easier way.

Re: Revisiting "Let's Build a Compiler"

#26

This article sums it up perfectly. I was interested in building a compiler long before going to college and this was the most accessible body of work. Building a recursive descent parser from scratch was an eye opener to 17yo me on how a seemingly very complex problem that I had no idea how to approach can be made simple by breaking it down into the right primitives.

That's a good point - recursive descent as a general lesson in program design, in addition to being a good way to write a parser.

Table driven parsers (using yacc/etc) used to be emphasized in old compiler writing books such as Aho & Ullman's famous "dragon (front cover) book". I'm not sure why - maybe part efficiency for the slower computers of the day, and part because in the infancy of computing a more theoretical/algorithmic approach seemed more sophisticated and preferable (the cannonical table driven parser building algorithm was one of Knuth's algorithms).

Nowadays it seems that recursive descent is the preferred approach for compilers because it's ultimately more practical and flexible. Table driven can still be a good option for small DSLs and simple parsing tasks, but recursive descent is so easy that it's hard to justify anything else, and LLM code generation now makes that truer than ever!

There is a huge difference in complexity between building a full-blown commercial quality optimizing compiler and a toy one built as a learning exercise. Using something like LLVM as a starting point for a learning exercise doesn't seem very useful (unless your goal is to build real compilers) since it's doing all the heavy lifting for you.

I guess you can argue about how much can be cut out of a toy compiler for it still to be a useful learning exercise in both compilers and tackling complex problems, but I don't see any harm in going straight from parsing to code generation, cutting out AST building and of course any IR and optimization. The problems this direct approach causes for code generation, and optimization, can be a learning lesson for why a non-toy compiler uses those!

A fun approach I used at work once, wanting to support a pretty major C subset as the language supported by a programmable regression test tool, was even simpler ... Rather than having the recursive descent parser generate code, I just had it generate executable data structures - subclasses of Statement and Expression base classes, with virtual Execute() and Value() methods respectively, so that the parsed program could be run by calling program->Execute() on the top level object. The recursive descent functions just returned these statement or expression values directly. To give a flavor of it, the ForLoopStatement subclass held the initialization, test and increment expression class pointers, and then the ForLoopStatement::Execute() method could just call testExpression->Value() etc.

Re: Revisiting "Let's Build a Compiler"

#27

> 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 recentl…

This is something I've noticed on academic vs "practicing" coders. Academics tend to build in layers, though not always, and "practicing" coders tend to build in pipes give or take. The layers approach might give you buildable code, but is hard to exercise and run. Both approaches can work though, especially if you build in executable chunks, but you have to focus on the smallest chunk you can actually run.

Re: Revisiting "Let's Build a Compiler"

#28
I printed this out (so I could have it with me everywhere) and read it when I was younger. It was so cool to see it come together so quickly. Some of these works (this one, Beej's guides, etc) are some of the best CS documentation we have and don't get nearly the credit they deserve.

Re: Revisiting "Let's Build a Compiler"

#29
post #4
post #3

Earlier quoted context omitted.

"breaking things down into the right primitives" is the real key to programming. There are many books and web pages about algorithms, but I wish there were more searchable and browsable resources for how to approach problems through primitives.

The process of breaking a complex problem down into the right primitives requires great understanding of the original problem in the first place. Whats blocking me during programming usually are edge cases I had no idea about. Its still hard to find good material on compilers if you are not into reading dry ass books. Thats a me problem though, I simply cant force myself to read boring factual only content (one of th…

> The process of breaking a complex problem down into the right primitives requires great understanding of the original problem in the first place.

Yes, but with experience that just becomes a matter of recognizing problem and design patterns. When you see a parsing problem, you know that the simplest/best design pattern is just to define a Token class representing the units of the language (keywords, operators, etc), write a NextToken() function to parse characters to tokens, then write a recursive descent parser using that.

Any language may have it's own gotchas and edge cases, but knowing that recursive descent is pretty much always going to be a viable design pattern (for any language you are likely to care about), you can tackle those when you come to them.

Post reply on HN