Tutorial Membuat Interpreter dan Compiler (bagian 7)

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

Source code untuk artikel ini: expr4.zip

Sekarang kita akan masuk ke topik yang cukup rumit, yaitu masalah tipe data. Sebelumnya saya membuat bahasa sederhana yang hanya mendukung integer saja, kini saya akan menambahkan dukungan tipe data lain (double dan string). Wikipedia memiliki artikel yang cukup panjang mengenai Type system, bahkan ada teori formal yang disebut sebagai Type theory, tapi dalam tutorial kita hanya akan melihat dari sisi praktis saja. Dalam tutorial ini saya akan mengimplementasikan bahasa Delta, bahasa ini memiliki aneka tipe data (float, int, dan string).

Banyak cara dalam mengoperasikan tipe

Pertama, saya ingin memaparkan beberapa sifat bahasa pemrograman dalam menangani tipe. Bahasa tertentu memiliki pemeriksaan yang ketat, sedangkan bahasa lain memiliki pemeriksaan yang lemah. Beberapa bahasa mewajibkan deklarasi tipe (static typing), misalnya C dan Pascal, tapi ada juga bahasa yang tidak mewajibkan (dynamic typing), misalnya PHP dan Perl. Dalam tutorial ini kita akan menambahkan pemeriksaan tipe statik. Mengubah pemeriksaan statik menjadi pemeriksaan dinamik tidak sulit (kita hanya perlu menunda pemeriksaan tipe sampai ketika eksekusi dilakukan).

Rumitnya berurusan dengan banyak tipe adalah ketika terjadi operasi yang melibatkan tipe yang berbeda. Untuk memperjelas, mari kita lihat beberapa contoh perbedaan penangan tipe di beberapa bahasa.

Dalam hampir semua bahasa, jika kita menambah integer dengan floating point, maka hasilnya adalah floating point. Sifat dasar tersebut sepertinya cukup jelas dan masuk akal, tapi bisa menimbulkan masalah, misalnya jika kita memiliki tipe float 32 bit, dan tipe integer 32 bit:

/*contoh program dalam bahasa C*/
#include <stdio.h>

void hitung(int X)
{
    float a = 10.0;
    int b = 10;
    int c;
    int d;

    c = (b + a) - X;
    d = (b - X) + a;
    printf("c = %d d = %d\n", c, d);

}

int main()
{
    hitung(1);
    hitung(2);
    hitung(16777217);
    return 0;
}

Keluaran program tersebut adalah:

 c = 19 d = 19
 c = 18 d = 18
 c = -16777196 d = -16777197

Perhatikan baris terakhir, c tidak sama dengan d. Mengapa bisa begitu? dalam floating point 32 bit, representasi di atas 16777217 menjadi tidak eksak. Karena masalah automatic type coercion hal ini menimbulkan hasil yang tidak diharapkan (Unintended consequence).

Memahami batasan bit dalam tiap tipe bukan merupakan hal yang mudah, jadi saya akan memberikan contoh lain masalah penanganan tipe. Banyak bahasa memiliki perbedaan dalam penanganan String dan tipe lain.

Dalam Java, kita bisa menambah String dengan integer, dan hasilnya adalah string:

String s = "Hello " + 123;

atau int ditambah String hasilnya adalah String:

String x = 123 + "123";

Tapi jika kita melakukan integer + integer + string, maka integer akan dijumlahkan, lalu diubah ke string

String x = 12 + 123 + "123";

Tapi perhatikan bahwa kita tidak bisa mengalikan suatu string dengan integer.

Di PHP, string dan integer bisa ditambahkan, akan ada koersi (coercion, perubahan tipe secara implisit), dari string menjadi integer. Hasil program berikut ini adalah 3:

$a = 1 + "2";
echo $a;

Sifat Python berbeda dari Java dan PHP, misalnya kita tidak bisa menjumlahkan string dengan int, tapi bisa mengalikan string dengan int (hasilnya adalah string diulang n kali).

