Small, fast and of course self-compiling! The project is the better compiler out of a first project named OTCC which won the IOCCC (yes, the obfuscated C programming contest - less than 2k bytes). https://bellard.org/otcc/
Even (cough) C++ is better than C for this use case, e.g. comparing LLVM vs. GCC. But C++ has problems too though -- the TableGen language in LLVM is an interesting example of why a big C++ project like LLVM/Clang doesn't want all its logic/specification in C++. They really use C++ to the hilt and they still want/need a DSL.
Other interesting options would be Swift, Rust, OCaml, or Go. Swift, Rust, and OCaml all have algebraic data types.
I guess choosing something other than C or C++ is a little unusual because there is the tendency to self-host. The Rust, OCaml, and Go compilers are written in Rust, OCaml, and Go, respectively. I'm pretty sure Swift is a lot of C++, but that will probably change as times goes on.
For big optimizing compilers you appreciate abstractions like C++ enables. for example, a data flow Analysis framework.
At the risk of sparking controversy, I think Go is a great example of "how would they do it now?" which goes further than compiler techniques and back to rethinking what the runtime should even be.
Agreed, but I wish they released the runtime as a separate component, in the same spirit as LLVM.
Is it considered best practice today to use Flex & Bison for the front-end of (for example) a basic C compiler?
99% of the time spent writing a compiler will be elsewhere.
If you want a correct to the last drop C preprocessor, prepare to spend 6 months on that, minimum. Or just use Warp:
https://github.com/facebookarchive/warp
and then you can have fun just working on the compiler bit.
>99% of the time spent writing a compiler will be elsewhere.
Out of interest, has the distribution of labor in that remaining 99% changed at all since you first started out?
For example, do things like LLVM change anything?
I still wind up spending nearly all my time on the D front end, and not the lexer/parser. The time spent on the lexer/parser is a rounding error.
Besides, build the lexer/parser by hand. The mystery of how to do them will evaporate, and you'll find you can apply that knowledge to all sorts of other tasks.
Regarding LALR parsers, the best thing about them is that they error out if they think your grammar might be ambiguous, which is specially useful if you are designing your own language. It is also easier to evolve a grammar if you build the parser with a high level tool than if you code everything by hand. For example, a top down parser needs to know at every point what are the valid lookahead tokens that might appear and adding a new rule to the grammar might require changing the lookahead tokens in many different spots.
The place were yacc doesn't shine is if your grammar isn't context free, in which case you might need to use hacky tricks or advanced parsing techniques. Bottom up parser generators also aren't the best at generating informative error messages (but they do a decent job of doing a bare minimum default error message)
IOW, it looks like the hardest work may still be up to you, but now you're dependent on external tools and self-hosting has become a bit questionable/problematic (you'd want those tools to be compiled by your compiler as well, right?).
Parsing tokens, OTOH, isn't a big deal. You don't need heavy artillery for them.
IOW, if you haven't had lots of experience with those tools or compiler creation already, you may not gain much from using them for the first time. If you have, you may reuse your experience and possibly even code.
I asked the question in the context where self-hosting was required, but I probably don't need it.
On the other-hand, more dependencies does complicate things. So I was interested to know whether Flex & Bison (or equivalent tools in other languages e.g. Alex & Happy for Haskell) were worth the extra complexity.
I started from one of the Appel books that I had on my book shelf for years: Modern Compiler Implementation in ML.
Reading Appel, he writes "The task of constructing LR(1) or LALR(1) grammars is simple enough to be automated. And is so tedious to do by hand that LR parsing for realistic grammars is rarely done except using parser-generator tools." p68. (Agreeing with what you advise, even Appel admits that the lexing of tokens isn't a big deal.)
But after that the only C compiler (for example) that I could find that used Bison (or Yacc) was the Portable C Compiler, so I was starting to suspect that Appel might be taking a rather academic view.
You can see here that the original C compilers use a hand-written expression parser using the Shunting Yard algorithm, and I'm sure the rest of the front end is hand-written too:
http://www.oilshell.org/blog/2017/04/22.html
C predates lex/yacc so the syntax wasn't really designed with them in mind.
Some other potential problems:
(1) Distinguishing types and variables in C:
https://eli.thegreenplace.net/2011/05/02/the-context-sensiti...
https://en.wikipedia.org/wiki/The_lexer_hack
(2) The preprocessor. While it doesn't prevent you from using lex/yacc for the C parser, this is an entirely separate lexer and parser that is always written by hand AFAICT.
Awk, on the other hand, is almost always parsed with lex/yacc. The language was almost designed around those tools.
I think if you're writing say your first compiler you should try to use lex and yacc. I think you can solve the context-sensitivity problem but I haven't done it myself. Operator precedence probably requires some extensions or maybe you can encode the many levels of C precedence in the grammar.
One thing I found from implementing a shell parser [1] is that the POSIX spec uses yacc grammars, even if the languages aren't best parsed with bottom-up parsing. For example, all shells except bash use top-down parsing.
Analogously, the C grammar may "manually" encode the operator precedence, even though the original C parsers and current ones like GCC/Clang actually use an operator precedence algorithm like the Shunting Yard algorithm, not yacc's shift-reduce algorithm.
e.g. this grammar manually encodes it:
https://www.lysator.liu.se/c/ANSI-C-grammar-y.html
Not that yacc can't parse expressions -- it's just something interesting I found.
Also, for shell, there are a bunch of parsing rules that are necessarily listed separately from the grammar, and I'm sure the same is true for C.
[1] http://www.oilshell.org/blog/tags.html?tag=parsing-shell#par...
The Portable C Compiler was written using Yacc. [1]
>I think if you're writing say your first compiler you should try to use lex and yacc.
I've actually started with something higher level still: BNFC [2], which has taken me quite a long way quite quickly. It generates lexer, parser, operator precedence and AST along the lines of the Appel books for a variety of backends: C, Java, OCAML, Haskell etc.
But now I'm running into bugs and limitations in BNFC. I may fix some of those if I can, but at some point I will probably dump BNFC and then hand-edit the generated Flex/Bison.
Maybe that's what people do eventually - start with a high-level tool and then move lower?
Edit (comment above unaltered): I got down voted by someone which tells me I missed an obvious answer to my question. Did I miss something?
I've noticed a lot of compiler projects lately tend to default to LLVM, I'm curious of the strengths and weaknesses of LLVM vs ASM since the author here chose to compile to ASM.
I wanted something small and easily self-hosting. A compiler on a floppy (as in old times). I hope you guys know what floppies are. :)
smlrc.c runs in 96KB on a PIC32 MIPS microcontroller.
LLVM is big and if I don't implement enough of the language (and I hear LLVM is s written in C++, not C), I can't recompile LLVM.
I can write an assembler. Or I can use FASM, which is self-hosting, if I can't compile NASM or YASM.
I wonder why not 64Bit? Hardly anyone uses 32bit processors anymore, and even in the Windows world 64Bit systems are slowly taking over, so it would seem more logical to me to compile for modern processors, instead of 16bit architecture.
- additional 64-bit types will need additional code for type checks and conversions, switch, etc
- new or improved code generator is needed (ideally, 64-bit non-pointer types should also be supported in 32-bit programs, possibly as register pairs)
- new object and executable formats to handle
- possibly new ABIs to support (pushing arguments onto the stack and never bothering to align the stack pointer onto a multiple of 8 or 16 boundary is kinda easy, it's pretty much free)
And the above work hasn't been done.
On the contrary, nearly everyone has a processor that can run 32 bit programs now.
Although I've never tried using an interpreter in a program, bridging calls from a script inside your compiled program seems difficult.
As of a few months ago, it is possible to compile (an old, non-C++ version of) gcc with tcc, which is a good position to be in:
https://lists.gnu.org/archive/html/tinycc-devel/2017-05/msg0...
http://gcc.gnu.org/install/prerequisites.html
"versions of GCC prior to 4.8 also allow bootstrapping with a ISO C89 compiler and versions of GCC prior to 3.4 also allow bootstrapping with a traditional (K&R) C compiler."
So you might need to build tcc -> gcc 3.3 -> gcc 4.7 -> gcc 7.2
I like that they allow FASM.
And that there is no "standard library".
> The standard C library is work-in-progress and it's close to completion.