Tutorial Membuat Interpreter dan Compiler (bagian 9)

part 1 part 2 part 3 part 4 part 5 part 6 part 7 part 8 part 9

Source code untuk artikel ini: expr5.zip

Pada bagian ini saya akan membahas mengenai eksekusi berbasis byte code. Agar pembahasan bisa lebih jelas, saya tidak akan mempergunakan grammar yang mendukung banyak tipe. Untuk itu saya akan mundur menggunakan grammar Kasus yang hanya mendukung integer. Kita akan maju lagi menggunakan bahasa Delta yang mendukung float dan double ketika membahas virtual machine Parrot.

Definisi bytecode

Mari kita definisikan beberapa bytecode. Sebagian sifat instruksi-instruksi sudah dibahas di bagian sebelumnya. Misalnya add sudah dibahas, dan mul, div, mod, sub memiliki sifat yang sama. Instruksi cmpge dan perbandingan lainnya melakukan perbandingan antara dua nilai di stack, sama dengan yang ada pada tutorial sebelumnya.

Dalam tutorial sebelumnya, instruksi jump melompat ke alamat absolut, tapi dalam kasus ini saya akan menggunakan semantik bahwa jump melompat n byte dari instruksi saat ini. Anda akan mengerti mengapa ini dipilih ketika kita menghasilkan instruksi untuk if dan while.

Instruksi Kode Semantik
pushvar A 0 push isi variabel A ke stack
push number 1 push sebuah angka stack
pop A 2 pop isi stack ke variabel A
pushstr S 3 push sebuah string ke stack
add 4 jumlahkan dua nilai di stack
neg 5 negasikan nilai teratas di stack (X = -X)
sub 6 kurangkan dua nilai di stack
mul 7 kalikan dua nilai di stack
div 8 kalikan dua nilai di stack
mod 9 lakukan operasi mod dua nilai di stack
cmplt 10 lakukan pebandingan < (less than) terhadap dua nilai di stack
cmple 11 lakukan pebandingan <= (less than or equal) terhadap dua nilai di stack
cmpgt 12 lakukan pebandingan > (greater than) terhadap dua nilai di stack
cmpge 13 lakukan pebandingan >= (greater than or equal) terhadap dua nilai di stack
cmpeq 14 lakukan pebandingan == (equal) terhadap dua nilai di stack
cmpne 15 lakukan pebandingan != (not equal) terhadap dua nilai di stack
jump 16 lompat ke lokasi memori tertentu relatif terhadap posisi saat ini
jumpfalse 17 lompat ke lokasi memori tertentu relatif terhadap posisi saat ini jika isi top of stack adalah 0 (dianggap false). Jika tidak, lanjut ke instruksi berikut.
call 18 panggil suatu instruksi nomor N

Satu instruksi lagi yang penting adalah call yang akan memanggil fungsi nomor tertentu. Untuk saat ini kita hanya akan mendefinisikan fungsi berikut:

Nomor Fungsi
1 printvar
2 printstr
3 printline
4 input

Pemberian nomor untuk suatu fungsi ini umum digunakan di banyak sistem. Jika Anda pernah memprogram di DOS, ini seperti tabel fungsi untuk suatu interrupt. Jika Anda pernah memprogram assemblly di Linux, ini seperti nomor entry untuk syscall.

Semua konstanta instruksi dimasukkan dalam enum Instructions, sehingga mudah untuk dirujuk.

public enum Instruction {
    PUSHVAR(0), PUSH(1), POP(2), PUSHSTR(3), ADD(4), 
    NEG(5), SUB(6), MUL(7), DIV(8),  MOD (9), 
    CMPLT(10), CMPLE(11), CMPGT(12), 
    CMPGE(13), CMPEQ(14), CMPNE(15), JUMP (16), 
    JUMPFALSE(17), CALL(18), UNDEFINED(19);
    /*dan beberapa method*/
}

Representasi blok kode

Setiap ekspresi akan menghasilkan satu blok kode. Blok kode ini terdiri atas satu atau lebih instruksi. Sebuah blok direpresentasikan dalam kelas CodeBlock. Setiap baris kode bisa memiliki instruksi dan parameter dan ini direpresentasikan dalam kelas CodeLine. Method-method yang ada dalam CodeBlock meliputi addInstruction untuk menambah instruksi,getLength untuk mendapatkan jumlah instruksi dalam blok, getCode untuk mendapatkan instruksi ke i.