Di Pascal, semua hal yang saya sebutkan sebelumnya tidak diperbolehkan. Anda akan menemukan banyak sekali contoh-contoh lain dalam hal perbedaan penanganan tipe (yang saya sebutkan baru hal dasar yang ada di permukaan).

Hal penting yang bisa dipelajari dalam contoh-contoh di atas adalah: ada banyak aturan yang harus kita implementasikan dalam suatu bahasa ketika bahasa itu mendukung banyak tipe. Banyak buku compiler yang melewatkan hal ini, atau hanya memberikan contoh kecil saja. Di sini saya akan memberikan contoh yang cukup lengkap dalam hal penanganan tipe.

Perubahan Grammar

Kita akan mendukung tipe floating point, jadi kita perlu bisa menerima literal floating point.

FLOATING_POINT
    :   ('0'..'9')+ '.' ('0'..'9')* Exponent
    |   '.' ('0'..'9')+ Exponent 
    |   ('0'..'9')+ ( Exponent)
    ;

fragment
Exponent : ('e'|'E') ('+'|'-')? ('0'..'9')+ ;

Perhatikan kata kunci fragment, yang menyatakan bahwa Exponent hanyalah sebuah potongan yang tidak bisa berdiri sendiri, sehingga tidak akan dibuatkan node dalam tree. Dalam contoh ini, Exponent digunakan untuk mempersingkat dan memperjelas penulisan aturan untuk FLOATING_POINT.

Kita ubah literal string supaya lebih mirip C (menggunakan petik dua bukan tunggal):

STRING :    '\"' STRING_CONTENT '\"' ;

fragment
STRING_CONTENT :    
    ( EscapeSequence | ~('\\'|'"') )* ;

fragment
EscapeSequence
    :   '\\' ('b'|'t'|'n'|'f'|'r'|'\"'|'\''|'\\')
    ;

Perhatikan bahwa gramamar sudah mendukung escape character, tapi saat ini escape character diabaikan oleh interpreter.

Pernyataan input dan print dapat dipandang sebagai fungsi/prosedure. Untuk membuat program menjadi lebih jelas dan lebih general, kita akan membuang pernyataan input, dan print, dan menggantinya menjadi function_call. Dua fungsi baru juga akan diperkenalkan, yaitu sin dan cos.

function_call   : 
    built_in_functions '(' function_parameter ')' 
    -> ^(FUNCTION_CALL built_in_functions function_parameter)
    ;
    
function_parameter
    :
      expr (',' expr)* -> ^(FUNCTION_PARAMETER expr)
    ;   

built_in_functions
    :
    'print'
    | 'println'
    | 'input'
    | 'sin'
    | 'cos'
    ;

Tentunya bagian deklarasi juga harus diubah karena sekarang kita mendukung banyak tipe. Kita ingin bisa mendeklarasikan seperti ini:

 int a;
 float b,c;

Sehingga kita perlu mengubah rule deklarasi menjadi:

declarations
    :   
    declaration ';' (declaration ';')* 
    -> ^(DECLARATIONS declaration)
    ;

declaration
    : vartype ID ( ',' ID)*   -> ^(DECLARATION vartype ID);
        
vartype :   
    'int'
    | 'string'
    | 'float'
    ;

Representasi tipe dan aturan tipe

Tipe data direpresentasikan sebagai enum, saya mendefinisikan tipe VOID, STRING, INTEGER, dan FLOATING_POINT. Tipe VOID didefinisikan karena kegunaannya ada dua, pertama untuk menandai adanya kesalahan, dan sebagai tipe untuk fungsi yang tidak mengembalikan nilai (seperti print dan input).

public enum DataType  {
    VOID, STRING, INTEGER, FLOATING_POINT;
};

Kita memerlukan aturan, yang menentukan apa tipe hasil jika diberikan dua tipe, misalnya:

 FLOAT + INT -> FLOAT
 INT + FLOAT -> FLOAT
 INT = FLOAT -> Int (assigment float ke int hasilnya int)

Dalam bahasa lain, mungkin saja ada aturan seperti ini:

 STRING + INT -> STRING (Java)
 STRING * INT -> STRING (Python)

