Membuat VM Sederhana Dengan Rust - Bagian 2

Pada bagian 1, kita sudah membuat file bytecode.rs yang men-generate code dan mencetak hasilnya.


Buat file vm.rs dan tambahkan code berikut:

use crate::bytecode;

pub struct VM {
  code: Vec<isize>,
  stack: Vec<isize>,
  
  pc: usize, //program counter or ip (instruction pointer)
  sp: isize, //stack pointer
  
  debug: bool,
}

pada bagian use crate::bytecode; kita akan menggunakan module bytecode.rs yang telah kita buat dan gunakan di main.rs, kegunaan mod bytecode; pada file main.rs di bagian 1 adalah agar kita dapat menggunakannya di file lain.


Biasanya sebuah VM terdiri dari frame, call stack, globals, locals dan lainnya, dikarenakan kita hanya membuat VM yang sangat sederhana maka kita membuat struct VM yang berisikan:

  • code berupa instructions yang telah kita generate menjadi code.
  • stack dimana kita akan menyimpan data kita.
  • pc (program counter) atau biasanya disebut ip (instruction pointer), adalah petunjuk sebuah instruksi yang menyimpan sebuah alamat (memory address) untuk di eksekusi berikutnya.
  • sp (stack pointer) fungsinya mirip sepeti pc/ip tetapi menunjukkan lokasi dimana alamat (memory address) pada sebuah stack.
  • debug hanya digunakan untuk men-debug.

untuk lebih jelasnya mengenai Rust numeric type, seperti isize dan usize dapat dilihat disini https://doc.rust-lang.org/reference/types/numeric.html.


tambahkan code berikut di vm.rs setelah code diatas

impl VM {
  pub fn new(code: Vec<isize>, capacity: usize, debug: bool) -> Self {
    VM {
      code: code,
      stack: Vec::with_capacity(capacity),
      pc: 0,
      sp: -1,
      debug: debug,
    }
  }
}

Kita menambahkan associated functions new pada VM. Disini stack merupakan Vector dengan kapasitas yang telah kita berikan pada parameter capacity serta stack pointer yang dimulai dari -1.


Mengenai Array dan Vector lebih jelasnya bisa dilihat disini https://www.cs.brandeis.edu/~cs146a/rust/doc-02-21-2015/book/arrays-vectors-and-slices.html


Sebelum melanjutkan dibagian vm.rs ini, kembali ke file bytecode.rs dan tambahkan code berikut di bagian paling bawah (bukan didalam Impl Instruction):

.....

pub fn disassemble(code: isize, pc: usize) -> (Opcode, isize) {
  match code {
    code if code == Opcode::CONST(code).code() => (Opcode::CONST(pc as isize), 1),
    code if code == Opcode::ADD.code() => (Opcode::ADD, 0),
    code if code == Opcode::SUB.code() => (Opcode::SUB, 0),
    code if code == Opcode::PRINT.code() => (Opcode::PRINT, 0),
    code if code == Opcode::HALT.code() => (Opcode::HALT, 0),
    _ => (Opcode::HALT, 0),
  }
}

fungsi disassemble untuk mengembalikan tuple berupa nama Opcode dan jumlah Operand pada Opcode tersebut.


Kembali ke file vm.rs tambahkan code berikut setelah new

impl VM {
  pub fn new ...
  
  // tambahkan disini
  pub fn run(mut self, pc: usize) {
    self.pc = pc;
  
    while self.pc < self.code.len()  {
      let opcode = self.code[self.pc];
      if self.debug {
        let instructions = bytecode::disassemble(opcode, self.pc);
        if instructions.1 < 1 {
          println!("[{:04X}] {:?}", self.pc, instructions.0);
        }
        if instructions.1 == 1 {
          println!("[{:04X}] {:?} {:?}", self.pc, instructions.0, self.code[self.pc+1]);
        }
      }
      
      self.pc += 1;
      match opcode {
        opcode if opcode == bytecode::Opcode::CONST(opcode).code() => {
          let value = self.code[self.pc];
          self.pc += 1;
          self.sp += 1;
          let current_sp = self.sp as usize;
          self.stack.insert(current_sp, value as isize);
        },
        opcode if opcode == bytecode::Opcode::ADD.code() => {
          let rhs = self.stack.pop().expect("invalid value");
          let lhs = self.stack.pop().expect("invalid value");		
          let value = lhs + rhs;
          self.stack.push(value);
          
          self.sp -= 1;
        },
        opcode if opcode == bytecode::Opcode::SUB.code() => {
          let rhs = self.stack.pop().expect("invalid value");
          let lhs = self.stack.pop().expect("invalid value");
          let value = lhs - rhs;
          self.stack.push(value);
          
          self.sp -= 1;
        },
        opcode if opcode == bytecode::Opcode::PRINT.code() => {
          let current_sp = self.sp as usize;
          let value = self.stack[current_sp];
          self.sp -= 1;
          println!("{}", value)
        },
        opcode if opcode == bytecode::Opcode::HALT.code() => {
          break
        },
        _ => panic!("Opcode {} is not supported", opcode),
      }
    }
  }
}

Fungsi run adalah untuk mengeksekusi sebuah code yang telah kita masukan. opcode disini diambil melalui pc/ip pada code, kemudian kita mengecek apakah kita akan melakukan debug pada program, jika kita melakukan debug pada program maka akan memanggil fungsi disassemble yang telah kita buat di file bytecode.rs, debug akan mencetak instruction yang diberikan.


selama program berjalan maka pc (program counter) akan berubah sesuai dengan instruksi yang diberikan.


Ketika melakukan perintah CONST, sp (stack poiner) akan bergerak (dengan menambahkan) dan mengisi data ke stack sesuai dengan alamat stack pointer tersebut. Untuk perintah ADD dan SUB sama-sama mengambil nilai (pop) dari stack (isi stack berkurang) yang kemudian akan dilakukan operasi ADD (penjumlahan) atau SUB (pengurangan) dimana hasil tersebut akan dimasukan ke dalam stack. Kemudian perintah PRINT akan mengambil data dari stack.


Setelah itu kembali ke file main.rs dan tambahkan code berikut:

mod bytecode;
mod vm;

fn main() {
  //code sebelumnya di bagian-1
  ...
  let pc_start = 0;
  let stack_size = 100;
  let debug = true;
  
  let vm = vm::VM::new(code, stack_size, debug);
  vm.run(pc_start)
}

jalankan program yang telah dibuat

cargo run

hasilnya berupa nilai dari penjumlahan dan penguran:

[0000] CONST(0) 10
[0002] CONST(2) 20
[0004] ADD
[0005] CONST(5) 9
[0007] SUB
[0008] PRINT
21
Aldi Perdana

Aldi Perdana