Untuk menyimpan dan meload bytecode, ada method write yang menulis ke DataOutputStream, dan load yang membaca dari DataInputStream;

public class CodeBlock {

    static class CodeLine {
        Instruction i;
        int param;
    }
    Vector<CodeLine> lines = new Vector<CodeLine>();

    void addInstruction(Instruction i, int param) {/**/}
    CodeLine getCode(int i) { /**/ }
    void append(CodeBlock next) { /**/ }
    int getLength() { /**/ }
    void write(DataOutputStream dos) {/**/}
    void load(DataInputStream dis) {/**/}
}

Menghasilkan bytecode

Sekarang saatnya menghasilkan bytecode. Pembuatan bytecode dilakukan oleh kelas KasusBC (BC = bytecode compiler). Kelas ini hasil modifikasi dari kelas KasusLang. Anda bisa melihat sekilas dari kerangka kelas ini bahwa semua method sekarang menghasilkan sebuah CodeBlock. Itu adalah modifikasi pertama: byte code compiler menghasilkan kode, jadi ada nilai kembalian berupa blok kode yang dihasilkan

public class KasusBC {
    CommonTree root;
    CodeBlock result;

    Hashtable<String, Integer> variables;
    Hashtable<String, Instruction> binary_inst_map;

    Vector<String> strings = new Vector<String>();

    KasusBC(CommonTree root) { }
    CodeBlock evaluateExpr(CommonTree expr) { }
    CodeBlock evaluateDeclaration(CommonTree  decl) { }
    CodeBlock evaluateInput(CommonTree expr)  { }
    CodeBlock evaluatePrint(int type, CommonTree expr)  {  }
    CodeBlock evaluateAssignment(CommonTree astmt)  { }
    CodeBlock evaluateIf(CommonTree astmt)  { }
    CodeBlock evaluateWhile(CommonTree astmt)  { }
    CodeBlock evaluateStatement(CommonTree expr) { }
    CodeBlock evaluateStatementList(CommonTree exprlist) { }

}

Untuk mempersingkat kode, deklarasi throws Exception tidak akan ditampilkan di artikel (tapi tentu saja tetap ada di source code).

Mari kita lihat mulai dari evaluateStatementList. Method ini jelas: untuk setiap baris statement, kumpulkan CodeBlock (gabung setiap bytecode) dan kembalikan hasilnya:

    CodeBlock evaluateStatementList(CommonTree exprlist) {
        CodeBlock cb = new CodeBlock();
        for (Object e: exprlist.getChildren()) {
            CodeBlock st = evaluateStatement((CommonTree)e);
            cb.append(st);
        }
        return cb;
    }

Method evaluateStatement juga sangat sederhana: kembalikan codeblock sesuai dengan pernyataan yang ditemui:

    CodeBlock evaluateStatement(CommonTree expr) {
        switch (expr.getType()) {
        case KasusLexer.DECLARATION:
            return evaluateDeclaration(expr);
        case KasusLexer.INPUT:
            return evaluateInput((CommonTree)expr.getChild(0));
        case KasusLexer.PRINT:
        case KasusLexer.PRINTLN:
            return evaluatePrint(expr.getType(), 
                   (CommonTree)expr.getChild(0));
        case KasusLexer.ASSIGNMENT:
            return evaluateAssignment(expr);
        case KasusLexer.IF_STATEMENT:
            return evaluateIf(expr);
        case KasusLexer.WHILE_STATEMENT:
            return evaluateWhile(expr);
        }
        return null; //should not happen
    }

Mari kita mulai dengan deklarasi. Sekarang kita tidak memerlukan tempat penyimpanan nilai variabel (itu dibutuhkan ketika eksekusi). Namun demikian, kita perlu bisa menerjemahkan nama variabel menjadi angka. Itu sebabnya hastable variables masih digunakan, tapi fungsinya berbeda. Fungsi variabel ini sekarang untuk menyimpan variabel X mendapat nomor berapa.

Hashtable<String, Integer> variables = new Hashtable<String, Integer>();

