Setelah kita membuat Lexer dan Parser kita memerlukan 1 hal lagi yaitu fungsi untuk mengevaluasi expression yang sudah ada.
Untuk kode yang sudah lengkap bisa dilihat di github: https://github.com/aldidana/hitung
Kita bisa saja mengevaluasi expression yang sudah ada langsung menggunakan Rust tanpa llvm atau kita juga bisa mentranslate expression yang ada tadi menjadi sebuah Bytecode yang akan dijalankan di VM
Namun untuk tutorial kali ini kita akan menjalankannya dengan menggunakan JIT (Just In Time) compiler dari llvm.
LLVM adalah sebuah compiler infrastructure dimana kita dapat membuat sebuah instruksi berupa intermediate representation atau IR, kemudian llvm juga dapat meng-compile IR menjadi binary yang berdiri sendiri (standalone binary) atau melakukan Just In Time compilation pada saat runtime.
Dibawah ini adalah contoh IR dari kode if 1 < 2 then 1 else 0
.
Kita akan menggunakan library Inkwell. Inkwell merupakan sebuah high-level wrapper dari library llvm-sys.
LLVM
Untuk mengikutin ini pastikan kia sudah meng-install llvm versi 10.
Untuk mengecek versi llvm yang sudah terinstall bisa menggunakan perintah ini:
llvm-config --version
Struktur
hitung
│ Cargo.toml
│
└───src
| main.rs
| lexer.rs
| token.rs
| expression.rs
| parser.rs
| jit.rs //file baru
Cargo.toml
Buka file cargo.toml dan tambahkan library inkwell.
[package]
name = "hitung"
version = "0.1.0"
authors = ["Nama Author"]
edition = "2018"
[dependencies]
inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "master", features = ["llvm10-0"] }
JIT (Just In Time)
Kita akan membutuhkan 3 objek yang ada di llvm.
- Context: Bagian inti/core dari llvm
- Module: Tempat dimana fungsi dan IR di simpan
- Builder: Yang akan melakukan instruksi kemudian memasukannya ke dalam BasicBlock
Buka file jit.rs dan tambahkan barisan kode berikut ini:
use std::collections::HashMap;
use std::path::Path;
use inkwell;
use inkwell::builder::Builder;
use inkwell::context::Context;
use inkwell::module::Module;
use inkwell::values::PointerValue;
use inkwell::FloatPredicate;
use inkwell::OptimizationLevel;
use crate::expression::Expression;
use crate::lexer::Lexer;
use crate::parser::Parser;
use crate::token::Token;
pub type FuncSign = unsafe extern "C" fn() -> f64;
Kita menambahkan beberapa module yang berkaitan dengan inkwell dan membuat tipe data yang dibutuhkan untuk hasil dari fungsi yang dipanggil oleh llvm.
Untuk bagian pub type FuncSign = unsafe extern "C" fn() -> f64;
disini adalah kita akan memanggil eksternal kode agar dapat menggunakan suatu fungsi dari bahasa pemrograman lain dalam hal ini bahasa pemrograman C, bagian "C" diatas disebut ABI (Application Binary Interface) sedangkan keyword extern yang memfasilitasi FFI (Foreign Function Interface). Penggunaan keyword extern sendiri tidak membutuhkan keyword unsafe tetapi untuk memanggilnya dibutuhkan keyword unsafe, dikarenakan Rust tidak mengetahui aturan dan jaminan dari bahasa lain tersebut.
Tambahkan kode berikut setelah kode diatas:
pub struct Compiler<'ctx> {
context: &'ctx Context,
module: Module<'ctx>,
builder: Builder<'ctx>,
variables: HashMap<String, PointerValue<'ctx>>,
debug: bool,
}
Struct Compiler terdiri dari Context untuk menyimpan konteks llvm selama program berjalan, Module, Builder, Variables untuk menyimpan Global Variable berupa HashMap yang isinya adalah sebuah pointer, Debug untuk menampilkan IR yang telah dibuat.
Kemudian lanjutkan dengan membuat associate function dan method untuk Compiler diatas.
Fungsi new diatas menerima sebuah reference Context dari fungsi yang memanggilnya karena kita membutuhkannya selama program berjalan, fungsi ini juga akan membuat sebuah module yang dinamai "hitung".
Fungsi compile_source akan memanggil Lexer dan Parser untuk menghasilkan expression dari kode sumber yang sudah dimasukan, fungsi ini akan memanggil fungsi jit_compile jika dapat menghasilkan expression yang benar.
Tambahkan fungsi jit_compile berikut setelah fungsi compile_source:
Fungsi jit_compile akan membuat sebuah fungsi "berhitung" di dalam module "hitung" dengan tipe data f64 dan fungsi ini tidak mempunyai parameter. Selanjutnya kita membuat sebuah basic block yang dinamai "entry", basic block adalah urutan untuk instruksi, basic block disini mengacu pada fungsi yang sudah dibuat yaitu fungsi "berhitung". Lalu kita menginformasikan builder untuk mulai dari/berpindah ke basic block ini. Basic block dari fungsi ini juga membutuhkan tipe data yang akan dikembalikan, oleh karena itu tipe data yang akan dikembalikan diambil setelah mengevaluasi expression yang ada dari fungsi eval.
Berikut ini contoh fungsi dan basic block llvm:
define double @berhitung() {
entry:
ret double 1.000000e+00
}
Selanjutnya membuat execution_engine dari module yang sudah kita buat sebelumnya, execution_engine akan menjalankan fungsi yang sudah diisi basic block. Disini juga kita akan mencetak setiap hasil dari module tersebut ke file hitung.ll jika Debug diset menjadi true. Kemudian execution_engine melepas module yang kita buat sebelumnya agar kita bisa membuat kembali execution_engine baru dengan module yang sama dari setiap kode sumber yang dimasukan.
Aritmatika
Sebelum ke tahap selanjutnya, disini akan membahas fungsi aritmatika yang ada di llvm.
Misalnya kita mempunyai sebuah fungsi penjumlahan seperti ini:
fn add(lhs: f64, rhs: f64) -> f64 {
lhs + rhs
}
Dalam library Inkwell kita bisa memanggilnya dengan fungsi yang mirip seperti fungsi diatas:
builder.build_float_add(lhs, rhs, "add") // nama bisa diisi apa saja
Maka fungsi diatas akan langsung mengembalikan jawaban yang sesuai.
Eval
Kembali ke file jit.rs, setelah fungsi jit_compile tambahkan kode berikut:
catatan: fungsi eval dipanggil dengan cara recursive untuk mengevaluasi expression lainnya.
Jika expression itu adalah expression untuk variable maka kita akan mencari apakah variable tersebut ada atau tidak, variable saat ini disimpan di global variable, jika variable tersebut ada maka kita mengambil pointer dari isi variable tersebut.
Jika expression berupa angka atau num maka kita hanya perlu mengembalikan angka tersebut dengan tipe data yang sudah ditentukan (dalam hal ini f64)
Jika expression tersebut berupa Unary operator maka kita perlu menentukan bagian kiri dan bagian kanan yang ada dari expression tersebut, misalnya jika +3 maka akan menjadi 3 tetapi jika -3 tetap menjadi -3, disini kita hanya mengecek unary untuk +angka dan -angka.
Masih didalam fungsi eval, tambahkan kode berikutnya:
Untuk binary operator:
- Jika expression berupa sebuah komparasi lebih besar (GT) dari atau lebih kecil dari (LT) llvm memiliki sebuah predicate sesuai dengan dua hal tersebut, komparasi akan mengembalikan angka 1 jika true dan 0 jika false.
- Jika expression berupa Variable Assignment maka kita akan mengambil angka dari variable tersebut lalu menyimpannya di global variable, disini juga kita langsung mengembalikan angka dari variable tersebut.
- Jika Binary operator bukan dari kedua diatas, maka binary operator tersebut adalah sebuah aritmatika.
Kemudian jika expression yang ditemukan berupa tanda kurung maka kita akan mengevaluasi expression yang ada didalamnya.
Terakhir masih dengan fungsi yang sama yaitu fungsi eval, tambahkan kode dibawah ini:
Untuk expression terakhir adalah branching/conditional atau pencabangan. Kita menambahkan 3 basic block dari fungsi yang sudah dibuat, yaitu basic block untuk then, else dan cont yang dinamai "entry", jika memiliki nama basic block yang sama makan nama tersebut akan ditambahkan angka, misalnya "entry" -> "entry1" -> "entry2". Untuk menentukan cabang mana yang akan dijalankan adalah dengan phi node dengan memberikan daftar instruksi dari sebuah basic block dan nilai dari basic block itu sendiri.
Test JIT
Tambahkan kode ini di bagian paling bawah untuk mengetest fungsi yang ada pada file jit.rs ini:
impl<'ctx> Compiler<'ctx> {
....
}
// tambahkan test disini
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_eval_from_expression() {
let expression = Expression::Binary(
Box::new(Expression::Binary(
Box::new(Expression::from(3)),
Token::Add,
Box::new(Expression::from(2)),
)),
Token::Sub,
Box::new(Expression::from(2)),
);
let context = Context::create();
let module = context.create_module("test_hitung");
let builder = context.create_builder();
let mut compiler = Compiler {
context: &context,
module,
builder,
variables: Default::default(),
debug: false,
};
let actual = compiler.jit_compile(expression).unwrap();
assert_eq!(3.0, actual);
}
#[test]
fn test_eval_from_source() {
let context = Context::create();
let mut compiler = Compiler::new(&context, false);
let actual = compiler.compile_source(r"2 + 2 * 3 / 2").unwrap();
assert_eq!(5.0, actual);
}
#[test]
fn test_eval_from_source_if_then_else() {
let context = Context::create();
let mut compiler = Compiler::new(&context, false);
let actual = compiler.compile_source(r"if 1 < 2 then 123 else 456").unwrap();
assert_eq!(123.0, actual);
}
}
jalankan test dan pastikan semuanya sukses
cargo test
Main
Saatnya kembali ke file main.rs untuk menggunakan jit compiler yang telah dibuat, pastikan file main.rs menjadi seperti dibawah ini:
use std::io;
use std::io::Write;
use inkwell::context::Context;
mod expression;
mod jit;
mod lexer;
mod parser;
mod token;
use jit::Compiler;
fn main() {
let mut debug = false;
for arg in std::env::args() {
match arg.as_str() {
"debug" => debug = true,
_ => (),
}
}
let context = Context::create();
let mut compiler = Compiler::new(&context, debug);
loop {
// repl
println!();
print!("> ");
io::stdout().flush().expect("Error when flush stdout.");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Could not read from standard input.");
match compiler.compile_source(input.as_str()) {
Ok(result) => println!("{}", result),
Err(err) => {
eprintln!("Error {:?}", err);
break
},
}
}
}
Jalankan program dan aktifkan debug mode
cargo run debug
Silahkan ketikan input yang diinginkan, contoh:
a = 2 // tekan enter
a + 2 * 3 tekan enter
Maka hasilnya adalah 8
contoh menghitung:
2 + ( 4 + 5) * 7 / 2 * 4
Hasilnya 128
contoh if then else:
if 20 > 15 then 100 else 99
Hasilnya 100
Jika debug aktif pastikan anda dapat melihat file hitung.ll
Masih banyak sekali hal yang dapat ditingkatkan di bahasa "hitung" ini, misalnya menambahkan fungsi dan memanggil fungsi tersebut, for loop, if then else yang tidak hanya 1 level.
Sekian.