mycc - an alternative C compiler, implemented as a POC for fun.
As a proof of concept, over the course of 3 weeks and 2800 lines of code, I implemented mycc: a compiler for a subset of the C language (roughly close to the C99 standard) built on top of myc. Mycc already successfully compiles and runs LangArena — a benchmark suite consisting of 50 tests and 9,000 lines of non-trivial C code (json, base64, multithreaded matmul, neural net, compression, maze A*, bf interpreter, and others) with heavy macros like uthash. For the parser, I used libclang — it’s an overhead, but it’s the simplest solution for a POC.
How it works:
C source -> SyntaxTree(libclang/clang.cr) -> TypedAST(mycc) -> IR(myc) -> [LLVM/QBE/C] -> binary
The most challenging part was SyntaxTree -> TypedAST. The file ast_builder.cr contains 1700 lines of code. libclang returns a non-normalized AST with many edge cases that need to be transformed into a consistent form. Additionally, C has a ton of implicit behavior: implicit type conversions, array decay, and others. Only the edge cases necessary for compiling LangArena are implemented here; there’s likely still a lot uncovered.
The TypedAST(mycc) -> IR(myc) stage — as expected, is one of the simplest (codegen.cr, 700 lines) — a single-pass generation of stack-based IR directly from the AST.
Difference from Clang:
clang: C -> Parse(libclang) -> Clang CodeGen -> LLVM -> binary
mycc: C -> Parse(libclang) -> mycc CodeGen -> IR(myc) -> [LLVM/QBE/C] -> binary
Limitations:
Rare features are not implemented: 2D VLA, complex numbers, variadic macros, longjmp, bitfields, and anonymous nested structs. I wouldn’t try building Linux or sqlite with it. It has only been tested on arm64 and linux64.
LangArena Benchmark:
Compares Clang, Gcc, Cproc(QBE), and Mycc on LangArena benchmark.
The ./c directory of LangArena contains 29 .c files (230KB total). All compilers build these files sequentially without using cache or threads. Link time is not counted — it’s roughly 50ms for all, and precompiled dependencies were also used. The results show:
Build time — total build time for all 29 files
Build rss — average RSS during compilation per file
Bench Runtime — benchmark execution time
Results for linux64, gcc 13.3, clang 20.1.
| Compiler |
Build time |
Build rss |
Bench Runtime |
| clang(-O3) |
3079ms |
105Mb |
52.1s |
| gcc(-O3) |
3495ms |
34Mb |
52.3s |
| cproc |
932ms |
12Mb |
72.7s |
| mycc(llvm, --release) |
4269ms |
101Mb |
53.2s |
| mycc(qbe, --release) |
2939ms |
86Mb |
72.8s |
| mycc(c, --release, clang) |
5091ms |
102Mb |
52.1s |
| mycc(c, --release, gcc) |
5128ms |
86Mb |
53.7s |
Currently, mycc is slower at compile time in the benchmarks. Run time is close. This is due to several suboptimal stages:
- libclang adds parsing overhead of about 10ms per file.
- IR is generated as text and then parsed again — this adds another ~10ms of overhead per file (also, because of this, error locations are not yet available).
- There are no self optimization passes yet — and probably never will be :)
- The code generation is primitive, with a lot of redundant load/store operations.
- Haven’t managed to beat clang or gcc yet — but coming for you.