Kapan pengisian dilakukan? di bagian evaluateDeclaration. Caranya cukup dengan memberi nomor yang berurutan untuk setiap nama variabel. Method in merupakan satu-satunya yang mengembalikan CodeBlock kosong.

    CodeBlock evaluateDeclaration(CommonTree  decl) {
        int ctr = 0;
        for (Object e: decl.getChildren()) {
            CommonTree et = (CommonTree)e;
            variables.put(et.getText(), ctr++);
        }
        return new CodeBlock(); //empty
    }

Kita akan melewati dulu bagian INPUT, PRINT, dan PRINTLN dan masuk ke bagian ASSIGNMENT. Suatu assignment ke variabel 'X = expresi` akan memiliki bytecode seperti ini:

 //aneka ekspresi
 POP X

Namun X di sini harus digantikan dengan nomor variabel. Jadi untuk menghasilkan bytecode assignment, pertama hasilkan dulu bytecode untuk sisi kanan (sisi ekspresi), lalu tambahkan instruksi POP, dengan nomor variabel yang didapatkan dari hashtable variables.

    CodeBlock evaluateAssignment(CommonTree astmt) {
        CommonTree var = (CommonTree)astmt.getChild(0);
        checkVar(var.getText());
        CommonTree expr = (CommonTree)astmt.getChild(1);
        CodeBlock result = new CodeBlock();
        result.append(evaluateExpr(expr));
        result.addInstruction(Instruction.POP,
                      variables.get(var.getText()));       
        return result;
    }

Bagian yang paling panjang dalam pembuatan ekspresi adalah bagian evaluateExpr:

CodeBlock evaluateExpr(CommonTree expr) {
    CommonTree left = (CommonTree)expr.getChild(0);
    CommonTree  right = null;
    if (expr.getChildCount()==2) {
        right = (CommonTree)expr.getChild(1);
    }
    if (expr.getType()==KasusLexer.INT) {
        int v = Integer.parseInt(expr.getText());
        return new CodeBlock(Instruction.PUSH, v);
    }
    if (expr.getType()==KasusLexer.ID) {
        checkVar(expr.getText());
        return new CodeBlock(Instruction.PUSHVAR,
                 (int)variables.get(expr.getText()));
    }
    if (expr.getText().equals("+")) {
        if (expr.getChildCount()==1) {
            return evaluateExpr(left);
        }
    }
    if (expr.getText().equals("-")) {
        if (expr.getChildCount()==1) {
            CodeBlock e = evaluateExpr(left);
            e.addInstruction(Instruction.NEG, 0);
            return e;
        }
    }
    CodeBlock b1 = evaluateExpr(left);
    CodeBlock b2 = evaluateExpr(right);
    b1.append(b2);
    b1.addInstruction(binary_inst_map.get(expr.getText()), 0);
    return b1;
}

Sebuah literal integer, misalnya 17 akan dimasukkan ke stack:

 push 17

Ini ditangani di bagian

      if (expr.getType()==KasusLexer.INT) {
        int v = Integer.parseInt(expr.getText());
        return new CodeBlock(Instruction.PUSH, v);
    }

Sebuah variabel, misalnya A akan dimasukkan ke stack dengan PUSHVAR. Parameternya adalah nomor variabel:

pushvar 1

Ini ditangani di bagian:

    if (expr.getType()==KasusLexer.ID) {
        checkVar(expr.getText());
        return new CodeBlock(Instruction.PUSHVAR,
                     (int)variables.get(expr.getText()));
    }

Operator unary + tidak mengubah apapun. Operator unary - menghasilkan instruksi NEG:

    if (expr.getText().equals("-")) {
        if (expr.getChildCount()==1) {
            CodeBlock e = evaluateExpr(left);
            e.addInstruction(Instruction.NEG, 0);
            return e;
        }
    }

Semua instruksi binary ( +, -, dsb) diterjemahkan menjadi

 /*ekspresi 1*/
 /*ekspresi 2*/
 INSTRUKSI

Misalnya untuk +, instruksi adalah ADD:

 /*ekspresi 1*/
 /*ekspresi 2*/
 ADD

