Menciptakan Bahasa Dinamis Di Java: Part 4 – Memanggil Java Libraries

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

Pada artikel sebelumnya, saya memisahkan antara proses kompilasi (untuk menghasilkan file class) dan proses menjalankan output (hasil kompilasi). Tujuannya adalah agar perbedaan ini terlihat jelas: kode program perlu di-compile terlebih dahulu sebelum dijalankan. Akan tetapi dari sisi kepraktisan, menjalankan dua perintah yang berbeda adalah sesuatu yang merepotkan. Oleh sebab itu, kali ini saya akan mencoba menggabungkan kedua proses tersebut menjadi satu. Saya akan mengubah class com.wordpress.thesolidsnake.bahasaku.Main menjadi seperti berikut ini:

package com.wordpress.thesolidsnake.bahasaku;

import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuLexer;
import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser;
import com.wordpress.thesolidsnake.bahasaku.target.jvm.BahasaKuVisitor;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        if (args.length==0) {
            System.out.println("Tool ini dipakai untuk menjalankan program BahasaKu\n\n" +
                "Contoh penggunaan:\n" +
                "bahasaku namafilesource.baku\n" +
                "bahasaku source1.baku source2.baku\n" +
                "bahasaku -trace namafilesource.baku\n");
            return;
        }

        boolean enableTrace = false;
        List sourceFiles = new ArrayList();

        // Melakukan parsing argumen
        // Dapat melakukan kompilasi lebih dari satu file
        for (String arg: args) {
            if ("-trace".equals(arg)) {
                enableTrace = true;
            } else {
                sourceFiles.add(arg);
            }
        }

        // Memulai proses kompilasi
        for (String sourceFile: sourceFiles) {
            Path file = Paths.get(sourceFile);
            if (!file.toFile().exists()) {
                System.out.println("Tidak dapat menemukan file: [" + file + "]");
                continue;
            }

            try {

                // Front-end
                ANTLRInputStream antlrInputStream = new ANTLRInputStream(Files.newInputStream(file));
                BahasaKuLexer lexer = new BahasaKuLexer(antlrInputStream);
                CommonTokenStream commonTokenStream = new CommonTokenStream(lexer);
                BahasaKuParser parser = new BahasaKuParser(commonTokenStream);
                ParseTree parseTree = parser.file();

                // Back-end
                BahasaKuVisitor jvmVisitor = new BahasaKuVisitor(file.toFile(), enableTrace);
                jvmVisitor.visit(parseTree);
                jvmVisitor.generateOutput();

                // Menjalankan Program
                String namaFileLengkap = file.toFile().getName();
                String namaClass = namaFileLengkap.substring(0, namaFileLengkap.lastIndexOf('.'));
                ProcessBuilder pb = new ProcessBuilder(System.getProperty("java.home") + "/bin/java",
                    "-classpath",System.getProperty("java.class.path") + File.pathSeparatorChar + file.getParent().toAbsolutePath().toFile().toString(), namaClass);
                if (enableTrace) {
                    System.out.println("Mengerjakan " + pb.command());
                }
                pb.inheritIO();
                pb.start().waitFor();


            } catch (Exception ex) {
                System.out.println("Terjadi kesalahan [" + ex.getMessage() + "]");
            }
        }

    }

}

Agar lebih nyaman lagi, saya akan membuat sebuah kode program BahasaKu di lokasi proyek, misalnya di src\test\resources\Latihan.baku. Kemudian saya memodifikasi build.grade menjadi seperti berikut ini:

...
ext {
   ...
   sourceForRun = 'src/test/resources/Latihan.baku'
}
...
run {
    args sourceForRun
}

Hal ini akan menyebabkan setiap kali pemanggilan task run, Gradle akan menjalankan compiler BahasaKu dengan argumen berupa isi dari variabel sourceForRun, yaitu lokasi source code BahasaKu. Dengan demikian, mulai sekarang, saya dapat menguji compiler tanpa harus meninggalkan IntelliJ IDEA dan membuka command prompt lagi, seperti yang terlihat pada gambar berikut ini:

Menjalankan Program BahasaKu Langsung Dari IDEA

Menjalankan Program BahasaKu Langsung Dari IDEA

Selanjutnya, saya akan mengimplementasikan bagian yang membuat instance object baru. Pada BahasaKu, saya memakai keyword buat untuk membuat instance dari sebuah class (sama seperti new di bahasa Java/C). BahasaKu bukanlah bahasa yang OOP. Saya tidak dapat mendefinisikan class baru melalui BahasaKu tapi ia dapat mengakses dan memanggil method untuk class yang sudah ada. Java memiliki banyak kumpulan class siap pakai yang disebutnya sebagai Java Class Library (JCL) yang meliputi Swing (GUI), JDBC (database), collection, dan masih banyak lagi. Informasi mengenai class library yang disediakan oleh Java dapat dilihat di http://docs.oracle.com/javase/7/docs/technotes/guides/index.html#otherbase. Sebuah bahasa pemograman, walaupun dirancang dengan baik, tidak akan begitu berguna bila tidak memiliki dukungan library yang matang dan stabil. Programmer akan menghabiskan waktunya mengimplementasikan segala sesuatunya sendiri ketimbang berkonsentrasi pada permasalahan yang hendak diungkapkannya melalui kode! Menariknya, karena Java sudah berusia tua dan memiliki komunitas besar, ada banyak library lain yang dibuat oleh pihak ketiga (biasanya open-source) untuk menutupi kekurangan fitur yang tidak disediakan oleh Java. Sebagai contoh, library pihak ketiga yang umum dipakai untuk keperluan aplikasi bisnis adalah library untuk membuat, menampilkan dan mencetak laporan. Java secara bawaan tidak memiliki fasilitas yang berkaitan dengan laporan, tapi JasperReports menyediakan library untuk keperluan tersebut.

