Полный исходник с моими правками можно скачать здесь: https://github.com/arktur04/rust-llvm-toy-frontend
В настоящее время я работаю над компилятором, который написан на Rust, и порождает LLVM IR. LLVM API выглядит немного пугающе для новичков, и по нему не так много руководств (и все они на C++, поэтому не вполне очевидно, как сделать то же самое на Rust). Я бы хотел, чтобы кто-то протянул мне руку помощи, когда я начинал всё это, и эта статья является тем, что я хотел бы показать самому себе в то время.
В Rust наилучшая возможность взаимодействия с LLVM — через крейт llvm-sys. Один добрый человек разместил документацию к нему здесь. Конечно, вам следует также изучить руководство по LLVM, так как оно поможет вам понять, как LLVM “думает”. Этот пост, в основном, является переводом на Rust подмножества из этого руководства.
Полный исходный код для этого руководства находится здесь.
Получаем рабочее окружение для разработки
Для начинающих, вот способ, чтобы начать работать с LLVM:
# `curl` is just so we can next install Rust
sudo apt-get -y install clang curl llvm-3.8-dev
curl https://sh.rustup.rs -sSf | sh
# The `llvm-sys` crate expects something called `llvm-config` on your PATH.
sudo ln -s /usr/bin/llvm-config-3.8 /usr/bin/llvm-config
Если вы работаете на свежей Ubuntu (возможно, вам понадобится apt-get update), то всё готово, можно начинать. Если нет, вы можете запуститься в виртуальной машине Vagrant, через Vagrantfile:
Vagrant.configure("2") do |config|
config.vm.box = "bento/ubuntu-16.04"
end
Вы можете приступить, запустив cargo init llvm-example --bin и поместив следующее(взятое из llvm-sys) в src/main.rs:
//! Construct a function that does nothing in LLVM IR.
extern crate llvm_sys as llvm;
use std::ptr;
fn main() {
unsafe {
// Set up a context, module and builder in that context.
let context = llvm::core::LLVMContextCreate();
let module = llvm::core::LLVMModuleCreateWithName(b"nop".as_ptr() as *const _);
let builder = llvm::core::LLVMCreateBuilderInContext(context);
// Get the type signature for void nop(void);
// Then create it in our module.
let void = llvm::core::LLVMVoidTypeInContext(context);
let function_type = llvm::core::LLVMFunctionType(void, ptr::null_mut(), 0, 0);
let function = llvm::core::LLVMAddFunction(module, b"nop".as_ptr() as *const _,
function_type);
// Create a basic block in the function and set our builder to generate
// code in it.
let bb = llvm::core::LLVMAppendBasicBlockInContext(context, function,
b"entry".as_ptr() as *const _);
llvm::core::LLVMPositionBuilderAtEnd(builder, bb);
// Emit a `ret void` into the function
llvm::core::LLVMBuildRetVoid(builder);
// Dump the module as IR to stdout.
llvm::core::LLVMDumpModule(module);
// Clean up. Values created in the context mostly get cleaned up there.
llvm::core::LLVMDisposeBuilder(builder);
llvm::core::LLVMDisposeModule(module);
llvm::core::LLVMContextDispose(context);
}
}
И в вашем Cargo.toml:
[package]
name = "llvm-example"
version = "0.1.0"
authors = ["Ulysse Carion <ulysse@ulysse.io>"]
[[bin]]
name = "main"
[dependencies]
llvm-sys = "0.2"
Вы должны получить следующее:
vagrant@vagrant:/vagrant$ cargo run
Compiling llvm-example v0.1.0 (file:///vagrant)
Running `target/debug/main`
; ModuleID = 'nop'
define void @nop() {
entry:
ret void
}
Ура! Мы можем начинать писать свою собственную программу.
Немного менее тривиальная программа
Для начала, скомпилируем программу, которая просто возвращает код завершения путём возврата целого из функции main.
Вот как я это сделал: (Нам скоро понадобится парсер, и я добавил его сейчас; я использовал крейт peg):
[package]
name = "llvm-example"
version = "0.1.0"
authors = ["Ulysse Carion <ulysse@ulysse.io>"]
[[bin]]
name = "main"
[dependencies]
llvm-sys = "38"
peg = "0.5.4"
peg-syntax-ext = "0.5.2"
#![feature(plugin)]
#![plugin(peg_syntax_ext)]
extern crate llvm_sys as llvm;
use std::ffi::CString;
use std::fs::File;
use std::io::Read;
use std::ptr;
fn main() {
let mut input = String::new();
let mut f = File::open("in.ex").unwrap();
f.read_to_string(&mut input).unwrap();
let parsed_input = parser::program(&input).unwrap();
unsafe {
codegen(parsed_input);
}
}
peg! parser(r#"
#[pub]
program -> String
= i:int_literal "n" { i }
int_literal -> String
= [0-9]+ { match_str.to_owned() }
"#);
unsafe fn codegen(input: String) {
let context = llvm::core::LLVMContextCreate();
let module = llvm::core::LLVMModuleCreateWithName(b"example_module".as_ptr() as *const _);
let builder = llvm::core::LLVMCreateBuilderInContext(context);
// In LLVM, you get your types from functions.
let int_type = llvm::core::LLVMInt64TypeInContext(context);
let function_type = llvm::core::LLVMFunctionType(int_type, ptr::null_mut(), 0, 0);
let function = llvm::core::LLVMAddFunction(module, b"main".as_ptr() as *const _, function_type);
let entry_name = CString::new("entry").unwrap();
let bb = llvm::core::LLVMAppendBasicBlockInContext(context, function, entry_name.as_ptr());
llvm::core::LLVMPositionBuilderAtEnd(builder, bb);
// The juicy part: construct a `LLVMValue` from a Rust value:
let int_value: u64 = input.parse().unwrap();
let int_value = llvm::core::LLVMConstInt(int_type, int_value, 0);
llvm::core::LLVMBuildRet(builder, int_value);
// Instead of dumping to stdout, let's write out the IR to `out.ll`
let out_file = CString::new("out.ll").unwrap();
llvm::core::LLVMPrintModuleToFile(module, out_file.as_ptr(), ptr::null_mut());
llvm::core::LLVMDisposeBuilder(builder);
llvm::core::LLVMDisposeModule(module);
llvm::core::LLVMContextDispose(context);
}
peg! parser(r#"
#[pub]
program -> String
= i:int_literal "n" { i }
int_literal -> String
= n:$([0-9]+) { n.to_owned() }
"#);
Работает! Проверяем:
vagrant@vagrant:/vagrant$ cat in.ex
42
vagrant@vagrant:/vagrant$ cargo run
Running `target/debug/main`
vagrant@vagrant:/vagrant$ lli-3.8 out.ll ; echo $?
42
Круто! Вот так выглядит out.ll:
; ModuleID = 'example_module'
define i64 @main() {
entry:
ret i64 42
}
Арифметика
Добавим поддержку сложения, вычитания, умножения, и деления чисел. Чтобы сделать это, нам нужно расширить нашу грамматику. Давайте введём enum, который представляет AST:
pub enum Expr {
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
Literal(String),
}
Также нам нужно расширить грамматику:
// `product` and `sum` are that way to get operator precedence
peg! parser(r#"
use super::Expr;
#[pub]
program -> Expr
= e:expression "n" { e }
expression -> Expr
= sum
sum -> Expr
= a:product _ "+" _ b:sum { Expr::Add(Box::new(a), Box::new(b)) }
/ a:product _ "-" _ b:sum { Expr::Sub(Box::new(a), Box::new(b)) }
/ product
product -> Expr
= a:int_literal _ "*" _ b:product { Expr::Mul(Box::new(a), Box::new(b)) }
/ a:int_literal _ "/" _ b:product { Expr::Div(Box::new(a), Box::new(b)) }
/ int_literal
int_literal -> Expr
= [0-9]+ { Expr::Literal(match_str.to_owned()) }
_ = " "*
"#);
// `product` and `sum` are that way to get operator precedence
peg! parser(r#"
use super::Expr;
#[pub]
program -> Expr
= e:expression "n" { e }
expression -> Expr
= sum
sum -> Expr
= a:product _ "+" _ b:sum { Expr::Add(Box::new(a), Box::new(b)) }
/ a:product _ "-" _ b:sum { Expr::Sub(Box::new(a), Box::new(b)) }
/ product
product -> Expr
= a:int_literal _ "*" _ b:product { Expr::Mul(Box::new(a), Box::new(b)) }
/ a:int_literal _ "/" _ b:product { Expr::Div(Box::new(a), Box::new(b)) }
/ int_literal
int_literal -> Expr
= n:$([0-9]+) { Expr::Literal(n.to_owned()) }
_ = " "*
"#);
Затем мы генерируем код. Вы можете определять строки, такие как “addtmp”, и они будут использоваться как часть имени соответствующего регистра в IR.
// When you write out instructions in LLVM, you get back `LLVMValueRef`s. You
// can then use these references in other instructions.
unsafe fn codegen_expr(context: LLVMContextRef, builder: LLVMBuilderRef, expr: Expr) -> LLVMValueRef {
match expr {
Expr::Literal(int_literal) => {
let int_type = llvm::core::LLVMInt64TypeInContext(context);
llvm::core::LLVMConstInt(int_type, int_literal.parse().unwrap(), 0)
},
Expr::Add(lhs, rhs) => {
let lhs = codegen_expr(context, builder, *lhs);
let rhs = codegen_expr(context, builder, *rhs);
let name = CString::new("addtmp").unwrap();
llvm::core::LLVMBuildAdd(builder, lhs, rhs, name.as_ptr())
},
Expr::Sub(lhs, rhs) => {
let lhs = codegen_expr(context, builder, *lhs);
let rhs = codegen_expr(context, builder, *rhs);
let name = CString::new("subtmp").unwrap();
llvm::core::LLVMBuildSub(builder, lhs, rhs, name.as_ptr())
},
Expr::Mul(lhs, rhs) => {
let lhs = codegen_expr(context, builder, *lhs);
let rhs = codegen_expr(context, builder, *rhs);
let name = CString::new("multmp").unwrap();
llvm::core::LLVMBuildMul(builder, lhs, rhs, name.as_ptr())
},
Expr::Div(lhs, rhs) => {
let lhs = codegen_expr(context, builder, *lhs);
let rhs = codegen_expr(context, builder, *rhs);
let name = CString::new("divtmp").unwrap();
llvm::core::LLVMBuildUDiv(builder, lhs, rhs, name.as_ptr())
},
}
}
Сейчас вы можете исполнять программы типа 10 * 4 + 20 / 2 — 8! Очень круто, как мне кажется.
Переменные
Мы пойдём по простому пути и допустим, что наша программа не делает разных раздражающих вещей, таких как ссылки на неопределённые переменные. Мы просто сохраняем переменные в регистры, и сохраняем их в HashMap<String, LLVMValueRef>. Это работает, потому что программа имеет только один путь исполнения.
Расширяем язык и парсер:
pub enum Expr {
Literal(String),
Ref(String),
Assign(String, Box<Expr>),
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
}
peg! parser(r#"
use super::Expr;
#[pub]
program -> Vec<Expr>
= e:(expression ** "n") "n" { e }
expression -> Expr
= i:identifier _ "=" _ s:sum { Expr::Assign(i, Box::new(s)) }
/ sum
sum -> Expr
= a:product _ "+" _ b:sum { Expr::Add(Box::new(a), Box::new(b)) }
/ a:product _ "-" _ b:sum { Expr::Sub(Box::new(a), Box::new(b)) }
/ product
product -> Expr
= a:ref_or_literal _ "*" _ b:product { Expr::Mul(Box::new(a), Box::new(b)) }
/ a:ref_or_literal _ "/" _ b:product { Expr::Div(Box::new(a), Box::new(b)) }
/ ref_or_literal
ref_or_literal -> Expr
= i:identifier { Expr::Ref(i) }
/ int_literal
identifier -> String
= [a-zA-Z]+ { match_str.to_owned() }
int_literal -> Expr
= [0-9]+ { Expr::Literal(match_str.to_owned()) }
_ = " "*
"#);
peg! parser(r#"
use super::Expr;
#[pub]
program -> Vec<Expr>
= e:(expression ** "n") "n" { e }
expression -> Expr
= i:identifier _ "=" _ s:sum { Expr::Assign(i, Box::new(s)) }
/ sum
sum -> Expr
= a:product _ "+" _ b:sum { Expr::Add(Box::new(a), Box::new(b)) }
/ a:product _ "-" _ b:sum { Expr::Sub(Box::new(a), Box::new(b)) }
/ product
product -> Expr
= a:ref_or_literal _ "*" _ b:product { Expr::Mul(Box::new(a), Box::new(b)) }
/ a:ref_or_literal _ "/" _ b:product { Expr::Div(Box::new(a), Box::new(b)) }
/ ref_or_literal
ref_or_literal -> Expr
= i:identifier { Expr::Ref(i) }
/ int_literal
identifier -> String
= n:$([a-zA-Z]+) { n.to_owned() }
int_literal -> Expr
= n:$([0-9]+) { Expr::Literal(n.to_owned()) }
_ = " "*
"#);
Затем добавляем поддержку для двух новых выражений:
unsafe fn codegen_expr(context: LLVMContextRef, builder: LLVMBuilderRef, names: &mut HashMap<String, LLVMValueRef>, expr: Expr) -> LLVMValueRef {
match expr {
// ...
Expr::Ref(name) => {
*names.get(&name).unwrap()
},
Expr::Assign(name, expr) => {
let new_value = codegen_expr(context, builder, names, *expr);
names.insert(name, new_value);
new_value
},
}
}
И немного изменяем функцию codegen:
let int_type = llvm::core::LLVMInt64TypeInContext(context);
let zero = llvm::core::LLVMConstInt(int_type, 0, 0);
let mut names = HashMap::new();
let mut return_value = zero; // return value on empty program
for expr in input {
return_value = codegen_expr(context, builder, &mut names, expr);
}
llvm::core::LLVMBuildRet(builder, return_value);
Вуаля! Проверяем:
vagrant@vagrant:/vagrant$ cat in.ex
a = 3
b = 76
a + b
vagrant@vagrant:/vagrant$ cargo run
Running `target/debug/main`
vagrant@vagrant:/vagrant$ cat out.ll
; ModuleID = 'example_module'
define i64 @main() {
entry:
ret i64 79
}
If
С if всё становится несколько сложнее. Самый простой путь заставить его работать, этоо сохранять локальные переменные в стеке, и позволить LLVM произвести оптимизацию. В LLVM, вы создаёте стековую переменную командой alloca, и затем читаете/пишете командами load/store.
Для того, чтобы сделать это, мы опять расширяем язык и грамматику, добавляя новые правила парсера:
expression -> Expr
= if_expression
/ i:identifier _ "=" _ s:expression { Expr::Assign(i, Box::new(s)) }
/ sum
if_expression -> Expr
= "if" _ e:expression _ "{n" _ then_body:statements _ "}" _ "else" _ "{n" _ else_body:statements _ "}" {
Expr::If(Box::new(e), then_body, else_body)
}
И добавляем новый тип узла AST:
pub enum Expr {
Literal(String),
Ref(String),
Assign(String, Box<Expr>),
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
If(Box<Expr>, Vec<Expr>, Vec<Expr>),
}
Наконец, генерируем код для выражения if:
unsafe fn codegen_expr(context: LLVMContextRef, builder: LLVMBuilderRef, func: LLVMValueRef, names: &mut HashMap<String, LLVMValueRef>, expr: Expr) -> LLVMValueRef {
match expr {
// ...
Expr::If(condition, then_body, else_body) => {
let condition_value = codegen_expr(context, builder, func, names, *condition);
let int_type = llvm::core::LLVMInt64TypeInContext(context);
let zero = llvm::core::LLVMConstInt(int_type, 0, 0);
// `is_nonzero` is a 1-bit integer
let name = CString::new("is_nonzero").unwrap();
let is_nonzero = llvm::core::LLVMBuildICmp(builder, llvm::LLVMIntPredicate::LLVMIntNE, condition_value, zero, name.as_ptr());
// It's fine to create blocks first, and then fill them in later.
let entry_name = CString::new("entry").unwrap();
let then_block = llvm::core::LLVMAppendBasicBlockInContext(context, func, entry_name.as_ptr());
let else_block = llvm::core::LLVMAppendBasicBlockInContext(context, func, entry_name.as_ptr());
let merge_block = llvm::core::LLVMAppendBasicBlockInContext(context, func, entry_name.as_ptr());
llvm::core::LLVMBuildCondBr(builder, is_nonzero, then_block, else_block);
llvm::core::LLVMPositionBuilderAtEnd(builder, then_block);
let mut then_return = zero;
for expr in then_body {
then_return = codegen_expr(context, builder, func, names, expr);
}
llvm::core::LLVMBuildBr(builder, merge_block);
llvm::core::LLVMPositionBuilderAtEnd(builder, else_block);
let mut else_return = zero;
for expr in else_body {
else_return = codegen_expr(context, builder, func, names, expr);
}
llvm::core::LLVMBuildBr(builder, merge_block);
// Position the builder so that it's ready to work on the next
// expression.
llvm::core::LLVMPositionBuilderAtEnd(builder, merge_block);
zero
}
}
}
Много кода, но он делает то, что мы ожидали. Теперь мы можем запустить такую программу:
a = 1
if a {
a = 42
} else {
a = 13
}
a
которая порождает такой IR:
; ModuleID = 'example_module'
define i64 @main() {
entry:
%a = alloca i64
store i64 1, i64* %a
%a1 = load i64, i64* %a
%is_nonzero = icmp ne i64 %a1, 0
br i1 %is_nonzero, label %entry2, label %entry3
entry2: ; preds = %entry
store i64 42, i64* %a
br label %entry4
entry3: ; preds = %entry
store i64 13, i64* %a
br label %entry4
entry4: ; preds = %entry3, %entry2
%a5 = load i64, i64* %a
ret i64 %a5
}
Однако мы пока не закончили. Сейчас наше «выражение» if всегда равно нулю. Вместо этого, if должно приравниваться к then_return если выполняется путь “then”, либо else_return в противном случае.
Как заставить LLVM отслеживать, какой путь был выполнен? С помощью узла “Phi”. Вы отдаёте инструкции phi список пар (блок, значение), и затем phi-узел будет возвращать значение, соответствующее блоку, который выполнялся перед ним.
На этом мы закончим с if. Замети, что мы должны обновить then_block и else_block. Это делается для того, чтобы получить последний блок в конструкции “then”/”else”, а ранее then_block был первым блоком в “then”/”else".
// ...
// This is mostly the same code as before, just note the new calls to
// `LLVMGetInsertBlock`.
llvm::core::LLVMPositionBuilderAtEnd(builder, then_block);
let mut then_return = zero;
for expr in then_body {
then_return = codegen_expr(context, builder, func, names, expr);
}
llvm::core::LLVMBuildBr(builder, merge_block);
let then_block = llvm::core::LLVMGetInsertBlock(builder);
llvm::core::LLVMPositionBuilderAtEnd(builder, else_block);
let mut else_return = zero;
for expr in else_body {
else_return = codegen_expr(context, builder, func, names, expr);
}
llvm::core::LLVMBuildBr(builder, merge_block);
let else_block = llvm::core::LLVMGetInsertBlock(builder);
// Insert the phi node
llvm::core::LLVMPositionBuilderAtEnd(builder, merge_block);
let phi_name = CString::new("iftmp").unwrap();
let phi = llvm::core::LLVMBuildPhi(builder, int_type, phi_name.as_ptr());
let mut values = vec![then_return, else_return];
let mut blocks = vec![then_block, else_block];
llvm::core::LLVMAddIncoming(phi, values.as_mut_ptr(), blocks.as_mut_ptr(), 2);
phi
И вот он, удивительный компилятор:
vagrant@vagrant:/vagrant$ cat in.ex
a = 1
b = 0
c = if a {
if b {
11
} else {
40
}
} else {
if b {
10
} else {
20
}
}
c + 2
vagrant@vagrant:/vagrant$ cargo run
Running `target/debug/main`
vagrant@vagrant:/vagrant$ lli-3.8 out.ll ; echo $?
42
Круто! Вот код, который генерируется для данного примера входной программы:
; ModuleID = 'example_module'
define i64 @main() {
entry:
%a = alloca i64
%b = alloca i64
%c = alloca i64
store i64 1, i64* %a
store i64 0, i64* %b
%a1 = load i64, i64* %a
%is_nonzero = icmp ne i64 %a1, 0
br i1 %is_nonzero, label %entry2, label %entry3
entry2: ; preds = %entry
%b5 = load i64, i64* %b
%is_nonzero6 = icmp ne i64 %b5, 0
br i1 %is_nonzero6, label %entry7, label %entry8
entry3: ; preds = %entry
%b10 = load i64, i64* %b
%is_nonzero11 = icmp ne i64 %b10, 0
br i1 %is_nonzero11, label %entry12, label %entry13
entry4: ; preds = %entry14, %entry9
%iftmp16 = phi i64 [ %iftmp, %entry9 ], [ %iftmp15, %entry14 ]
store i64 %iftmp16, i64* %c
%c17 = load i64, i64* %c
%addtmp = add i64 %c17, 2
ret i64 %addtmp
entry7: ; preds = %entry2
br label %entry9
entry8: ; preds = %entry2
br label %entry9
entry9: ; preds = %entry8, %entry7
%iftmp = phi i64 [ 11, %entry7 ], [ 40, %entry8 ]
br label %entry4
entry12: ; preds = %entry3
br label %entry14
entry13: ; preds = %entry3
br label %entry14
entry14: ; preds = %entry13, %entry12
%iftmp15 = phi i64 [ 10, %entry12 ], [ 20, %entry13 ]
br label %entry4
}
Отметим паттерн, который образуют блоки: за исключением блока entry, они образуют группы по три, где первым идёт ветка «then», затем ветка «else», затем блок «merge» (который можно узнать по инструкции phi). Это следствие того факта, что каждый раз, когда мы обнаруживаем выражение «if», мы добавляем три новых блока в main. Тройки блоков расположены в том порядке, в котором происходит обход дерева AST.
Вот и всё! Надеюсь, что сейчас у вас есть достаточная основа, чтобы действовать самостоятельно.
Автор: Владимир