Skip to content

fuad1502/oonta

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

crates.io CI

Oonta: OCaml to LLVM IR Compiler

Table of Contents

Introduction

Oonta is a compiler front-end for the OCaml programming language: it generates LLVM intermediate representation (IR) from OCaml source code.

Oonta uses the JJIK parser generator and JLEK lexer generator to perform the parsing and lexing stages. For building the IR, Oonta does not depend on the LLVM API.

Note

This project is part of the "Compiler Toys" project, originally meant as a learning exercise on Compilers.

Important

This project is still a work in progress, many OCaml features are not yet supported. For example, polymorphic functions and types, and modules are not yet supported. Additionally, the runtime does not yet provide a garbage collection service. See the issues tab for the list of work items.

Quick Start (Ubuntu)

Install Rust and LLVM (sudo apt install llvm), then:

cargo install oonta # installs the oonta command
wget https://github.com/fuad1502/oonta/releases/download/v0.2.0/liboonta_runtime-0.2.0-Linux.deb
sudo dpkg -i liboonta_runtime-0.2.0-Linux.deb # installs oonta runtime library
cat << EOF > main.ml
let rec factorial x = if x <= 1 then 1 else x * factorial (x - 1)
let () = print_int (factorial 5)
EOF
oonta --opt --exec main.ml
./main.out # prints `120`

Benchmark against ocamlopt

Two benchmarks are provided:

Merge sort (benchmark/merge_sort.ml):

type list =
  | Empty
  | Cat of (int * list)

let rec map f lst =
  match lst with
  | Empty -> ()
  | Cat (x, l) ->
      let () = f x in
      map f l

let rec create_lst_aux n acc =
  match n with
  | 0 -> acc
  | _ -> create_lst_aux (n - 1) (Cat (read_int (), acc))

let create_lst n = create_lst_aux n Empty

let rec split_aux lst acc_left acc_right =
  match lst with
  | Empty -> (acc_left, acc_right)
  | Cat (head, rest) -> split_aux rest acc_right (Cat (head, acc_left))

let split lst = split_aux lst Empty Empty

let rec merge left right =
  match (left, right) with
  | Empty, right -> right
  | lest, Empty -> lest
  | Cat (left_head, left_rest), Cat (right_head, right_rest) ->
      if left_head <= right_head then
        Cat (left_head, merge left_rest right)
      else
        Cat (right_head, merge left right_rest)

let rec merge_sort lst =
  match lst with
  | Empty -> lst
  | Cat (_, Empty) -> lst
  | _ ->
      let splitted_lst = split lst in
      let left =
        match splitted_lst with
        | lst, _ -> lst
      in
      let right =
        match splitted_lst with
        | _, lst -> lst
      in
      merge (merge_sort left) (merge_sort right)

let n = read_int ()
let lst = create_lst n
let sorted_lst = merge_sort lst
let () = map print_int sorted_lst

And insertion sort (benchmark/insertion_sort.ml):

type list =
  | Empty
  | Cat of (int * list)

let rec map f lst =
  match lst with
  | Empty -> ()
  | Cat (x, l) ->
      let () = f x in
      map f l

let rec create_lst_aux n acc =
  match n with
  | 0 -> acc
  | _ -> create_lst_aux (n - 1) (Cat (read_int (), acc))

let create_lst n = create_lst_aux n Empty

let rec insert elem lst =
  match lst with
  | Empty -> Cat (elem, lst)
  | Cat (head, tail) ->
      if elem <= head then
        Cat (elem, lst)
      else
        Cat (head, insert elem tail)

let rec insertion_sort lst =
  match lst with
  | Empty -> lst
  | Cat (head, tail) -> insert head (insertion_sort tail)

let n = read_int ()
let lst = create_lst n
let sorted_lst = insertion_sort lst
let () = map print_int sorted_lst

Execute the following command inside the repository root folder to run the benchmarks.

python3 benchmark/benchmark.py 1000000 0 # run the merge_sort.ml benchmark on a list with 1 million elements
python3 benchmark/benchmark.py 10000 0 # run the insertion_sort.ml benchmark on a list with 10000 elements

Important

Currently, for running the benchmark on large inputs, increase the stack size limit with ulimit -s unlimited

On my Ubuntu machine (AMD Ryzenβ„’ 7 7700X Γ— 16), with ocamlopt version 5.4.0 and LLVM version 20.1.8, the result of running the above benchmarks are as follows:

Benchmarking: merge_sort.ml
Elapsed time (./benchmark/ocamlopt.out): 0.8747 seconds
Elapsed time (./benchmark/oonta.out): 0.7445 seconds
> 14.88% faster