Karena semua bentuknya seragam, maka lebih mudah jika kita membuat hashtable yang menerjemahkan operator menjadi instruksi:

    binary_inst_map = new Hashtable<String, Instruction>();
    binary_inst_map.put("<",Instruction.CMPLT);        
    binary_inst_map.put(">",Instruction.CMPGT);        
    binary_inst_map.put(">=",Instruction.CMPGE);       
    binary_inst_map.put("<=",Instruction.CMPLE);       
    binary_inst_map.put("==",Instruction.CMPEQ);      
    binary_inst_map.put("!=",Instruction.CMPNE);      
    binary_inst_map.put("+",Instruction.ADD);     
    binary_inst_map.put("-",Instruction.SUB);     
    binary_inst_map.put("*",Instruction.MUL);     
    binary_inst_map.put("%",Instruction.MOD);     
    binary_inst_map.put("/",Instruction.DIV);     

Sehingga untuk semua instruksi sisa, dapat ditangani dengan:

    CodeBlock b1 = evaluateExpr(left);
    CodeBlock b2 = evaluateExpr(right);
    b1.append(b2);
    b1.addInstruction(binary_inst_map.get(expr.getText()), 0);
    return b1;

Kondisi

Sekarang kita masuk ke kode yang memproses if. Kode if, tanpa else akan memiliki struktur bytecode seperti ini:

   /*ekspresi kondisi*/
   /*jika tidak terpenuhi, lompat ke DONE*/
   JUMPFALSE DONE 
   /*instruksi bagian if*/


 DONE:
   /*instruksi lain setelah IF*/

Jadi ada instruksi JUMPFALSE yang melewati bagian yang hanya dieksekusi jika kondisi terpenuhi. Berapa panjangnya bagian ini bisa didapatkan dengan getLength. Di sinilah pemakaian instruksi relatif lebih mudah, kita ingin menyatakan: lompat maju N instruksi ke depan, tidak peduli berapa alamat instruksi saat ini.

Jika ada bagian else, maka sedikit lebih rumit:

   /*ekspresi kondisi*/
   /*jika tidak terpenuhi, lompat ke ELSE*/
   JUMPFALSE ELSE

   /*instruksi bagian if*/

   JUMP DONE
   ELSE:
   /*instruksi bagian else*/
   DONE:
   /*instruksi lain*/

Alur instruksi jika kondisi dipenuhi adalah: eksekusi bagian IF, lalu skip bagian else dengan instruksi JUMP. Jika kondisi tidak terpenuhi, lompat ke bagian ELSE.

Berikut ini adalah implementasi untuk penanganan kedua kasus tersebut. Perhatikan adanya tambahan +1, karena lompatan dilakukan N instruksi sejak instruksi ini (jadi instruksi JUMP dihitung 1 instruksi).

CodeBlock evaluateIf(CommonTree astmt)  {
    CommonTree expr = (CommonTree)astmt.getChild(0);
    CommonTree ifpart = (CommonTree)astmt.getChild(1);

    CodeBlock res = evaluateExpr(expr);
    CodeBlock ifblock = evaluateStatementList(ifpart);
    
    if (astmt.getChildCount()==2) { /*if*/
        res.addInstruction(Instruction.JUMPFALSE,
                   ifblock.getLength()+1);
        res.append(ifblock);
    } else { /*if else*/
        CommonTree elsepart = (CommonTree)astmt.getChild(2);
        CodeBlock elseblock = evaluateStatementList(elsepart);
        ifblock.addInstruction(Instruction.JUMP, 
                       elseblock.getLength()+1);            
        res.addInstruction(Instruction.JUMPFALSE,
                   ifblock.getLength()+1);
        res.append(ifblock);
        res.append(elseblock);
    }
    return res;
}

Loop While

Loop sebenarnya hanyalah if ditambah dengan JUMP, jadi

 while kondisi {
   BLOK
 }

Akan menjadi seperti ini dalam bytecode (BAGIAN A adalah kondisi, BAGIAN B adalah aksi):

 START:
     /* Ekspresi kondisi*/  (BAGIAN A)
     JUMPFALSE DONE /*jika sudah tidak terpenuhi, maka selesai*/
     /*BLOK instruksi*/     (BAGIAN B)
     JUMP START /*ulangi periksa kondisi*/
 DONE:

Kode berikut ini menghasilkan bytecode untuk while. Perhatikan bahwa instruksi JUMP START berarti kita melompat mundur, sehingga kita menggunakan nilai negatif.

