Membuat VM Sederhana Dengan Rust - Bagian 1

untuk full code bisa dilihat disini https://github.com/aldidana/simple-vm

VM (Virtual Machine) adalah sebuah program yang menyerupai sebuah komputer dengan mensimulasikan CPU untuk melakukan berbagai macam perintah layaknya sebuah komputer fisik berdasarkan tujuan VM tersebut dibuat.

VM terdiri dari 2 arsitektur:

  1. Stack based
  2. Register based

Pada post kali ini kita akan membuat Stack based VM. Salah satu contoh dari Stack based VM adalah JVM (Java Virtual Machine), stack based VM juga lebih mudah diimplementasikan dibandingkan Register based.

Cara Kerja VM

Cara kerja VM ini cukup sederhana:

  1. Mengambil instruksi pada daftar instruksi atau code.
  2. Decode instruksi tersebut.
  3. Eksekusi hasil dari decode.

Instructions

Instructions adalah serangkaian perintah untuk melakukan suatu tugas/task, seperti mendeklarasikan suatu variabel dan menambahkannya dengan variabel lainnya jika kedua variabel tersebut bertipe data numeric. Instructions ini terdiri dari Operation Code (opcode) dan Operands.

Untuk mencetak 8 + 10, berikut ini adalah contoh perintahnya:

CONST 8 // mendeklarasikan sebuah variabel dengan nilai 8
CONST 10 // mendeklarasikan sebuah variabel dengan nilai 10
ADD // menambahkan kedua variabel diatas
PRINT // mencetak

Stack

Sesuai dengan VM yang akan diimplementasikan yaitu Stack based, sama seperti tumpukan ketika mencuci piring, piring yang telah dicuci disimpan di paling atas maka stack ini LIFO (Last In First Out) untuk menambahkan (push) maupun mengurangi (pop) hanya melalui posisi paling atas.


Struktur Code

Untuk mengikuti ini pastikan download dan install Rust pada link berikut ini https://www.rust-lang.org/learn/get-started

setelah menginstall Rust, jalankan perintah berikut:

cargo new simple-vm

Kemudian kita akan membuat struktur seperti dibawah ini:

simple-vm
│   Cargo.toml   
│
└───src
	| main.rs
	| vm.rs
	| bytecode.rs

tambahkan code berikut ini pada file bytecode.rs

#[derive(Debug)]
pub enum Opcode {
  CONST(isize), // 0
  ADD, // 1
  SUB, // 2
  PRINT, // 3 
  HALT, // 4
}

impl Opcode {
  pub fn code(&self) -> isize {
    match *self {
      Opcode::CONST(_v) => 0,
      Opcode::ADD => 1,
      Opcode::SUB => 2,
      Opcode::PRINT => 3,
      Opcode::HALT => 4,
	}
  }
}

#[derive(Debug)] adalah attribute dengan mengimplementasikan Debug pada enum Opcode sehingga dengan otomatis kita bisa memformatnya menggunakan {:?}

pub enum Opcode {
  CONST(isize), // 0
  ADD, // 1
  SUB, // 2
  PRINT, // 3 
  HALT, // 4
}

disini kita membuat enum Opcode dengan data diatas yang merepresentasikan nomor/code sesuai dengan urutannya, misalnya untuk CONST(isize) adalah enum constructor yang berisikan tipe data isize yang merepresentasikan code 0. Tanda // hanyalah sebuah comment untuk menjelaskan urutannya.

impl Opcode {
  pub fn code(&self) -> isize {
    match *self {
      Opcode::CONST(_v) => 0,
      Opcode::ADD => 1,
      Opcode::SUB => 2,
      Opcode::PRINT => 3,
      Opcode::HALT => 4,
	}
  }
}

kemudian disini Opcode mempunyai fungsi code untuk mengembalikan nomor/code yang sudah kita sesuaikan.

masih pada bagian file bytecode.rs tambahkan code berikut ini setelahnya.

#[derive(Debug)]
pub struct Instruction {
  insructions: Vec<Opcode>,
}

kita membuat struct Instruction yang berisikan instructions dengan tipe data Vector dari Opcode

impl Instruction {
  pub fn new(opcodes: Vec<Opcode>) -> Self {
    Instruction {
      insructions: opcodes,
    }
  }

  pub fn generate_code(&self) -> Vec<isize> {
    let mut code: Vec<isize> = Vec::new();

    for insruction in self.insructions.iter() {
      match insruction {
        Opcode::CONST(value) => {
          code.push(insruction.code());
          code.push(*value);
        },
        Opcode::ADD => code.push(insruction.code()),
        Opcode::SUB => code.push(insruction.code()),
        Opcode::PRINT => code.push(insruction.code()),
        Opcode::HALT => code.push(insruction.code()),
      };
    }

    code
  }
}

dari struct Instruction diatas kita akan mengimplementasikan beberapa fungsi. Fungsi new adalah associated functions yang tidak mengambil self untuk parameter pertamanya, fungsi ini akan membuat Instruction baru yang berisikan instructions dari opcode. fungsi generate_code adalah fungsi yang mengambil Instruction sendiri, kemudian kita mendeklarasikan variabel code dengan tipe data Vector dari isize, lalu kita akan melakukan pengecekan (match) sesuai enum pada instructions tersebut.


pada file main.rs, tambahkan code berikut:

mod bytecode;

fn main() {
  let instructions: Vec<bytecode::Opcode> = vec!(
        bytecode::Opcode::CONST(10),
        bytecode::Opcode::CONST(20),
        bytecode::Opcode::ADD,
        bytecode::Opcode::CONST(9),
        bytecode::Opcode::SUB,
        bytecode::Opcode::PRINT,
    );

    let instruction = bytecode::Instruction::new(instructions);
    let code = instruction.generate_code();

    println!("{:?}", code);
}

disini kita menggunakan module bytecode.rs yang telah kita buat sebelumnya. Untuk jelasnya mengenai module bisa dilihat disini https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html. Lalu kita memasukan beberapa Opcode pada instructions yang kemudian digunakan untuk men-generate sebuah code untuk di eksekusi oleh VM. Kemudian jalankan program yang telah kita buat dengan perintah berikut

cargo run

maka akan keluar hasil sebagai berikut

[0, 10, 0, 20, 1, 0, 9, 2, 3]

angka tersebut berupa opcode dan operands dari instructions yang telah kita masukan.


bagian 2