Feb 2026 - Mar 2026
C Compiler
This is a C90 compiler. I had to do it as Flex and Bison into an AST, then a walk that dumps three-address IR, then a backend that lowers that to 32-bit RISC-V assembly you can assemble, link, and actually run on Spike. Target is RV32IMFD. Tests take our .s, assemble it with the RISC-V GCC, link it against a C driver, and run it under spike --isa=rv32gc pk. There are about 540 of them. If the driver says the function returned 5, it had better have returned 5.
It's a C90 subset, not a full compiler. Functions, declarations, enums, structs, typedefs, pointers, arrays, the usual control flow and operators. No do/while, goto, union, arrow, casts, or the comma operator. Typedef only works because the lexer and parser share a map, TYPE_NAME vs IDENTIFIER, otherwise typedef int MyInt; MyInt x; doesn't even parse. Locals get renamed so two different x's in two blocks aren't the same stack slot. The backend is a stack machine. There's no register allocator. Everything lives on the stack, frames are 16-byte aligned, integer args in a0-a7, floats in fa0-fa7, ra saved so recursion works.
A few things that bit us. Forward declarations used to emit ALLOC_GLOBAL and then the linker would explode, so they emit nothing now. We wrote a dead-code pass, plugged it in, watched it delete live code, and unplugged it. Mixing int and float isn't actually lowered correctly, ++ and -- only work on simple variables, and ARRAY_LOAD of 8 bytes emits ld, which is an RV64 instruction, on an RV32 target.
Reflections
If I did it again I'd actually wire the dead-code pass in instead of leaving it sitting in a file, add proper int/float conversion, and stop emitting ld on RV32. A real register allocator would have been the fun one, but putting everything on the stack is why jal didn't trash our temps.
I like that it runs on Spike instead of just pretty-printing C. If the simulated exit code is wrong, the compiler is wrong. It's not a full C90 compiler, which is fine. That was never the point.