myc

MyCompiler - A small IR for building programming languages.

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.
  • ~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.
  • Includes mycc: a C subset compiler using mycIR as backend and libclang for C 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. 2900 tests pass.

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-ll clang(-O3) 1529 1302ms 638ms
myc myc-llvm(--release) 443 1093ms 613ms
qbe-ssa qbe + clang(as+linker) 345 240ms + 91ms 770ms
myc myc-qbe(--release) 443 760ms 785ms
c clang(-O3) 128 1420ms 638ms
myc myc-c(--release) 443 1445ms 636ms

Myc adds zero overhead over LLVM and C backends. QBE is an exception: ~400-700ms compile overhead due to suboptimal code generation (will be fixed by future peephole passes).

More benchmarks from examples/

Benchmark myc-llvm compile myc-llvm run myc-qbe compile myc-qbe run myc-c compile myc-c run
bf 62ms 1304ms 116ms 4033ms 117ms 1424ms
loop 87ms 359ms 107ms 375ms 112ms 243ms
mandel 1105ms 614ms 759ms 787ms 1459ms 641ms

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.

  • 19 main opcodes: PUSH, LOCAL, STORE, CALL, PARAM, BINARY, UNARY, FIELD, DEREF, ADDR, AS, SELECT, MALLOC, CREATE, INSPECT, PRINTF, STACK, SIZEOF, TO
  • 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

mycc: a C subset compiler

~700 lines of Crystal. Uses mycIR as backend and libclang for C parsing.

# Build
shards install; crystal build src/cli/mycc.cr -o ./mycc

# Show mycIR output
./mycc spec/examples/mycc/complex/loop.cc

# Build and run (LLVM backend)
./mycc spec/examples/mycc/complex/loop.cc | ./myc-llvm r --release

# Show LLVM IR dump
./mycc spec/examples/mycc/complex/loop.cc | ./myc-llvm d

# Show optimized LLVM IR dump
./mycc spec/examples/mycc/complex/loop.cc | ./myc-llvm d --release

Compile and run examples

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

Example: Brainfuck compiler with myc IR.

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

Example: factorial in mycIR, examples/ir/fact.myc, translation

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: ./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)

License

Licensed under the Apache License, Version 2.0.

Thanks

Repository

myc

Owner
Statistic
  • 15
  • 0
  • 0
  • 0
  • 0
  • 30 minutes ago
  • June 19, 2026
License

Apache License 2.0

Links
Synced at

Fri, 26 Jun 2026 13:19:59 GMT

Languages