Benchmarking: insertion_sort.ml
Elapsed time (./benchmark/ocamlopt.out): 0.1325 seconds
Elapsed time (./benchmark/oonta.out): 0.3481 seconds
> 262.68% slower

User Guide

oonta --help

Dependencies

With no command line options given, the oonta command will simply generates LLVM IR without requiring any runtime dependencies. However, it provides --opt, -O, --compile / -c and --exec / -e options to optimize the generated IR, compile the generated IR to an object code and executable, respectively, which requires certain dependencies to function. Internally, oonta will invoke the following commands when given those options:

# with --opt
opt -S -O3 -o <.ll file> <.ll file>
# with --compile
llc -O3 -relocation-model=pic --filetype=obj -o <output> <.ll file>
# with --exec
clang -o <output> <.o file> -loonta_runtime

On Ubuntu, install the llvm package to make opt, llc, and clang available.

sudo apt install llvm

Warning

The generated IR uses opaque pointers. If the LLVM version is older than version 15, the --opt / --compile / --exec options might not work. If you're on Debian/Ubuntu, see https://apt.llvm.org/ for instructions on installing other versions.

Additionally, with --exec, you need the Oonta runtime library: liboonta_runtime.a. If you're running an x64 linux machine, you can obtain the static library from the release page. If not, you would need to build the runtime from source by following the guide below.

Note

Currently the runtime only provides memory allocation requests using bump allocation. Garbage collection service is not yet available.

Feature Highlights

Debug compile phases

Use the --verbose / -v option to debug each compile phase.

cat << EOF > main.ml
let rec factorial x = if x <= 1 then 1 else x * factorial (x - 1)
let () = print_int (factorial 5)
EOF
oonta --exec -v main.ml
=> Lexing & Parsing Start
=> Lexing & Parsing End (1 ms)
=> Build AST Start
factorial = 
FunExpr
β”œβ”€β–Έ parameters: [x]
β”œβ”€β–Έ captures: []
β”œβ”€β–Έ recursive: yes
└─▸ body:
    CondExpr
    β”œβ”€β–Έ condition:
    β”‚   BinOpExpr
    β”‚   β”œβ”€β–Έ operator: <=
    β”‚   β”œβ”€β–Έ lhs:
    β”‚   β”‚   VarExpr ("x")
    β”‚   └─▸ rhs:
    β”‚       LiteralExpr (1)
    β”œβ”€β–Έ then expr:
    β”‚   LiteralExpr (1)
    └─▸ else expr:
        BinOpExpr
        β”œβ”€β–Έ operator: *
        β”œβ”€β–Έ lhs:
        β”‚   VarExpr ("x")
        └─▸ rhs:
            ApplicationExpr
            β”œβ”€β–Έ function:
            β”‚   VarExpr ("factorial")
            └─▸ binds:
                └─▸ (0)
                    BinOpExpr
                    β”œβ”€β–Έ operator: -
                    β”œβ”€β–Έ lhs:
                    β”‚   VarExpr ("x")
                    └─▸ rhs:
                        LiteralExpr (1)

() = 
ApplicationExpr
β”œβ”€β–Έ function:
β”‚   VarExpr ("print_int")
└─▸ binds:
    └─▸ (0)
        ApplicationExpr
        β”œβ”€β–Έ function:
        β”‚   VarExpr ("factorial")
        └─▸ binds:
            └─▸ (0)
                LiteralExpr (5)

=> Build AST End (0 ms)
=> Resolve types Start
Top level bindings:
factorial: (int -> int)
=> Resolve types End (0 ms)
=> Transform application expressions Start
=> Transform application expressions End (0 ms)
=> Build LLVM module Start
=> Build LLVM module End (0 ms)
=> Write LLVM module Start
=> Write LLVM module End (1 ms)
=> LLVM backend Start
=> LLVM backend End (117 ms)

Error reporting

Line   1|let x = foo 3
                 ^--
Error: cannot infer expression type: Unbound value foo
Line   1|let rec f x = f
                       ^
Error: cannot infer expression type: Cannot unify 'b with ('a -> 'b)
Line   1|let () = 1 + 2
                  ^----
Error: cannot bind expression of type int to ()

Building from source

  1. Install C++ build dependencies
sudo apt install build-essential cmake
  1. Install cargo tool
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  1. Clone repository
git clone https://github.com/fuad1502/oonta.git
  1. Build liboonta_runtime.a
cmake -S runtime -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
  1. Install liboonta_runtime.a
sudo cmake --install build
  1. Build oonta
cargo build
cargo test

Why is it called Oonta?

Oonta, is based on the Indonesian word unta, which translates to "camel".

About

OCaml to LLVM IR compiler front-end πŸͺ

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published