Tahap pertama sebuah compiler adalah mengubah kode sumber (source code) menjadi sebuah token, token ini adalah sebuah kata yang dapat dimengerti oleh bahasa pemrograman tersebut, tahap ini disebut Lexical Analysis atau juga Scanner.
Untuk kode yang sudah lengkap bisa dilihat di github: https://github.com/aldidana/hitung
Pada gambar diatas setiap character dalam kode sumber akan ditentukan masuk ke bagian token yang sesuai.
Membuat Program Baru
Untuk mengikuti ini pastikan Rust sudah ter-install, kemudian ikuti perintah dibawah ini untuk membuat program Rust baru:
cargo new hitung
Struktur File
Struktur yang akan dibuat untuk saat ini:
hitung
│ Cargo.toml
│
└───src
| main.rs
| lexer.rs
| token.rs
Token
Kita akan memulai pada file token.rs, tambahkan kode berikut:
Dibagian ini kita membuat sebuah enum Token dengan nama yang sudah ditentukan, enum juga dapat diberikan sebuah tipe data didalamnya seperti Token::Num(f64) diatas.
Selanjutnya Token juga dapat melakukan konversi dari tipe data i32 menjadi f64, dengan implementasi trait From, trait From ini memiliki metode atau fungsi from yang mengambil tipe data generic, contoh:
trait From<T> {
pub fn from(_: T) -> Self
}
// Token mempunyai metode atau fungsi from
Token::from(2) // akan menjadi tipe data f64
Token::Num(2.0)
Pada kode terakhir token disini untuk menentukan seberapa beban atau prioritas sebuah token dibanding token lainnya misalnya perkalian lebih dulu di evaluasi dibandingkan dengan penjumlahan, sehingga 3+2*2 adalah 7 bukan 10, tetapi jika hasilnya ingin 10 bisa menggunakan tanda kurung (3+2)*2.
Test Token
Jangan lupa menambahkan test untuk token.rs, tambahkan kode berikut ini setelah baris terakhir dari kode yang ada di dalam token.rs:
Disini kita membuat test untuk mengecek left binding power dari sebuah token.
Lexer
Lanjut ke file lexer.rs, pada bagian awal baris tambahkan kode berikut ini:
Jangan khawatir akan dijelaskan mengenai barisan kode tersebut. Disini kita akan menggunakan Peekable dan Chars. Peekable yaitu sebuah iterator yang mempunyai metode atau fungsi peek() yang akan mengembalikan sebuah reference dari elemen berikutnya, sehingga posisi sebuah char tidak akan maju ke elemen berikutnya, contohnya seperti ini:
let array = [1, 2, 3]; //sebuah array
let mut iter = array.iter().peekable(); // maka iter akan mempunyai metode atau fungsi peek
iter.peek() // akan mengembalikan tipe optional sehinnga isinya Some(1)
iter.peek() // Some(1)
iter.peek() // Some(1)
// Tidak peduli seberapa banyak kita memanggil fungsi peek() iterator tidak akan maju ke elemen berikutnya
iter.next() // maju ke elemen berikutnya
iter.peek() // Some(2)
Kemudian menggunakan Chars karena akan menyimpan input dari kode sumber menjadi sebuah Peekable yang didalamnya mempunyai tipe Chars. Selanjutnya untuk token adalah meng-import agar dapat mengakses Enum Token yang sudah dibuat sebelumnya.
Tambahkan kode berikut ini setelah kode lexer.rs terakhir:
Pada bagian ini kita membuat Struct Lexer dengan reference yang ditandai oleh lifetime 'a, Struct Lexer mempunyai field input berupa Peekable yang mempunyai tipe data Chars, Chars juga membutuhkan lifetime parameter.
Berikut in mengenai penjelasan sederhana mengenai lifetime diatas:
struct DB {
.....
}
struct App {
database: &DB, // borrow atau meminjam struct DB diatas
}
// kode diatas tidak akan bisa dicompile
// dikarenakan struct DB tidak diketahui lifetimenya
struct App<'a> {
database: &'a DB,
}
// Kode diatas berhasil dicompile
// karena field database yang mempunyai tipe DB mempunyai lifetime yang sama seperti App
// Struct App tidak boleh outlive dari reference yaitu struct DB
fungsi new() dari Lexer adalah associated functions untuk membuat sebuah instance dari tipe data itu sendiri.
.... {
fn new() -> Self // ini adalah associated function karena tidak mengambil self pada parameter pertama
fn move(&mut self) // methods
fn attack(self) // methods
}
Sedangkan fungsi lex() adalah sebuah methods dari Lexer, fungsi ini akan melakukan perulangan hingga input berupa kode sumber selesai atau berakhir.
Masih di dalam file lexer.rs, tambahkan kode dibawah ini tepat setelah fungsi lex dan masih di dalam block impl dari Lexer
fungsi berikutnya ini cukup sederhana, megecek input dari kode sumber yang akan menjadi token yang sudah kita tentukan.
&mut self pada fungsi adalah agar kita dapat mengubah data dari object itu sendiri, tanpa mengambil atau memindahkan ownership.
&mut self: mutable reference tanpa mengambil/memindahkan ownership
- &mut self: mutable reference tanpa mengambil/memindahkan ownership
- mut self: mutable dengan mengambil/memindahkan ownership
- self: immutable dengan mengambil/memindahkan ownership
- &self: immutable tanpa mengambil/memindahkan ownership
Test Lexer
Terakhir kita tambahkan test untuk lexer.rs pada baris terakhir
Main
Kembali ke fungsi utama untuk menjalankan program ini yaitu main.rs, tambahkan kode berikut:
Pada bagian ini kita menggunakan standard library untuk io (input/output) dan juga Trait Write agar io::stdout() dapat menjalankan fungsi flush(). Kemudian kita membaca setiap input yang masuk ke dalam program dan memanggil fungsi yang ada pada Lexer untuk menghasilkan sebuah token.
Jalankan program dengan perintah berikut pada terminal:
cargo run
Kemudian ketik input yang diinginkan lalu tekan enter pada keyboard, misalnya:
if 1 > 2 then 1 else 0
maka akan menghasilkan token sebagai berikut:
[If, Num(1.0), GT, Num(2.0), Then, Num(1.0), Else, Num(0.0), EOF]
contoh lain:
a = 123
maka hasilnya:
[IDENTIFIER("a"), ASSIGN, Num(123), EOF]
Sekian untuk bagian pertama dari Membuat Bahasa Pemrograman Sederhana dengan Rust dan LLVM.