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.

Perihal Solid Snake
I'm nothing...

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

  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 3 – Membuat Operasi Aritmatika | 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: