From fc289a670b4304fb008d03edc6e010e7adee17ea Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Mon, 25 Apr 2022 23:29:54 -0500 Subject: [PATCH] Error handling --- vcore/src/cpu.rs | 171 ++++++++++++++++++++++------- vcore/src/opcodes.rs | 60 ++++++++++ vtest/src/integration_tests.rs | 193 ++++++++++++++++++++++++++++++++- 3 files changed, 381 insertions(+), 43 deletions(-) diff --git a/vcore/src/cpu.rs b/vcore/src/cpu.rs index b271acd..bd9b44f 100644 --- a/vcore/src/cpu.rs +++ b/vcore/src/cpu.rs @@ -11,9 +11,11 @@ pub struct CPU { pc: Word, // program counter, address of the low byte of the instruction dp: Word, // data pointer, address of the low byte of one cell above the data stack sp: Word, // stack pointer, address of the low byte of the return stack - iv: Word, // interrupt vector + iv: [Word; 256], // interrupt vectors int_enabled: bool, // interrupt enable bit halted: bool, // Whether the CPU is halted + sp_top: Word, // The last value given for the sp, or 0x400 + dp_btm: Word, // The last value given for the dp, or 0x100 } #[derive(Copy, Clone, Eq, PartialEq, Debug)] @@ -23,6 +25,14 @@ struct Instruction { length: u8, } +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +enum ExecutionError { + DivZero, + DataUnderflow, + StackUnderflow, + Overflow, +} + impl PeekPoke for CPU { fn peek(&self, addr: Word) -> u8 { self.memory.peek(addr) @@ -45,9 +55,11 @@ impl CPU { pc: 1024.into(), dp: 256.into(), sp: 1024.into(), - iv: 1024.into(), + iv: [1024.into(); 256], int_enabled: false, halted: true, + sp_top: 0x400.into(), + dp_btm: 0x100.into(), } } @@ -55,9 +67,11 @@ impl CPU { self.pc = 1024.into(); self.dp = 256.into(); self.sp = 1024.into(); - self.iv = 1024.into(); + self.iv = [1024.into(); 256]; self.int_enabled = false; self.halted = true; + self.sp_top = 0x400.into(); + self.dp_btm = 0x100.into(); } fn push_data>(&mut self, word: A) { @@ -127,7 +141,7 @@ impl CPU { self.push_data(arg) } - if instruction.opcode.is_binary() { + if instruction.opcode.arity() == 2 { let x = self.pop_data(); let y = self.pop_data(); @@ -165,7 +179,9 @@ impl CPU { Opcode::Storew => self.memory.poke24(x, y), Opcode::Setsdp => { self.dp = x; - self.sp = y + self.sp = y; + self.dp_btm = self.dp; + self.sp_top = self.sp } Opcode::Brz => { if y == 0 { @@ -177,13 +193,18 @@ impl CPU { return self.pc + i32::from(x); } } + Opcode::Setiv => { + self.iv[u8::from(x) as usize] = y; + } _ => unreachable!(), } self.pc + instruction.length as i32 } else { match instruction.opcode { Opcode::Nop => { /* No action required */ } - Opcode::Rand => {} // TODO remove this whole instruction + Opcode::Rand => { + todo!() + } Opcode::Not => { let x = self.pop_data(); self.push_data(x == 0) @@ -194,8 +215,12 @@ impl CPU { Opcode::Dup => self.push_data(self.peek_data()), Opcode::Pick => { let index = self.pop_data(); - let val = self.memory.peek24(self.dp - (i32::from(index) + 1) * 3); - self.push_data(val) + let addr = self.dp - (i32::from(index) + 1) * 3; + if addr >= self.dp_btm { + self.push_data(self.memory.peek24(addr)); + } else { + self.push_data(0); + } } Opcode::Rot => { let x = self.pop_data(); @@ -229,7 +254,6 @@ impl CPU { let x = self.pop_data(); self.int_enabled = x != 0; } - Opcode::Setiv => self.iv = self.pop_data(), Opcode::Sdp => { self.push_data(self.sp); self.push_data(self.dp + 3) // The +3 accounts for the word we're about to push @@ -253,6 +277,89 @@ impl CPU { } } + /// Checks if an instruction would cause an error if executed, and returns it if so. + /// There are four possible error conditions, evaluated in order: + /// + /// - If an instruction requires more stack space after execution than exists + /// - If an instruction would pop more from the data stack than is on it + /// - If an instruction would pop more from the return stack than is on it + /// - If an instruction would divide (or modulus) by zero + /// + /// So, running `div` on a stack containing a single 0 is a `DataUnderflow`, not a `DivZero`. + /// + /// Also, this function doesn't consider the requirements for actually handling the error, which + /// could change a `DivZero` into an `Overflow`: handling any error consumes one more stack cell, + /// for the new return address when calling the interrupt handler. So, although this may + /// return `DivZero` the actual error thrown may be `Overflow`. + fn error(&self, instruction: Instruction) -> Option { + use ExecutionError::*; + use Opcode::{Div, Mod}; + let Instruction { opcode, arg, .. } = instruction; + let data_height: usize = usize::from(self.dp - self.dp_btm) / 3; + let stack_height: usize = usize::from(self.sp_top - self.sp) / 3; + let room: i32 = i32::from(self.sp - self.dp) / 3; + let net = opcode.net_stack() + arg.map_or(0, |_| 1); + + // If we're out of room, any instruction with an arg is out. Also, if the total amount + // we'll add to the stack (including arg) overflows, then we overflow + if arg.is_some() && room < 1 || net > room { + return Some(Overflow); + } + + // Figure out if there might be an underflow + let provided_args = data_height + if arg.is_some() { 1 } else { 0 }; + + // Arity taller than data stack? + if opcode.arity() > provided_args { + return Some(DataUnderflow); + } + + // Underflowing the rstack? + if opcode.r_arity() > stack_height { + return Some(StackUnderflow); + } + + // The top of the stack during the instruction + let top = arg.unwrap_or_else(|| self.peek_data()); + + // Dividing by zero? + if (opcode == Div || opcode == Mod) && top == 0u32 { + return Some(DivZero); + } + + None + } + + fn handle_error(&mut self, error: ExecutionError) -> Word { + // If the error is not an overflow then we still might turn it into one: we require one new + // stack frame to handle every non-overflow error (for the new return stack cell) so if we + // don't have that we'll just pretend this is an overflow. + if self.dp >= self.sp && error != ExecutionError::Overflow { + return self.handle_error(ExecutionError::Overflow); + } + + // The procedure for handling an overflow is a bit different, because we need to have some + // stack available to do it. First, we reset the stack to its original size, and then we + // put the old sp and dp on the data stack: + if error == ExecutionError::Overflow { + let (old_sp, old_dp) = (self.sp, self.dp); + (self.sp, self.dp) = (self.sp_top, self.dp_btm); + self.push_data(old_sp); + self.push_data(old_dp); + } + + // Now we have room to handle whatever this is, so, handle it: + let int = match error { + ExecutionError::DivZero => 0, + ExecutionError::DataUnderflow => 1, + ExecutionError::StackUnderflow => 2, + ExecutionError::Overflow => 3, + }; + self.int_enabled = false; + self.push_call(self.pc); + self.iv[int] + } + pub fn tick(&mut self) { if self.halted { return; @@ -260,8 +367,11 @@ impl CPU { match self.fetch() { Ok(instr) => { - //println!("Instr: {}", instr.opcode); - self.pc = self.execute(instr) + self.pc = if let Some(err) = self.error(instr) { + self.handle_error(err) + } else { + self.execute(instr) + } } Err(invalid_opcode) => { @@ -281,14 +391,14 @@ impl CPU { pub fn run_to_halt(&mut self) { self.start(); - while !self.halted && self.pc < 1100 { + while !self.halted { self.tick() } } pub fn get_stack(&self) -> Vec { let mut v = Vec::new(); - let mut curr = Word::from(256); + let mut curr = Word::from(self.dp_btm); while curr < self.dp { v.push(self.memory.peek24(curr)); curr += 3 @@ -298,39 +408,16 @@ impl CPU { pub fn get_call(&self) -> Vec { let mut v = Vec::new(); - let mut curr = Word::from(1024); + let mut curr = Word::from(self.sp_top); while curr > self.sp { curr -= 3; v.push(self.memory.peek24(curr)); } v } -} -impl Opcode { - fn is_binary(self) -> bool { - use Opcode::*; - self != Nop - && self != Not - && self != Rand - && self != Pop - && self != Dup - && self != Pick - && self != Rot - && self != Jmp - && self != Jmpr - && self != Call - && self != Ret - && self != Hlt - && self != Load - && self != Loadw - && self != Setint - && self != Setiv - && self != Sdp - && self != Pushr - && self != Popr - && self != Peekr - && self != Debug + pub fn sdp(&self) -> (Word, Word) { + (self.sp, self.dp) } } @@ -459,6 +546,8 @@ mod tests { simple_opcode_test(vec![5], Dup, vec![5, 5]); simple_opcode_test(vec![5, 3], Swap, vec![3, 5]); simple_opcode_test(vec![10, 20, 30, 2], Pick, vec![10, 20, 30, 10]); + simple_opcode_test(vec![10, 0], Pick, vec![10, 10]); + simple_opcode_test(vec![10, 1], Pick, vec![10, 0]); simple_opcode_test(vec![1, 4, 9], Rot, vec![4, 9, 1]); simple_opcode_test(vec![1, 4, 9], Pop, vec![1, 4]); } @@ -567,9 +656,9 @@ mod tests { #[test] fn test_cpu_reset() { let mut cpu = CPU::new(Memory::default()); - cpu.iv = 12345.into(); + cpu.iv[2] = 12345.into(); cpu.reset(); - assert_eq!(cpu.iv, 1024); + assert_eq!(cpu.iv[2], 1024); } #[test] diff --git a/vcore/src/opcodes.rs b/vcore/src/opcodes.rs index c2cef20..c79d239 100644 --- a/vcore/src/opcodes.rs +++ b/vcore/src/opcodes.rs @@ -349,6 +349,66 @@ impl From for u8 { } } +impl Opcode { + /// The number of things required to be on the data stack for this opcode to function. + pub fn arity(self) -> usize { + use Opcode::*; + match self { + Nop | Rand | Ret | Hlt | Sdp | Popr | Peekr | Debug => 0, + + Not | Pop | Dup | Jmp | Jmpr | Call | Load | Loadw | Setint | Pushr | Pick => 1, + + Add | Sub | Mul | Div | Mod | And | Or | Xor | Gt | Lt | Agt | Alt | Lshift + | Rshift | Arshift | Swap | Brz | Brnz | Store | Storew | Setsdp | Setiv => 2, + + Rot => 3, + } + } + + /// The number of things required to be on the return stack for this opcode to function. + pub fn r_arity(self) -> usize { + use Opcode::*; + + match self { + Peekr | Popr | Ret => 1, + + Nop | Rand | Hlt | Sdp | Debug | Not | Pop | Dup | Jmp | Jmpr | Call | Load | Loadw + | Setint | Setiv | Pushr | Add | Sub | Mul | Div | Mod | And | Or | Xor | Gt | Lt + | Agt | Alt | Lshift | Rshift | Arshift | Swap | Brz | Brnz | Store | Storew + | Setsdp | Rot | Pick => 0, + } + } + + /// The net change in stack usage (in cells) after executing this instruction. This is used to + /// tell whether this instruction will cause an overflow. Execution happens in three phases: + /// first the argument is pushed (which might trigger overflow); then the operands are popped + /// (which cannot overflow); then the result is pushed (which might overflow). This fn tells + /// you the number of result cells minus the number of operand cells, so, the delta in stack + /// usage for steps 2 and 3. Additionally, this fn counts data and return stack cells together, + /// so, + /// ``` + /// # use vcore::opcodes::Opcode; + /// assert_eq!(Opcode::Pushr.net_stack(), 0) + /// ``` + /// because moving a cell from data to return doesn't increase stack usage. + pub fn net_stack(self) -> i32 { + use Opcode::*; + + match self { + Sdp => 2, + + Peekr | Rand | Dup => 1, + + Nop | Hlt | Debug | Not | Call | Load | Loadw | Pushr | Popr | Pick | Rot | Swap => 0, + + Pop | Jmp | Jmpr | Add | Sub | Mul | Div | Mod | And | Or | Xor | Gt | Lt | Agt + | Alt | Lshift | Rshift | Arshift | Ret => -1, + + Setint | Store | Storew | Setsdp | Setiv | Brz | Brnz => -2, + } + } +} + #[test] fn test_decode() { assert_eq!(Opcode::try_from(18), Ok(Opcode::Pop)); diff --git a/vtest/src/integration_tests.rs b/vtest/src/integration_tests.rs index 84458ed..048541b 100644 --- a/vtest/src/integration_tests.rs +++ b/vtest/src/integration_tests.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use vasm::assemble_snippet; use vcore::memory::{Memory, PeekPokeExt}; +use vcore::word::Word; use vcore::CPU; fn cpu_test<'a, T: IntoIterator>(code: T) -> CPU { @@ -25,11 +26,15 @@ fn test_rstack<'a, T: IntoIterator>( assert_eq!(cpu.get_call(), expected_rstack); } -fn test_mem<'a, T: IntoIterator>(code: T, expected_memory: BTreeMap) { +fn test_mem<'a, T: IntoIterator>( + code: T, + expected_memory: BTreeMap, +) -> CPU { let cpu = cpu_test(code); for (addr, byte) in expected_memory { assert_eq!(cpu.peek8(addr), byte) } + cpu } #[test] @@ -174,6 +179,17 @@ fn test_pick() { hlt" .lines(), vec![10, 20, 10], + ); + + test_stack( + ".org 1024 + nop 10 + nop 20 + pick 2 + pick 5 + hlt" + .lines(), + vec![10, 20, 0, 0], ) } @@ -188,7 +204,7 @@ fn test_store() { hlt" .lines(), [(201, 10), (203, 0x56), (204, 0x34), (205, 0x12)].into(), - ) + ); } #[test] @@ -232,3 +248,176 @@ fn test_sdp() { vec![10, 1021, 265], ) } + +#[test] +fn test_underflow() { + test_mem( + ".org 0x400 + push 100 + store 10 + push onunder + setiv 1 + add 3 + hlt + onunder: push 200 + store 10 + hlt" + .lines(), + [(10, 200)].into(), + ); + + test_mem( + ".org 0x400 + push 100 + store 10 + push onrunder + setiv 2 + peekr + hlt + onrunder: push 200 + store 10 + hlt" + .lines(), + [(10, 200)].into(), + ); + + test_mem( + ".org 0x400 + push 100 + store 10 + push onunder + setiv 1 + div 0 ; This div should be an underflow, not a div 0 + hlt + onunder: push 200 + store 10 + hlt" + .lines(), + [(10, 200)].into(), + ); +} + +#[test] +fn test_div_zero() { + test_mem( + ".org 0x400 + push 100 + store 10 + push ondiv0 + setiv 0 + push 5 + div 0 + hlt + ondiv0: push 200 + store 10 + hlt" + .lines(), + [(10, 200)].into(), + ); + + test_mem( + ".org 0x400 + push 100 + store 10 + push ondiv0 + setiv 0 + push 5 + mod 0 ; Mods can raise this too + hlt + ondiv0: push 200 + store 10 + hlt" + .lines(), + [(10, 200)].into(), + ); +} + +#[test] +fn test_overflow() { + let cpu = test_mem( + ".org 0x400 + push onover + setiv 3 ; set the overflow handler + push 25 + setsdp 10 ; new stack of five cells + pushr 0 + dup 0 ; spare room for handler; 2 cells left + push 1 + push 2 ; fine so far + .org 0x4ff ; bunch of nops and then... + push 3 ; boom! + store ; never happens + onover: hlt" + .lines(), + [(16, 1), (19, 2)].into(), // expect the two successful pushes to be still there + ); + + // Data stack now contains the collided pointers + assert_eq!(vec![Word::from(22), Word::from(22)], cpu.get_stack()); + + // Call stack contains the address of the offending instruction + assert_eq!(vec![Word::from(0x4ff)], cpu.get_call()); + + // Stack is now the previous bottom values, minus three cells: + assert_eq!((22.into(), 16.into()), cpu.sdp()); +} + +#[test] +fn test_simple_overflow() { + let cpu = test_mem( + ".org 0x400 + push onover + setiv 3 ; set the overflow handler + push 25 + setsdp 10 ; new stack of five cells + pushr 0 + dup 0 ; spare room for handler; 2 cells left + push 1 + push 2 ; fine so far + .org 0x4ff ; bunch of nops and then... + add 5 ; full stack + argument! + store ; never happens + onover: hlt" + .lines(), + [(16, 1), (19, 2)].into(), // expect the two successful pushes to be still there + ); + + // Data stack now contains the collided pointers + assert_eq!(vec![Word::from(22), Word::from(22)], cpu.get_stack()); + + // Call stack contains the address of the offending instruction + assert_eq!(vec![Word::from(0x4ff)], cpu.get_call()); + + // Stack is now the previous bottom values, minus three cells: + assert_eq!((22.into(), 16.into()), cpu.sdp()); +} + +#[test] +fn test_overflow_promotion() { + let cpu = test_mem( + ".org 0x400 + push onover + setiv 3 ; set the overflow handler + push 25 + setsdp 10 ; new stack of five cells + pushr 0 + dup 0 ; spare room for handler; 2 cells left + push 1 + push 0 ; fine so far + .org 0x4ff ; bunch of nops and then... + div ; This is a div 0, but there's no room to handle it, so it's promoted to overflow + store ; never happens + onover: hlt" + .lines(), + [(16, 1), (19, 0)].into(), // expect the two successful pushes to be still there + ); + + // Data stack now contains the collided pointers + assert_eq!(vec![Word::from(22), Word::from(22)], cpu.get_stack()); + + // Call stack contains the address of the offending instruction + assert_eq!(vec![Word::from(0x4ff)], cpu.get_call()); + + // Stack is now the previous bottom values, minus three cells: + assert_eq!((22.into(), 16.into()), cpu.sdp()); +}