Skip to content

kostya/myc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

160 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

myc - MyCompiler

A small IR for building programming languages.

What is it?

  • Simple DSL over LLVM/QBE.
  • Your language AST -> mycIR -> [LLVM / QBE / C] -> binary.
  • ~29 stack-based opcodes.
  • Whole IR spec fits in 30 minutes of reading.
  • Compiles to native code via LLVM, QBE, or C.
  • Fast compilation, "zero" overhead (I hope).
  • ~7000 lines in Crystal.
  • Includes mycc as POC: a C subset compiler using myc as backend and libclang for parsing.

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.

Current status

Alpha. But already powerful. All 3 backends work smoothly. 4100 tests pass. mycc can compile LangArena benchmark (230kb non-trivial C code).

Ultimate goal

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

Benchmark:

Mandelbrot renderer from mandel.bf (by Erik Bosman). All IRs represent the same program. This shows whether Myc adds overhead over direct backend usage. Running on Ryzen3800+Linux in benchmark/brainfuck-compiler.

IR Compiler IR size, Kb Compile time Run time
llvm-ll clang(-O3) 1529 1528ms 621ms
myc myc-llvm(--release) 486 1528ms 631ms
qbe-ssa qbe + clang(as+linker) 345 199ms + 63ms 807ms
myc myc-qbe(--release) 486 1047ms 833ms
c clang(-O3) 128 1576ms 635ms
myc myc-c(--release) 486 1822ms 638ms

Myc adds "zero" overhead over the LLVM and C backends. The myc-qbe backend adds overhead due to suboptimal code generation, which will be addressed by future peephole optimization passes.

Install

Requires Crystal to compile the myc compiler.

Quick Start (compile and run first program).

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

Build

git clone https://github.com/kostya/myc
cd myc

# 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
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

mycIR

All opcodes self documented. Also see examples.

  • 23 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.
  • 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.

Examples:

Brainfuck compiler with myc IR.

cd benchmark/brainfuck-compiler
python3 bf2myc.py mandel.bf | ../../myc-llvm run --release

More examples.

./myc-llvm run --release examples/ir/mandel.myc
./myc-llvm run --release examples/ir/bf.myc
./myc-llvm run --release examples/ir/loop.myc

Factorial in mycIR, examples/ir/fact.myc, translation

examples/ir/fact.myc
FUNC fact
  ARGS
    TYPE i32
  RETURN
    TYPE i32
  BODY
    PUSH 1
    PARAM 0
    BINARY less_eq
    IF
      THEN
        PUSH 1
        RET
    ENDIF
    PUSH 1
    PARAM 0
    BINARY sub
    CALL fact
    PARAM 0
    BINARY mul
    RET
ENDFUNC

FUNC main
  BODY
    PUSH 5
    CALL fact
    INSPECT
ENDFUNC
LLVM Backend `./myc-llvm dump examples/ir/fact.myc`
; ModuleID = 'fact'
source_filename = "fact"
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-n32:64-S128-Fn32"
target triple = "arm64-apple-darwin23.3.0"

@str = private constant [15 x i8] c"fact(%d) = %d\0A\00"

define i32 @fact(i32 %0) {
alloca:
  %__myc_result = alloca i32, align 4
  br label %body

body:                                             ; preds = %alloca
  %1 = icmp sle i32 %0, 1
  br i1 %1, label %then, label %endif

ret:                                              ; preds = %endif, %then
  %2 = load i32, ptr %__myc_result, align 4
  ret i32 %2

then:                                             ; preds = %body
  store i32 1, ptr %__myc_result, align 4
  br label %ret

endif:                                            ; preds = %body
  %3 = sub i32 %0, 1
  %4 = call i32 @fact(i32 %3)
  %5 = mul i32 %0, %4
  store i32 %5, ptr %__myc_result, align 4
  br label %ret
}

define void @main() {
alloca:
  br label %body

body:                                             ; preds = %alloca
  %0 = call i32 @fact(i32 5)
  %1 = call i32 (ptr, ...) @printf(ptr @str, i32 5, i32 %0)
  br label %ret

ret:                                              ; preds = %body
  ret void
}

