Topik ini menetapkan set instruksi untuk mesin sasaran berbasis stack yang menjadi sasaran codegen sepanjang modul ini. Pembaca memperoleh enumerasi opcode sebagai IntEnum, tabel dokumentasi yang menyatakan stack effect dan semantik tiap opcode, serta konvensi penyandian bytecode sebagai list[tuple[str, ...]]. Setelah membaca topik ini, pembaca dapat menyebutkan seluruh opcode stack VM, menjelaskan perbedaan instruksi aritmetika, komparasi, dan kontrol alur, serta membaca bytecode yang dihasilkan codegen.
Stack VM sebagai sasaran codegen#
Pilihan sasaran codegen menentukan bentuk keluaran back end. Pada jalur ini, codegen menargetkan mesin sasaran berbasis stack, yaitu mesin yang menyimpan operand sementara pada sebuah tumpukan dan menyebut operand secara implisit lewat puncak tumpukan tersebut. Instruksi ADD tidak menyebut nama register, melainkan mengambil dua nilai dari puncak tumpukan, menghitung jumlahnya, lalu mendorong hasil itu kembali. Akibatnya, satu instruksi cukup disandikan sebagai opcode tanpa operand, sehingga bytecode menjadi ringkas.
Alasan utama pemakaian model stack adalah kesederhanaan codegen. Karena urutan instruksi pada mesin stack sama dengan urutan evaluasi pohon ekspresi, codegen cukup menelusuri AST secara post-order: kunjungi anak kiri, kunjungi anak kanan, lalu keluarkan opcode operator. Tidak diperlukan alokasi register, sehingga pass alokasi register yang menjadi beban modul optimasi dapat ditunda. Sebagian besar bytecode portabel produksi, termasuk JVM bytecode dan WebAssembly, mengikuti model yang sama persis untuk alasan ini.
Set instruksi yang ditetapkan di sini mencakup lima belas opcode fungsional ditambah satu pseudo-instruksi LABEL. Kelompok pertama adalah instruksi aritmetika ADD, SUB, MUL, dan DIV yang masing-masing mengambil dua puncak tumpukan, menghitung hasil a OP b dengan a berada di bawah b, lalu mendorong hasilnya. Kelompok kedua adalah komparasi EQ, LT, dan GT yang menghasilkan bool. Kelompok ketiga adalah kontrol alur JUMP dan JUMP_IF_FALSE, sedangkan kelompok terakhir adalah akses lingkungan STORE dan LOAD, lalu PUSH, POP, dan PRINT.
Tabel set instruksi#
Tabel berikut merangkum mnemonic, stack effect, dan semantik tiap opcode. Stack effect menyatakan perubahan kedalaman tumpukan setelah instruksi dijalankan, dengan +1 berarti menambah satu elemen, -1 berarti mengurangi satu, dan 0 berarti tetap.
| Opcode | Mnemonic | Stack | Semantik |
|---|---|---|---|
PUSH | PUSH x | +1 | dorong nilai literal x ke puncak stack |
POP | POP | -1 | buang elemen puncak stack |
ADD | ADD | -1 | pop dua (a di bawah b), dorong a + b |
SUB | SUB | -1 | pop dua (a di bawah b), dorong a - b |
MUL | MUL | -1 | pop dua (a di bawah b), dorong a * b |
DIV | DIV | -1 | pop dua (a di bawah b), dorong a / b |
EQ | EQ | -1 | pop dua, dorong bool (a == b) |
LT | LT | -1 | pop dua, dorong bool (a < b) |
GT | GT | -1 | pop dua, dorong bool (a > b) |
NOT | NOT | 0 | pop satu, dorong bool negasinya |
JUMP | JUMP lbl | 0 | lompat ke label lbl |
JUMP_IF_FALSE | JUMP_IF_FALSE lbl | -1 | pop satu, lompat ke lbl bila falsy |
STORE | STORE name | -1 | pop satu, simpan ke env[name] |
LOAD | LOAD name | +1 | dorong env[name] ke puncak stack |
PRINT | PRINT | -1 | pop satu, cetak ke stdout |
LABEL | LABEL name | 0 | penanda posisi lompatan (pseudo) |
Konvensi a di bawah b pada ADD, SUB, MUL, dan DIV krusial karena urutan operand menentukan hasil untuk operasi nonkomutatif. Bila codegen membangkitkan a - b, urutan yang benar adalah mengevaluasi a lebih dahulu sehingga a berada di bawah, lalu mengevaluasi b sehingga b berada di puncak, baru SUB dipanggil. Jika urutan terbalik, hasil menjadi b - a tanpa pesan error apa pun. Demikian pula DIV untuk pembagian dan LT atau GT untuk komparasi. Codegen bertanggung jawab menjaga urutan ini, sedangkan VM hanya mengikuti urutan pop yang ditetapkan.
Pseudo-instruksi LABEL bukan opcode fungsional. Tugas LABEL hanya menandai posisi di bytecode yang menjadi sasaran JUMP dan JUMP_IF_FALSE. Pendekatan ini dipakai agar bytecode tetap dapat dibaca manusia: lompatan menyebut nama label seperti L_end_1 alih-alih indeks numerik yang harus dihitung ulang setiap kali instruksi di depannya berubah. Interpreter membangun pemetaan nama label ke indeks instruksi pada satu pass awal, kemudian mengganti nama label dengan indeks saat eksekusi.
Konvensi penyandian bytecode#
Bytecode disimpan sebagai list[tuple[str, ...]]. Setiap instruksi adalah sebuah tuple dengan elemen pertama berupa nama opcode sebagai string, diikuti operand bila ada. Instruksi tanpa operand direpresentasikan sebagai tuple satu elemen, sedangkan instruksi dengan operand direpresentasikan sebagai tuple dua atau tiga elemen. Contoh bytecode untuk program let x = 1 + 2; print x; adalah [("PUSH", 1), ("PUSH", 2), ("ADD",), ("STORE", "x"), ("LOAD", "x"), ("PRINT",)].
Berkas vm_spec.py berisi enumerasi opcode sebagai IntEnum dan tabel dokumentasi yang menjadi kontrak codegen dan interpreter VM. Enumerasi sebagai IntEnum memberi dua keuntungan: nama opcode dapat dirujuk sebagai Opcode.PUSH sehingga codegen terhindar dari salah ketik string, dan nilai integer tersedia bila bytecode perlu disandikan ke format biner pada modul lanjutan.
"""Spesifikasi set instruksi stack VM sebagai sasaran codegen.
Kontrak set instruksi (dipakai ulang oleh codegen dan interpreter VM):
PUSH x dorong nilai literal x ke puncak stack
POP buang elemen puncak
ADD SUB MUL DIV pop dua (a di bawah b), dorong hasil a OP b
EQ LT GT pop dua, dorong bool hasil komparasi a OP b
NOT pop satu, dorong bool negasinya
JUMP lbl lompat ke label lbl
JUMP_IF_FALSE lbl pop satu, lompat ke lbl bila falsy
STORE name pop satu, simpan ke env[name]
LOAD name dorong env[name]
PRINT pop satu, cetak ke stdout
LABEL name penanda posisi lompatan (pseudo-instruksi)
Bytecode disimpan sebagai list[tuple[str, ...]], mis.:
[("PUSH", 1), ("PUSH", 2), ("ADD",), ("STORE", "x"),
("LOAD", "x"), ("PRINT",)]
Pseudo-instruksi LABEL menandai posisi lompatan; ia dievaluasi sebagai
no-op oleh interpreter, tetapi dipakai oleh pass resolusi label untuk
membangun pemetaan nama label ke indeks instruksi. Pemakaian LABEL
menjaga bytecode tetap dapat dibaca manusia dan menghindari dua pass
backpatching pada codegen.
"""
from enum import IntEnum
class Opcode(IntEnum):
"""Enumerasi opcode stack VM beserta pseudo-instruksi LABEL."""
PUSH = 1
POP = 2
ADD = 3
SUB = 4
MUL = 5
DIV = 6
EQ = 7
LT = 8
GT = 9
NOT = 10
JUMP = 11
JUMP_IF_FALSE = 12
STORE = 13
LOAD = 14
PRINT = 15
LABEL = 16 # pseudo-instruksi: penanda posisi lompatan
# Tabel dokumentasi: mnemonic, stack effect (perubahan kedalaman), semantik.
SPEC: dict[str, tuple[str, str, str]] = {
"PUSH": ("PUSH x", "+1", "dorong nilai literal x ke puncak stack"),
"POP": ("POP", "-1", "buang elemen puncak stack"),
"ADD": ("ADD", "-1", "pop dua (a di bawah b), dorong a + b"),
"SUB": ("SUB", "-1", "pop dua (a di bawah b), dorong a - b"),
"MUL": ("MUL", "-1", "pop dua (a di bawah b), dorong a * b"),
"DIV": ("DIV", "-1", "pop dua (a di bawah b), dorong a / b"),
"EQ": ("EQ", "-1", "pop dua, dorong bool (a == b)"),
"LT": ("LT", "-1", "pop dua, dorong bool (a < b)"),
"GT": ("GT", "-1", "pop dua, dorong bool (a > b)"),
"NOT": ("NOT", "0", "pop satu, dorong bool negasinya"),
"JUMP": ("JUMP lbl", "0", "lompat ke label lbl"),
"JUMP_IF_FALSE": ("JUMP_IF_FALSE lbl", "-1", "pop satu, lompat ke lbl bila falsy"),
"STORE": ("STORE name", "-1", "pop satu, simpan ke env[name]"),
"LOAD": ("LOAD name", "+1", "dorong env[name] ke puncak stack"),
"PRINT": ("PRINT", "-1", "pop satu, cetak ke stdout"),
"LABEL": ("LABEL name", "0", "penanda posisi lompatan (pseudo)"),
}
def main() -> None:
print("=== Set instruksi stack VM ===")
print(f"{'opcode':<16} {'mnemonic':<20} {'stack':<6} semantik")
print("-" * 76)
for op in Opcode:
mnemonic, stack, sem = SPEC[op.name]
print(f"{op.name:<16} {mnemonic:<20} {stack:<6} {sem}")
print()
print("=== Konfigurasi awal VM ===")
print("stack : []")
print("env : {}")
print("pc : 0")
print("instr : list[tuple[str, ...]]")
print(f"opcode : {len(Opcode)} buah (PUSH .. LABEL)")
if __name__ == "__main__":
main()Program utama mencetak tabel set instruksi dan konfigurasi awal VM. Konfigurasi awal menyatakan bahwa stack dimulai kosong, env dimulai kosong, program counter pc dimulai pada indeks nol, dan instr disimpan sebagai list tuple. Keluaran berikut diperoleh dengan menjalankan python3 vm_spec.py.
=== Set instruksi stack VM ===
opcode mnemonic stack semantik
----------------------------------------------------------------------------
PUSH PUSH x +1 dorong nilai literal x ke puncak stack
POP POP -1 buang elemen puncak stack
ADD ADD -1 pop dua (a di bawah b), dorong a + b
SUB SUB -1 pop dua (a di bawah b), dorong a - b
MUL MUL -1 pop dua (a di bawah b), dorong a * b
DIV DIV -1 pop dua (a di bawah b), dorong a / b
EQ EQ -1 pop dua, dorong bool (a == b)
LT LT -1 pop dua, dorong bool (a < b)
GT GT -1 pop dua, dorong bool (a > b)
NOT NOT 0 pop satu, dorong bool negasinya
JUMP JUMP lbl 0 lompat ke label lbl
JUMP_IF_FALSE JUMP_IF_FALSE lbl -1 pop satu, lompat ke lbl bila falsy
STORE STORE name -1 pop satu, simpan ke env[name]
LOAD LOAD name +1 dorong env[name] ke puncak stack
PRINT PRINT -1 pop satu, cetak ke stdout
LABEL LABEL name 0 penanda posisi lompatan (pseudo)
=== Konfigurasi awal VM ===
stack : []
env : {}
pc : 0
instr : list[tuple[str, ...]]
opcode : 16 buah (PUSH .. LABEL)Keluaran memperlihatkan dua kelompok informasi. Bagian pertama adalah tabel set instruksi yang menjadi kontrak codegen dan interpreter VM, sedangkan bagian kedua adalah konfigurasi awal yang menjadi titik mula eksekusi. Lima belas opcode fungsional ditambah satu pseudo-instruksi LABEL menjadi himpunan lengkap yang dipakai sepanjang modul ini. Codegen membangkitkan hanya instruksi dari himpunan ini, sedangkan interpreter VM hanya mengenali opcode dari himpunan yang sama, sehingga kontrak set instruksi menjadi sumber kebenaran bagi keduanya.
Ringkasan#
- Codegen pada jalur ini menargetkan mesin stack karena urutan instruksi sama dengan urutan evaluasi pohon, sehingga pass alokasi register dapat ditunda.
- Set instruksi mencakup
PUSH,POP,ADD,SUB,MUL,DIV,EQ,LT,GT,NOT,JUMP,JUMP_IF_FALSE,STORE,LOAD,PRINT, ditambah pseudo-instruksiLABEL. - Konvensi
a di bawah bpada aritmetika dan komparasi menetapkan urutan operand yang menjadi tanggung jawab codegen, bukan VM. - Bytecode disimpan sebagai
list[tuple[str, ...]], dengan lompatan menyebut nama label alih-alih indeks numerik.
Referensi#
- Stack machine — Wikipedia — Karakteristik mesin sasaran berbasis stack beserta pembanding dengan mesin register.
- WebAssembly Specification — Section 2.4 Structure — Contoh set instruksi stack VM produksi pada mesin modern.
- Crafting Interpreters — Robert Nystrom — Bab 15 sampai 18 membahas desain set instruksi stack VM dan penyandian bytecode.