Untuk memungkinkan BahasaKu membuat instance baru dari sebuah class, saya akan men-override method visitBuatObject() sehingga terlihat seperti pada kode program berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    private static final Handle CONSTRUCTOR_HANDLE;

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

    ...

    @Override
    public Object visitBuatObject(@NotNull BahasaKuParser.BuatObjectContext ctx) {
        String namaClass = ctx.qualifiedName().getText();
        int jumlahArgumen = 0;
        if (ctx.arguments().exprList()!=null) {
            jumlahArgumen = ctx.arguments().exprList().expr().size();
        }
        mv.visitTypeInsn(NEW, namaClass.replace('.', '/'));
        mv.visitInsn(DUP);
        visit(ctx.arguments());
        mv.visitInvokeDynamicInsn("buat", MethodType.genericMethodType(jumlahArgumen).toMethodDescriptorString(),
            CONSTRUCTOR_HANDLE, namaClass);
        return null;
    }

    ...

}

Saya kembali memakai instruksi dynamicinvoke pada kode program di atas. Oleh sebab itu, saya perlu membuat kode program bootstrap yang akan menghubungkannya dengan sebuah call site. Saya segera membuat class com.wordpress.thesolidsnake.bahasaku.target.jvm.MethodCallBootstrap yang isinya seperti berikut ini:

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

import java.lang.invoke.*;
import java.lang.reflect.Constructor;
import java.util.Arrays;

public class MethodCallBootstrap {

    private static final MethodHandle CONSTRUCTOR;

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

    public static Class getClassForName(String namaClass) throws Error {
        try {
            return Class.forName(namaClass);
        } catch (ClassNotFoundException ex) {
            throw new Error("Class Java tidak ditemukan: [" + namaClass + "]");
        }
    }

    public static boolean periksaPrimitif(Class primitiveClass, Object arg) {
        if (primitiveClass==long.class) return Long.class.isInstance(arg);
        if (primitiveClass==int.class) return Integer.class.isInstance(arg);
        if (primitiveClass==boolean.class) return Boolean.class.isInstance(arg);
        return false;
    }

    public static boolean isParameterSesuai(Class[] parameterTypes, Object[] args) {
        if (parameterTypes.length != args.length) return false;
        for (int i=0; i<parameterTypes.length; i++) {
            if (parameterTypes[i].isPrimitive()) {
                if (!periksaPrimitif(parameterTypes[i], args[i])) return false;
            } else if (!parameterTypes[i].isInstance(args[i])) {
                return false;
            }
        }
        return true;
    }

    public static Object constructor(String namaClass, Object[] args) {
        Class cls = getClassForName(namaClass);
        for (Constructor c: cls.getConstructors()) {
            if (isParameterSesuai(c.getParameterTypes(), args)) {
                try {
                    return MethodHandles.lookup().unreflectConstructor(c).invokeWithArguments(args);
                } catch (Throwable ex) {
                    throw new RuntimeException("Gagal membuat instance baru: " + ex.getMessage());
                }
            }
        }
        throw new Error("Tidak menemukan constructor yang tepat untuk " +
            namaClass+ " dengan argumen " + Arrays.deepToString(args));
    }

    public static CallSite constructorBootstrap(MethodHandles.Lookup lookup, String name, MethodType type, String namaClass) throws Exception {
        return new ConstantCallSite(CONSTRUCTOR.bindTo(namaClass).asCollector(Object[].class, type.parameterCount()).asType(type));
    }
}

Sekarang, saya bisa mencoba membuat instance dari sebuah class Java dan menampilkan object tersebut di BahasaKu, seperti yang terlihat pada gambar berikut ini:

Membuat Instance Dari Class

Tentu saja bila hanya membuat instance dari class akan terasa tidak lengkap bila tidak diikuti dengan kemampuan untuk memanggil method pada instance tersebut. Oleh sebab itu, saya men-overide method visitPanggilMethod() dengan kode program seperti yang terlihat berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    private static final Handle METHODCALL_HANDLE;

    static {
        ...
        METHODCALL_HANDLE = new Handle(H_INVOKESTATIC,
            "com/wordpress/thesolidsnake/bahasaku/target/jvm/MethodCallBootstrap",
            "methodCallBootstrap",
            "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;)Ljava/lang/invoke/CallSite;I");
    }

    ...

    @Override
    public Object visitPanggilMethod(@NotNull BahasaKuParser.PanggilMethodContext ctx) {
        String namaMethod = ctx.IDENTIFIER().getText();
        int jumlahArgumen = 0;
        if (ctx.arguments().exprList()!=null) {
            jumlahArgumen = ctx.arguments().exprList().expr().size();
        }
        visit(ctx.expr());
        visit(ctx.arguments());
        int type = (ctx.op.getType() == BahasaKuParser.SELF)? 1: 0;
        mv.visitInvokeDynamicInsn("panggil", MethodType.genericMethodType(jumlahArgumen+1).toMethodDescriptorString(),
            METHODCALL_HANDLE, namaMethod, type);
        return null;
    }   

    ...    

}

Pada kode program di atas, karena saya menggunakan instruksi invokedynamic, maka saya perlu membuat kode program bootstrapnya. Method di atas akan menangani dua jenis pemanggilan method, yaitu yang melalui operator titik (.) dan operator sharp (#). Saya akan memakai class bootstrap yang sudah saya buat sebelumnya, yaitu com.wordpress.thesolidsnake.bahasaku.target.jvm.MethodCallBootstrap. Yang perlu saya lakukan adalah menambahkan method bootstrap methodCallBootstrap() seperti yang terlihat pada kode program berikut ini:

public class MethodCallBootstrap {

    ...

    private static final MethodHandle METHODCALL;
    private static final MethodHandle METHODCALL_RETURNSELF;

    static {
        try {
            ...
            METHODCALL = lookup.findStatic(MethodCallBootstrap.class, "methodCall",
                MethodType.methodType(Object.class, String.class, Object[].class));
            METHODCALL_RETURNSELF= lookup.findStatic(MethodCallBootstrap.class, "methodCallReturnSelf",
                MethodType.methodType(Object.class, String.class, Object[].class));
        } catch (Exception ex) {
            throw new Error("Terjadi kesalahan tak terduga [" + ex.getMessage() + "]");
        }
    }

    ...

    public static Object methodCall(String namaMethod, Object[] args) {
        Class cls = args[0].getClass();
        Object[] realArgs = Arrays.copyOfRange(args, 1, args.length);
        for (Method m: cls.getMethods()) {
            if (m.getName().equals(namaMethod) && isParameterSesuai(m.getParameterTypes(), realArgs)) {
                try {
                    MethodHandle currentMethod = MethodHandles.lookup().unreflect(m);
                    return currentMethod.bindTo(args[0]).invokeWithArguments(realArgs);
                } catch (Throwable ex) {
                    throw new RuntimeException("Gagal mengerjakan method: " + ex);
                }
            }
        }
        throw new RuntimeException("Tidak menemukan method " + namaMethod + " untuk object " +
            args[0]+ " (" + args[0].getClass() + ") dengan argumen " + Arrays.deepToString(args));
    }

    public static Object methodCallReturnSelf(String namaMethod, Object[] args) {
        methodCall(namaMethod, args);
        return args[0];
    }

    public static CallSite methodCallBootstrap(MethodHandles.Lookup lookup, String name, MethodType type,
           String namaMethod, int returnSelf) throws Exception {
        if (returnSelf==1) {
            return new ConstantCallSite(METHODCALL_RETURNSELF.bindTo(namaMethod).
                asCollector(Object[].class, type.parameterCount()).asType(type));
        } else {
            return new ConstantCallSite(METHODCALL.bindTo(namaMethod).
                asCollector(Object[].class, type.parameterCount()).asType(type));
        }
    }

}

Untuk menguji fungsi pemanggilan method, saya membuat kode program BahasaKu yang terlihat seperti pada gambar berikut ini:

Memanggil Method

Memanggil Method

Pada kode program di atas, saya menggunakan class java.util.ArrayList untuk menampung data dan class java.util.Random untuk menghasilkan bilangan acak. Ini adalah salah satu keuntungan membuat bahasa pemograman di platform Java, yaitu dapat memanfaatkan class library yang sudah ada.

Walaupun memakai class library yang sama seperti pada bahasa pemograman Java, sebuah bahasa baru di JVM bisa saja memberikan ‘sensasi’ yang berbeda. Sebagai contoh, BahasaKu memiliki operator # untuk pemanggilan method yang akan selalu mengembalikan object saat ini. Tujuannya adalah melakukan chaining (pemanggilan berantai). Ini adalah sesuatu yang tidak mungkin terjadi pada bahasa pemograman Java bila method tidak mengembalikan object yang dimaksud. Tapi, BahasaKu memungkinkan chaining terjadi tak peduli apapun hasil kembalian dari method, seperti yang terlihat pada kode program berikut ini:

list <- buat java.util.ArrayList()
random <- buat java.util.Random()
list#add(random.nextInt(10))#add(random.nextInt(10))#add(random.nextInt(10))
tampilkan 'Angka acak #1: ' + list.get(0)
tampilkan 'Angka acak #2: ' + list.get(1)
tampilkan 'Angka acak #3: ' + list.get(2)

Contoh lainnya, berikut ini adalah kode program BahasaKu yang menampilkan frame GUI:

Membuat GUI Dengan BahasaKu

Pada seri artikel ini, saya telah menunjukkan bahwa bahasa pemograman untuk platform Java (atau Java Virtual Machine) tidak harus selalu berupa bahasa pemograman Java (atau Java Programming Language). Seseorang bisa saja membuat bahasa pemograman baru yang berjalan di JVM. Hal ini semakin mudah dilakukan terutama bila dibantu oleh tools seperti ANTLR sebagai parser/lexer generator dan ASM sebagai bytecode generator. Pada sebuah situs, http://www.is-research.de/info/vmlanguages, terdaftar sekitar 300 bahasa untuk JVM (tentunya ini tidak semua bahasa tersebut akan menjadi mainstream).

Iklan

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.

Menciptakan Bahasa Dinamis Di Java: Part 2 – Membuat Back End Dengan ASM

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

Back-end di compiler bertugas menerjemahkan teks yang sudah dibaca oleh front-end menjadi target yang diinginkan. Biasanya, target adalah instruksi mesin yang dapat langsung dimengerti oleh CPU. Instruksi mesin tersebut harus dikemas dalam sebuah format file yang dimengerti oleh sistem operasi. Sebagai contoh, biarpun sama-sama memakai prosesor x86 yang sama dengan instruksi mesin yang sama, hasil kompilasi untuk platform Windows dan Linux akan berbeda. Pada sistem operasi Windows, instruksi mesin harus dikemas dalam format Portable Executable dimana pengguna lebih mengenalnya sebagai file EXE. Pada sistem operasi Unix (dan turunannya seperti Linux), instruksi mesin harus dikemas dalam format Executable and Linkable Format. Dengan adanya pemisahan compiler menjadi front-end dan back-end, sebuah front-end yang sama dapat dipakai pada beberapa back-end berbeda sehingga dapat mendukung beberapa platform yang berbeda.

BahasaKu tidak men-target instruksi CPU secara langsung, melainkan ke bytecode Java. Sebagai informasi, setiap hasil kompilasi program Java selalu berada dalam bentuk bytecode bukan instruksi mesin yang dimengeri CPU. Bytecode Java berperan sebagai bahasa perantara sehingga file yang sama dapat berjalan di beberapa platform berbeda. Referensi struktur file class Java dan daftar instruksi bytecode dapat ditemukan di Java Virtual Machine Specification. Secara garis besar, instruksi bytecode lebih sederhana dan lebih terbatas bila dibandingkan dengan instruksi CPU x86. Walaupun demikian, menulis kode program byte per byte (setiap instruksi diwakili oleh sebuah byte) tetap merupakan hal yang melelahkan dan rentan terhadap kesalahan.

Yup! Pada zaman sekarang, dimana sudah bermunculan banyak bahasa pemograman tingkat tinggi, menulis program byte per byte akan terlihat sulit dan aneh. Mungkin itu sebabnya pada zaman dahulu kala, tidak semua orang bisa menjadi programmer 🙂 Beruntungnya, di Java terdapat library ASM. Library ini menyediakan API untuk men-build bytecode secara user friendly dan juga dapat melakukan perhitungan ukuran stack frame secara otomatis. Saya segera mengubah file build.gradle saya menjadi seperti berikut ini (agar Gradle akan men-download ASM bagi saya):

...
dependencies {
    compile group: 'org.antlr', name: 'antlr4', version: '4.1'
    compile group: 'org.ow2.asm', name: 'asm-all', version: '4.2'
}
...

Satu hal yang sering dijumpai dalam bytecode Java adalah type descriptor yang berupa:

  • Z untuk tipe boolean
  • C untuk tipe char
  • B untuk tipe byte
  • S untuk tipe short
  • I untuk tipe int
  • F untuk tipe float
  • J untuk tipe long
  • D untuk tipe double
  • Lnama/package/namaClass; untuk sebuah object. Contoh: Ljava/lang/Object; atau Ljavax/swing/JFrame;
  • Awali dengan tanda [ untuk array. Contoh: int[] diwakili [I dan Object[] diwakili dengan [Ljava/lang/Object;
  • V untuk void

Method descriptor mewakili signature dari sebuah method. Sebagai contoh, signature untuk void main(String[] args) adalah ([Ljava/lang/String;)V. Contoh lainnya, signature untuk int tambah(int angka1, int angka2) adalah (II)I.

Agar code completion di IntelliJ IDEA dapat mengenali class dari ASM, jangan lupa men-klik tombol Refresh Gradle Project di window JetGradle seperti yang terlihat pada gambar berikut ini:

Memperbaharui dependency Gradle

Memperbaharui dependency Gradle

Saya akan mulai dengan membuat sebuah implementasi visitor yang diberi nama BahasaKuVisitor.java di package com.wordpress.thesolidsnake.bahasaku.target.jvm. Dengan menggunakan visitor design pattern, saya akan menelusuri parse tree yang telah dihasilkan oleh front-end. Karena JVM menerima unit kompilasi dalam bentuk class, maka langkah pertama yang saya lakukan adalah membuat sebuah class melalui bytecode, seperti yang terlihat pada kode program berikut ini:

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

import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuBaseVisitor;
import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser;
import org.antlr.v4.runtime.misc.NotNull;
import org.objectweb.asm.*;
import org.objectweb.asm.util.TraceClassVisitor;
import java.io.*;
import java.nio.file.*;
import static org.objectweb.asm.Opcodes.*;

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    private ClassWriter cw;
    private ClassVisitor cv;
    private MethodVisitor mv;
    private Label label0;
    private boolean enableTrace;
    private File sourceFile;
    private String namaSourceFile;

    public BahasaKuVisitor(File sourceFile) {
        this(sourceFile, false);
    }
    public BahasaKuVisitor(File sourceFile, boolean enableTrace) {
        this.sourceFile = sourceFile;
        if (!sourceFile.getName().endsWith(".baku")) {
            throw new RuntimeException("Nama source file [" + sourceFile.getName() + "] tidak diakhiri dengan .baku");
        }
        this.namaSourceFile = sourceFile.getName().substring(0, sourceFile.getName().lastIndexOf('.'));
        this.enableTrace = enableTrace;

        cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        if (enableTrace) {
            cv = new TraceClassVisitor(cw, new PrintWriter(System.out));
        } else {
            cv = cw;
        }

        // Membuat sebuah class public dengan nama (tanpa package) sesuai dengan namaFile
        cv.visit(V1_7, ACC_PUBLIC + ACC_SUPER, namaSourceFile, null, "java/lang/Object", null);

        // Membuat method static void main(String[] args).
        // Seluruh implementasi kode program BahasaKu akan ada di method ini.
        mv = cv.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
        mv.visitCode();
        label0 = new Label();
        mv.visitLabel(label0);
    }

}

Pada kode program di atas, saya menggunakan visitor dari ASM untuk membuat bytecode yang mewakili sebuah class. Lagi-lagi visitor design pattern seperti pada ANTLR. Karena proses menghasilkan bytecode sangat rentan terhadap kesalahan, saya menyediakan pilihan untuk memakai TraceClassVisitor yang akan akan mencetak instruksi yang dihasilkan ke layar. Fitur ini akan mempermudah saya untuk mencari kesalahan, tapi umumnya tidak akan begitu berguna bagi programmer yang memakainya.

Hasil kompilasi kode program BahasaKu adalah dalam bentuk sebuah file class (bytecode Java) dengan nama yang sama seperti nama source. Class ini hanya memiliki sebuah method, yaitu public static void main(String[] args) yang didalamnya berisi implementasi kode program BahasaKu. Sangat sederhana sekali, bukan?

Berikutnya, saya akan mulai menerjemahkan setiap grammar satu per satu. Saya akan mulai dari yang paling sederhana, yaitu ekspresi yang mewakili data angka, String dan boolean. Untuk itu saya perlu men-override method visitAngka(), visitString(), visitBooleanTrue() dan visitBooleanFalse() yang dihasilkan ANTLR, seperti pada contoh berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    @Override
    public Object visitAngka(@NotNull BahasaKuParser.AngkaContext ctx) {
        Integer angka = Integer.valueOf(ctx.getText());
        mv.visitTypeInsn(NEW, "java/lang/Integer");
        mv.visitInsn(DUP);
        mv.visitIntInsn(BIPUSH, angka);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Integer", "", "(I)V");
        return angka;
    }

    @Override
    public Object visitString(@NotNull BahasaKuParser.StringContext ctx) {
        String str = ctx.getText().substring(1, ctx.getText().length()-1);
        mv.visitLdcInsn(str);
        return str;
    }

    @Override
    public Object visitBooleanTrue(@NotNull BahasaKuParser.BooleanTrueContext ctx) {
        mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;");
        return Boolean.TRUE;
    }

    @Override
    public Object visitBooleanFalse(@NotNull BahasaKuParser.BooleanFalseContext ctx) {
        mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "FALSE", "Ljava/lang/Boolean;");
        return Boolean.FALSE;
    }

    ...

}

Seluruh tipe data pada BahasaKu adalah objek. Pada kode program di atas, saya menerjemahkan angka menjadi objek Integer. Demikian juga, literal iya akan diterjemahkan menjadi Boolean.TRUE dan literal tidak akan diterjemahkan menjadi Boolean.FALSE. Nilai mereka akan di-push ke dalam stack dan siap dipakai oleh perintah yang berada di level di atasnya pada syntax tree.

Berikutnya, saya akan membuat implementasi yang akan menerjemahkan keyword tampilkan menjadi pemanggilan System.out.println() seperti pada berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    @Override
    public Object visitTampilkan(@NotNull BahasaKuParser.TampilkanContext ctx) {
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        visit(ctx.expr());
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V");
        return null;
    }
    ...

}

Walaupun belum membuat implementasi semua rule, saya sudah tidak sabar untuk mencoba memakai BahasaKu. Oleh sebab itu, saya akan membuat sebuah method yang berguna untuk menulis file class (atau output dari compiler BahasaKu) yang terlihat seperti berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    public void generateOutput() {
        mv.visitInsn(RETURN);
        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();
        }
    }

    ...

}

