fern_vm/
vm.rs

1// Copyright (C) 2024 Ethan Uppal and Utku Melemetci. All rights reserved.
2
3use std::vec;
4
5use crate::{
6    arch::{
7        InstructionAddress, LocalAddress, Word, ARGUMENT_LOCALS, CODE_SIZE,
8        LOCALS_SIZE, RETURN_LOCALS,
9    },
10    opcode::{Op, IMM_BITS, IMM_EXT_BITS},
11};
12
13macro_rules! sign_extend_to {
14    ($t:ty, $value:expr, $bits:expr) => {{
15        let value = $value as $t;
16        let sign_bit = 1 << ($bits - 1);
17        if value & sign_bit != 0 {
18            let extension = !((1 << $bits) - 1);
19            value | extension
20        } else {
21            value
22        }
23    }};
24}
25
26struct StackFrame {
27    locals: [Word; LOCALS_SIZE],
28    return_address: InstructionAddress,
29}
30
31pub struct VM {
32    code: Box<[Word]>,
33    code_length: InstructionAddress,
34    call_stack: Vec<StackFrame>,
35    ip: InstructionAddress,
36}
37
38#[derive(Debug)]
39pub enum VMError {
40    InvalidOp,
41    InvalidArgs,
42    CodeOversized,
43    InvalidIP,
44}
45
46pub type VMResult = Result<(), VMError>;
47
48impl Default for VM {
49    fn default() -> Self {
50        Self {
51            code: vec![0; CODE_SIZE].into_boxed_slice(),
52            code_length: 0,
53            call_stack: vec![],
54            ip: 0,
55        }
56    }
57}
58
59impl VM {
60    pub fn load(&mut self, program: &[Op]) -> VMResult {
61        for (i, op) in program.iter().enumerate() {
62            self.code[i] = op.encode_packed();
63        }
64        self.code_length = program.len();
65        self.call_stack.clear();
66        self.call_stack.push(StackFrame {
67            locals: [0; LOCALS_SIZE],
68            return_address: 0, /* once the initial frame is popped, execution
69                                * stops, so it doesn't  matter what address
70                                * we have here */
71        });
72        Ok(())
73    }
74
75    pub fn run(&mut self) -> VMResult {
76        while !self.call_stack.is_empty() {
77            self.step()?;
78        }
79        Ok(())
80    }
81
82    fn decode_op(&self) -> Op {
83        Op::decode_packed(self.code[self.ip]).unwrap()
84    }
85
86    fn step(&mut self) -> VMResult {
87        match self.decode_op() {
88            Op::Mov(to, from) => {
89                self.write_local(to, self.read_local(from));
90                self.jump_to(self.ip + 1)
91            }
92            Op::MovI(to, constant) => {
93                let extended = sign_extend_to!(Word, constant, IMM_BITS);
94                self.write_local(to, extended);
95                self.jump_to(self.ip + 1)
96            }
97            Op::Add(to, first, second) => {
98                let sum = self.read_local(first) + self.read_local(second);
99                self.write_local(to, sum);
100                self.jump_to(self.ip + 1)
101            }
102            Op::Ret => {
103                let frame = self.current_frame();
104                let ra = frame.return_address;
105
106                let popped = self.call_stack.pop().expect(
107                    "call stack expected to always have one frame while running.",
108                );
109                if let Some(frame_below) = self.call_stack.last_mut() {
110                    frame_below.locals[RETURN_LOCALS]
111                        .copy_from_slice(&popped.locals[RETURN_LOCALS]);
112                }
113
114                self.jump_to(ra)
115            }
116            Op::Call(offset) => {
117                let as_usize: usize = offset
118                    .try_into()
119                    .expect("illegal offset, too large for platform"); // explodes on microcontrollers
120                let extended =
121                    sign_extend_to!(InstructionAddress, as_usize, IMM_EXT_BITS);
122                let new_ip = self.ip.wrapping_add(extended);
123                // handle negative offsets
124
125                let mut new_frame = StackFrame {
126                    locals: [0; LOCALS_SIZE],
127                    return_address: self.ip + 1,
128                };
129                new_frame.locals[ARGUMENT_LOCALS].copy_from_slice(
130                    &self.current_frame().locals[ARGUMENT_LOCALS],
131                );
132
133                self.call_stack.push(new_frame);
134
135                self.jump_to(new_ip)
136            }
137            Op::Nop => Ok(()),
138        }?;
139
140        Ok(())
141    }
142
143    fn jump_to(&mut self, new_ip: usize) -> VMResult {
144        if new_ip >= self.code_length {
145            return Err(VMError::InvalidIP);
146        };
147
148        self.ip = new_ip;
149
150        Ok(())
151    }
152
153    fn read_local(&self, address: LocalAddress) -> Word {
154        self.current_frame().locals[address as usize]
155    }
156
157    fn write_local(&mut self, address: LocalAddress, value: Word) {
158        self.current_frame_mut().locals[address as usize] = value;
159    }
160
161    fn current_frame(&self) -> &StackFrame {
162        self.call_stack.last().expect(
163            "call stack expected to always have one frame while running.",
164        )
165    }
166
167    fn current_frame_mut(&mut self) -> &mut StackFrame {
168        self.call_stack.last_mut().expect(
169            "call stack expected to always have one frame while running.",
170        )
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::VM;
177    use crate::opcode::{ExtendedImmediate, Op};
178
179    #[test]
180    fn basic_program() {
181        let mut vm = VM::default();
182        vm.load(&[Op::MovI(0, 1), Op::MovI(1, 2), Op::Add(2, 0, 1), Op::Ret])
183            .expect("invalid program");
184
185        for _ in 0..3 {
186            vm.step().expect("failed to run program");
187        }
188
189        assert_eq!(1, vm.current_frame().locals[0]);
190        assert_eq!(2, vm.current_frame().locals[1]);
191        assert_eq!(3, vm.current_frame().locals[2]);
192
193        vm.step().expect("failed to run program");
194        assert!(vm.call_stack.is_empty());
195    }
196
197    #[test]
198    fn call_return() {
199        let mut vm = VM::default();
200        vm.load(&[
201            // func main
202            Op::MovI(0, 1),
203            Op::MovI(1, 2),
204            Op::Call(2), // call add
205            Op::Ret,
206            // func add
207            Op::Add(0, 0, 1),
208            Op::Ret,
209        ])
210        .expect("invalid program");
211
212        for _ in 0..5 {
213            vm.step().expect("failed to run program");
214        }
215
216        assert_eq!(3, vm.current_frame().locals[0]);
217        assert_eq!(2, vm.current_frame().locals[1]);
218
219        vm.step().expect("failed to run program");
220        assert!(vm.call_stack.is_empty());
221    }
222
223    #[test]
224    fn call_neg_offset() {
225        let mut vm = VM::default();
226        let program = [
227            // func main
228            Op::MovI(0, 3),
229            Op::Call(4), // call double
230            Op::Ret,
231            // func add
232            Op::Add(0, 0, 1),
233            Op::Ret,
234            // func double
235            Op::Mov(1, 0),
236            Op::Call((0 as ExtendedImmediate).wrapping_sub(3)), // call add
237            Op::Ret,
238        ];
239
240        vm.load(&program).expect("invalid program");
241
242        for _ in 0..7 {
243            vm.step().expect("failed to run program");
244        }
245
246        assert_eq!(6, vm.current_frame().locals[0]);
247
248        vm.step().expect("failed to run program");
249        assert!(vm.call_stack.is_empty());
250    }
251}