myc(MyCompiler) — a small IR for building languages over LLVM, QBE, C

What is it?

  • Simple DSL over LLVM/QBE.
  • Your language AST → mycIR → [LLVM / QBE / C] → binary.
  • ~25 stack-based opcodes.
  • Whole IR spec fits in 15 minutes of reading.
  • Compiles to native code via LLVM, QBE, or C.
  • Fast compilation, zero overhead.
  • ~6500 lines in Crystal.

Why?

  • I was writing my own language and got tired of fighting with LLVM IR. SSA, phi nodes, basic blocks — X(.
  • Usually when you write your own language, you first build a parser and generate an AST. Then comes the hell stage — translating your AST into LLVM or another backend. Myc takes on all that complexity.
  • LLVM is complex.
  • Myc is simple and fun.
  • Stack-based opcodes are easy to emit from AST with a simple one-pass tree walk.
  • You’re not locked into one backend. LLVM for speed, QBE for fast compiles, C for anywhere.

Ultimate goal

Beat LLVM (joke). Real goal: beat gcc :).

Benchmark:

Mandelbrot renderer from mandel.bf (by Erik Bosman). All IR represent the same program. Shows whether Myc adds overhead over direct backend usage. Running on Macbook M1 in benchmark/brainfuck-compiler.

IR Compiler IR size, Kb Compile time Run time
llvm clang(-O3) 1529 1303ms 638ms
myc myc-llvm(–release) 443 1092ms 613ms
qbe-ssa qbe + clang(linker) 345 241ms + 91ms 771ms
myc myc-qbe(–release) 443 756ms 786ms
c clang(-O3) 128 1420ms 638ms
myc myc-c(–release) 443 1436ms 636ms

Quick Start (compile and run first program).

echo 'FUNC main BODY PUSH "Hello myc\n" PRINTF 0 ENDFUNC' | crystal src/cli/llvm.cr r

Status:

Alpha. But already powerful. All 3 backends work smoothly. 2900 tests pass.

myc on GitHub

Nice!

I was looking for something like this a few weeks ago! Thank you for sharing! :heart:

A text-based IR reminds me of recent matz/spinel AST → IR → C stages.

Queueing this to my list of projects to investigate on a weekend! :blush:

Cheers,

Fantastic, I’ll give it a try

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.

Your work on mycc shows how capable myc already is.

You are tempting me to do an exercise in trying to create my own language/experiment with it :grin:. But I’m fighting myself with this temptation :laughing: