Apa itu Compiler?

Peta pipeline lengkap compiler dari kode sumber hingga eksekusi pada mesin sasaran.

Daftar isi

Compiler adalah program yang menerjemahkan kode sumber dari satu bahasa pemrograman ke representasi lain yang dapat disimpan dan dijalankan ulang tanpa compiler itu sendiri. Topik ini memetakan pipeline compiler dari teks sumber hingga eksekusi pada mesin sasaran, kemudian menunjukkan setiap tahap melalui skrip Python yang dijalankan di sandbox. Pembaca memperoleh peta kerja yang menjadi rujukan saat setiap tahap pipeline dibahas lebih mendalam.

Lingkup penerjemahan#

Compiler menerima masukan berupa kode sumber dalam bahasa sumber (source language) dan menghasilkan keluaran dalam bahasa sasaran (target language). Bahasa sumber dapat berupa Python, C, atau Rust, sedangkan bahasa sasaran dapat berupa kode mesin prosesor, bytecode untuk mesin virtual, atau bahkan bahasa tingkat tinggi lain. Yang membedakan compiler dari penerjemah jenis lain adalah keluarannya selalu berupa representasi sasaran yang dapat disimpan menjadi berkas dan dijalankan ulang tanpa compiler.

Pemisahan masukan dan keluaran ini mengarah pada pembagian front end dan back end. Front end mengurus bagian yang bergantung pada bahasa sumber, yaitu pemindaian, penguraian, dan analisis semantik. Back end mengurus bagian yang bergantung pada mesin sasaran, yaitu pembangkitan kode dan optimasi. Di antara keduanya terdapat representasi antara (IR, intermediate representation) yang menjadi jembatan netral terhadap kedua belah pihak.

Tahap-tahap pipeline#

Pipeline compiler secara umum terdiri atas enam tahap berurutan. Setiap tahap memiliki masukan dan keluaran yang jelas, dan kesalahan pada satu tahap dapat dilaporkan sebelum tahap berikutnya dijalankan. Diagram berikut memetakan alur keenam tahap beserta keluaran antar-tahapnya.

KodeSumberLexerParserAnalisisSemantikIRCodegenMesinSasaranteksderet tokenASTAST bertipeIRbytecodeeksekusi
Pipeline compiler dari kode sumber hingga mesin sasaran
  1. Lexing (scanning) memecah teks sumber menjadi deret token. Lexer mengelompokkan karakter menjadi unit leksikal seperti angka, identifier, dan operator.
  2. Parsing menyusun deret token menjadi pohon sintaks abstrak (AST). Parser memeriksa struktur gramatika dan melaporkan error sintaksis sebelum tahap berikutnya dijalankan.
  3. Analisis semantik memeriksa makna program di atas AST. Tahap ini mengisi tabel simbol, memeriksa tipe, dan memastikan setiap variabel dideklarasikan sebelum dipakai.
  4. Pembentukan IR menerjemahkan AST ke representasi antara yang lebih dekat ke mesin, biasanya berupa kode tiga alamat atau instruksi stack VM.
  5. Pembangkitan kode menerjemahkan IR ke bahasa sasaran, yaitu bytecode mesin virtual atau kode mesin native.
  6. Runtime menjalankan keluaran compiler pada mesin sasaran. Pada stack VM, runtime adalah interpreter yang membaca bytecode, sedangkan pada target native, runtime adalah prosesor yang menjalankan kode mesin secara langsung.

Contoh pipeline mini#

Pipeline tidak harus berupa program besar. Skrip Python berikut mengimplementasikan lima tahap pipeline untuk ekspresi aritmetika 1 + 2 * 3: lexing, parsing, anotasi tipe sebagai analog analisis semantik, codegen ke bytecode stack VM, dan eksekusi bytecode oleh VM. Pipeline ini dijalankan end-to-end dalam satu berkas dan keluarannya dapat diverifikasi.