Bahasa Delta hanya akan memakai aturan sederhana: 1. String bisa ditambah dengan String menghasilkan String 2. Operasi int dan float akan menghasilkan float 3. Operasi perbandingan mengahasilkan integer (0 artinya false, dan selain itu true)

Metode yang akan kita pakai sederhana: kita memiliki tabel berupa operator, tipe operand kiri, dan tipe operand kanan, serta hasilnya. Jika kita memerlukan informasi, kita cukup melakukan pencarian pada tabel tersebut. Metode lain adalah dengan menggunakan banyak pernyataan if untuk menangani seluruh kemungkinan.

    class TypeCoercionRule {
        DataType left;
        DataType right;
        String operator;
        DataType result;        
    }

Aturan-aturan tersebut dibungkus dalam kelas TypeRule (kelas ini memiliki banyak TypeCoercionRule), detail isi kelas ini tidak penting, yang perlu diketahui hanyalah, kelas ini akan bisa memberi jawaban dari pertanyaan ini: Diberikan sebuah operator, tipe operand kiri, dan tipe operand kanan, apa tipe data hasil dari operasi tersebut? Hal ini dilakukan dengan memanggil:

TypeRule.getInstance().coerce(operator, lefttype, righttype);

Symbol Table

Dalam tutorial sebelumnya semua simbol adalah variabel, dan tipenya pasti integer. Dalam bahasa Delta, ada dua jenis simbol: variabel dan fungsi. setiap variabel harus punya nama, dan punya tipe, serta nilai. Sebuah fungsi (print, input, sin, dan cos) juga kita anggap sebagai sebuah simbol yang memiliki tipe kembalian (print dan input mengembalikan VOID, sedangkan sin dan cos mengembalikan tipe floating point).

static class Symbol {   
    private String name; /*name of symbol*/
    private DataType datatype; /*datatype of symbol*/
    private SymbolType symboltype; /*FUNCTION or VARIABLE*/
    private ValueObject value; /*value of symbol, only for VARIABLE*/
}

Kelas SymbolTable merupakan tabel yang memiliki banyak objek Symbol. Isi kelas SymbolTable juga tidak penting, tapi untuk mengambil dan menaruh nilai symbol, kita menggunakan kelas SymbolTable. Nilai simbol disimpan dalam tipe ValueObject yang akan dibahas setelah bagian pemeriksaan tipe.

Pemeriksaan tipe

Pemeriksaan tipe bisa dilakukan sebelum eksekusi dilakukan (misalnya C, Pascal) atau ketika eksekusi dilakukan (misalnya Python). Dalam tutorial ini, pemeriksaan dilakukan sebelum eksekusi dilakukan. Menurut saya pemeriksaan sebelum eksekusi akan memperjelas kode untuk keperluan tutorial, karena kode eksekusi tidak perlu lagi memeriksa tipe (tipe dijamin sudah benar).

Berikut ini bagian program utama (untuk mempersingkat, bagian inisialisasi parser sudah dihapus karena sama dengan tutorial-tutorial sebelumnya).

class Delta {

    public static void main(String argv[]) throws Exception {
           /* parser initialization removed */
        SymbolTable symtab = new SymbolTable();
        InternalFunctions functions = new InternalFunctions(symtab);
        DeltaTypeChecker tc = new DeltaTypeChecker(symtab);
        tc.eval(root);      
        DeltaInterpreter el = new DeltaInterpreter(symtab, 
                                functions);
        el.eval(root);      
    }
}

Pertama, tabel simbol dibuat, lalu fungsi-fungsi internal (print, input, dsb) didefinisikan (kelas InternalFunctions akan dibahas kemudian). Pemeriksaan tipe akan dilakukan oleh DeltaTypeChecker, jika ada tipe yang tidak valid (misalnya 1 + "string"), maka eksepsi akan dilemparkan. Jika lolos dari pemeriksaan tipe, interpreter akan dipanggil.

Kerangka kelas DeltaTypeChecker sama dengan interpreter, bedanya adalah: kita tidak mengoperasikan nilai, tapi mengoperasikan tipe. Berikut adalah kerangka kelas DeltaTypeChecker, Anda bisa membandingkannya dengan kerangka kelas interpreter, keduanya hampir sama.

