Skip to content

Instantly share code, notes, and snippets.

@monomere
Created June 3, 2025 06:51
Show Gist options
  • Select an option

  • Save monomere/20648c2e6dccffac4692babbc01bc5f3 to your computer and use it in GitHub Desktop.

Select an option

Save monomere/20648c2e6dccffac4692babbc01bc5f3 to your computer and use it in GitHub Desktop.
wip vm thing
#![feature(cfg_boolean_literals)] // optional, remove the ratatui ui if needed
use std::mem::MaybeUninit;
#[derive(Debug)]
pub enum Obj {
Func(std::rc::Rc<VmFunc>),
Str(String),
}
#[derive(Debug, Clone, Copy)]
#[repr(transparent)]
pub struct ObjRef(pub usize);
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct Value(pub u64);
const QNAN: u64 = 0x7ffc000000000000; // 0111111111111100000000000000000000000000000000000000000000000000
const QNAN_HI: u64 = 0xfffc000000000000; // 1111111111111100000000000000000000000000000000000000000000000000
impl std::fmt::Debug for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_f64() {
write!(f, "{}", self.as_f64())
} else if self.is_i64() {
write!(f, "{}", self.as_i64())
} else if self.is_ref() {
write!(f, "{{{:?}}}", self.as_ref())
} else {
write!(f, "Value({:08x})", self.0)
}
}
}
impl Value {
const MIN_INT: i64 = -(1i64 << 47) as i64;
const MAX_INT: i64 = (1i64 << 47) - 1 as i64;
#[inline(always)]
pub fn from_i64(x: i64) -> Self {
assert!(matches!(x, Self::MIN_INT..=Self::MAX_INT));
let a = Self(QNAN | (x as u64 & (!0 >> 16)));
assert!(a.is_i64());
a
}
#[inline(always)]
pub fn from_f64(x: f64) -> Self {
Self(x.to_bits())
}
#[inline(always)]
pub fn from_ref(x: ObjRef) -> Self {
Self((1 << 63) | QNAN | x.0 as u64)
}
#[inline(always)]
pub fn is_f64(self) -> bool {
self.0 & QNAN != QNAN
}
#[inline(always)]
pub fn is_i64(self) -> bool {
self.0 & QNAN_HI == QNAN
}
#[inline(always)]
pub fn is_ref(self) -> bool {
self.0 & QNAN_HI == QNAN_HI
}
#[inline(always)]
pub fn as_i64(self) -> i64 {
(self.0 & (!0 >> 16)) as i64
}
#[inline(always)]
pub fn as_ref(self) -> ObjRef {
ObjRef((self.0 & (!0 >> 16)) as usize)
}
#[inline(always)]
pub fn as_f64(self) -> f64 {
f64::from_bits(self.0)
}
#[inline(always)]
pub fn to_f64(self) -> Option<f64> {
if self.is_f64() {
Some(self.as_f64())
} else if self.is_i64() {
Some(self.as_i64() as f64)
} else {
None
}
}
pub fn is_zero(self) -> bool {
self.0 == 0 || self.as_i64() == 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct Opcode(pub u8);
impl Opcode {
pub const MOV: Self = Self(0); // Register Move
pub const INT: Self = Self(1);
pub const VAL: Self = Self(2);
pub const ADD: Self = Self(3);
pub const SUB: Self = Self(4);
pub const MUL: Self = Self(5);
pub const DIV: Self = Self(6);
pub const DEC: Self = Self(0x20); // Decrease by integer
pub const JMP: Self = Self(0x40); // Jump
pub const BEZ: Self = Self(0x41); // Branch if Equal to Zero
pub const BNZ: Self = Self(0x42); // Branch if Not Zero
pub const RET: Self = Self(0x43); // Return
pub const INV: Self = Self(0x44); // Invoke
pub const SLA: Self = Self(0x80); // Stack Load, Absolute
pub const SLR: Self = Self(0x81); // Stack Load, Relative
pub const SSA: Self = Self(0x82); // Stack Store, Absolute
pub const SSR: Self = Self(0x83); // Stack Store, Relative
pub const DUP: Self = Self(0x84); // Duplicate on stack
pub const RES: Self = Self(0x85); // Reserve stack
pub const RER: Self = Self(0x86); // Register Reserve stack
pub const SSS: Self = Self(0x87); // Set Sense of Self
pub const INP: Self = Self(0xFD); // Input
pub const OUT: Self = Self(0xFE); // Output
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct Instruction(pub u64);
impl Instruction {
pub fn opcode(self) -> Opcode {
Opcode((self.0 & 0xFF) as u8)
}
pub fn reg_a(self) -> u8 {
((self.0 >> 8) & 0xFF) as u8
}
pub fn reg_b(self) -> u8 {
((self.0 >> 16) & 0xFF) as u8
}
pub fn reg_c(self) -> u8 {
((self.0 >> 24) & 0xFF) as u8
}
pub fn int(self) -> i64 {
self.0 as i64 >> 16
}
}
#[derive(Debug)]
pub struct VmFunc {
pub name: String,
pub code: Vec<u64>,
pub arg_count: usize,
pub reg_count: usize,
}
#[derive(Debug)]
pub struct CallMetadata {
pub name: String,
pub args: Vec<Value>,
}
pub struct Vm {
// pub terminal: &'term mut ratatui::DefaultTerminal,
pub stack: Vec<Value>,
pub objects: Vec<Obj>,
pub callstack: Vec<CallMetadata>,
}
macro_rules! vm_impl__arith_instr_ {
($name:ident ($op:tt), $pc:ident, $regs:ident, $func:ident, $instr:ident) => {{
$pc += 1;
let lhs = $regs[$instr.reg_a() as usize];
let rhs = $regs[$instr.reg_b() as usize];
let out = &mut $regs[$instr.reg_c() as usize];
if lhs.is_ref() || rhs.is_ref() {
panic!("objects not yet implemented");
}
if lhs.is_f64() {
*out = Value::from_f64(lhs.as_f64() + if rhs.is_f64() {
rhs.as_f64()
} else {
rhs.as_i64() as f64
});
} else if rhs.is_f64() {
*out = Value::from_f64(if lhs.is_f64() {
lhs.as_f64()
} else {
lhs.as_i64() as f64
} + rhs.as_f64());
} else {
*out = Value::from_i64(lhs.as_i64() + rhs.as_i64())
}
}};
}
impl Vm {
pub fn run_func(&mut self, stack_base: usize, func: std::rc::Rc<VmFunc>) -> usize {
// self.callstack.push(CallMetadata {
// name: func.name.clone(),
// args: self.stack[stack_base .. stack_base + func.arg_count].to_vec(),
// });
const REG_COUNT_LOCAL_MAX: usize = 32;
if func.reg_count > REG_COUNT_LOCAL_MAX {
panic!("too many registers used by function")
}
let mut regs_local = [Value(0); REG_COUNT_LOCAL_MAX];
let regs = &mut regs_local[0..func.reg_count];
// let mut regs_vec = MaybeUninit::<Box<[Value]>>::uninit();
// let regs = if func.reg_count > REG_COUNT_LOCAL_MAX {
// regs_vec.write(vec![Value(0); func.reg_count].into_boxed_slice()).as_mut()
// } else {
// &mut regs_local[0..func.reg_count]
// };
let mut pc = 0;
while pc < func.code.len() {
let instr = Instruction(func.code[pc]);
#[cfg(false)]
if ENABLE_DEBUGGING {
self.terminal.draw(|frame| {
use ratatui::{
widgets::{Table, Paragraph, Row},
layout::{Layout, Constraint, Direction},
style::Stylize,
};
// Block::bordered().title("VM").render(frame.area(), frame.buffer_mut());
// Table::new(rows, widths)
let outer_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(35), Constraint::Fill(0)])
.split(frame.area());
let left_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(1), Constraint::Fill(1)])
.split(outer_layout[0]);
let right_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Fill(1), Constraint::Fill(1)])
.split(outer_layout[1]);
let right_top_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Fill(1), Constraint::Fill(1)])
.split(right_layout[0]);
frame.buffer_mut().cell_mut((left_layout[0].x, left_layout[0].y + pc as u16)).unwrap().set_char('>');
Paragraph::new(&*disas(&func.code)).render(left_layout[1], frame.buffer_mut());
Table::new(
regs.iter().enumerate().map(|(i, r)| Row::new([format!("${i}"), format!("{r:?}")])),
[Constraint::Length(5), Constraint::Fill(1)]
).block(Block::bordered().title("Registers")).render(right_top_layout[0], frame.buffer_mut());
Table::new(
self.callstack.iter().enumerate().map(|(i, r)| Row::new([i.to_string(), {
struct Tmp<'a> { r: &'a CallMetadata }
impl std::fmt::Display for Tmp<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}(", self.r.name)?;
for (i, arg) in self.r.args.iter().enumerate() {
if i != 0 { write!(f, ", ")?; }
write!(f, "{:?}", arg)?;
}
write!(f, ")")
}
}
format!("{}", Tmp { r: &r, })
}])),
[Constraint::Length(5), Constraint::Fill(1)],
).block(Block::bordered().title("Calls")).render(right_top_layout[1], frame.buffer_mut());
Table::new(
self.stack.iter().enumerate().map(|(i, r)| {
if i >= stack_base {
Row::new([format!("[{}]", i - stack_base), format!("{r:?}")])
} else {
Row::new([format!("[ ]"), format!("{r:?}")]).gray()
}
}),
[Constraint::Length(5), Constraint::Fill(1)]
).block(Block::bordered().title("Stack")).render(right_layout[1], frame.buffer_mut());
}).unwrap();
{
use ratatui::crossterm::event::{self, Event, KeyEvent, KeyCode, KeyModifiers};
match event::read().unwrap() {
Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
..
}) => {
ratatui::restore();
std::process::exit(0);
}
_ => {}
}
}
}
match instr.opcode() {
Opcode::MOV => {
pc += 1;
regs[instr.reg_a() as usize] = regs[instr.reg_b() as usize];
}
Opcode::INT => {
pc += 1;
regs[instr.reg_a() as usize] = Value::from_i64(instr.int());
}
Opcode::VAL => {
pc += 1;
regs[instr.reg_a() as usize] = Value(func.code[pc]);
pc += 1;
}
Opcode::ADD => vm_impl__arith_instr_!(add (+), pc, regs, func, instr),
Opcode::SUB => vm_impl__arith_instr_!(sub (-), pc, regs, func, instr),
Opcode::MUL => vm_impl__arith_instr_!(mul (*), pc, regs, func, instr),
Opcode::DIV => vm_impl__arith_instr_!(div (/), pc, regs, func, instr),
Opcode::DEC => {
pc += 1;
let lhs = regs[instr.reg_a() as usize];
if lhs.is_ref() {
panic!("objects not yet implemented");
}
if lhs.is_f64() {
regs[instr.reg_a() as usize] = Value::from_f64(lhs.as_f64() - instr.int() as f64);
} else {
regs[instr.reg_a() as usize] = Value::from_i64(lhs.as_i64() - instr.int());
}
},
Opcode::OUT => {
pc += 1;
println!("output: {:?}", regs[instr.reg_a() as usize]);
}
Opcode::RES => {
pc += 1;
self.stack.resize(self.stack.len() + instr.int() as usize, Value(0));
}
Opcode::RER => {
pc += 1;
let r = regs[instr.reg_a() as usize];
if !r.is_i64() { panic!("tried to resize struct with non-integer.") }
self.stack.resize(self.stack.len() + r.as_i64() as usize + instr.int() as usize, Value(0));
}
Opcode::SLA => {
pc += 1;
regs[instr.reg_a() as usize] = self.stack[stack_base + instr.int() as usize];
}
Opcode::SSA => {
pc += 1;
self.stack[stack_base + instr.int() as usize] = regs[instr.reg_a() as usize];
}
Opcode::DUP => {
pc += 1;
*self.stack.last_mut().unwrap() = self.stack[stack_base + instr.int() as usize];
}
Opcode::INP => {
use std::io::Write;
pc += 1;
let mut ln = String::new();
print!("input: ");
std::io::stdout().flush().unwrap();
std::io::stdin().read_line(&mut ln).unwrap();
regs[instr.reg_a() as usize] = Value::from_i64(ln.trim().parse().unwrap());
}
Opcode::BEZ =>
if regs[instr.reg_a() as usize].is_zero() {
pc = pc.checked_add_signed(instr.int() as isize).expect("out of bounds BEZ instruction");
} else {
pc += 1;
}
Opcode::BNZ =>
if !regs[instr.reg_a() as usize].is_zero() {
pc = pc.checked_add_signed(instr.int() as isize).expect("out of bounds BNZ instruction");
} else {
pc += 1;
}
Opcode::JMP => pc = pc.checked_add_signed(instr.int() as isize).expect("out of bounds JMP instruction"),
Opcode::INV => {
pc += 1;
let a = regs[instr.reg_a() as usize];
if !a.is_ref() {
panic!("tried to invoke non-function value ({a:?})");
}
let o = &self.objects[a.as_ref().0];
let Obj::Func(f) = o else { panic!("tried to invoke non-function object") };
if stack_base + f.arg_count > self.stack.len() {
panic!("stack underflow for function invocation")
}
let stack_size = self.stack.len();
let arg_count = f.arg_count;
let ret_count = self.run_func(stack_size - arg_count, f.clone());
self.stack.truncate(stack_size - arg_count + ret_count);
if self.stack.len() < stack_base {
panic!("stack overconsumed by called function")
}
}
Opcode::RET => {
// self.callstack.pop();
return instr.int() as usize;
},
Opcode::SSS => {
pc += 1;
self.objects.push(Obj::Func(func.clone()));
regs[instr.reg_a() as usize] = Value::from_ref(ObjRef(self.objects.len() - 1));
}
op => panic!("unknown opcode {}", op.0)
}
}
// if func.reg_count > REG_COUNT_LOCAL_MAX {
// unsafe {
// regs_vec.assume_init_drop();
// }
// }
// self.callstack.pop();
0
}
}
pub fn disas(code: &[u64]) -> String {
let mut res = String::new();
let mut pc = 0;
while pc < code.len() {
let instr = Instruction(code[pc]);
match instr.opcode() {
Opcode::MOV => {
pc += 1;
res += &format!("${} ← ${}", instr.reg_a(), instr.reg_b());
}
Opcode::INT => {
pc += 1;
res += &format!("${} ← {}", instr.reg_a(), instr.int());
}
Opcode::VAL => {
pc += 1;
res += &format!("${} ← {:?}", instr.reg_a(), Value(code[pc]));
pc += 1;
}
Opcode::ADD => {
pc += 1;
res += &format!("${} ← ${} + ${}", instr.reg_c(), instr.reg_a(), instr.reg_b());
},
Opcode::SUB => {
pc += 1;
res += &format!("${} ← ${} - ${}", instr.reg_c(), instr.reg_a(), instr.reg_b());
},
Opcode::MUL => {
pc += 1;
res += &format!("${} ← ${} * ${}", instr.reg_c(), instr.reg_a(), instr.reg_b());
},
Opcode::DIV => {
pc += 1;
res += &format!("${} ← ${} / ${}", instr.reg_c(), instr.reg_a(), instr.reg_b());
},
Opcode::DEC => {
pc += 1;
res += &format!("${} ← ${} - {}", instr.reg_a(), instr.reg_a(), instr.int());
},
Opcode::OUT => {
pc += 1;
res += &format!("output ${}", instr.reg_a());
}
Opcode::RES => {
pc += 1;
res += &format!("reserve {}", instr.int());
}
Opcode::RER => {
pc += 1;
res += &format!("reserve ${} + {}", instr.reg_a(), instr.int());
}
Opcode::SLA => {
pc += 1;
res += &format!("${} ← [{}]", instr.reg_a(), instr.int());
}
Opcode::SSA => {
pc += 1;
res += &format!("[{}] ← ${}", instr.int(), instr.reg_a());
}
Opcode::DUP => {
pc += 1;
res += &format!("[^] ← [{}]", instr.int());
}
Opcode::INP => {
res += &format!("${} ← input", instr.reg_a());
}
Opcode::BEZ => {
pc += 1;
res += &format!("if ${} = 0 then jump {:+}", instr.reg_a(), instr.int());
}
Opcode::BNZ => {
pc += 1;
res += &format!("if ${} ≠ 0 then jump {:+}", instr.reg_a(), instr.int());
}
Opcode::JMP => {
pc += 1;
res += &format!("jump {:+}", instr.int());
}
Opcode::INV => {
pc += 1;
res += &format!("invoke ${}", instr.reg_a());
}
Opcode::RET => {
pc += 1;
res += &format!("return {} value{}", instr.int(), if instr.int() == 1 { "" } else { "s" });
},
Opcode::SSS => {
pc += 1;
res += &format!("${} ← self", instr.reg_a());
}
op => {
pc += 1;
res += &format!("unknown opcode {}", op.0);
}
}
res += "\n";
}
res
}
fn main() {
let fib = std::rc::Rc::new(VmFunc {
name: "fib".to_string(),
arg_count: 1,
reg_count: 3,
code: vec![
u64::from_le_bytes([Opcode::RES.0, 0,1,0,0,0,0,0]), // reserve 1
u64::from_le_bytes([Opcode::SLA.0, 1, 0,0,0,0,0,0]), // $1 ← [0]
u64::from_le_bytes([Opcode::DUP.0, 0,0,0,0,0,0,0]), // [^] ← [0]
u64::from_le_bytes([Opcode::BEZ.0, 1, 14,0,0,0,0,0]), // if $1 = 0 then goto R1
u64::from_le_bytes([Opcode::DEC.0, 1, 1,0,0,0,0,0]), // $1 ← $1 - 1
u64::from_le_bytes([Opcode::BEZ.0, 1, 12,0,0,0,0,0]), // if $1 = 0 then goto R1
u64::from_le_bytes([Opcode::SSS.0, 0, 0,0,0,0,0,0]), // $0 ← self
u64::from_le_bytes([Opcode::SSA.0, 1, 1,0,0,0,0,0]), // [1] ← $1
u64::from_le_bytes([Opcode::INV.0, 0, 0,0,0,0,0,0]), // ([1] ←) inv $0
u64::from_le_bytes([Opcode::SLA.0, 2, 1,0,0,0,0,0]), // $2 ← [1]
u64::from_le_bytes([Opcode::DEC.0, 1, 1,0,0,0,0,0]), // $1 ← $1 - 1
u64::from_le_bytes([Opcode::SSA.0, 1, 1,0,0,0,0,0]), // [1] ← $1
u64::from_le_bytes([Opcode::INV.0, 0, 1,0,0,0,0,0]), // ([1] ←) inv $0
u64::from_le_bytes([Opcode::SLA.0, 1, 1,0,0,0,0,0]), // $1 ← [1]
u64::from_le_bytes([Opcode::ADD.0, 1, 2, 1, 0,0,0,0]), // $1 ← $1 + $2
u64::from_le_bytes([Opcode::SSA.0, 1, 0,0,0,0,0,0]), // [0] ← $1
u64::from_le_bytes([Opcode::RET.0, 0,1,0,0,0,0,0]), // ret 1 ([0])
u64::from_le_bytes([Opcode::INT.0, 0, 1,0,0,0,0,0]), // $0 ← 0
u64::from_le_bytes([Opcode::SSA.0, 0, 0,0,0,0,0,0]), // [0] ← $0
u64::from_le_bytes([Opcode::RET.0, 0,1,0,0,0,0,0]), // ret 1 ([0])
],
});
// let mut terminal = if ENABLE_DEBUGGING {
// ratatui::init()
// } else {
// ratatui::Terminal::new(ratatui::backend::TestBackend::new(5, 5)).unwrap()
// };
let mut vm = Vm {
// terminal: &mut terminal,
stack: vec![Value::from_i64(40)],
objects: vec![],
callstack: vec![],
};
vm.run_func(0, fib);
// ratatui::restore();
println!("{:?}", vm.stack[0]);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment