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.

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.

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:

Mycc: New inliner (did I beat gcc and clang?).

A week after the mycc release, I added an optimization pass - inlining. Here are the results. I also changed the default compilation mode - now it’s like Golang: fast compilation and decent runtime.

LangArena benchmark without mycc overhead:

All measurements are single-threaded, without caches.

Compiler Build time Runtime
clang(-O3) 3093ms 52.1s
clang(-O1) 2753ms 54.5s
clang(-O0) 1649ms 141.2s
gcc(-O3) 3512ms 52.2s
cproc 922ms 73.0s
myc-llvm(default) 858ms 59.3s
myc-llvm(final) 1778ms 53.3s
myc-qbe(default) 482ms 68.5s
myc-c(default, clang) 1863ms 60.0s
myc-c(final, clang) 3139ms 53.7s

To get clean measurements, I saved all IR files generated by mycc: mycc file.c d > file.myc into the LangArena/myc directory. This removes libclang overhead and double IR conversion (current mycc pain points) from the measurements. Essentially, this is pure Myc IR → binary compilation time, without the C parser for LangArena benchmark. You could argue that comparing without C parsing isn’t fair. And you’d be right. But let’s be honest - mycc’s C parsing is very rough and add big overhead (POC, 3 weeks, libclang). If I write a proper fast parser for C like cproc did, it would add an estimated ~400ms to the compilation time (based on cproc minus myc-qbe results). But I don’t want to invest time in the C frontend right now - mycc is a POC, a tool to generate IR for testing Myc.

Did I beat clang and gcc like I originally wanted? On runtime alone - no. But on compile time vs runtime ratio - I think I did. At least this result satisfy me. Just keep in mind you’d need to add C parsing time (~400ms est.) to build time.