Menciptakan Bahasa Dinamis Di Java: Part 3 – Membuat Operasi Aritmatika


Artikel ini merupakan bagian dari seri artikel tentang pembuatan bahasa dinamis BahasaKu yang berjalan di Java yang terdiri dari:

  1. Membuat Front End Dengan ANTLR
  2. Membuat Back End Dengan ASM
  3. Membuat Operasi Aritmatika
  4. Memanggil Java Libraries

Kode program untuk seri artikel ini dapat ditemukan di https://github.com/JockiHendry/BahasaKu

Kali ini saya akan membuat implementasi untuk rule assignment. Seperti biasa, saya akan men-override salah satu method visitor yaitu visitAssignment() seperti yang terlihat pada kode program berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    private List namaVariabel;

    ...

    @Override
    public Object visitAssignment(@NotNull BahasaKuParser.AssignmentContext ctx) {
        String nama= ctx.IDENTIFIER().getText();
        int index = namaVariabel.indexOf(nama) + 1;
        if (index <= 0) {
            namaVariabel.add(nama);
            index = namaVariabel.size();
        }
        visit(ctx.expr());
        mv.visitVarInsn(ASTORE, index);
        return null;
    }   

    ...

    public void generateOutput() {
        mv.visitInsn(RETURN);

    // PENAMBAHAN MULAI DISINI

        Label label = new Label();
        mv.visitLabel(label);
        for (int i=0; i<namaVariabel.size(); i++) {
            mv.visitLocalVariable(namaVariabel.get(i), "Ljava/lang/Object;",
                null, label0, label, i);
        }

    // PENAMBAHAN BERAKHIR DISINI

        mv.visitMaxs(0, 0);
        mv.visitEnd();
        cv.visitEnd();

        Path targetFile = Paths.get(sourceFile.getAbsoluteFile().getParent().toString(), namaSourceFile + ".class");
        System.out.println("Menghasilkan target file di lokasi: " + targetFile.toString() + "n");
        try {
            Files.write(targetFile, cw.toByteArray(),
                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
        } catch (Exception ex) {
            System.out.println("Terjadi kesalahan saat membuat file target!");
            ex.printStackTrace();
        }
    }


}

Pada bahasa pemograman Java, seluruh variabel harus dideklarasikan sebelum dipakai. BahasaKu mengambil pendekatan yang berbeda dimana ia tidak mensyaratkan deklarasi variabel. Setiap variabel di BahasaKu dapat langsung dipakai. Tentu saja ini membuat compiler harus bekerja lebih keras. Pada kode program di atas, saya melakukan pemeriksaan pada setiap assignment. Bila terdapat assignment dengan nama variabel yang belum terdaftar sebelumnya, maka saya akan menyimpan nama variabel baru tersebut. Pada akhirnya, saya tetap memiliki daftar nama seluruh variabel di kode program BahasaKu.

Walaupun kode program terasa lebih sederhana bila seluruh variabel tidak perlu dideklarasikan, ini menimbulkan kekurangan tersendiri. Sebagai contoh, pada kode program BahasaKu berikut ini, programmer melakukan salah ketik nama varibel:

nilai1 <- 10
total <- total + nilai1
nilai2 <- 20
totel <- total + nilai2
tampilkan total

Hasil dari kode program di atas tidak akan sesuai harapan karena compiler BahasaKu akan mendeklarasikan 2 variabel berbeda, yaitu total dan totel. Padahal, variabel totel adalah salah ketik dan yang dimaksud adalah total.

Lalu bagaimana dengan tipe data? Bahasa pemograman Java adalah bahasa pemograman yang statically typed dimana setiap variabel wajib dideklarasikan dengan tipe data tertentu. Sebaliknya, BahasaKu mengikuti perilaku pada bahasa pemograman yang dynamically typed dimana sebuah variabel tidak dibatasi pada tipe data yang pasti. Sebagai contoh, pada kode program berikut ini, variabel a boleh di-isi berulang kali dengan tipe data yang berbeda:

a <- 10
tampilkan a
a <- 'Solid Snake'
tampilkan a
a <- iya
tampilkan a