Berikutnya, saya membuat sebuah file Main.java di package com.wordpress.thesolidsnake.bahasaku yang isinya seperti berikut ini:

package com.wordpress.thesolidsnake.bahasaku;

import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuLexer;
import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser;
import com.wordpress.thesolidsnake.bahasaku.target.jvm.BahasaKuVisitor;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        if (args.length==0) {
            System.out.println("Compiler BahasaKu\n\n" +
                "Contoh proses kompilasi:\n" +
                "bahasaku namafilesource.baku\n" +
                "bahasaku source1.baku source2.baku\n" +
                "bahasaku -trace namafilesource.baku\n");
            return;
        }

        boolean enableTrace = false;
        List sourceFiles = new ArrayList();

        // Melakukan parsing argumen
        // Dapat melakukan kompilasi lebih dari satu file
        for (String arg: args) {
            if ("-trace".equals(arg)) {
                enableTrace = true;
            } else {
                sourceFiles.add(arg);
            }
        }

        // Memulai proses kompilasi
        for (String sourceFile: sourceFiles) {
            Path file = Paths.get(sourceFile);
            if (!file.toFile().exists()) {
                System.out.println("Tidak dapat menemukan file: [" + file + "]");
                continue;
            }

            try {

                // Front-end
                ANTLRInputStream antlrInputStream = new ANTLRInputStream(Files.newInputStream(file));
                BahasaKuLexer lexer = new BahasaKuLexer(antlrInputStream);
                CommonTokenStream commonTokenStream = new CommonTokenStream(lexer);
                BahasaKuParser parser = new BahasaKuParser(commonTokenStream);
                ParseTree parseTree = parser.file();

                // Back-end
                BahasaKuVisitor jvmVisitor = new BahasaKuVisitor(file.toFile(), enableTrace);
                jvmVisitor.visit(parseTree);
                jvmVisitor.generateOutput();

            } catch (Exception ex) {
                System.out.println("Terjadi kesalahan [" + ex.getMessage() + "]");
                ex.printStackTrace();
            }
        }

    }

}