class DeltaTypeChecker {

    SymbolTable symtab;

    void checkVar(String var) throws 
         InterpreterException  { /*..*/ }
    DataType evalFunctionCall(CommonTree expr) 
         throws InterpreterException { /*..*/ }
    DataType evalExpr(CommonTree expr) 
         throws InterpreterException { /*..*/ }
    void evalIf(CommonTree astmt) throws Exception { /*..*/ }
    void evalWhile(CommonTree astmt) throws Exception { /*..*/ }
    void evalDeclaration(CommonTree  decl) { /*..*/ }
    void evalDeclarations(CommonTree  decl) { /*..*/ }
    void evalAssignment(CommonTree astmt) throws Exception {/*..*/}
    void evalStatement(CommonTree expr) 
         throws Exception  { /*..*/ }
    void evalStatementList(CommonTree exprlist) 
         throws Exception  { /*..*/ }
    void eval(CommonTree root) throws Exception  { /*..*/ }
    public DeltaTypeChecker(SymbolTable _symtab) { /*..*/ }
}

Satu-satunya hal yang berpindah dari interpreter ke type checker adalah masalah deklarasi. Variabel akan dimasukkan ke symbol table di dalam DeltaTypeChecker, karena tanpa adanya tabel tersebut, pemeriksaan tipe variabel tidak bisa dilakukan.

    void evalDeclaration(CommonTree  decl) {
        CommonTree type = (CommonTree)decl.getChild(0);
        String typestr = type.getText();
        DataType datatype = DataType.VOID;
        ValueObject defaultval = null;
        if (typestr.equals("float")) {
            datatype = DataType.FLOATING_POINT;
        } else if (typestr.equals("string")) {
            datatype = DataType.STRING;
        } else if (typestr.equals("int")) {
            datatype = DataType.INTEGER;
        }

        for (int i = 1; i<decl.getChildCount(); i++) {
            CommonTree et = (CommonTree)decl.getChild(i);
            symtab.putVariable(et.getText(), datatype);
        }
    }

