six thoughts on generating c
33 points by fanf
33 points by fanf
I also finished my language's reference implementation's C backend recently. My #1 tip if you're considering generating C would be: get familiar with statement expressions and their limitations.
(Yes they're a gcc extension, but clang also supports them.)
They're really helpful when compiling conditional or pattern matching expressions to C. They have a few limitations explained in the link, but the big one for me (which I think isn't mentioned in the link actually) was that you can't generate a return in a statement expression and expect the expression's type to just match the expected one from the context. So let's say I'm generating a function call in C: f(arg1, arg2, arg3), and arg2 is an expression in my language that can't be an expression in C, so I use a statement expression like
f(
arg1, // compiled to a C expr
({ ... }), // statement expression here
arg3, // compiled to a C expr
)
In that ... part I can have a return, but I still need an expression as the last statement. This isn't valid:
f(
arg1,
({ statement1;
return 123; }),
arg3,
)
Because that last thing needs to be an expression. So I have to generate a dummy value after the return, with the right type:
f(
arg1,
({ statement1;
return 123;
(MyStruct) {0}; }), // the actual dummy value depends on the type expected
arg3,
)
Initially I was YOLOing my way without having the expected type in my expression compiler. When I discovered this I had to manually plumb expression types all the way from the type checker through monomorphic AST and lowered AST, and then to the expression compiler...
always-inline functions remove any possible performance penalty for data abstractions
I find this to be slightly hyperbolic. It's only true if code size is not part of what you're measuring as "performance" (e.g. on the web/Wasm you want to produce as tiny executables as possible). It also ignores downstream effects e.g. on the instruction cache.
The examples in the blog post are all typesafe accessors which should compile down to less code than a call to a function that does the same thing, even in the absence of a clever optimizer. (I would expect one of these inline functions to compile down to the address part of a load or store instruction, i.e. less than one instruction. It’s hard to make a function call in less than one instruction.) It’s true that you need be aware of the potential for code bloat, i.e. you should examine the size of the inlined code vs the size of a function call. But inlined code can be smaller, especially when it unlocks further optimizations such as common subexpression elimination.
Counterpoint, I've been generating Rust code as a placeholder for my language backend for years. It's certainly not flawless, but it's stood up to the test of time wayyyyy better than I expected.
That sounds very interesting, did you by chance write anything up about that already or are you considering it?