Class di atas akan mengerjakan lexer dan parser dari ANTLR (front-end) serta menghasilkan file class (back-end). Saya perlu memberi tahu Gradle bahwa class ini adalah class yang harus dijalankan pertama kali dengan menambahkan baris berikut ini pada build.gradle:

...
apply plugin: 'application'
mainClassName = 'com.wordpress.thesolidsnake.bahasaku.Main'
...

Setelah itu, klik tombol Refresh Gradle Project seperti yang ditunjukkan pada gambar 1. Kemudian, saya akan men-double click pada task installApp seperti yang diperlihatkan pada gambar berikut ini:

Menjalankan task installApp

Menjalankan task installApp

Gradle akan membuat folder install dimana di dalamnya terdapat folder bin dan lib, seperti yang terlihat pada gambar berikut ini:

Struktur Folder Yang Dihasilkan

Struktur Folder Yang Dihasilkan

Pada folder lib akan terdapat seluruh jar yang dibutuhkan. Sementara itu, pada folder lib tersedia launcher yang dapat dipakai untuk menjalankan proyek. Launcher Bahasaku.bat dipakai untuk platform Windows, sementara itu platform lainnya dapat memakai launcher Bahasaku.

Sekarang, saya akan mencoba sebuah membuat sebuah program BahasaKu. Sebagai contoh, saya akan membuat sebuah file dengan nama C:\contoh\Latihan.baku yang isinya seperti ini:

tampilkan 'Solid Snake'
tampilkan 10
tampilkan 20
tampilkan 'Percobaan'

Saya dapat men-compile file di atas dengan perintah seperti berikut ini:

Menjalankan Program Yang Memakai BahasaKu

Menjalankan Program Yang Memakai BahasaKu

Saya menjalankan compiler BahasaKu dengan menyertakan -trace supaya saya dapat melihat instruksi yang dihasilkan oleh ASM. Terlihat bahwa dari file Latihan.baku akan tercipta sebuah file Latihan.class. Output dalam bentuk bytecode Java ini dapat dikerjakan oleh JVM dengan menggunakan perintah java sama seperti program Java lainnya. Jadi, ini menunjukkan sebuah fakta bahwa sebuah program untuk Java tidak harus selalu dibuat dalam bahasa pemograman Java. Fakta menarik lainnya adalah karena output BahasaKu adalah sebuah bytecode dan class Java biasa, maka ia dapat dipanggil oleh program Java lainnya (yang memakai bahasa pemograman Java)! Ini memungkinkan terjadinya polyglot programming dimana programmer menggabungkan lebih dari satu bahasa pemograman di proyek yang sama.

Saat ini, compiler BahasaKu masih belum mendukung variabel dan operasi aritmatika. Oleh sebab itu, pada tulisan berikutnya, saya akan men-implementasi-kan rule yang berhubungan dengan variabel dan operator aritmatika.

Menciptakan Bahasa Dinamis Di Java: Part 1 – Membuat Front End Dengan ANTLR

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

Hari ini ada yang berbeda dengan editor blog saya: WordPress.com kini mendukung Markdown. Bila sebelumnya saya harus mengetik dengan menggunakan syntax HTML, kini saya dapat menggunakan syntax Markdown yang lebih nyaman dipakai untuk menulis artikel pemograman. Informasi mengenai fasilitas terbaru di WordPress.com ini dapat dibaca di http://en.support.wordpress.com/markdown/. Saya akan merayakannya dengan menulis artikel mengenai pembuatan bahasa pemograman dinamis yang berjalan di Java.

Saat orang-orang berbicara tentang Java, biasanya yang ada dalam benak mereka adalah bahasa pemograman Java. Tapi Java tidak hanya sekedar bahasa pemograman, ia juga adalah nama untuk sebuah mesin virtual (Java Virtual Machine). Pada awalnya memang hanya bahasa pemograman Java yang dapat dijalankan di Java Virtual Machine (JVM). Akan tetapi seiring dengan tren polyglot programming, lahirlah bahasa-bahasa lain untuk JVM seperti Groovy, Scala, JRuby, Jython, dan sebagainya. Sebagai perbandingan, platform .NET juga memiliki banyak bahasa seperti C# dan VB. Salah satu perbedaan utamanya adalah kebanyakan bahasa pemograman untuk Java diciptakan oleh komunitas (dan bersifat open-source), bukan oleh pihak Oracle selaku pemilik Java.

Kenapa mengembangkan bahasa baru yang berjalan di JVM? Salah satu keuntungannya adalah sifat multi-platform Java. Hasil kompilasi bahasa dapat berjalan di seluruh platform yang didukung oleh Java. Selain itu, bahasa baru yang berjalan di JVM dapat mengakses library Java yang sudah ada (mulai dari fungsi matematika, kalender, database, GUI, dsb) tanpa harus membuat semuanya dari awal.

Sebagai latihan, saya akan membuat sebuah bahasa dengan tipe data dinamis yang berjalan di Java (JVM). Bahasa ini tidak akan memiliki fitur yang lengkap karena tujuan utamanya adalah sebagai bahan pembelajaran. Saya akan menyebutnya sebagai BahasaKu.

Tidak seperti pada bahasa pemograman Java dimana seluruh variabel harus dideklarasikan dengan tipe data yang statis, variabel pada bahasa pemograman Bahasaku tidak memiliki tipe data yang statis dan tidak perlu dideklarasikan terlebih dahulu. BahasaKu mendukung empat jenis tipe data, yaitu angka, String, boolean (dengan nilai berupa iya dan tidak), dan object.

Berikut ini adalah contoh hasil rancangan BahasaKu:

  1. Tipe data String harus diapit kutip tunggal seperti 'contoh'.
  2. Sebuah object dapat dibuat dengan keyword buat, misalnya buat javax.swing.JFrame().
  3. Operator yang didukung adalah <- untuk assignment, . (titik) untuk mengakses elemen dari sebuah object, dan operator aritmatika (+, -, *, /).
  4. BahasaKu memiliki operator # yang akan mengabaikan hasil kembalian pemanggilan method dan selalu mengembalikan object yang sedang aktif.
  5. Perintah tampilkan dapat dipakai untuk mencetak sebuah ekspresi ke console.
  6. Masing-masing perintah harus dalam satu baris (tidak perlu ada tanda titik koma).

Contoh kode program BahasaKu akan terlihat seperti berikut ini:

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

Sampai disini, saya sudah berhasil merancang BahasaKu dengan fungsi yang sangat terbatas. Karena ini hanya sebuah latihan, saya tidak terlalu memikirkan kerancuan yang mungkin timbul pada grammar BahasaKu, seperti kombinasi syntax yang mungkin bermakna ganda. Ini dapat menimbulkan permasalahan sama seperti pada kesalahpahaman antar dua manusia karena kerancuan pada kata yang dipakai (sehingga beda persepsi).

Walaupun BahasaKu sudah selesai dirancang, ia tidak akan pernah bisa dipakai oleh programmer bila tidak ada compiler-nya. Sebuah compiler akan menerjemahkan teks yang ditulis oleh programmer menjadi instruksi yang dapat dimengerti oleh mesin. Compiler BahasaKu memiliki beberapa tugas yang secara garis besar adalah:

  1. Lexical analysis: mengubah tulisan yang diketik programmer menjadi token.
  2. Syntactic analysis: memastikan setiap token sesuai dengan aturan bahasa (syntax atau grammar).
  3. Code generation: menghasilkan bytecode yang dapat dijalankan oleh JVM.

Proses lexical analysis dan syntactic analysis sering disebut sebagai front-end. Sementara itu, proses code generation sering disebut sebagai back-end. Tidak peduli apa target akhir dari kompilasi, proses front-end selalu sama. Oleh sebab itu, saya akan mulai dengan membuat front-end terlebih dahulu.

Front-end terdiri atas lexer dan parser. Tugas lexer adalah mengubah deretan huruf dan angka yang diketik oleh programmer menjadi token (sama seperti seorang anak kecil yang merangkai huruf untuk membentuk kata). Contoh token pada bahasa yang saya buat di atas adalah tampilkan (sebuah keyword), <- (sebuah operator), 'contoh' (sebuah String), dan sebagainya. Langkah berikutnya adalah tugas parser yang akan merangkai token tersebut dalam struktur data yang disebut syntax tree atau parse tree (sama seperti seorang anak kecil yang belajar membaca kalimat yang terdiri atas beberapa kata dengan grammar seperti subjek + predikat + object).

