fn_vm 1.0.1

A lightweight frame based virtual machine, meant as the base for rigz_vm.
Documentation

fn_vm

A lightweight frame/register based VM that can be used to run functions. This will be the underlying VM used by the rigz_vm, I don't want to iron down the VM yet so this is a very simple base VM.

All problems in computer science can be solved by another level of indirection... 
Except for the problem of too many layers of indirection.

Inspired by iridium

How it works

This is based on the idea that if my VM and AST are using the same underlying type for values, but not just a byte array, I'll be able to build a simpler runtime to bridge the gap between the two. Additionally, I didn't want to implement the same instructions I see in most other VMs when all I needed for now is to call a couple of rust functions.

fn_vm can store any value that implements the VMValue trait, there is an implementation for all primitive types.

By default, everything happens in the first frame; each frame is a function definition and can be entered/exited by calling an instruction. Once the value register is used, the register is removed (unless the instruction is meant to provide a register for later steps like LOAD or COPY)

So all you get are a few types, a builder, and the VM (composed of Frames).

pub type VMFunction<T> = fn(&mut VM<T>, args: Vec<T>) -> Result<Option<T>, VMError>;
pub type HCFFunction<T> = fn(&mut VM<T>) -> Result<(), VMError>;

#[derive(Clone, Debug)]
pub enum FrameValue<T: Clone> {
    Value(T),
    Frame(Box<Frame<T>>),
}

#[derive(Clone, Debug)]
pub struct Frame<T: Clone> {
    pub instructions: Vec<u8>,
    pub pc: usize,
    pub locals: IndexMap<String, Box<FrameValue<T>>>,
}

impl <T: Clone> Frame<T> {
    pub(crate) fn new() -> Self {
        Frame {
            instructions: vec![],
            pc: 0,
            locals: Default::default(),
        }
    }
}

pub trait VMValue<T: VMValue<T> + Clone + Default> {
    fn to_bytes(&self) -> Vec<u8>;

    fn from_bytes(vm: &mut VM<T>) -> T;
}

pub struct VM<T: Clone + VMValue<T> + Default> {
    pub fp: usize,
    pub frames: Vec<Frame<T>>,
    pub functions: Vec<VMFunction<T>>,
    pub registers: Vec<T>,
    pub stack: Vec<usize>,
    pub heap: Bump,
    pub program_data: Bytes,
    pub hcf_trigger: Option<HCFFunction<T>>,
}

This VM offers 15 instructions:

  • NOP: No operation, move the pc to the next instruction
  • LOAD: Load value into register, LOAD R1 10
  • COPY: Copy a value from one register to another
  • FN: Call provided a function, FN <out_register>
  • CALL: Call a frame, used to call functions
  • CALLR: Call a frame stored in index_register #, used to call frames created by CFR
  • RET: Return from a frame, RET
  • GLV: Get local value, GLV r1
  • SLV: Set local value, SLV value
  • SLR: Set local value from a register, SLR r1
  • SMV: Set local mutable value, SMV value
  • SMR: Set local mutable value from a register, SMR r1
  • CFR: Create frame, CFR index_r1 [instructions]
  • DFR: Delete frame, DFR f#
  • HCF: Halt and catch fire, stop the VM (an optional hcf_trigger can be passed in if this command is received)

With IVD as a default command for any invalid command.

FN

This is by far the most complicated instruction since it's really a pass through to you.

Here's an example of calling it with the builder:

NOTE: into() is used to convert to a Length, from small_len to support up to usize args.

fn run() {
    let vm = VMBuilder::new()
            .set_value(5.into(), 42) // store 42 in r5
            .set_value(4.into(), 42) // store 42 in r4
        .add_function_instruction(0, vec![5.into(), 4.into()], 3.into())
        .add_function(move |_, args| {
            let a = args[0];
            let b = args[1];
            Ok(Some(a + b))
        })
        .build();
    vm.run().unwrap();
    assert_eq!(vm.registers[3], 84);
}