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