#pragma weak, asm labels, always_inline, debug information, attribute regparm, inline assembly, etc...
All of this is doable via incremental lowering from the AST (this is how clang does it) but it is fairly painful.
Even the relatively banal:
enum e;
void (*fp)(enum e);
enum e { v = 1ULL << 32 };
results in quite a bit of gymnastics! (https://github.com/llvm-mirror/clang/blob/202433cccd87a4e7d1...)Which can be a bad or good thing, depending on how you decide to look at the issue.
I've been wondering about this ever since Go came out. If I recall correctly, LLVM was already a viable option back then.
The latest release of Go earlier this week featured a pretty big revamp of their backend with a new SSA-based middle/backend. I wonder how much work went into that and could they have achieved that faster had they been on LLVM since day 1.
- ease of iteration and freedom to make huge breaking changes (and they did make use of it, see e.g. various generations of the GC, or the SSA work);
- thorough knowledge of the internals of the compiler they used (given they were it's authors; also this is linked to the reason I listed above);
- one of the explicit goals for the Go toolchain was that it must compile very fast (which I believe is also somewhat at odds with advanced optimizations, like those used in LLVM) - they even quipped something like "we designed Go while waiting for C++ to compile";
- additionally, though I'm not sure if it was ever spelled explicitly, I suppose they wouldn't want to get tied to the LLVM as a huge dependency, including having to adjust to each and every change in LLVM API, regardless if sensible or not (I seem to recall from so me articles that LLVM does introduce breaking changes in API from time to time?);
[1] https://golang.org/doc/faq#What_compiler_technology_is_used_...
And the fact that the first people working on Go have a good knowledge of the Plan9 toolchain probably gave them a head start. But that benefit has probably eroded long ago.
And I completely disagree with the Go team's emphasis on compile times. Fast result programs are far more important than fast compile times. Fast compile times are nice to have, but not much more than that. But this is just my opinion, it's as good as anyone else's.
Fast compile times also let you run your test suite every time you hit ctrl + s.
Fast compile times also let you run tools like gomft without feeling like it is slowing you down.
I agree that fast programs are essential and (ideally) not mutually exclusive with fast compile times.
They are. It is always a tradeoff.
You can configure the optimization level to 0, then only constant folding is performed when the IR is built, and nothing else is done.
Anyway, the most difficult part of writing the compiler was probably the garbage collector.
It was done for implementation speed. They had a compiler backend which they, the early Go team, were all familiar with. It was small so they could quickly change it to suit Go, rather than the other way around.
Russ Cox expressed the sentiment that if they had tried to use LLVM there would have been a lot of up front work that would have taken a long time and the Go project probably would have fizzled out. Using the plan 9 tool chain they were up and running very quickly allowing them to focus on the most important work, designing the language itself.
(Probably shouldn't attribute this to Russ Cox, I've probably read into his (rather briefer than what I wrote above) comments)
In my opinion this was the right choice. If LLVM turns out to be the best backend for Go it's easy to use it later. However, the language itself is largely fixed after 1.0. Better to focus on the language than the compiler backend which can be swapped out later.
Depending on C++ code and keep alive the belief that you can only create compilers in C and C++?
As many without experience in compiler development think.
Also, and this is specially painful when compiling Rust or Swift from source, saving themselves the pain of waiting around 4h for a clean compile.
I can assure you it really does take 4 hours.
And to bring the point home, I can link you to the Swift issue where I was discussing this matter.
[1]: Btw, if LLVM compile times are getting to you, Use. Ccache. It is amazeballs.
Also I never had 4h builds with single core processors back when I was working full time with C++. The slowest ones were around 2h.
A core duo would run circles around those computer systems, hence my dismay how much hardware LLVM requires for building.
Also that is surely a side effect how they are using C++, which desperately needs modules.
Rust is working on doing it
That's interesting. Is this still MIR [1], or has this evolved? The MIR design document is from 2015.Currently, initial support for lowering MIR to LLVM IR is close to complete and it might be enabled in the next beta (or the one after that).
None of the analysis passes have started their transition to MIR, though (there is a well-typed check to catch bugs in MIR generation, but that's about it).
There's also talk of using MIR for compile-time (pure) function evaluation [1] and some out-of-tree experimentation with interpreting it [2].
[1] https://internals.rust-lang.org/t/mir-constant-evaluation/31...
MIR looks a bit more low-level than Haskell's Core. Like Core, MIR is typed, but unlike Core, match statements disappear. This is interesting, because matching is really the main computational engine of Core. I wonder if there's a good explanation why it was deemed advantageous to desugar match.
I also wonder if there are plans to integrate the MIR interpreter into the compiler. IIRC, interpretation of Core between optimisation phases is used to good effect in Haskell.
If you're instead referring to matching over ADT (enum) variants, that is a MIR primitive, the "Switch" terminator (there is also a "SwitchInt" for integer-like values, as in C or LLVM IR).
As for the interpreter, it's quite likely that we'll use (something like) it for evaluating constants, but not entirely certain atm.
I personally hope we can get the CTFE semantics I described (see [1] above), which despite being pure, go far beyond C++17 constexpr capabilities (e.g. you could potentially run an entire compiler for a different language at compile-time).
I imagine that if Rust plugs in full CTFE then a lot of compiler optimisations can be written at the MIR level, which should simplify the compiler.
The benefits would be two-fold:
We could do transformations LLVM can't figure out itself, like NVRO: `fn bar() -> T {let mut x = ...; foo(&mut x); x} ... box bar()` can (theoretically) end up calling `foo` with a pointer to the heap instead of having to copy `x` from the stack later.
And we can lift the burden of LLVM trudging through monomorphized instances from MIR: since MIR still has type parameters, you can transform it and simplify the resulting LLVM IR for all instances, e.g.:
mem::replace(mut_ref, x) is {mem::swap(mut_ref, &mut x); x} which can be reduced once to {Return = * arg0; * arg0 = arg1;} in MIR and translated to a load, a store and a ret (for immediates) or two memcpy's (for indirect argument & return), depending on the type.
As most of the time rustc spends is in LLVM optimizations, this will result in compilation time speedups linear in the number of instances, sometimes exponential in the size of the original code.
1. Running a type-checker on the IR "is a very powerful consistency check on the compiler itself. Imagine that you write an 'optimisation; that accidentally generates code that treats an integer value as a function, and tries to call it. The chances are that the program will segmentation fault, or fail at runtime in a bizarre way. Tracing a seg-fault back to the particular optimisation pass that broke the program is a long road. Now imagine instead that we run [the type-checker on the IR] after every optimisation pass: it will report a precisely located error immediately after the offending optimisation. What a blessing [...] in practice we have found that it is surprisingly hard to accidentally write optimisations that are type-correct but not semantically correct."
2. Running a type-checker after translation to IR serves as a "100% independent check on the type inference engine".
3. A high-level typed IR is a "[s]anity check on the design of the source language."
All quotes from [1].
[1] S. Marlow, S. Peyton-Jones, The Glasgow Haskell Compiler. http://www.aosabook.org/en/ghc.html
I would say that is just mainstream compiler development catching up with how mainframes work.
The idea of using some form of IL that gets JIT or AOT compiled goes back to the mainframe years.
IBM i (aka OS/400) and Unisys Libra (aka Burroughs) are probably the best examples still alive.
Hence why Apple, Google and Microsoft ideas of distributing IL instead of pure native code in their OSes, isn't actually that new.
I would have said this too a few years ago, but Go is a huge counterexample. It's become very popular without using LLVM, and the compiler that does use a mature, optimizing backend (gccgo) doesn't get nearly as much use as the Plan 9-based one.
I should also mention that increasingly it's become popular to have a mid-level IR that you lower the AST to before lowering to LLVM IR. GHC does it, Swift is doing it, and Rust is working on doing it. The reasons are (a) you can do language-specific optimizations like devirtualization, which are often pretty hard to do in LLVM; (b) going from AST to LLVM IR is often a big, messy leap for any language that isn't C.