[HN Gopher] Thoughts on Generating C
       ___________________________________________________________________
        
       Thoughts on Generating C
        
       Author : ingve
       Score  : 182 points
       Date   : 2026-02-09 13:54 UTC (9 hours ago)
        
 (HTM) web link (wingolog.org)
 (TXT) w3m dump (wingolog.org)
        
       | rirze wrote:
       | Love how he put a paragraph for someone asking, "why not generate
       | Rust?". Beautiful.
        
         | artemonster wrote:
         | <something something about having a hammer and seeing nails
         | everywhere> :)
        
         | pjc50 wrote:
         | The lifetimes argument is extremely sound: this is information
         | which you need from the developer, and not something that is
         | easy to get when generating from a language which does not
         | itself have lifetimes. It's an especially bad fit for the GC
         | case he describes.
        
           | bux93 wrote:
           | I mean, the argument boils down to "the language I'm
           | compiling FROM doesn't have the same safeguards as rust". So
           | obviously, the fault lies there. If he'd just compile FROM
           | rust, he could then compile TO rust without running into
           | those limitations. A rust-to-rust compiler (written in rust)
           | would surely be ideal.
        
             | manwe150 wrote:
             | I'd be willing to sell you a rust to rust compiler. In
             | fact, I'll even generalize it to do all sorts of other
             | languages too at no extra charge. I just need a good
             | name...maybe rsync?
             | 
             | Snark aside, the output targets of compilers need to be
             | unsafe languages typically, since the point of a high level
             | compiler in general is to verify difficult proofs, then
             | emit constructs consistent with those proof results, but
             | simplified so that they cannot be verified anymore, but can
             | run fast since those proofs aren't needed at runtime
             | anymore. (Incidentally this is both a strength and weakness
             | of C, since it provides very little ability for the
             | compiler to do proofs, the output is generally close to the
             | input, while other languages typically have much more
             | useful compilers since they do much more proof work at
             | compile time to make runtime faster, while C just makes the
             | programmer specify exactly what must be done, and leaves
             | the proof of correctness up to the programmer)
        
               | cozzyd wrote:
               | Compilers named after animals are the the most popular,
               | so I might suggest cat?
        
           | Findecanor wrote:
           | > not something that is easy to get when generating from a
           | language which does not itself have lifetimes
           | 
           | Not easy, but there _are_ compilers that do it.
           | 
           | Lobster [0] started out with automatic reference counting. It
           | has inferred static typing, specialising functions based on
           | type, reminiscent of how Javascript JIT compilers do it. Then
           | the type inference engine was expanded to also specialise
           | functions based on ownership/borrowing type of its arguments.
           | RC is still done for variables that don't fit into the
           | ownership system but the executed ops overall got greatly
           | reduced. The trade-off is increased code size.
           | 
           | I have read a few older papers about eliding reference
           | counting ops which seem to be resulting in similar elisions,
           | except that those had not been expressed in terms of
           | ownership/borrowing.
           | 
           | I think newer versions of the Swift compiler too infer
           | lifetimes to some extent.
           | 
           | When emitting Rust you could now also use reference counting
           | smart pointers, even with cycle detection [1]. Personally I'm
           | interested in how ownership information could be used to
           | optimise tracing GC.
           | 
           | [0]:https://aardappel.github.io/lobster/memory_management.htm
           | l
           | 
           | [1]:https://www.semanticscholar.org/paper/Breadth-first-
           | Cycle-Co...
        
             | warangal wrote:
             | I was also reading through lobsters Memory management,
             | which (i think) currently implements "borrow first"
             | semantics, to do away with a lot of run-time reference
             | counting logic, which i think is a very practical approach.
             | Also i have doubts if reference counting overhead ever
             | becomes too much for some languages to never consider RC ?
             | 
             | Tangentially, i was experimenting with a runtime library to
             | expose such "borrow-first" semantics, such "lents" can be
             | easily copied on a new thread stack to access shared
             | memory, and are not involved in RC . Race-conditions
             | detection helps to share memory without any explicit move
             | to a new thread. It seems to work well for simpler data-
             | structures like sequence/vectors/strings/dictionary, but
             | have not figured a proper way to handle recursive/dynamic
             | data-structures!
        
           | jcranmer wrote:
           | If I were targeting Rust for compilation, I wouldn't do
           | lifetimes, instead everything would be unsafe Rust using raw
           | pointers.
           | 
           | I'd have to do an actual project to see how annoying it is to
           | lower semantics to unsafe Rust to know for sure, but my guess
           | is you'd be slightly better off because you don't have to
           | work around implicit conversions in C, the more gratuitous
           | UBs in C, and I think I'd prefer the slightly more complete
           | intrinsic support in Rust over C.
        
       | whizzter wrote:
       | Having done this for a dozen of experiments/toys I fully agree
       | with most of the post, would be nice if the the addition of
       | must_tail attribute could be reliable across the big 3 compilers,
       | but it's not something that can be relied on (luckily Clang seems
       | to be fairly reliable on Windows these days).
       | 
       | 2 additional points,
       | 
       | 1: The article mentions DWARF, even without it you can use #line
       | directives to give line-numbers in your generated code (and this
       | goes a very long way when debugging), the other part is local
       | variables and their contents.
       | 
       | For variables one can get a good distance by using a C++ subset(a
       | subset that doesn't affect compile time, so avoid any std::
       | namespaced includes) instead and f.ex. "root/gc/smart" ptr's,etc
       | (depending on language semantics), since the variables will show
       | up in a debugger when you have your #line directives (so "sane"
       | name mangling of output variables is needed).
       | 
       | 2: The real sore point of C as a backend is GC, the best GC's are
       | intertwined with the regular stack-frame so normal stack-walking
       | routines also gives everything needed for accuracte GC (required
       | for any moving GC designs, even if more naive generation
       | collectors are possible without it).
       | 
       | Now if you want accurate somewhat fast portable stack-scanning
       | the most sane way currently is to maintain a shadow-stack, where
       | you pass prev-frame ptrs in calls and the prev-frame ptr is a ptr
       | to the end of a flat array that is pre-pended by a magic ptr and
       | the previous prev-frame ptr (forming a linked list with the cost
       | of a few writes, one extra argument with no cleanup cost).
       | 
       | Sadly, the performant linked shadow-stack will obfuscate all your
       | pointers for debugging since they need to be clumped into one
       | array instead of multiple named variables (and restricts you from
       | on-stack complex objects).
       | 
       | Hopefully, one can use the new C++ reflection support for shadow-
       | stacks without breaking compile times, but that's another story.
        
         | ufo wrote:
         | Related to shadow stacks, I've had trouble convincing the C
         | optimizer that no one else is aliasing my heap-allocated helper
         | stacks. Supposedly there ought to be a way to tell it using
         | restrict annotations, but those are quite fiddly: only work for
         | function parameters, and can be dusmissed for many reasons.
         | Does anyone know of a compiler that successfully used restrict
         | pointers in their generated code? I'd love to be pointed
         | towards something that works.
        
           | jaen wrote:
           | Note that declaring no aliasing is probably unsafe for
           | concurrent or moving garbage collectors, as then the C
           | compiler can conveniently "forget" to either store or load
           | values to the shadow stack at some points...
           | 
           | (though it is fine if GC can only happen inside a function
           | call and the call takes the shadow stack as an argument)
        
         | Findecanor wrote:
         | > ... [pointers] need to be clumped into one array ...
         | 
         | You could put each stack frame into a struct, and have the
         | first field be a pointer to a const static stack-map data
         | structure or function that enumerates the pointers within the
         | frame.
         | 
         | BTW, the passed pointer to this struct could also be used to
         | implement access to the calling function's variables, for when
         | you have nested functions and closures.
        
       | Joker_vD wrote:
       | > And finally, source-level debugging is gnarly. You would like
       | to be able to embed DWARF information corresponding to the code
       | you residualize; I don't know how to do that when generating C.
       | 
       | I _think_ emitting something like                   #line 12
       | "source.wasm"
       | 
       | for each line of your source before the generated code for that
       | line does something that GDB recognizes well enough.
        
         | gopalv wrote:
         | If you have ever used something like yacc/bison, debugging it
         | is relatively sane with gdb.
         | 
         | You can find all the possible tricks in making it debuggable by
         | reading the y.tab.c
         | 
         | Including all the corner cases for odd compilers.
         | 
         | Re2c is a bit more modern if you don't need all the history of
         | yacc.
        
           | kazinator wrote:
           | Debugging Yacc is completely insane with gdb, for other
           | reasons, like that grammar rules aren't functions you can
           | just put a breakpoint on, and see their backtrace, etc, as
           | you can with a recursive descent parser.
           | 
           | But yes, you can put a line-oriented breakpoint on your
           | action code and step through it.
        
         | Silphendio wrote:
         | Nim compiles to C, and it has a compiler iotion that does this.
        
       | FpUser wrote:
       | This is weird. As soon as I thought about the subject the
       | relevant article showed up on HN.
       | 
       | I was thinking about how to embed custom high level language into
       | my backend application written in C++. Each individual script
       | would compile to native shared lib loadable on demand so that the
       | performance stays high. For this I was contemplating exactly this
       | approach. Compile this high level custom language with very
       | limited feature set to plain C and then have compiler that comes
       | with Linux finish the job.
        
         | drivebyhooting wrote:
         | What's your use case for this?
         | 
         | I've done a few such things for compiling ML models, but in the
         | end I always regretted it.
        
       | sph wrote:
       | Has anyone defined a strict subset of C to be used as target for
       | compilers? Or ideally a more regular and simpler language, as
       | writing a C compiler itself is fraught with pitfalls.
        
         | rwmj wrote:
         | Not precisely, but C-- (hard to search for!) was a C-like (or C
         | subset?) intermediate language for compilers to generate.
         | 
         | I found this Reddit thread that gives a bit more detail:
         | 
         | https://www.reddit.com/r/haskell/comments/1pbbon/c_as_a_proj...
         | 
         | and the project link:
         | 
         | https://www.cs.tufts.edu/~nr/c--/
        
         | stephenbennyhat wrote:
         | https://en.wikipedia.org/wiki/C-- for example?
        
         | nxobject wrote:
         | For portability, hopefully C89 as well?
        
         | manwe150 wrote:
         | Sounds like why LLVM was created? (and derivatives like MLIR
         | and NaCL) Its IR is intended be be C-like, except that
         | everything is well-defined and substantially more expressive
         | than C.
        
         | nickpsecurity wrote:
         | I think one could also use a subset compatible with a formal
         | semantics of C. Maybe the C semantics in K Framework, CompCert
         | C, or C0 from Verisoft. Alternatively, whatever is supported in
         | open-source, verification tooling.
         | 
         | Then, we have both a precise semantics and tools to help
         | produce robust output.
        
       | kccqzy wrote:
       | I've done something similar during my intern days as well. We had
       | a Haskell-based C AST library that supports the subset of C we
       | generate, and an accompanying pretty printing library for
       | generating C code that has good formatting by default. It really
       | was a reasonable approach for good high-level abstraction power
       | and good optimizations.
        
       | titzer wrote:
       | I think I may end up coming full circle on Virgil. Circa 2005
       | Virgil I compiled to C and then with avr-gcc to AVR. I did that
       | because who the heck wants to write an AVR backend? Circa 2009 I
       | wrote a whole new compiler for Virgil III and since then it has
       | JVM, x86, x86-64, wasm, wasm-gc and (incomplete) arm64.
       | 
       | I like compiler backends, but truth be told, I grow weary of
       | compiler backends.
       | 
       | I have considered generating LLVM IR but it's too quirky and
       | unstable. Given the Virgil wasm backend already has a shadow
       | stack, it should now be possible for me to go back to square one
       | and generate C code, but manage roots on the stack for a precise
       | GC.
       | 
       | Hmm....
        
         | aseipp wrote:
         | FWIW I think the LLVM bitcode format has stronger compatibility
         | guarantees than the text IR. But I agree it's a bit of a pain
         | either way; plus, if you forgo linking to the library and just
         | rely on whatever 'llc' the user has installed, figuring out
         | bugs is not a fun time...
        
       | yxhuvud wrote:
       | "static inline", the best way of getting people doing bindings in
       | other languages to dislike your library (macros are just as bad,
       | FWIW).
       | 
       | I really wish someone on the C language/compiler/linker level
       | took a real look at the problem and actually tried to solve it in
       | a way that isn't a pain to deal with for people that integrate
       | with the code.
        
         | LtWorf wrote:
         | If it's not in the .h file it's supposed to be a private
         | function.
        
           | zabzonk wrote:
           | you can access it using extern from anywhere:
           | // a.c         int f( int x ) {             return x + 1;
           | }              // b.c         extern int f(int x );
           | int main() {             int y = f(41);         }
           | 
           | but if f() had been defined as static, you couldn't do this.
        
             | bigfishrunning wrote:
             | "private function" doesn't mean "you can't know about
             | this", it means "you shouldn't rely on this as a stable
             | interface to my code".
             | 
             | Just because you can use the information you have to call a
             | given function, doesn't mean you aren't violating an
             | interface.
        
               | zabzonk wrote:
               | my point was that f() had been defined static then you
               | can't access it from outside the translation unit it is
               | defined in - in other words, it is "private". i'm afraid
               | i'm unclear what your point is.
        
             | jlarocco wrote:
             | I don't see what you're getting at with respect to writing
             | bindings.
             | 
             | The whole point of using "static" in that way is to prevent
             | people from using it outside of the file.
             | 
             | If you need to call a static function (inline or otherwise)
             | from outside of the compilation unit to use the API, then
             | it's a bug in the API, not a problem with static.
             | 
             | I agree with you about pre-processor macros, though.
        
       | WalterBright wrote:
       | I've thought of doing that, but it's too much fun writing an
       | optimizer and code generator!
       | 
       | (My experience with "compile to C" is with cfront, the original
       | C++ implementation that compiled to C. The generated code was
       | just terrible to read.)
        
       | 20k wrote:
       | Static inline functions can sometimes serve as an optimisation
       | barrier to compilers. Its very annoying. I've run into a lot of
       | cases when targeting C as a compilation target where swapping
       | something out into an always-inline function results in worse
       | code generation, because compilers have bugs sadly
       | 
       | There's also the issue in that the following two things don't
       | have the same semantics in C:                   float v = a * b +
       | c;
       | 
       | vs                   static_inline float get_thing(float a, float
       | b) {             return a*b;         }              float v =
       | get_thing(a, b) + c;
       | 
       | This is just a C-ism (floating point contraction) that can make
       | extracting things into always inlined functions still be a big
       | net performance negative. The C spec mandates it sadly!
       | 
       | uintptr_t's don't actually have the same semantics as pointers
       | either. Eg if you write:                   void
       | my_func(strong_type1* a, strong_type2* b);
       | 
       | a =/= b, and we can pull the underlying type out. However, if you
       | write:                   void
       | my_func(some_type_that_has_a_uintptr_t1 ap,
       | some_type_that_has_a_uintptr_t2 bp) {             float* a =
       | get(ap);             float* b = get(bp);         }
       | 
       | a _could_ equal b. Semantically the uintptr_t version doesn 't
       | provide any aliasing semantics. Which may or may not be what you
       | want depending on your higher level language semantics, but its
       | worth keeping the distinction in mind because the compiler won't
       | be able to optimise as well
        
         | quotemstr wrote:
         | Compiler bugs and standards warts suck, but you know what sucks
         | more? Workarounds for compiler bugs and edge cases that become
         | pessimizing folk wisdom that we can dispell only after decades,
         | if ever. It took about that long to convince the old guards of
         | various projects that we could have inline functions instead of
         | macros. I don't want to spook them into renewed skepticism.
        
           | thomasahle wrote:
           | Maybe they just checked with a compiler and got the same
           | code?
        
         | kazinator wrote:
         | The inline function receives the operands as arguments, and so
         | whatever they are, they get converted to float. Thus the inline
         | code is effectively like this:                 float v =
         | (float) ((float) a) * ((float) b) + c;
         | 
         | Since v is float, the cast representing the return conversion
         | can be omitted:                 float v = ((float) a) *
         | ((float) b) + c;
         | 
         | Now, if _a_ and _b_ are already _float_ , then it's equivalent.
         | Otherwise not; if they are _double_ or _int_ , we get _double_
         | or _int_ multiplication in the original open code.
        
           | jcranmer wrote:
           | > Now, if a and b are already float, then it's equivalent.
           | 
           | Not necessarily! Floating-point contraction is allowable
           | essentially within statements but not across them. By
           | assigning the result of a * b into a value, you prohibit
           | contraction from being able to contract with the addition
           | into an FMA.
           | 
           | In practice, every compiler has fast-math flags which says
           | stuff it and allows all of these optimizations to occur
           | across statements and even across inline boundaries.
           | 
           | (Then there's also the issue of FLT_EVAL_METHOD, another area
           | where what the standard says and what compilers actually do
           | are fairly diametrically opposed.)
        
             | garaetjjte wrote:
             | It seems to me that either you want to allow for
             | contraction everywhere, or not all. Allowing it only
             | _sometimes_ is worst of both worlds.
        
               | jcranmer wrote:
               | If you allow contraction after inlining, whether or not
               | an FMA will get contracted becomes subject to the
               | vicissitudes of inlining and other compiler decisions
               | that can be hard-to-predict. It turns out to be a lot
               | harder of a problem to solve than it appears at first
               | glance.
        
             | kazinator wrote:
             | The first mention of contraction in the standard (I'm
             | looking at N3220 draft that I have handy) is:
             | 
             | A floating expression may be contracted, that is, evaluated
             | as though it were a single opera- tion, thereby omitting
             | rounding errors implied by the source code and the
             | expression evalua- tion method.86) The FP_CONTRACT pragma
             | in <math.h> provides a way to disallow contracted
             | expressions. Otherwise, whether and how expressions are
             | contracted is implementation-defined.
             | 
             | If you're making a language that generates C, it's probably
             | a good idea to pin down which C compilers are supported,
             | and control the options passed to them. Then you can more
             | or less maintain the upper hand on issues like this.
        
       | kazinator wrote:
       | Generators don't have to put out portable code. You document what
       | compilers are required for the output and that's something you
       | can change with any given release of your generator. Then the
       | generated code uses whatever works with those compilers. If you
       | use the output with some other compiler, then that's undefined
       | behavior w.r.t. the documentation of the generator; you are on
       | your own. "Whatever works" could be something undocumented that
       | works _de facto_.
        
       | yearolinuxdsktp wrote:
       | Java JIT compilers perform function inlining across virtual
       | function boundaries... this is why JIT'd Java can outperform the
       | same C or C++ code. Couple it with escape analysis to transfer
       | short-lived allocations to be stack-allocated (avoiding GC).
       | 
       | Often times virtual functions are implemented in C to provide an
       | interface (such as filesystem code in the Linux kernel) via
       | function pointers---just like C++ vtable lookups, these cannot be
       | inlined at compile time.
       | 
       | What I wonder is whether code generated in C can be JIT-optimized
       | by WASM runtimes with similar automatic inlining.
        
       | uecker wrote:
       | What language features would make C better as a target language
       | for compilers?
        
         | cozzyd wrote:
         | Not strictly for compilers (which probably don't need to use
         | macros much), but for normal macro-codegen it would be very
         | useful to have some some way to add line returns in macro-
         | generated code so that it's easier to inspect with gcc -E
        
         | girvo wrote:
         | By piggybacking off GCC et al you gain very easy
         | portability/access to a bunch of platforms that most languages
         | would never attempt to support.
         | 
         | We used this in production with Nim for embedded firmware
         | engineering at my previous job doing industrial IoT systems,
         | which let us write a much nicer language than C (and much
         | faster, much safer), with code-sharing between the network
         | server (and its comms protocol) and the firmware code itself.
         | 
         | All can be done with C itself of course, but this let us
         | achieve it faster and in a much nicer fashion
        
       | themafia wrote:
       | > it could be that you end up compiling a function with, like 30
       | arguments, or 30 return values; I don't trust a C compiler to
       | reliably shuffle between different stack argument needs at tail
       | calls to or from such a function.
       | 
       | Yet you trust it to generate the frame for this leviathan in the
       | first place. Sometimes C is about writing quality code,
       | apparently, sometimes it's about spending all day trying to
       | outsmart the compiler rather than take advantage of it.
        
       | bjourne wrote:
       | Last I checked static inline was merely a hint that compilers
       | need not take. They all do, but by definition it's not a zero
       | cost abstraction.
        
       | jbreckmckye wrote:
       | I wonder what Zig would be like as an ILR. Easy cross
       | compilation, plus, you can compile with runtime checks to help
       | debug your compiler output. Might be fun for a sideproject
        
       | spankalee wrote:
       | I'm not a C programmer - having coded in high-level languages
       | only for the past 20 years - but I've been doing a lot of WASM
       | recently, and eagerly looking forward to the stack switching
       | proposal so I don't have to implement an asincify-type transform
       | for an async/await feature.
       | 
       | If it's true that a C program doesn't have control of the stack,
       | what does that mean for supporting the stack switching in
       | Wastrel? Can you not reify the stack and replace it with another
       | from a suspended async function? Do you need some kind of
       | userland stack for all stacks once you support WASM stack
       | switching?
        
       ___________________________________________________________________
       (page generated 2026-02-09 23:00 UTC)