python pipeline_kecil.py
"""Pipeline mini compiler: ekspresi aritmetika -> token -> AST -> bytecode -> hasil.

Pipeline ini memperlihatkan lima tahap pipeline compiler pada ekspresi
`1 + 2 * 3`: lexing, parsing, anotasi tipe (analog analisis semantik),
codegen ke bytecode stack VM, dan eksekusi bytecode oleh VM.
"""

# --- Lexer: teks -> deret token ---
def lex(s):
    toks = []
    i = 0
    while i < len(s):
        c = s[i]
        if c.isspace():
            i += 1
            continue
        if c.isdigit():
            j = i
            while j < len(s) and s[j].isdigit():
                j += 1
            toks.append(("NUM", int(s[i:j])))
            i = j
            continue
        if c == "+":
            toks.append(("PLUS", c))
        elif c == "*":
            toks.append(("MUL", c))
        elif c == "(":
            toks.append(("LP", c))
        elif c == ")":
            toks.append(("RP", c))
        else:
            raise SyntaxError(f"karakter tak dikenal: {c!r}")
        i += 1
    toks.append(("EOF", None))
    return toks


# --- Parser: deret token -> AST (recursive descent) ---
# expr  := term (("+" term))*
# term  := factor (("*" factor))*
# factor := NUM | "(" expr ")"
class Parser:
    def __init__(self, toks):
        self.toks = toks
        self.i = 0

    def peek(self):
        return self.toks[self.i]

    def eat(self, t):
        cur = self.toks[self.i]
        if cur[0] != t:
            raise SyntaxError(f"diharapkan {t}, dapat {cur[0]}")
        self.i += 1
        return cur

    def expr(self):
        node = self.term()
        while self.peek()[0] == "PLUS":
            self.eat("PLUS")
            node = ("add", node, self.term())
        return node

    def term(self):
        node = self.factor()
        while self.peek()[0] == "MUL":
            self.eat("MUL")
            node = ("mul", node, self.factor())
        return node

    def factor(self):
        cur = self.peek()
        if cur[0] == "NUM":
            self.eat("NUM")
            return ("num", cur[1])
        if cur[0] == "LP":
            self.eat("LP")
            node = self.expr()
            self.eat("RP")
            return node
        raise SyntaxError(f"diharapkan NUM atau (, dapat {cur[0]}")


# --- Analisis semantik ringan: anotasi tipe (semua int) ---
def annotate(node):
    if node[0] == "num":
        return ("num", node[1], "int")
    op = node[0]
    return (op, annotate(node[1]), annotate(node[2]), "int")


# --- Codegen: AST -> bytecode stack VM ---
def codegen(node):
    if node[0] == "num":
        return [("PUSH", node[1])]
    out = codegen(node[1]) + codegen(node[2])
    out.append(("ADD",) if node[0] == "add" else ("MUL",))
    return out


# --- VM: eksekusi bytecode ---
def vm(code):
    stk = []
    for ins in code:
        if ins[0] == "PUSH":
            stk.append(ins[1])
        elif ins[0] == "ADD":
            b, a = stk.pop(), stk.pop()
            stk.append(a + b)
        elif ins[0] == "MUL":
            b, a = stk.pop(), stk.pop()
            stk.append(a * b)
    return stk[-1]


if __name__ == "__main__":
    src = "1 + 2 * 3"
    print(f"sumber     : {src}")
    toks = lex(src)
    print(f"token      : {toks}")
    ast = Parser(toks).expr()
    print(f"AST        : {ast}")
    typed = annotate(ast)
    print(f"AST bertipe: {typed}")
    bc = codegen(ast)
    print(f"bytecode   : {bc}")
    print(f"hasil      : {vm(bc)}")
