| Crates.io | fn_vm |
| lib.rs | fn_vm |
| version | 4.0.0 |
| created_at | 2024-07-01 02:23:16.86046+00 |
| updated_at | 2024-07-17 07:00:12.225415+00 |
| description | A lightweight frame based virtual machine, meant as the base for rigz_vm. |
| homepage | |
| repository | https://gitlab.com/magicfoodhand/fn_vm |
| max_upload_size | |
| id | 1288305 |
| size | 55,135 |
A lightweight frame/register based VM that can be used to run functions.
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
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 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>;
pub trait LazyCompiler<T: VMValue<T>> {
fn compile(&self, length: Length) -> Result<Frame<T>, VMError>;
}
#[derive(Clone, Debug, PartialEq)]
pub enum FrameValue<T: VMValue<T>> {
Value(T),
Frame(Frame<T>),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Frame<T: VMValue<T>> {
pub instructions: Vec<u8>,
pub pc: usize,
pub locals: IndexMap<String, FrameValue<T>>,
pub parent: Option<usize>,
}
pub struct VM<T: VMValue<T>> {
pub fp: usize,
pub frames: Vec<Frame<T>>,
pub functions: Vec<VMFunction<T>>,
pub registers: HashMap<Length, T>,
pub index_registers: HashMap<Length, Length>,
pub bit_registers: HashMap<Length, bool>,
pub stack: Vec<usize>,
pub lazy_compiler: Option<Box<dyn LazyCompiler<T>>>,
pub program_data: Bytes,
pub hcf_trigger: Option<HCFFunction<T>>,
}
pub trait VMValue<T: VMValue<T>>: Display + Debug + Clone + PartialEq + Logical<T> + Add<Output=T> + Mul<Output=T> + Div<Output=T> + PartialOrd + Sub<Output=T> + BitAnd<Output=T> + BitOr<Output=T> + BitXor<Output=T> + Rem<Output=T> + Not<Output=T> + Neg<Output=T> + Shr<Output=T> + Shl<Output=T> {
}
This VM offers the following instructions:
NOPADD r1 r2MOD r1 r2SUB r1 r2MUL r1 r2DIV r1 r2SHR r1 r2SHL r1 r2XOR r1 r2BAND r1 r2BOR r1 r2BXOR r1 r2LXOR r1 r2LAND r1 r2LOR r1 r2NOT r1BNOT r1NEG r1AND r1 r2OR r1 r2EQ r1 r2BEQ br1 br2IEQ ir1 ir2NEQ r1 r2BNEQ br1 br2INEQ ir1 ir2REV r1LT r1 r2LTE r1 r2GT r1 r2GTE r1 r2COPY r1 r2CIR ir1 ir2CBR br1 br2CALL f#CALLR ir1RET <from> <to>GLV r1 <name>MVL fr1 <name>CPL fr1 <name>CPM fr1 <name>DLV fr1 <name>DLV <name>SLR r1 <name>SMR r1 <name>SMF fr1 <name>CFR ir1 <len> [instructions]DFR f#DFI ir1 , // ir1, delete frameFN <op> <len> [registers] <out_register>IVFPSH r1PSHV <value>POP r1HCFlazy_compiler be set. LZY l#
pub trait LazyCompiler<T: Clone + PartialEq> {
fn compile(&self, length: Length) -> Result<Frame<T>, VMError>;
}
With IVD as a default command for any invalid command.
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);
}