Untuk mendukung penggunaan tipe data berbeda pada sebuah variabel yang sama, maka compiler BahasaKu akan mendeklarasikan seluruh variabel dengan tipe java.lang.Object. Karena seluruh object (termasuk angka, String, dan boolean) diturunkan dari java.lang.Object, maka variabel dengan tipe java.lang.Object dapat menampung seluruh tipe data yang ada. Btw, mengapa di bahasa pemograman Java tidak mendeklarasikan seluruh variabel sebagai java.lang.Object untuk mencapai efek dinamis? Hal tersebut tidak akan begitu berguna karena bahasa pemograman Java bersifat statically typed sehingga untuk memakai variabel tersebut harus dilakukan casting dimana-mana.

Berikutnya, saya akan lanjut membuat kode program untuk menerjemahkan ekspresi yang terdiri atas nama variabel, seperti yang terlihat pada kode program berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    @Override
    public Object visitIdentifier(@NotNull BahasaKuParser.IdentifierContext ctx) {
        String nama = ctx.IDENTIFIER().getText();
        int index = namaVariabel.indexOf(nama) + 1;
        if (index <= 0) {
            throw new RuntimeException("Ada variabel yang tak dikenal, yaitu: [" + nama + "]");
        }
        mv.visitVarInsn(ALOAD, index);
        return nama;
    }

    ...

}

Pada kode program di atas, bila nama identifier tidak ditemukan pada daftar variabel yang sudah diidentifikasi, maka compiler akan memunculkan pesan kesalahan. Sebagai contoh, berikut ini adalah kode program yang salah (karena variabel nama belum di-isi saat akan di-cetak):

tampilkan nama
nama <- 'Solid Snake

Berikut ini adalah contoh pesan kesalahan yang dijumpai:

C:BahasakubuildinstallBahasakubin>bahasaku C:contohLatihan.baku
Terjadi kesalahan [Coba cek program-mu! Ada variabel yang tak dikenal, yaitu: [n
ama]]
java.lang.RuntimeException: Coba cek program-mu! Ada variabel yang tak dikenal,
yaitu: [nama]
        at com.wordpress.thesolidsnake.bahasaku.target.jvm.BahasaKuVisitor.visit
Identifier(BahasaKuVisitor.java:115)
        at com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser$Identifie
rContext.accept(BahasaKuParser.java:474)
        at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractPars
eTreeVisitor.java:44)
        at com.wordpress.thesolidsnake.bahasaku.target.jvm.BahasaKuVisitor.visit
Tampilkan(BahasaKuVisitor.java:92)
        at com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser$Tampilkan
Context.accept(BahasaKuParser.java:189)
        at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visitChildren(Abst
ractParseTreeVisitor.java:68)
        at com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuBaseVisitor.visi
tStatement(BahasaKuBaseVisitor.java:85)
        at com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser$Statement
Context.accept(BahasaKuParser.java:134)
        at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visitChildren(Abst
ractParseTreeVisitor.java:68)
        at com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuBaseVisitor.visi
tFile(BahasaKuBaseVisitor.java:117)
        at com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser$FileConte
xt.accept(BahasaKuParser.java:67)
        at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractPars
eTreeVisitor.java:44)
        at com.wordpress.thesolidsnake.bahasaku.Main.main(Main.java:60)

Langkah berikutnya adalah melakukan implementasi untuk visitor yang menghasilkan operasi aritmatika. Oleh sebab itu, saya membuat kode program seperti berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    @Override
    public Object visitPembagian(@NotNull BahasaKuParser.PembagianContext ctx) {
        visit(ctx.expr(0));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        visit(ctx.expr(1));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        mv.visitInsn(IDIV);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;");
        return null;
    }

    @Override
    public Object visitPerkalian(@NotNull BahasaKuParser.PerkalianContext ctx) {
        visit(ctx.expr(0));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        visit(ctx.expr(1));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        mv.visitInsn(IMUL);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;");
        return null;
    }

    @Override
    public Object visitPenjumlahan(@NotNull BahasaKuParser.PenjumlahanContext ctx) {
        visit(ctx.expr(0));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        visit(ctx.expr(1));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        mv.visitInsn(IADD);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;");
        return null;
    }

    @Override
    public Object visitPengurangan(@NotNull BahasaKuParser.PenguranganContext ctx) {
        visit(ctx.expr(0));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        visit(ctx.expr(1));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        mv.visitInsn(ISUB);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;");
        return null;
    }

    ...

}

Pada kode program di atas, saya tidak melakukan optimalisasi. Implementasi yang lebih optimal adalah memperhitungkan konstanta sehingga tidak harus selalu memanggil instruksi IDIV, IMUL, IADD, dan ISUB.

Saya akan mencoba operasi aritmatika dengan kode program berikut ini:

a <- 10
b <- 20
tampilkan a + b
c <- a * a - b * b
tampilkan c
c <- c / 100
tampilkan c
c <- c + 100
tampilkan c

Hasilnya akan terlihat seperti berikut ini:

C:contoh> java Latihan
30
-300
-3
97

Operasi aritmatika pada BahasaKu memprioritaskan perkalian dan pembagian (sama seperti pada matematika) berkat urutan yang didefinisikan di grammar ANTLR.

Walaupun bahasa yang dynamically typed seperti BahasaKu lebih mudah dan lebih nyaman dipakai, compiler akan sulit mencegah terjadinya kesalahan. Sebagai contoh, ekspresi a + b saat ini hanya valid jika baik nilai a dan nilai b adalah angka. Tapi karena tipe variabel pada BahasaKu bersifat dinamis, maka compiler tidak dapat menebak apakah a dan b adalah angka atau bukan bila program belum dijalankan. Dengan demikian, saya tidak dapat mencegah terjadinya kesalahan pada saat melakukan kompilasi program. Sebagai contoh, kode program berikut ini dapat di-compile dengan sukses:

a <- 10
b <- 20
tampilkan a + b
a <- 'Solid'
b <- 'Snake'
tampilkan a + b

Akan tetapi kesalahan akan terjadi pada saat program dijalankan:

C:contoh> java Latihan
30
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot
 be cast to java.lang.Integer
        at Latihan.main(Unknown Source)

Bahasa pemograman statically typed seperti C/C++ dan Java dapat mendeteksi kesalahan di atas pada saat kompilasi karena tipe sebuah variabel dapat ditentukan dengan melihat kode program. Anehnya, saya melihat kecenderungan pada mahasiswa pemula dimana mereka lebih tertarik pada bahasa dinamis dengan alasan lebih sedikit pesan kesalahan yang muncul. Bahkan saya pernah menemukan mahasiswa yang menyembunyikan (atau mengabaikan) puluhan pesan warning & error di PHP, berpura-pura bahwa semuanya baik-baik saja. Padahal, compiler selalu berusaha sebisa mungkin untuk mendeteksi kesalahan secara dini dan menginformasikannya pada programmer sebelum terjadi ‘kasus’.

Pada contoh di atas, terlihat bahwa saya tidak bisa menggabungkan String dengan menggunakan operator +. Tapi bukankah sebaiknya fitur ini ada? Oleh sebab itu, saya perlu mengimplementasikannya. Saya akan menggunakan fasilitas invokedynamic yang diperkenalkan sejak Java 7. Instruksi invokedynamic cukup membuat heboh karena instruksi ini sama sekali tidak berguna untuk bahasa pemograman Java (compiler javac tidak akan pernah menghasilkan instruksi invokedynamic!). Hanya pembuat bahasa untuk JVM yang memanfaatkan fitur ini. Penjelasan mengenai invokedynamic dapat dijumpai di http://docs.oracle.com/javase/7/docs/technotes/guides/vm/multiple-language-support.html#invokedynamic.

Untuk memakai invokedynamic, saya perlu membuat sebuah bootstrap method yang akan dipanggil saat JVM menjumpai instruksi invokedynamic tersebut. Sebagai contoh, saya membuat file OperatorBootstrap.java di package com.wordpress.thesolidsnake.bahasaku.target.jvm yang isinya seperti berikut ini:

package com.wordpress.thesolidsnake.bahasaku.target.jvm;

import java.lang.invoke.*;

public class OperatorBootstrap {