CodeBlock evaluateWhile(CommonTree astmt)  {
    CommonTree expr = (CommonTree)astmt.getChild(0);
    CommonTree whilepart = (CommonTree)astmt.getChild(1); 
    CodeBlock result = evaluateExpr(expr); /*BAGIAN A*/

    /*BAGIAN B*/
    CodeBlock statements =  evaluateStatementList(whilepart); 

    result.addInstruction(Instruction.JUMPFALSE,
                   statements.getLength()+2);

    statements.addInstruction(Instruction.JUMP,
                  -(statements.getLength() + 
                   result.getLength()));
    result.append(statements);
    return result;
}

Satu hal lain yang perlu diperhatikan adalah: kita melompat sebanyak statements.getLength()+2, karena dalam kasus ini statements / BAGIAN B belum mengandung jump start. Karena instruksi jump done (jump ke akhir) sudah ditambahkan ke BAGIAN A, maka untuk jump start (ke bagian awal), offset start adalah panjang BAGIAN A + BAGIAN B.

Input dan Print

Input dan Print diimplementasikan dengan instruksi call. Karena hanya ada satu instruksi untuk mengisi sebuah variabel, yaitu pop, maka fungsi input akan memasukkan hasil pembacaan dari user ke stack, dan hasilnya akan dipop.

Secara umum input a akan menjadi

CALL INPUT
POP A
CodeBlock evaluateInput(CommonTree expr)  {
    String var = expr.getText();
    checkVar(var);
    CodeBlock result = new CodeBlock();
    result.addInstruction(Instruction.CALL,
                  Instruction.INPUT);
    result.addInstruction(Instruction.POP,
                  variables.get(var));
    return result;
}

Instruksi untuk print dibagi menjadi 3: printvar, printstr, dan printline.

  • Instruksi printvar akan mencetak integer di top register,
  • instruksi printstr akan mencetak string
  • Instruksi printline akan mencetak baris kosong

Mencetak variabel A dalam bytecode:

 push a
 call PRINTVAR

Untuk mencetak string, kita perlu mengubah string menjadi angka. Angka ini --seperti biasa-- saya buat berurutan. Setiap string baru disimpan di Vector<String> strings. Misalnya ingin mencetak "Hello World", maka string "Hello World" akan masuk ke tabel string, dan mendapat nomor. Misalnya nomor 1.

 pushstr 1
 call PRINTSTR

Jika kita memanggil versi PRINTLN, kita tambahkan instruksi:

 call PRINTLINE

Untuk menambah baris baru.

Berikut ini kode lengkap untuk evaluatePrint:

CodeBlock evaluatePrint(int type, CommonTree expr)  {
    CodeBlock result = new CodeBlock();
    if (expr.getType()==KasusLexer.STRING) {
        int stringnumber = strings.size();
        String s = expr.getText();
        /*remove first ' and last ' from string*/
        s = s.replaceAll("^'", "");
        s = s.replaceAll("'$", "");
        strings.add(s);
        result.addInstruction(Instruction.PUSHSTR,
                      stringnumber);
        result.addInstruction(Instruction.CALL,
                      Instruction.PRINTSTR);

    } else {
        String var = expr.getText();
        checkVar(var);
        result.addInstruction(Instruction.PUSHVAR,
                      variables.get(var));
        result.addInstruction(Instruction.CALL,
                      Instruction.PRINTVAR);
    }
    if (type==KasusLexer.PRINTLN) {
        result.addInstruction(Instruction.CALL,
                      Instruction.PRINTLINE);
    }
    return result;
}

Menuliskan hasil bytecode ke file

Untuk menuliskan hasil bytecode, saya menggunakan kelas DataOuputStream. Kelas ini memungkinkan kita menulis aneka tipe data Java ke file dengan mudah (dan bisa dibaca lagi dengan mudah menggunakan DataInputStream). Informasi yang dituliskan ke file adalah:

  • Jumlah variabel
  • Jumlah String
  • Daftar semua string
  • Bytecode

Perhatikan bahwa nama variabel tidak dituliskan ke file, karena nama variabel tidak penting dalam eksekusi. Dalam suatu implementasi kadang nama variabel juga dimasukkan ke file bytecode untuk keperluan debugging (misalnyasupaya mudah melacak apa nama variabel no 42).

void writeToFile(String filename)  {
    FileOutputStream fos = new FileOutputStream(filename);
    DataOutputStream dos = new DataOutputStream(fos);
    /*write number of variables*/
    dos.writeInt(variables.size());
    /*write number of strings*/
    dos.writeInt(strings.size());
    /*write all the strings*/
    for (String str: strings) {
        dos.writeUTF(str);
    }       
    result.write(dos);
    fos.close();
}

