Crates.io | fn_vm |
lib.rs | fn_vm |
version | 4.0.0 |
source | src |
created_at | 2024-07-01 02:23:16.86046 |
updated_at | 2024-07-17 07:00:12.225415 |
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:
NOP
ADD r1 r2
MOD r1 r2
SUB r1 r2
MUL r1 r2
DIV r1 r2
SHR r1 r2
SHL r1 r2
XOR r1 r2
BAND r1 r2
BOR r1 r2
BXOR r1 r2
LXOR r1 r2
LAND r1 r2
LOR r1 r2
NOT r1
BNOT r1
NEG r1
AND r1 r2
OR r1 r2
EQ r1 r2
BEQ br1 br2
IEQ ir1 ir2
NEQ r1 r2
BNEQ br1 br2
INEQ ir1 ir2
REV r1
LT r1 r2
LTE r1 r2
GT r1 r2
GTE r1 r2
COPY r1 r2
CIR ir1 ir2
CBR br1 br2
CALL f#
CALLR ir1
RET <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>
IVF
PSH r1
PSHV <value>
POP r1
HCF
lazy_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);
}