Saya akan mulai dengan membuat sebuah proyek baru di IntelliJ IDEA dengan nama BahasaKu di lokasi C:\BahasaKu seperti yang terlihat pada gambar berikut ini:

Membuat Proyek Baru

Membuat Proyek Baru

Agar proyek lebih mudah dikelola, saya akan menggunakan [Gradle] (http://www.gradle.org) yang telah terpasang di sistem saya. Saya kemudian membuat sebuah file build.gradle di proyek dengan isi seperti berikut ini:

apply plugin: 'java'

repositories {
   mavenCentral()
}

dependencies {
   compile group: 'org.antlr', name: 'antlr4', version: '4.1'
}

Pada Gradle script di atas, terlihat bahwa proyek membutuhkan library ANTLR. Gradle akan men-download library ini secara otomatis bagi saya (oleh sebab itu saya perlu terhubung ke internet). Apa itu ANTLR? ANTLR (ANother Tool For Language Recognition) adalah sebuah lexer dan parser generator dengan input berupa file grammar. Penggunaan ANTLR akan sangat mempermudah dan mempersingkat pembuatan front-end compiler. Contoh proyek yang memakai ANTLR adalah Groovy, Jython, Hibernate (untuk query HQL), dan Twitter (untuk search query).

Untuk menjalankan proyek ini, yang perlu saya lakukan adalah memilih menu View, Tool Windows, JetGradle. Setelah itu saya perlu melakukan asosiasi dengan build.gradle di proyek saat ini, lalu memilih tab Tasks, kemudian men-double click pada build. Gradle juga mungkin akan men-download file yang dibutuhkannya dari internet belum belum tersedia di repository lokal, seperti yang terlihat pada gambar berikut ini:

Men-build Proyek

Men-build Proyek

Berikutnya, saya akan membuat sebuah file grammar ANTLR (yang isinya masih kosong) dengan nama BahasaKu.g4 di lokasi src/main/resources seperti yang terlihat pada gambar berikut ini:

Membuat file grammar

Membuat file grammar

Saya juga akan membuat sebuah contoh program yang memakai bahasa pemograman BahasaKu di lokasi src/test/resources dengan nama Contoh.baku yang isinya seperti berikut ini:

a <- 10
b <- 20
c <- a + b
tampilkan c
tampilkan c * 2 + a * 2 - b * 2
nama <- 'Solid Snake'
tampilkan nama
a <- iya
tampilkan a
b <- tidak
tampilkan b
acak <- buat java.util.Random()
tampilkan acak.nextInt()
f <- buat javax.swing.JFrame()
f.setSize(100,100)#setDefaultCloseOperation(3)#setVisible(iya)

Saat ini saya sudah dapat langsung mengetik isi file grammar di file BahasaKu.g4. Akan tetapi, agar prosesnya menjadi lebih nyaman, saya akan menggunakan sebuah IDE terpisah khusus untuk ANTLR, yaitu ANTLRWorks 2.1 yang dapat di-download di http://tunnelvisionlabs.com/products/demo/antlrworks. Tool berbasis NetBeans Platform ini menyediakan syntax highlightning, code completion dan kemudahan pengujian file grammar.

setelah men-download dan men-extract ANTLRWorks 2.1, saya dapat menjalankannya dengan men-double click file antlrworks2.exe di folder antlrworks2\bin. Kemudian, saya memilih menu File, Open, dan men-browse file BahasaKu.g4 yang ada di lokasi src/main/resources. Saya kemudian mengisi file grammar tersebut sehingga terlihat seperti berikut ini:

grammar BahasaKu;

//
// Parser Rules
//

file
    : (statement NEWLINE)* statement NEWLINE?
    ;

statement
    : tampilkan 
    | expr
    | assignment     
    ;

tampilkan
    : TAMPILKAN expr
    ;

assignment
    : IDENTIFIER ASSIGNMENT expr  
    ;

exprList
    : expr (KOMA expr)* 
    ;

arguments
    : KURUNG_BUKA exprList? KURUNG_TUTUP
    ;

expr
    :   expr op=(TITIK|SELF) IDENTIFIER arguments       # PanggilMethod    
    |   BUAT qualifiedName arguments                    # BuatObject
    |   expr KALI expr                                  # Perkalian
    |   expr BAGI expr                                  # Pembagian
    |   expr TAMBAH expr                                # Penjumlahan
    |   expr KURANG expr                                # Pengurangan    
    |   ANGKA                                           # Angka                                 
    |   BOOLEAN_TRUE                                    # BooleanTrue
    |   BOOLEAN_FALSE                                   # BooleanFalse
    |   STRING                                          # String
    |   IDENTIFIER                                      # Identifier                                                  
    ;

qualifiedName
    : IDENTIFIER ('.' IDENTIFIER)* 
    ;


//
// Lexer Rules
//

TAMPILKAN: 'tampilkan' ;

BUAT: 'buat' ;

BOOLEAN_TRUE: 'iya' ;

BOOLEAN_FALSE: 'tidak' ;

ASSIGNMENT: '<-' ;

KOMA: ',' ;

TITIK: '.' ;

SELF: '#' ;

TAMBAH: '+' ;

KURANG: '-' ;

KALI: '*' ;

BAGI: '/' ;

KURUNG_BUKA: '(' ;

KURUNG_TUTUP: ')' ;

STRING: '\'' .*? '\'' ;

IDENTIFIER: [a-zA-Z$_] [a-zA-Z0-9$_]* ;   

ANGKA:  [0-9]+ ;

NEWLINE:    '\r'? '\n' ;

WS  :   [ \t\r\n]+ -> skip ;

File grammar di atas mewakili aturan yang telah saya buat untuk BahasaKu sebelumnya. Terlihat bahwa saya menggabungkan rule untuk lexer dan parser pada file yang sama. Mereka dapat dipisah ke file yang berbeda untuk meningkatkan reusability.

Pada ANTLR, urutan definisi rule adalah hal yang penting. Sebagai contoh, di rule expr, aturan untuk perkalian dan pembagian muncul lebih dahulu sebelum penjumlahan dan pengurangan. Hal ini menyebabkan di back-end nanti, perkalian dan pembagian akan dikerjakan terlebih dahulu. Begitu juga untuk lexer, token yang lebih terspesialisasi seperti 'tampilkan' dan 'buat' harus muncul lebih dahulu sebelum token identifier (yang mewakili nama variabel dan nama method).

Rule seperti expr (yang mewakili ekspresi) dapat memiliki banyak alternatif. Oleh sebab itu, saya menggunakan label seperti # PanggilMethod, # BuatObject, dsb supaya ANTLR menghasilkan method terpisah (dengan nama yang sama sepeti pada label) sehingga mempermudah saya dalam membuat back-end untuk masing-masing jenis ekspresi tersebut.

Berikutnya, saya akan menguji apakah definisi grammar BahasaKu.g4 sesuai dengan contoh program BahasaKu yang sudah saya buat sebelumnya. Untuk itu, saya memilih menu Run, Run in TestRig…. Pada dialog yang muncul, saya men-browse ke lokasi contoh file program Contoh.baku, seperti yang terlihat pada gambar berikut ini:

Menguji Grammar

Setelah itu, saya men-klik tombol Finish. ANTLR akan menampilkan parse tree inspector seperti yang terlihat pada gambar berikut ini:

Visualisasi Parse Tree

Visualisasi Parse Tree

Bila tidak ada tulisan berwarna merah, maka itu berarti ANTLR dapat membaca source code BahasaKu dengan baik. ANTLR akan menyimpan hasil parsing-nya dalam bentuk struktur data tree atau lebih dikenal sebagai parse tree. Nantinya, di back-end, saya akan menelusuri tree ini dengan memakai pola visitor.

Mengapa ANTLR dan para pembuat compiler lebih senang menyimpan hasil parsing dalam bentuk tree? Mengapa tidak dalam array atau list, misalnya? Hal ini karena di back-end nanti, untuk menerjemahkan syntax menjadi instruksi mesin (atau bytecode Java), dibutuhkan navigasi ‘vertikal’ yang sangat sulit dicapai bila tidak menggunakan tree.

Langkah berikutnya adalah menghasilkan kode program parser dan lexer berdasarkan file grammar yang telah diuji sebelumnya. Saya memulai dengan memilih menu Run, Generate Recognizer…. Saya mengisi dialog yang muncul dengan informasi seperti berikut ini (bila lokasi output directory belum ada, saya perlu membuatnya terlebih dahulu):

Men-generate visitor berdasarkan grammar

Men-generate visitor berdasarkan grammar

Setelah itu, saya men-klik tombol Next. Kali ini saya dapat memilih untuk menghasilkan listener, visitor, atau keduanya. listener lebih mudah dipakai tetapi kurang fleksibel karena tidak dapat mengubah alur eksekusi. Bila memakai visitor, developer dapat mengubah atau mengabaikan alur eksekusi secara bebas. Secara umum, listener lebih tepat dipakai untuk membaca file source dengan alur yang pasti seperti membuat program yang menerjemahkan XML menjadi JSON (dan sebaliknya). Saya lebih sehingga menggunakan visitor sehingga saya memberikan tanda centang pada checkbox tersebut. Tidak lupa saya juga mengisi package untuk file yang dihasilkan seperti pada gambar berikut ini:

Men-generate visitor berdasarkan grammar

Men-generate visitor berdasarkan grammar

Setelah itu, saya men-klik tombol Next dan Finish.

Kembali ke project di IntelliJ IDEA, saya akan menemukan file baru dihasilkan oleh ANTLR seperti yang terlihat pada gambar berikut ini:

Kode Program Yang Di-Generate

Kode Program Yang Di-Generate

Sampai disini, front-end sudah selesai dibuat dan saya bisa melanjutkan ke tahap back-end.

Salah satu hal yang diperhatikan bila memakai ANTLR adalah setiap kali saya mengubah file grammar BahasaKu.g4, maka saya harus men-generate ulang visitor yang ada. Pada langkah di atas, saya melakukannya melalui ANTLRWorks. Sebuah solusi yang lebih otomatis adalah dengan menambahkan script pada build.gradle sehingga file akan digenerate secara otomatis setiap kali proyek dijalankan. Gradle memiliki plugin bawaan untuk ANTLR, namun sayangnya hanya mendukung hingga ANTLR 2. Oleh sebab itu, saya akan mengubah isi build.gradle menjadi seperti berikut ini:

apply plugin: 'java'

ext {
    antlrPackage = 'com.wordpress.thesolidsnake.bahasaku.grammar'
    antlrTargetDir = "src/main/java/${antlrPackage.replace('.','/')}"
    antlrGrammar = 'src/main/resources/BahasaKu.g4'
}

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'org.antlr', name: 'antlr4', version: '4.1'
}

task antlr4generate(type: JavaExec) {
    description 'Menghasilkan Visitors Dari ANTLR 4'
    main 'org.antlr.v4.Tool'
    classpath project.configurations.runtime
    args '-o', antlrTargetDir
    args '-no-listener'
    args '-visitor'
    args '-package', antlrPackage
    args antlrGrammar
}

compileJava {
    dependsOn antlr4generate
}

Sekarang, setiap kali saya men-compile file Java (menjalankan program), maka ANTLR akan menghasilkan file visitor terbaru untuk saya. Selain itu, saya juga dapat mengerjakan target antlr4generate secara manual seperti yang terlihat pada gambar berikut ini:

Men-generate Visitor ANTLR Langsung Dari IDE

Men-generate Visitor ANTLR Langsung Dari IDE