Sebagai pelengkap, kode method write pada kelas CodeBlock adalah:

void write(DataOutputStream dos)  {
    dos.writeInt(lines.size());
    for (CodeLine cl: lines) {
        dos.writeByte(cl.i.getInstNumber());
        dos.writeInt(cl.param);
    }
}

Interpreter

Sekarang bytecode sudah jadi, dan sudah tertulis di file. Berikutnya adalah menjalankan bytecode tersebut. Hal ini dilakukan oleh kelas Interpreter. Hal penting yang patut diperhatikan adalah: tidak seperti bagian compiler bytecode, kelas Interpreter ini tidak bergantung sama sekali pada ANTLR.

Berikut ini kerangka kelas Interpreter:

class Interpreter {
    int variables[];
    Vector<String> strings;
    CodeBlock cb;
    Scanner sc;
    Interpreter() {
        sc = new Scanner(System.in);
    }
    void load(String filename)  {}
    void call(int function,  Stack<Integer> s) {} 
    void run() { }
    public static void main(String argv[])  {}
}

Bagian main sangat singkat, buat instance Interpreter, load file, lalu jalankan.

public static void main(String argv[])  {
    if (argv.length!=1) {
        System.out.println("Usage: Interpreter <filename.bc>");
        return;
    }
    Interpreter i = new Interpreter();
    i.load(argv[0]);
    i.run();
}

Bagian load merupakan kebalikan dari bagian writeToFile yang ada di byte code compiler:

void load(String filename)   {
    FileInputStream fis = new FileInputStream(filename);
    DataInputStream dis = new DataInputStream(fis);

    /*prepare space for variable*/
    int varcount = dis.readInt();
    variables = new int[varcount];
    /*read the strings*/
    int stringcount = dis.readInt();
    strings = new Vector<String>();
    for (int i =0; i<stringcount; i++) {
        strings.add(dis.readUTF());
    }
    cb = new CodeBlock();
    cb.load(dis);
    fis.close();
}

Dan sebagai pelengkap, berikut ini kode method load di CodeBlock:

void load(DataInputStream dis)  {
    int size = dis.readInt();
    for (int i =0; i<size; i++) {
        Instruction ins = Instruction.getInstruction(dis.readByte());  
        int param = dis.readInt();
        addInstruction(ins, param);
    }
}

Sekarang masuk ke bagian jantung eksekusi. Ini hanya sebuah loop, di dalamnya ada sebuah switch (untuk masing-masing instruksi, seperti push, pop, dsb)

void run() {
    int ip, v1, v2;
    ip = 0;
    cb.print();
    Stack<Integer> s = new Stack<Integer>();
    while (ip<cb.getLength()) {
        CodeBlock.CodeLine cl = cb.getCode(ip);
        switch (cl.i) {
                case PUSHVAR: /*tangani pushvar*/
                 break;
                case POP: /*tangani pop*/
                 break;
               /*tangani aneka instruksi lain*/
               default:
               throw new Exception("undefined instruction");
        }
        ip++;
    }
}

Mari kita bahas satu per satu, masing-masing instruksi sangat singkat:

Instruksi pushvar akan menaruh nilai sebuah variabel ke dalam stack

    case PUSHVAR:
        s.push(variables[cl.param]);
        break;

Instruksi push akan menaruh sebuah nilai ke dalam stack

    case PUSH:
        s.push(cl.param);
        break;

Instruksi pop akan menaruh sebuah nilai top-of-stack ke dalam sebuah variabel

    case POP:
        v1 = s.pop();
        variables[cl.param] = v1;
        break;

Instruksi pushstr akan menaruh nomor string ke stack. Instruksi ini sebenarnya sama persis dengan push, tapi perbedaan namanya hanya memperjelas bahwa yang dipush adalah nomor string.

    case PUSHSTR:
        s.push(cl.param);
        break;

