Error handling

This commit is contained in:
2022-04-25 23:29:54 -05:00
parent 46f11eee57
commit fc289a670b
3 changed files with 381 additions and 43 deletions
+130 -41
View File
@@ -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<A: Into<Word>>(&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<ExecutionError> {
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<Word> {
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<Word> {
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]
+60
View File
@@ -349,6 +349,66 @@ impl From<Opcode> 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));
+191 -2
View File
@@ -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<Item = &'a str>>(code: T) -> CPU {
@@ -25,11 +26,15 @@ fn test_rstack<'a, T: IntoIterator<Item = &'a str>>(
assert_eq!(cpu.get_call(), expected_rstack);
}
fn test_mem<'a, T: IntoIterator<Item = &'a str>>(code: T, expected_memory: BTreeMap<u32, u8>) {
fn test_mem<'a, T: IntoIterator<Item = &'a str>>(
code: T,
expected_memory: BTreeMap<u32, u8>,
) -> 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());
}