declare i32 @printf(ptr, ...)
QBE Backend `./myc-qbe dump examples/ir/fact.myc`
data $str_0 = { b "fact(%d) = %d\n", b 0 }
export function w $fact(w %arg0) {
@start
  %__myc_result =l alloc8 4
  jmp @body
@body
  %t1 =w cslew %arg0, 1
  jnz %t1, @then_1, @endif_2
@then_1
  storew 1, %__myc_result
  jmp @ret
@endif_2
  %t2 =w sub %arg0, 1
  %t3 =w call $fact(w %t2)
  %t4 =w mul %arg0, %t3
  storew %t4, %__myc_result
  jmp @ret
@ret
  %ret_val =w loadw %__myc_result
  ret %ret_val
}

export function  $main() {
@start
  jmp @body
@body
  %t1 =w call $fact(w 5)
  %t2 =w call $printf(l $str_0, ..., w 5, w %t1)
  jmp @ret
@ret
  ret
}
C Backend `./myc-c dump examples/ir/fact.myc`
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>

int32_t fact(int32_t arg0);
void main();
int32_t fact(int32_t arg0) {
  int32_t __myc_result;
  int t1 = arg0 <= 1;
  if (t1) goto then_1; else goto endif_2;

then_1:;
  __myc_result = 1;
  goto ret;

endif_2:;
  int32_t t2 = arg0 - 1;
  int32_t t3 = fact(t2);
  int32_t t4 = arg0 * t3;
  __myc_result = t4;
  goto ret;

ret:;
  return __myc_result;
}
void main() {
  int32_t t5 = fact(5);
  int32_t t6 = printf("fact(%d) = %d\n", 5, t5);
  goto ret;

ret:;
  return;
}

Run tests

crystal spec

Usage

Usage
Usage: ./myc-llvm COMMAND [OPTIONS] INPUT [INPUT]* [OUTPUT]

Commands:

  compile|c  ; compile multiple .myc files into executable binary
             ;   ./myc-llvm c file.myc out
             ;   ./myc-llvm c --release *.myc out
             ;   cat file.myc | ./myc-llvm c --release out

  run|r      ; compile multiple .myc files and run the program
             ;   ./myc-llvm r file.myc
             ;   ./myc-llvm r --release file.myc
             ;   cat file.myc | ./myc-llvm r --release

  obj|o      ; compile one .myc file into object file (.o) for linking
             ;   ./myc-llvm o file.myc file.o
             ;   ./myc-llvm o --release file.myc file.o
             ;   cat file.myc | ./myc-llvm o --release file.o

  dump|d     ; output backend IR to console (for debugging and optimization analysis)
             ;   ./myc-llvm d file.myc
             ;   ./myc-llvm d --release file.myc
             ;   cat file.myc | ./myc-llvm d --release

  beautify|b ; format, validate, and add auto-comments(--annotate) to .myc files
             ;   ./#{cli_name} b .
             ;   ./#{cli_name} b --annotate src/
             ;   ./#{cli_name} b file1.myc file2.myc

  version|v  ; display version information
             ;   ./myc-llvm version

OPTIONS:
  --release ; compile in performance mode (optimizations enabled)
  --target=TARGET   (TARGET: arm64, x86_64, x86, wasm32, ...; default: native)

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, longjmp, bitfields, asm, volatile, 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

This is not just a random benchmark - each of the 50 LangArena tests validates its output using checksums. A compiler cannot "cheat" by deleting or skip work. This benchmark is also a part of CI.

To run it: cd benchmark; ruby run_lang_arena.rb, needs to add cproc,qbe,clang,gcc to PATH, compile mycc and install uthash and pcre2 (sudo apt install uthash-dev libpcre2-dev).

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.

mycc: build compiler.

Requires LLVM/libclang >= 20.

# sudo apt install llvm-20 libclang-20-dev
shards install
crystal build src/cli/mycc.cr --release -o ./mycc

mycc: usage example.

# compile file
./mycc c --release examples/mycc/sieve.c

# compile file with --backend option
./mycc c --release --backend llvm examples/mycc/sieve.c
./mycc c --release --backend qbe examples/mycc/sieve.c
./mycc c --release --backend c examples/mycc/sieve.c

# show optimized llvm ir dump
./mycc examples/mycc/sieve.c d | ./myc-llvm d --release

# 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 --release examples/mycc/sieve.c sieve.o
clang sieve.o -lm -o ./sieve

License

Licensed under the Apache License, Version 2.0.

Thanks

About

MyCompiler - A small IR for building programming languages.

Resources

License

Stars

19 stars

Watchers

2 watching

Forks

Packages

 
 
 

Contributors