Instruksi add, sub, mul, div, dan mod semua memiliki format yang sama. Saya tidak akan menjelaskan satu per satu:

    case ADD:
        v1 = s.pop();
        v2 = s.pop();
        s.push(v1 + v2);
        break;
    case SUB:
        v1 = s.pop();
        v2 = s.pop();
        s.push(v2 - v1);
        break;
    case MUL:               
        v1 = s.pop();
        v2 = s.pop();
        s.push(v2 * v1);
        break;
    case DIV:
        v1 = s.pop();
        v2 = s.pop();
        s.push(v2 / v1);
        break;
    case MOD:
        v1 = s.pop();
        v2 = s.pop();
        s.push(v2 % v1);
        break;

Instruksi neg adalah satu-satunya aritmatika yang hanya memakai satu operand. Operasi yang dilakukan: pop isi stack, negatifkan, lalu push lagi.

    case NEG:
        v1 = s.pop();
        s.push(-v1);
        break;              

Semua instruksi perbandingan juga memiliki format yang sama: lakukan perbandingan, jika true, masukkan 1 ke stack, dan jika false masukkan 0 ke stack.

    case CMPLT:
        v1 = s.pop();
        v2 = s.pop();
        s.push((v2 < v1)?1:0);
        break;
    case CMPLE:
        v1 = s.pop();
        v2 = s.pop();
        s.push((v2 <= v1)?1:0);
        break;
    case CMPGT:
        v1 = s.pop();
        v2 = s.pop();
        s.push((v2 > v1)?1:0);
        break;
    case CMPGE:
        v1 = s.pop();
        v2 = s.pop();
        s.push((v2 >= v1)?1:0);
        break;
    case CMPEQ:
        v1 = s.pop();
        v2 = s.pop();
        s.push((v2 == v1)?1:0);
        break;
    case CMPNE:
        v1 = s.pop();
        v2 = s.pop();
        s.push((v2 != v1)?1:0);
        break;

Instruksi jump akan melompat ke suatu lokasi. Perhatikan bahwa ada pernyataan continue agar eksekusi dilanjutkan di alamat ip yang baru (dan tidak ditambah 1 lagi).

    case JUMP: 
        ip = ip + cl.param;
        continue;

Instruksi jumpfalse mirip dengan jump, tapi sesuai namanya, jump hanya akan dilakukan jika top-of-stack bernilai 0 (alias false):

case JUMPFALSE:
    v1 = s.pop();
    if (v1==0) {
        ip = ip + cl.param;
        continue;
    }

Instruksi terakhir adalah call untuk input, print, dan println.

    case CALL:
        call (cl.param, s);
        break;

Mari kita lihat isi method call:

void call(int function,  Stack<Integer> s) {
    int v, val;
    switch (function) {
    case Instruction.PRINTVAR:
        v = s.pop();
        System.out.print(v);
        break;
    case Instruction.PRINTSTR:
        v = s.pop();
        System.out.print(strings.elementAt(v));
        break;
    case Instruction.PRINTLINE:
        System.out.println();
        break;
    case Instruction.INPUT:
        v = sc.nextInt();
        s.push(v);
        break;
    }
}

Mari kita bahas satu persatu. Fungsi printvar akan mencetak isi top of stack.

    case Instruction.PRINTVAR:
        v = s.pop();
        System.out.print(v);
        break;

Fungsi printstr akan mencetak string. Kita perlu melakukan konversi nomor string menjadi string itu sendiri

    case Instruction.PRINTSTR:
        v = s.pop();
        System.out.print(strings.elementAt(v));
        break;

Fungsi printline hanya akan mencetak baris kosong.

    case Instruction.PRINTLINE:
        System.out.println();
        break;

Dan instruksi input akan meminta masukkan dari user dan memasukkan hasilnya ke stack.

    case Instruction.INPUT:
        v = sc.nextInt();
        s.push(v);
        break;

Penutup

Membuat bytecode sendiri ternyata sangat melelahkan, karena:

  • Kita harus mendefinisikan semua set instruksi
  • Kita harus mengimplementasikan sendiri virtual machine
  • Untuk membuat interpreter yang cepat, kita harus mengoptimasi sendiri byte code compiler, dan virtual machine
  • Kita akan mengalami kesulitan untuk mendebug hasil bytecode (harus menulis tools sendiri untuk memeriksa hasil bytecode)

Oleh karena itu, biasanya tidak ada alasan yang baik untuk membuat sendiri bytecode Anda. Mulai artikel berikutnya, saya akan beralih menggunakan bytecode yang matang.

blog comments powered by Disqus

Copyright © 2009-2018 Yohanes Nugroho