Created
June 3, 2025 06:51
-
-
Save monomere/20648c2e6dccffac4692babbc01bc5f3 to your computer and use it in GitHub Desktop.
wip vm thing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #![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