text Keluaran
sumber     : 1 + 2 * 3
token      : [('NUM', 1), ('PLUS', '+'), ('NUM', 2), ('MUL', '*'), ('NUM', 3), ('EOF', None)]
AST        : ('add', ('num', 1), ('mul', ('num', 2), ('num', 3)))
AST bertipe: ('add', ('num', 1, 'int'), ('mul', ('num', 2, 'int'), ('num', 3, 'int'), 'int'), 'int')
bytecode   : [('PUSH', 1), ('PUSH', 2), ('PUSH', 3), ('MUL',), ('ADD',)]
hasil      : 7

Setiap tahap menghasilkan struktur data yang menjadi masukan tahap berikutnya. Token adalah deret pasangan ("jenis", nilai). AST adalah pohon berlabel yang merepresentasikan struktur ekspresi. Bytecode adalah deret instruksi yang dibaca VM dari depan ke belakang. Hasil akhir adalah 7, yang sesuai dengan evaluasi 1 + (2 * 3) karena presedensi operator * lebih tinggi daripada +, sehingga operasi perkalian dikelompokkan lebih dahulu oleh parser.

Mengapa dipisah menjadi tahap#

Pemisahan pipeline menjadi tahap berurutan membawa tiga keuntungan praktis. Pertama, setiap tahap memiliki masukan dan keluaran yang dapat diuji secara terpisah, sehingga bug pada lexer tidak tercampur dengan bug pada parser. Kedua, front end dapat dipakai ulang untuk beberapa target, sehingga satu parser C dapat menghasilkan kode untuk x86, ARM, dan WebAssembly. Ketiga, optimasi dilakukan pada IR yang netral terhadap bahasa sumber dan mesin sasaran, sehingga satu pass optimasi berlaku untuk semua kombinasi sumber dan target.

Konsekuensi praktis dari pemisahan ini muncul pada pengetesan. Setiap tahap dapat diuji dengan masukan dan keluaran yang berbentuk data, sehingga tes lexer hanya membandingkan deret token, tes parser hanya membandingkan AST, dan tes codegen hanya membandingkan deret instruksi. Bila tes gagal, kesalahan dapat diisolasi pada tahap tertentu tanpa menjalankan ulang seluruh pipeline. Pemisahan inilah yang membuat compiler besar seperti LLVM dapat dikembangkan oleh banyak kontributor secara paralel tanpa saling mengganggu.

Kesalahan umum#

Beberapa kesalahan konseptual sering muncul saat pertama kali mempelajari pipeline. Pertama, menganggap compiler hanya menghasilkan kode mesin, padahal bytecode untuk mesin virtual juga merupakan keluaran compiler yang sah. Kedua, menyamakan parsing dengan kompilasi: parsing hanya menghasilkan AST, sedangkan kompilasi meliputi seluruh pipeline. Ketiga, menganggap optimasi adalah tahap opsional yang ditambah di akhir, padahal pada compiler produksi, analisis semantik dan pembentukan IR sudah mempertimbangkan informasi yang dibutuhkan pass optimasi berikutnya.

Kesalahan keempat yang sering muncul adalah menganggap runtime bukan bagian dari compiler. Padahal, pada compiler yang menargetkan mesin virtual, runtime adalah pasangan wajib dari compiler: compiler menghasilkan bytecode, dan runtime menjalankannya. Bahasa tanpa runtime yang stabil akan menghasilkan program yang benar secara sintaksis tetapi gagal saat dijalankan. Implementasi stack VM yang menjadi pasangan compiler dibangun pada modul pembangkitan kode.

Ringkasan#

  • Compiler menerjemahkan kode sumber ke bahasa sasaran yang dapat disimpan dan dijalankan ulang tanpa compiler.
  • Pipeline terdiri atas lexing, parsing, analisis semantik, IR, codegen, dan runtime, masing-masing dengan masukan dan keluaran yang jelas.
  • Pemisahan front end dan back end melalui IR memungkinkan satu parser dipakai untuk banyak target.
  • Pipeline mini untuk ekspresi aritmetika sudah cukup memperlihatkan setiap tahap dengan keluaran yang dapat diverifikasi.

Referensi#