    private static final MethodHandle ARITMATIKA;

    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            ARITMATIKA = lookup.findStatic(OperatorBootstrap.class, "aritmatika",
                MethodType.methodType(Object.class, String.class, Object.class, Object.class));
        } catch (Exception ex) {
            throw new Error("Terjadi kesalahan tak terduga [" + ex.getMessage() + "]");
        }
    }

    public static Object aritmatika(String jenisOperator, Object arg1, Object arg2) throws Throwable {
        MethodHandle target;
        try {
            target = MethodHandles.lookup().findStatic(OperatorBootstrap.class, jenisOperator,
                MethodType.methodType(Object.class, arg1.getClass(), arg2.getClass()));
        } catch (Throwable e) {
            target = MethodHandles.lookup().findStatic(OperatorBootstrap.class, jenisOperator,
                MethodType.genericMethodType(2));
        }
        target.asType(MethodType.genericMethodType(2));
        return target.invokeWithArguments(arg1, arg2);
    }

    public static CallSite bootstrap(MethodHandles.Lookup lookup, String name, MethodType type) throws NoSuchMethodException, IllegalArgumentException {
        return new ConstantCallSite(ARITMATIKA.bindTo(name).asType(type));
    }

    public static Object tambah(Integer arg1, Integer arg2) {
        return arg1 + arg2;
    }

    public static Object tambah(String arg1, String arg2) {
        return arg1 + arg2;
    }

    public static Object tambah(Object arg1, Object arg2) {
        if (arg1.getClass()== String.class || arg2.getClass()==String.class) {
            return new StringBuilder().append(arg1).append(arg2);
        } else {
            throw new IllegalArgumentException("Operator tambah (+) tidak mendukung [" + arg1 + "] dan [" + arg2 + "]");
        }
    }

}

Kode program di atas akan memakai class di package java.lang.invoke seperti MethodHandles, MethodHandle dan MethodType untuk menentukan pemanggilan method. Penggunaan java.lang.invoke seharusnya lebih cepat bila dibandingkan dengan memakai reflection (sebelum Java 7, implementasi di atas hanya bisa dilakukan dengan melalui reflection!). Untuk meningkatkan kinerja, implementasi yang lebih baik dari di atas adalah dengan menggunakan inline caching.