Berikut ini adalah bagian penting dari kelas DeltaTypeChecker, yaitu bagian evalExpr. Di bagian awal ada beberapa pernyataan if untuk tipe-tipe dasar.

    DataType evalExpr(CommonTree expr) 
         throws InterpreterException {

        /*handle basic types*/
        if (expr.getType()==DeltaLexer.INT) {
            return DataType.INTEGER;
        }
        if (expr.getType()==DeltaLexer.STRING) {
            return DataType.STRING;
        }
        if (expr.getType()==DeltaLexer.FLOATING_POINT) {
            return DataType.FLOATING_POINT;
        }
        if (expr.getType()==DeltaLexer.FUNCTION_CALL) {
            return evalFunctionCall(expr);
        }
        /*if it is a variable, get the type from symbol table*/
        if (expr.getType()==DeltaLexer.ID) {
            checkVar(expr.getText());
            return symtab.get(expr.getText()).getDataType();
        }

Untuk ID (variabel) dan FUNCTION_CALL, tipe didapatkan dari SymbolTable. Kita beralih sebentar ke method evalFunctionCall untuk melihat bagaimana pemeriksaan tipe untuk fungsi. Parameter untuk fungsi boleh berupa ekspresi (misalnya print(2*3, a+2)), jadi tipe untuk parameter perlu diperiksa juga.

    DataType evalFunctionCall(CommonTree expr) 
         throws InterpreterException {
        CommonTree functionname = (CommonTree)expr.getChild(0);
        CommonTree parameters = (CommonTree)expr.getChild(1);
        SymbolTable.Symbol f = symtab.get(functionname.getText());      
        /*check the type on parameters*/
        for (Object param:parameters.getChildren()) {
            evalExpr((CommonTree)param);
        }
        return f.getDataType();
    }

Mari kita teruskan lagi pembahasan method evalExpr. Setelah semua tipe sederhana diproses, berikutnya adalah, memanggil method secara rekursif untuk mendapatkan tipe anak kiri:

        CommonTree left = (CommonTree)expr.getChild(0);
        DataType lefttype = evalExpr(left);

        /*void types can not be operated with anything*/
        if (lefttype==DataType.VOID) {
            throw new InterpreterException("type mismatch");
        }

Jika tipe kiri adalah void, misalnya karena ekspresi print(1)+2, maka eksepsi akan dilemparkan. Kita tidak bisa mengoperasikan void dengan apapun. Untuk operator yang memiliki anak kanan, kita cek tipe anak kanan (operator unary tidak punya anak kanan). Tipe kanan mungkin juga bisa void, misalnya 1 + print(Z).

        DataType righttype =  DataType.VOID;
        if (expr.getChildCount()==2) {
            CommonTree right = (CommonTree)expr.getChild(1);
            righttype =  evalExpr(right);
            if (righttype==DataType.VOID) {
                throw new InterpreterException("type mismatch");
            }
        }         

Untuk tipe unary '+' dan '-', asalkan tipe anak bukan STRING, maka hasil tipenya adalah tipe anak pertama (-INT hasilnya adalah INT, -FLOAT hasilnya adalah FLOAT):

        if (operator.equals("+") || operator.equals("-")) {
            if (expr.getChildCount()==1) {
                if (lefttype==DataType.STRING) {
                    throw new InterpreterException("type mismatch");
                } else {
                    return lefttype;
                }
            }
        }

Dengan adanya kelas TypeRule, pemeriksaan tipe sisanya menjadi mudah, cukup konsultasikan pada tabel aturan tipe yang sudah dibahas sebelumnya.

return TypeRule.getInstance().coerce(operator, lefttype, righttype);

Penyimpanan Nilai

Bagaimana metode penyimpanan untuk tipe int, float, dan string agar semua bisa diakses secara seragam? Ada beberapa pendekatan:

  1. Anda bisa memiliki kelas/interface ValueType, dan memiliki turunan IntValue, DoubleValue, dan StringValue.
  2. Di C Anda bisa memakai union, di Pascal Anda bisa memakai variant record.
  3. Anda bisa memiliki satu kelas yang dapat menyimpan ketiga nilai tersebut, dan ada informasi mengenai tipe apa yang saat ini dimiliki.

Karena hanya pendekatan 1 dan 3 yang bisa dilakukan di Java, saya akan mengambil yang sederhana, yaitu nomor 3. Kita definisikan sebuah kelas, yang bisa menyimpan ketiga jenis nilai, dan tipe apa yang saat ini disimpan.

class ValueObject {
    private DataType type;
    private String strValue;
    private double floatValue;
    private int intValue;

    public ValueObject() {
        type = DataType.VOID;
    }
    
    public ValueObject( int ival) {     
        type = DataType.INTEGER;
        intValue = ival;
    }
    public ValueObject( double fval) {      
        type = DataType.FLOATING_POINT;
        floatValue = fval;
    }
    public ValueObject( String str) {       
        type = DataType.STRING;
        strValue = str;
    }
}

Kadang kita butuh melakukan koersi ketika melakukan suatu operasi (misalnya 1 + 1.0), hal ini dilakukan dengan method cast. Aturannya sederhana, jika tipe INT di cast ke FLOAT, kita cukup mengalikannya dengan 1.0, sebaliknya dari FLOAT ke INT, kita lakukan pembulatan.

ValueObject cast(DataType targettype) {
    /*the type is already correct*/
    if (type==targettype) {
        return this;
    }
    ValueObject result = null;
    /*cast integer to double*/
    if (targettype==DataType.FLOATING_POINT &&
        type==DataType.INTEGER) {
        result = new ValueObject(intValue*1.0);
    }
    /*cast double to integer, use rounding*/
    if (targettype==DataType.INTEGER &&
        type==DataType.FLOATING_POINT) {
        result = new ValueObject(Math.rint(floatValue));
    }
    return result;
}

Di tutorial-tutorial sebelumnya, Anda sudah melihat betapa mudahnya mengoperasikan integer, untuk menambah dua integer, kita cukup menggunakan '+', bagaimana jika kita ingin mengoperasikan ValueObject? kita harus mendefinisikan method-method tersebut.

Umumnya yang dilakukan dalam method adalah menyamakan kedua tipe kiri dan kanan, lalu melakukan operasi sesuai dengan tipenya. Misalnya untuk add:

ValueObject add(ValueObject other) throws InterpreterException {
    DataType result = TypeRule.getInstance().coerce("+", type,
                    other.type);
    ValueObject v1 = cast(result);
    ValueObject v2 = other.cast(result);
    switch (result) {
    case FLOATING_POINT:
        return new ValueObject(
            v1.floatValue + v2.floatValue);
    case INTEGER:
        return new ValueObject(
            v1.intValue + v2.intValue);
    case STRING:
        return new ValueObject(
            v1.strValue + v2.strValue);
    }
    return null;
}

Pertama kita dapatkan tipe apa yang harus dihasilkan oleh +, dari TypeRule kita bisa mendapatkan bahwa jika kiri dan kanan adalah string, maka hasilnya adalah string. Jika salah satu kiri atau kanan adalah float, maka hasilnya float. Sisa kelas ValueObject adalah method-method untuk operasi yang lain ('-', '/', '*', '%' dan komparasi).

Bagian komparasi perlu dibahas sedikit. Tipe hasil komparasi pasti selalu integer, kita tidak perlu memanggil TypeRule.getInstance().coerce untuk tahu hal itu, tapi kita memanggil coerce untuk meng-cast sisi kiri dan kanan. Misalnya untuk membandingkan int dengan float, kita perlu meng-cast int menjadi float sebelum membandingkan.

Perbandingan a dengan b selalu bisa dilakukan dengan a-b. Jika hasilnya adalah nol, maka a sama dengan b, jika hasilnya negatif, berarti a kurang dari b, dan jika positif berarti a lebih besar dari b.

ValueObject compare(String operation, ValueObject other) 
     throws InterpreterException{
    /* comparison is v1-v2 */
    DataType result = TypeRule.getInstance().coerce("-", type,
                    other.type);
    ValueObject v1 = cast(result);
    ValueObject v2 = other.cast(result);
    int compare_result = 0;

    switch (result) {
    case FLOATING_POINT:
        compare_result = Double.compare(v1.floatValue,
                        v2.floatValue);
        break;
    case INTEGER:
        compare_result = v1.intValue - v2.intValue;
        break;
    case STRING:
        compare_result = v1.strValue.compareTo(v2.strValue);
    }
    boolean bresult = false;
    if (operation.equals("==")) 
        bresult = (compare_result == 0);
    if (operation.equals(">=")) 
        bresult = (compare_result >= 0);
    if (operation.equals("<=")) 
        bresult = (compare_result <= 0);
    if (operation.equals("<")) 
        bresult = (compare_result < 0);
    if (operation.equals(">")) 
        bresult = (compare_result > 0);
    return new ValueObject(bresult?1:0);
}

Fungsi Internal

Agar kita bisa menambah fungsi internal dengan mudah, setiap fungsi internal dipisahkan ke sebuah kelas. Ada interface bernapa InternalFunction yang perlu diimplementasikan oleh kelas untuk fungsi tertentu.

import org.antlr.runtime.tree.CommonTree;

interface InternalFunction {

    DataType getDataType();
    String getName();
    ValueObject evaluate(DeltaInterpreter interpreter, 
            CommonTree params) throws InterpreterException;

}

Ada 3 method yang perlu diimplementasikan: 1. Method yang mengembalikan jenis tipe data yang dikembalikan fungsi 2. Method yang mengembalikan nama fungsi 3. Method aksi/evaluasi

Contoh untuk fungsi print adalah:

class PrintFunction implements InternalFunction {
    public DataType getDataType() {
        return DataType.VOID;
    }
    public String getName() {
        return "print";
    }
    
    public ValueObject evaluate(DeltaInterpreter interpreter, 
           CommonTree params) throws InterpreterException {
        for (Object p:params.getChildren()) {
            ValueObject v = interpreter.evalExpr((CommonTree)p);
            switch (v.getType()) {
            case FLOATING_POINT:
                System.out.printf("%.2f", v.getFloatValue());
                break;
            case INTEGER:
                System.out.print(v.getIntValue());
                break;
            case STRING:
                System.out.print(v.getStringValue());
                break;              
            case VOID:
                throw new InterpreterException("Void can not "+
                      " be printed");
            }
        }
        return ValueObject.VOID_VALUE;
    }
}

Dan untuk fungsi sin:

class SinFunction implements InternalFunction {

    public DataType getDataType() {
        return DataType.FLOATING_POINT;
    }
    public String getName() {
        return "sin";
    }
    
    public ValueObject evaluate(DeltaInterpreter interpreter, 
           CommonTree params) throws InterpreterException {     
        if (params.getChildCount()!=1) {
            throw new InterpreterException("Argument "+
                  "for 'sin' must be one");
        }
        ValueObject v = interpreter.evalExpr(
                  (CommonTree)params.getChild(0));
        switch (v.getType()) {
        case FLOATING_POINT:
            return new ValueObject(Math.sin(v.getFloatValue()));
        case INTEGER:
            return new ValueObject(Math.sin(v.getIntValue()));
        case VOID:
        case STRING:
            throw new nterpreterException("Argument "+
                  " for 'sin' must be int or float");
        }       
        return ValueObject.VOID_VALUE;
    }
}

Semua objek yang merepresentasikan fungsi internal dikemas dalam kelas InternalFunctions (perhatikan bentuk jamak):

class InternalFunctions {
    Hashtable<String, InternalFunction> functions =  
              new Hashtable<String, InternalFunction>();
    SymbolTable st;

    void addFunction(InternalFunction intfunc) {
        functions.put(intfunc.getName(), intfunc);
        st.put(intfunc.getName(), intfunc.getDataType(), 
                SymbolTable.SymbolType.FUNCTION);
    }

    InternalFunctions(SymbolTable _st) {
        st = _st;
        addFunction(new PrintFunction());
        addFunction(new PrintLnFunction());
        addFunction(new InputFunction());
        addFunction(new SinFunction());
        addFunction(new CosFunction());
    }

    ValueObject evaluate(DeltaInterpreter interpreter, 
            CommonTree expr) throws InterpreterException{
        CommonTree function = (CommonTree)expr.getChild(0);
        String fname = function.getText();
        CommonTree params = (CommonTree)expr.getChild(1);
        return functions.get(fname).evaluate(interpreter, params);
    }
}

Jika Anda ingin menambah fungsi baru, cukup tambahkan satu kelas baru yang isinya adalah fungsi Anda, tambahkan satu baris kode untuk menambahkan fungsi dalam hashtable, lalu modifikasi grammar untuk mengenali fungsi baru Anda. Sebagai catatan: Anda juga bisa memodifikasi grammar agar bisa menerima nama fungsi apapun. Hal ini akan dibahas di tutorial mendatang.

Eksekusi

Hal terakhir adalah eksekusi yang dilakukan dalam DeltaInterpreter. Sekitar 80% isi kelas masih bisa dipahami dari tutorial sebelumnya, jadi saya akan membahas satu method saja, yaitu evalExpr. Perbedaan utama dari tutorial sebelumnya adalah:

  1. Tipe int berubah menjadi tipe ValueObject
  2. Operasi yang tadinya dilakukan langsung (a+b) sekarang harus memanggil method di kelas ValueObject (a+b menjadi a.add(b))
ValueObject evalExpr(CommonTree expr) throws InterpreterException{

    CommonTree left = (CommonTree)expr.getChild(0);
    CommonTree  right = null;
    if (expr.getChildCount()==2) {
        right = (CommonTree)expr.getChild(1);
    }
    if (expr.getType()==DeltaLexer.INT) {
        return new  ValueObject(Integer.parseInt(expr.getText()));
    }
    if (expr.getType()==DeltaLexer.FLOATING_POINT) {
        return new  ValueObject(Double.parseDouble(expr.getText()));
    }
    if (expr.getType()==DeltaLexer.STRING) {
        String s = expr.getText();
        s = s.replaceAll("^\"", "");
        s = s.replaceAll("\"$", "");
        return new  ValueObject(s);
    }
    if (expr.getType()==DeltaLexer.ID) {
        SymbolTable.Symbol var = symtab.get(expr.getText());
        return var.getValue();
    }
    if (expr.getType()==DeltaLexer.FUNCTION_CALL) {
        return evalFunctionCall(expr);
    }

    String operator = expr.getText();

    String compare_operators[] = {"<", ">", "<=", ">=", "==", "!="};

    for (String op: compare_operators) {
        if (op.equals(operator)) {
            ValueObject vleft = evalExpr(left);
            ValueObject vright = evalExpr(right);
            return vleft.compare(op, vright);
        }
    }

    if (operator.equals("+")) {
        if (expr.getChildCount()==1) {
            return evalExpr(left);
        } else {
            return evalExpr(left).add(evalExpr(right));
        }
    }
    if (operator.equals("-")) {
        if (expr.getChildCount()==1) {
            return evalExpr(left).neg();
        } else {
            return evalExpr(left).sub(evalExpr(right));
        }
    }
    if (operator.equals("*")) {
        return evalExpr(left).mul(evalExpr(right));
    }
    if (operator.equals("%")) {
        return evalExpr(left).mod(evalExpr(right));
    }
    if (operator.equals("/")) {
        return evalExpr(left).div(evalExpr(right));
    }
    return new ValueObject(); /*fallback, should not happen*/
}

Record dan Array

Record dan array tidak akan diimplementasikan pada tutorial ini, namun saya bisa memberi petunjuk singkat.

Untuk contoh, asumsikan kita memiliki tipe record Point yang menyatakan koordinat polar dengan r bertipe integer dan theta adalah double. Pada SymbolTable, record akan memiliki jenis tipe khusus, di dalamnya akan ada hashtable yang memiliki key berupa nama fielddalam record (r dan theta) , dan setiap key memiliki value berupa informasi tipe untuk field tersebut (r -> int dan theta ->double) .

Nilai record dapat diimplementasikan pada ValueObject dengan hash table. Suatu string nama field mengacu pada suatu ValueObject (misal (r -> 10 dan theta ->90.0).

Operator akses field (misalnya . atau ->) pada record juga merupakan sebuah operator biasa (seperti +), yaitu diberikan sebuah variabel, dan sebuah field, hasilnya adalah tipe untuk field tersebut. Jadi kita perlu mendefinisikan method untuk melakukan pengecekan pada SymbolTable.

Untuk contoh, asumsikan kita punya array a dengan index 1..n, masing-masing array bertipe integer. Untuk implementasi array, Anda perlu informasi mengenai jumlah elemen array di SymbolTable dan tipe masing-masing elemen array. Perhatikan bahwa dalam kebanyakan bahasa, semua elemen array memiliki tipe yang seragam, tapi ada bahasa di mana elemen ke-x tipenya boleh berbeda dari elemen ke-y.

Di ValueObject, tipe Array dapat disimpan sebagai vector of ValueObject. Ketika mengakses nilai, perlu diperhatikan batas indeks array.

Dalam pengecekan tipe, operator akses array (operator [], misalnya a[i]) juga merupakan operator yang memiliki dua operand (array a dan index i). Tipe yang perlu dicek adalah: i harus integer, dan tipe untuk ekspresi tersebut adalah sesuai dengan deklarasi array a.

Penutup

Demikianlah pembahasan interpreter yang mendukung aneka tipe data. Dalam artikel ini saya menggunakan banyak file, jadi saya menambahkan file build.xml agar kompilasi bisa dilakukan dengan mudah menggunakan program ant. Silakan Anda ubah lokasi file jar ANTLR dalam build.xml sebelum mengkompilasi.

Latihan: buatlah fungsi untuk mengubah string menjadi integer dan/atau float, serta sebaliknya, dari int atau float menjadi string.

blog comments powered by Disqus

Copyright © 2009-2018 Yohanes Nugroho