A small IR for building programming languages.
- Simple DSL over LLVM/QBE.
- Your language AST -> mycIR -> [LLVM / QBE / C] -> binary.
- ~31 stack-based opcodes.
- The whole IR spec fits in an hour of reading.
- Compiles to native code via LLVM, QBE, or C.
- Fast compilation, "zero" overhead (I hope).
- ~9000 lines in Crystal.
- Includes mycc: a C11-like compiler built on myc (~4000 lines).
- I was writing my own language and got tired of fighting with LLVM IR. SSA and 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.
- Alpha.
- All 3 backends work smoothly.
- ~900 tests × 9 backend modes - passed.
- Mycc compiles 9000 lines of C from LangArena and 26/34 C files of the Lua interpreter.
- Currently performs one transformation: inlining. More may come later.
- Two modes: default (fast, Go-like) and final (slow compile, ~5-15% faster runtime).
- QBE has a bug on Apple aarch64 with globals.
FUNC :main
BODY
PUSH :MYC_BACKEND
PUSH :MYC_FLAGS
PUSH "Hello from (myc)MyCompiler (flags = %s, backend = %s)!\n"
PRINTF 2
ENDFUNC
This scatter plot shows the tradeoff between compile time and runtime. The ideal is the bottom-left corner: fast compiles, fast runtime. This is the Pareto frontier - you can't improve one without sacrificing the other. Measured on pure IR files (*.myc vs *.ll). Link to benchmark and reproduce steps.
All opcodes self documented. Also see examples.
- 25 main opcodes: PUSH, LOCAL, STORE, CALL, PARAM, BINARY, UNARY, FIELD, DEREF, ADDR, AS, SELECT, MALLOC, CREATE, INSPECT, PRINTF, STACK, SIZEOF, TO, INVOKE, LABEL, GOTO, ALLOCA, SLOT, VA
- 6 Control flow: IF/THEN/ELSE, LOOP/INIT/COND/BODY/STEP, SWITCH/CASE, BREAK, NEXT, RET.
- Types: STRUCT, ENUM/VARIANT, FLAT + void, bool, i8..i64, u8..u64, f32, f64, ptr, :indirect, :valist.
Requires Crystal to compile the myc compiler.
echo 'FUNC main BODY PUSH "Hello myc\n" PRINTF 0 ENDFUNC' | crystal src/cli/llvm.cr r# compile Myc IR C backend
crystal build src/cli/c.cr --release -o myc-c
# compile Myc IR LLVM backend
# requires LLVM >= 15.0, install it system wide or provide LLVM_CONFIG env variable
crystal build src/cli/llvm.cr --release -o myc-llvm
# compile Myc IR Qbe backend
# uses `qbe` from PATH, or the one given by the QBE env variable,
# otherwise build it locally as shown below
git clone https://github.com/kostya/qbe.git plugins/qbe
cd plugins/qbe; make; cd -
crystal build src/cli/qbe.cr --release -o myc-qbe# run
./myc-llvm r examples/ir/mandel.myc
# compile and run
./myc-llvm c examples/ir/bf.myc && ./bf
# show llvm IR
./myc-llvm examples/ir/loop.myc d
# show qbe IR
./myc-qbe examples/ir/loop.myc d
# explain myc IR
./myc-llvm examples/ir/bf.myc e
# example brainfuck compiler with Myc
cd benchmark/brainfuck-compiler
python3 bf2myc.py mandel.bf | ../../myc-llvm run
cd ../../As a proof of concept, I built mycc (with assist of LLM): a compiler for a subset of C (roughly C11) on top of myc. This proves that creating languages is easy. For parsing, I used libclang - it adds overhead, but it's the simplest solution for a POC. It is only 4000 lines of code and already compiles quite complex C code.
C source -> SyntaxTree(libclang/clang.cr) -> TypedAST(mycc) -> IR(myc) -> [LLVM/QBE/C] -> binary
The most challenging part was SyntaxTree -> TypedAST. In the file ast_builder.cr - 2400 LOC. libclang returns a non-normalized Tree with many edge cases that need to be transformed into a consistent form. Additionally, C has a ton of implicit behavior.
The TypedAST(mycc) -> IR(myc) stage as expected, is one of the simplest (codegen.cr, 900 lines) - a single-pass generation of stack-based IR directly from the AST.
Missing features: atomic, long double, complex numbers, longjmp, bitfields, asm. I wouldn't try building sqlite with it. It has only been tested on arm64 and linux64.
Requires LLVM/libclang >= 20.
# sudo apt install llvm-20 libclang-20-dev
shards install
crystal build src/cli/mycc.cr --release -o ./mycc# compile file
./mycc c examples/mycc/sieve.c
# compile file with --backend option
./mycc c --backend llvm examples/mycc/sieve.c
./mycc c --backend qbe examples/mycc/sieve.c
./mycc c --backend c examples/mycc/sieve.c
# show myc dump
./mycc examples/mycc/sieve.c d
# show llvm ir dump
./mycc examples/mycc/sieve.c d | ./myc-llvm d
# show qbe dump
./mycc examples/mycc/sieve.c d | ./myc-qbe d
# include paths example
MYCC_INCLUDE='/opt/homebrew/include,/usr/local/include' ./mycc examples/mycc/sieve.c c
# Build object file for custom linking
./mycc o examples/mycc/sieve.c sieve.o
clang sieve.o -lm -o ./sieve
- Avant - Systems Programming Language
Licensed under the Apache License, Version 2.0.