Berikutnya, saya akan mengubah isi BahasakuVisitor.java dimana saya akan menggantikan implementasi aritmatika dengan pemanggilan instruksi invokedynamic seperti yang terlihat pada kode program berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    private static final Handle OPERATOR_HANDLE;

    static {
        OPERATOR_HANDLE = new Handle(H_INVOKESTATIC,
            "com/wordpress/thesolidsnake/bahasaku/target/jvm/OperatorBootstrap",
            "bootstrap",
            "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;");
    }

    ...

    @Override
    public Object visitPembagian(@NotNull BahasaKuParser.PembagianContext ctx) {
        visit(ctx.expr(0));
        visit(ctx.expr(1));
        mv.visitInvokeDynamicInsn("bagi", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", OPERATOR_HANDLE);
        return null;
    }

    @Override
    public Object visitPerkalian(@NotNull BahasaKuParser.PerkalianContext ctx) {
        visit(ctx.expr(0));
        visit(ctx.expr(1));
        mv.visitInvokeDynamicInsn("kali", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", OPERATOR_HANDLE);
        return null;
    }

    @Override
    public Object visitPenjumlahan(@NotNull BahasaKuParser.PenjumlahanContext ctx) {
        visit(ctx.expr(0));
        visit(ctx.expr(1));
        mv.visitInvokeDynamicInsn("tambah", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", OPERATOR_HANDLE);
        return null;
    }

    @Override
    public Object visitPengurangan(@NotNull BahasaKuParser.PenguranganContext ctx) {
        visit(ctx.expr(0));
        visit(ctx.expr(1));
        mv.visitInvokeDynamicInsn("kurang", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", OPERATOR_HANDLE);
        return null;
    }

    ...

}

Dampak penggunaan dari invokedynamic adalah kini pada saat menjalankan program, harus ada referensi ke library BahasaKu. Sebagai contoh, kini saya menjalankan sebuah program BahasaKu dengan cara seperti yang terlihat pada gambar berikut ini:

Menguji Operator Penjumlahan

Menguji Operator Penjumlahan

Berikutnya, saya akan melakukan implementasi untuk operator -. Khusus untuk ekspresi String, operator - akan menghilangkan bagian String lainnya. Saya juga membuat beberapa kombinasi lainnya, seperti yang terlihat pada kode program berikut ini (di OperatorBootStrap.java):

public class OperatorBootstrap {

    ...

    public static Object kurang(String arg1, String arg2) {
        return arg1.replace(arg2, "");
    }

    public static Object kurang(String arg1, Integer arg2) {
        return arg1.substring(0, arg1.length() - arg2);
    }

    public static Object kurang (Object arg1, Object arg2) {
        throw new IllegalArgumentException("Operator kurang (-) tidak mendukung [" + arg1 + "] dan [" + arg2 + "]");
    }

}

Saya akan mencoba operator tersebut, seperti yang terlihat pada gambar berikut ini:

Menguji Operator Pengurangan

Menguji Operator Pengurangan

Pada gambar di atas, operator - pada BahasaKu memiliki arti yang spesial untuk String. Ekspresi seperti 'Latihan.txt' - '.txt' memiliki arti berupa hilangkan '.txt' dari 'Latihan.txt'. Bila ingin menghilangkan 5 huruf terakhir dari sebuah String, saya dapat menggunakan ekspresi seperti 'Latihan' - 5. Sebagai alternatif lain, saya juga bisa membuat implementasi seperti pada bahasa dinamis lain dimana String akan dikonversi menjadi bilangan sehingga operator - tetap melakukan operasi matematika.

Berikutnya, saya akan melakukan implementasi untuk operator * (perkalian) yang terlihat seperti berikut ini:

public class OperatorBootstrap {

    ...

    public static Object kali(Integer arg1, Integer arg2) {
        return arg1 * arg2;
    }

    public static Object kali(String arg1, Integer arg2) {
        StringBuilder builder = new StringBuilder(arg1);
        for (int i=1; i<arg2; i++) {
            builder.append(arg1);
        }
        return builder.toString();
    }

    public static Object kali (Object arg1, Object arg2) {
        throw new IllegalArgumentException("Operator kali (*) tidak mendukung [" + arg1 + "] dan [" + arg2 + "]");
    }

}

Saya dapat mencoba operator tersebut, seperti yang terlihat pada gambar berikut ini:

Menguji Operator Perkalian

Pada gambar di atas, terlihat bahwa penggunaan operator * untuk sebuah String dan angka akan menyebabkan pengulangan String tersebut sebanyak angka. Sebagai contoh, ekspresi '='*20 akan menghasilkan '=' sebanyak 20 karakter.

Terakhir, saya akan melakukan implementasi untuk operator / (pembagian) yang terlihat seperti berikut ini:

public class OperatorBootstrap {

    ...

    public static Object bagi(Integer arg1, Integer arg2) {
        return arg1 / arg2;
    }

    public static Object bagi(Object arg1, Object arg2) {
        throw new IllegalArgumentException("Operator bagi (/) tidak mendukung [" + arg1 + "] dan [" + arg2 + "]");
    }

}

Tidak ada yang spesial pada operator / karena ia hanya melakukan operasi pada angka dan akan menolak operasi pada String, seperti yang diperlihatkan pada gambar berikut ini:

Menguji Operator Pembagian

Menguji Operator Pembagian

Akhirnya, BahasaKu sudah memiliki dukungan operasi aritmatika sederhana pada tipe data terbatas yang dimilikinya.

Perihal Solid Snake
I'm nothing...

3 Responses to Menciptakan Bahasa Dinamis Di Java: Part 3 – Membuat Operasi Aritmatika

  1. Ping-balik: Menciptakan Bahasa Dinamis Di Java: Part 4 – Memanggil Java Libraries | The Solid Snake

  2. Ping-balik: Menciptakan Bahasa Dinamis Di Java: Part 2 – Membuat Back End Dengan ASM | The Solid Snake

  3. Ping-balik: Menciptakan Bahasa Dinamis Di Java: Part 1 – Membuat Front End Dengan ANTLR | The Solid Snake

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: