A small IR for building programming languages.
- A simple DSL over LLVM/QBE/GM.
- Your language AST -> mycIR -> [LLVM / QBE / GM / C] -> binary.
- ~31 stack-based opcodes.
- The whole IR spec fits within an hour of reading.
- Compiles to native code via LLVM, QBE, GM or C.
- Fast compilation, "zero" overhead (I hope).
- ~10000 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 an 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, GM for both fast compile and speed (best of QBE and LLVM).
- Alpha.
- All 4 backends work smoothly.
- ~900 tests × 12 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.
- The GM backend is x86-64 only.
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. 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
# compile Myc IR GM backend
git clone --depth 1 --filter=blob:none --sparse https://github.com/vishvananda/cppgm-extended.git plugins/cppgm-extended
git -C plugins/cppgm-extended sparse-checkout set dev
make -C plugins/cppgm-extended/dev cppgm++ CXX=clang++ CPPGM_HOST_CXX=clang++ -j6
crystal build src/cli/gm.cr --release -o myc-gm# 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
# show gm IR
./myc-gm 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/GM/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
./mycc c --backend gm 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
# show gm dump
./mycc examples/mycc/sieve.c d | ./myc-gm 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.
