Tutorial Membuat Interpreter dan Compiler (bagian 5)
part 1 part 2 part 3 part 4 part 5 part 6 part 7 part 8 part 9
Source code untuk artikel ini: expr2.zip (sama dengan bagian 4)
Compiler Untuk Bulat
Dalam tutorial ini, saya meneruskan bagian 4. Sebelumnya kita sudah memiliki grammar dan interpreter untuk Bahasa Bulat. Berikutnya kita akan membuat compilernya. Tidak semua detail dibahas, karena sudah dibahas di bagian 3 ketika membuat compiler untuk ekspresi sederhana.
Saya sudah merencanakan untuk memakai LLVM di langkah-langkah berikutnya, tapi compiler untu bulat ini masih terlalu sederhana, jadi kita masih bisa memakai assembly langsung. Di sini Anda akan mulai melihat bahwa memakai assembly langsung akan semakin rumit. Compiler bulat ini sudah 198 baris. Sebagai pengingat, Assembly yang digunakan hanya ditargetkan untuk Linux.
Catatan: Kode-kode dalam bagian ini ada pada file
BulatComp.java
Penyimpanan variabel
Karena bahasa ini masih sederhana, semua masih bisa disimpan sebagai global. Jika kita mengenal scoping, maka kita perlu memakai stack (sebenarnya ini juga tidak terlalu sulit, tapi tidak diperlukan sekarang).
Sebuah variabel global integer bernama myvar
bisa dideklarasikan di assembly seperti ini:
.global myvar
.size myvar, 4
myvar:
.zero 4
Nah di Java, ini dihasilkan di bagian compileDeclaration
dengan kode berikut:
for (Object e: decl.getChildren()) {
CommonTree et = (CommonTree)e;
String var = et.getText();
if (!variables.containsKey(var)) {
result.append(".globl "+ var+"\n");
result.append(".size "+var+", 4\n");
result.append(var+":\n");
result.append(".zero 4\n");
}
variables.put(var, 0);
}
Perhatikan bahwa di bahasa Bulat, kita mengijinkan var a,b,a
, tapi a hanya akan didefinisikan sekali saja. Untuk memastikan itu, kita hanya akan menghasilkan deklarasi variabel, jika variabel belum ada di daftar variabel.
Fungsi main
Semua deklarasi dilakukan sebelum fungsi utama, setelah itu kita perlu memberikan template fungsi utama. Kita menambahkan beberapa string, "%s\n"
digunakan untuk memformat string dalam instruksi print
, string "%d\n"
untuk mencetak integer, dan "%d"
(tanpa \n) untuk membaca input dengan fungsi library C scanf
.
.section .rodata
.print_str_format:
.string "%s\n"
.print_int_format:
.string "%d\n"
.scan_int_format:
.string "%d"
.text
.globl main
.type main, @function
main:
Dengan kata lain di Java, kita melakukan ini:
result.append(".section .rodata\n");
result.append(".print_str_format:\n");
result.append(".string \"%s\\n\"\n");
result.append(".print_int_format:\n");
result.append(".string \"%d\\n\"\n");
result.append(".scan_int_format:\n");
result.append(".string \"%d\"\n");
result.append(".text\n");
result.append(".globl main\n");
result.append(".type main, @function\n");
result.append("main:\n");
Perhatikan betapa membosankannya baris-baris tersebut. Anda bisa memakai library stringtemplate
untuk memudahkannya, tapi kita masih bisa bertahan untuk saat ini.
Kode penting berikutnya adalah membaca input. Di C ada fungsi scanf
, yang bisa digunakan untuk membaca aneka data (karakter, string, integer, double). Untuk membaca sebuah integer kita bisa memanggil scanf
dengan parameter "%d"
dan alamat variabel, misalnya scanf("%d", &sisi)
. Pemanggilan scanf
ini mirip dengan printf
(sudah dibahas di bagian 3).
Instruksi pushl $namavar
akan mempush alamat variabel ke stack, dan kemudian kita mempush alamat string "%d"
(yang sudah didefinisikan di atas dengan nama .scan_int_format
), lalu memanggil scanf
, dan membersihkan stack dengan instruksi popl
.
void compileInput( CommonTree expr) throws Exception {
String var = expr.getText();
checkVar(var);
result.append("pushl $"+var+"\n");
result.append("push $.scan_int_format\n");
result.append("call scanf\n");
result.append("popl %ebx\n");
result.append("popl %ebx\n");
}
Mencetak string
Mencetak sebuah integer sangat mudah, tapi mencetak string agak sulit. Kita perlu meletakkan sebuah string di alamat memori, lalu memberikan alamat memori itu ke fungsi printf
.
Kita bisa meletakkan string-string tersebut di mana saja, tapi untuk mudahnya, semua string diletakkan di bagian akhir assembly setelah kode program terakhir. Kita perlu mendeklarasikan sebuah string untuk penyimpanan sementara, nanti setelah semua kode program dihasilkan, kita tempelkan string ini di akhir kode program.
StringBuffer strings = new StringBuffer();
Di assembly semua string harus diberi nama (tepatnya diberi label alamat), jadi kita perlu memberikan nama untuk merujuk pada string tersebut. Supaya tidak bentrok, kita gunakan saja nama dengan format ._str_XX
dengan xx adalah nomor string. Setiap string kita beri nomor menaik. Kita gunakan counter global agar semua nomor sifatnya unik:
int strcounter = 0;
Sekarang perhatikan kode berikut untuk menghasilkan instruksi print
. Bagian ini merupakan bagian yang agak rumit dibanding bagian yang lain.
void compilePrint( CommonTree expr) throws Exception {
if (expr.getType()==BulatLexer.STRING) {
String s = expr.getText();
/*remove first ' and last ' from string*/
s = s.replaceAll("^'", "");
s = s.replaceAll("'$", "");
strcounter++;
strings.append("._str_"+strcounter+":\n");
strings.append(".string \""+s+"\"\n");
result.append("push $._str_"+strcounter+"\n");
result.append("push $.print_str_format\n");
result.append("call printf\n");
result.append("popl %ebx\n");
result.append("popl %ebx\n");
} else {
String var = expr.getText();
checkVar(var);
result.append("push "+var+"\n");
result.append("push $.print_int_format\n");
result.append("call printf\n");
result.append("popl %ebx\n");
result.append("popl %ebx\n");
}
}
Bagian pertama adalah penanganan untuk print 'string'
, di bagian kedua dalah untuk print var
. Untuk setiap string, kita hasilkan ID baru, lalu simpan di variabel strings
:
strcounter++;
strings.append("._str_"+strcounter+":\n");
strings.append(".string \""+s+"\"\n");
Kita panggil printf
seperti pada tutorial sebelumnya, tapi parameter kedua (yang dipush pertama, karena urutannya terbalik) adalah string yang baru saja kita buat tadi:
result.append("push $._str_"+strcounter+"\n");
result.append("push $.print_str_format\n");
result.append("call printf\n");
result.append("popl %ebx\n");
result.append("popl %ebx\n");
Untuk bagian print dengan variabel, kita menggunakan push var
, tanpa $
karena kita ingin nilainya yang dicetak, bukan alamatnya:
result.append("push "+var+"\n");
result.append("push $.print_int_format\n");
result.append("call printf\n");
result.append("popl %ebx\n");
result.append("popl %ebx\n");
Variabel dan Unary minus
Dibandingkan dengan tutorial No 3, dalam compileExpr, kita perlu menambahkan kasus untuk menangani identifier/variable. Ini relatif mudah:
if (expr.getType()==BulatLexer.ID) {
checkVar(expr.getText());
result.append("movl "+expr.getText()+", %eax\n");
result.append("pushl %eax\n");
return;
}
Dan kasus untuk Unary:
if (expr.getChildCount()==1) {
if (expr.getText().equals("+")) {
/*nothing changed*/
compileExpr((CommonTree)expr.getChild(0));
return;
}
if (expr.getText().equals("-")) {
compileExpr((CommonTree)expr.getChild(0));
result.append("popl %eax\n");
result.append("negl %eax\n");
result.append("pushl %eax\n");
return;
}
}
Untuk unary '+' tidak ada instruksi khusus yang dihasilkan. Untuk unary minus, instruksi assembly negl
digunakan, instruksi ini gunanya untuk menegatifkan suatu register.
Kelemahan, latihan
Perhatikan bahwa jika kita membuat variabel bernama main, maka program tidak berjalan dengan benar. Coba cari tahu apa saja hal-hal yang bisa membuat compiler tidak berjalan dengan benar karena adanya reserved word dalam bahasa target. Salah satu solusi adalah selalu memberi prefix tertentu pada variabel, misalnya prefix '_' sehingga tidak akan ada bentrok.
blog comments powered by Disqus
Copyright © 2009-2018 Yohanes Nugroho