improve error handling

This commit is contained in:
Vinzenz Schroeter 2024-12-15 00:43:59 +01:00
parent 5ad6baa30d
commit a4a027b448
24 changed files with 238 additions and 112 deletions

21
Cargo.lock generated
View file

@ -165,6 +165,7 @@ version = "0.1.0"
dependencies = [
"ariadne",
"chumsky",
"thiserror",
]
[[package]]
@ -332,6 +333,26 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"

View file

@ -6,3 +6,4 @@ resolver = "2"
clap = { version = "4.5.23", features = ["derive"] }
shellwords = { version = "1.1.0" }
ariadne = { version = "0.5.0", features = ["auto-color"] }
thiserror = "2.0.6"

View file

@ -7,8 +7,6 @@ edition = "2021"
[dependencies]
chumsky = "0.9.3"
ariadne = {version = "0.5.0", optional = true}
ariadne = {version = "0.5.0"}
thiserror.workspace = true
[features]
default = ["error-report"]
error-report = ["dep:ariadne"]

View file

@ -1,5 +1,6 @@
use crate::instructions::{
try_load_binary_op, Instruction, InstructionName, ParametersError, TryLoadInstruction,
try_load_binary_op, ExecuteInstructionError, Instruction, InstructionName, InstructionResult,
ParametersError, TryLoadInstruction,
};
use crate::machine::{Machine, RegisterIndex, RegisterOrValue};
use std::fmt::Display;
@ -16,9 +17,14 @@ pub struct Addition {
}
impl Instruction for Addition {
fn execute(&self, machine: &mut Machine) {
let value = self.a.read(machine) + self.b.read(machine);
fn execute(&self, machine: &mut Machine) -> InstructionResult {
let value = self
.a
.read(machine)
.checked_add(self.b.read(machine))
.ok_or(ExecuteInstructionError::ArithmeticError)?;
self.r.write(machine, value);
Ok(())
}
}
@ -69,7 +75,8 @@ mod tests {
r: r0,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![65, 0]);
Instruction::execute(
&Addition {
@ -78,7 +85,8 @@ mod tests {
r: r1,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![65, 42]);
Instruction::execute(
&Addition {
@ -87,7 +95,8 @@ mod tests {
r: r1,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![65, 1379]);
}

View file

@ -1,5 +1,6 @@
use crate::instructions::{
try_load_binary_op, Instruction, InstructionName, ParametersError, TryLoadInstruction,
try_load_binary_op, ExecuteInstructionError, Instruction, InstructionName, InstructionResult,
ParametersError, TryLoadInstruction,
};
use crate::machine::{Machine, RegisterIndex, RegisterOrValue};
use std::fmt::{Debug, Display};
@ -16,9 +17,14 @@ pub struct Division {
}
impl Instruction for Division {
fn execute(&self, machine: &mut Machine) {
let value = self.a.read(machine) / self.b.read(machine);
self.r.write(machine, value)
fn execute(&self, machine: &mut Machine) -> InstructionResult {
let value = self
.a
.read(machine)
.checked_div(self.b.read(machine))
.ok_or(ExecuteInstructionError::ArithmeticError)?;
self.r.write(machine, value);
Ok(())
}
}
@ -69,7 +75,8 @@ mod tests {
r: r0,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![4, 0]);
Instruction::execute(
&Division {
@ -78,7 +85,8 @@ mod tests {
r: r1,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![4, -4]);
Instruction::execute(
&Division {
@ -87,7 +95,8 @@ mod tests {
r: r1,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![4, -334]);
}

View file

@ -1,5 +1,6 @@
use crate::instructions::{
check_param_count, Instruction, InstructionName, ParametersError, TryLoadInstruction,
check_param_count, Instruction, InstructionName, InstructionResult, ParametersError,
TryLoadInstruction,
};
use crate::machine::{Machine, RegisterOrValue};
use std::fmt::{Debug, Display};
@ -12,8 +13,9 @@ pub struct Jump {
}
impl Instruction for Jump {
fn execute(&self, machine: &mut Machine) {
fn execute(&self, machine: &mut Machine) -> InstructionResult {
machine.ip = self.to.read(machine) as usize;
Ok(())
}
}
@ -55,7 +57,8 @@ mod tests {
to: RegisterOrValue::Value(42),
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![0]);
assert_eq!(machine.ip, 42);
machine.registers[0].write(19);
@ -65,7 +68,8 @@ mod tests {
to: RegisterOrValue::Register(r0),
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![19]);
assert_eq!(machine.ip, 19);
}

View file

@ -1,6 +1,8 @@
use crate::instructions::{
check_param_count, Instruction, InstructionName, ParametersError, TryLoadInstruction,
check_param_count, Instruction, InstructionName, InstructionResult, ParametersError,
TryLoadInstruction,
};
use crate::isize_to_bool;
use crate::machine::{Machine, RegisterOrValue};
use std::fmt::{Debug, Display};
use std::rc::Rc;
@ -12,10 +14,11 @@ pub struct JumpIfZero {
}
impl Instruction for JumpIfZero {
fn execute(&self, machine: &mut Machine) {
if self.a.read(machine) == 0 {
fn execute(&self, machine: &mut Machine) -> InstructionResult {
if !isize_to_bool(self.a.read(machine)) {
machine.ip = self.to.read(machine) as usize;
}
Ok(())
}
}
@ -62,7 +65,8 @@ mod tests {
to: Value(42),
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![0]);
assert_eq!(machine.ip, 42);
machine.registers[0].write(19);
@ -73,7 +77,8 @@ mod tests {
to: RegisterOrValue::Register(r0),
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![19]);
assert_eq!(machine.ip, 42);
}

View file

@ -9,7 +9,6 @@ mod put_instruction;
mod sub_instruction;
mod teq_instruction;
mod tlt_instruction;
mod traits;
mod try_load;
mod tze_instruction;
@ -24,6 +23,29 @@ pub use put_instruction::*;
pub use sub_instruction::*;
pub use teq_instruction::*;
pub use tlt_instruction::*;
pub use traits::*;
pub use try_load::*;
pub use tze_instruction::*;
use crate::machine::Machine;
use std::fmt::{Debug, Display};
pub trait Instruction: Debug + Display {
// TODO: this needs to be able to return errors, e.g. for division by zero
fn execute(&self, machine: &mut Machine) -> InstructionResult;
}
type InstructionResult = Result<(), ExecuteInstructionError>;
#[derive(Debug, thiserror::Error)]
pub enum ExecuteInstructionError {
#[error("A break instruction has reached")]
Break,
#[error("An arithmetic exception occurred")]
ArithmeticError,
#[error("the current instruction pointer does not point to an instruction. Has the end of file been reached?")]
InvalidIp,
}
pub trait InstructionName {
const INSTRUCTION_NAME: &'static str;
}

View file

@ -1,5 +1,6 @@
use crate::instructions::{
try_load_binary_op, Instruction, InstructionName, ParametersError, TryLoadInstruction,
try_load_binary_op, ExecuteInstructionError, Instruction, InstructionName, InstructionResult,
ParametersError, TryLoadInstruction,
};
use crate::machine::{Machine, RegisterIndex, RegisterOrValue};
use std::fmt::{Debug, Display};
@ -16,9 +17,14 @@ pub struct Modulus {
}
impl Instruction for Modulus {
fn execute(&self, machine: &mut Machine) {
let value = self.a.read(machine) % self.b.read(machine);
self.r.write(machine, value)
fn execute(&self, machine: &mut Machine) -> InstructionResult {
let value = self
.a
.read(machine)
.checked_rem(self.b.read(machine))
.ok_or(ExecuteInstructionError::ArithmeticError)?;
self.r.write(machine, value);
Ok(())
}
}
@ -69,7 +75,8 @@ mod tests {
r: r0,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![19, 0]);
Instruction::execute(
&Modulus {
@ -78,7 +85,8 @@ mod tests {
r: r1,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![19, 19]);
Instruction::execute(
&Modulus {
@ -87,7 +95,8 @@ mod tests {
r: r1,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![19, 7]);
}

View file

@ -1,5 +1,6 @@
use crate::instructions::{
check_param_count, Instruction, InstructionName, ParametersError, TryLoadInstruction,
check_param_count, Instruction, InstructionName, InstructionResult, ParametersError,
TryLoadInstruction,
};
use crate::machine::{Machine, RegisterOrValue};
use std::fmt::{Debug, Display};
@ -9,8 +10,9 @@ use std::rc::Rc;
pub struct NoOperation {}
impl Instruction for NoOperation {
fn execute(&self, _: &mut Machine) {
fn execute(&self, _: &mut Machine) -> InstructionResult {
// intentionally left empty as this is the no operation instruction
Ok(())
}
}

View file

@ -1,5 +1,6 @@
use crate::instructions::{
check_param_count, Instruction, InstructionName, ParametersError, TryLoadInstruction,
check_param_count, Instruction, InstructionName, InstructionResult, ParametersError,
TryLoadInstruction,
};
use crate::machine::{Machine, RegisterIndex, RegisterOrValue};
use std::fmt::{Debug, Display};
@ -14,9 +15,10 @@ pub struct Put {
}
impl Instruction for Put {
fn execute(&self, machine: &mut Machine) {
fn execute(&self, machine: &mut Machine) -> InstructionResult {
let value = self.a.read(machine);
self.r.write(machine, value);
Ok(())
}
}
@ -65,7 +67,8 @@ mod tests {
r: r0,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![42, 0]);
Instruction::execute(
&Put {
@ -73,7 +76,8 @@ mod tests {
r: r1,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![42, 42]);
}
}

View file

@ -1,5 +1,6 @@
use crate::instructions::{
try_load_binary_op, Instruction, InstructionName, ParametersError, TryLoadInstruction,
try_load_binary_op, ExecuteInstructionError, Instruction, InstructionName, InstructionResult,
ParametersError, TryLoadInstruction,
};
use crate::machine::{Machine, RegisterIndex, RegisterOrValue};
use std::fmt::{Debug, Display};
@ -16,9 +17,14 @@ pub struct Subtraction {
}
impl Instruction for Subtraction {
fn execute(&self, machine: &mut Machine) {
let value = self.a.read(machine) - self.b.read(machine);
fn execute(&self, machine: &mut Machine) -> InstructionResult {
let value = self
.a
.read(machine)
.checked_sub(self.b.read(machine))
.ok_or(ExecuteInstructionError::ArithmeticError)?;
self.r.write(machine, value);
Ok(())
}
}
@ -69,7 +75,8 @@ mod tests {
r: r0,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![19, 0]);
Instruction::execute(
&Subtraction {
@ -78,7 +85,8 @@ mod tests {
r: r1,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![19, 42]);
Instruction::execute(
&Subtraction {
@ -87,7 +95,8 @@ mod tests {
r: r1,
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers.read_all(), vec![19, 1295]);
}

View file

@ -1,5 +1,7 @@
use crate::bool_to_isize;
use crate::instructions::{
check_param_count, Instruction, InstructionName, ParametersError, TryLoadInstruction,
check_param_count, Instruction, InstructionName, InstructionResult, ParametersError,
TryLoadInstruction,
};
use crate::machine::{Machine, RegisterOrValue};
use std::fmt::{Debug, Display};
@ -12,9 +14,10 @@ pub struct TestEqual {
}
impl Instruction for TestEqual {
fn execute(&self, machine: &mut Machine) {
fn execute(&self, machine: &mut Machine) -> InstructionResult {
let result = self.a.read(machine) == self.b.read(machine);
machine.registers[0].write(if result { 1 } else { 0 });
machine.registers[0].write(bool_to_isize(result));
Ok(())
}
}
@ -59,7 +62,8 @@ mod tests {
b: RegisterOrValue::Value(23),
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers[0].read(), 0);
machine.registers[0].write(-23);
@ -69,7 +73,8 @@ mod tests {
b: RegisterOrValue::Value(-23),
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers[0].read(), 1);
}

View file

@ -1,5 +1,7 @@
use crate::bool_to_isize;
use crate::instructions::{
check_param_count, Instruction, InstructionName, ParametersError, TryLoadInstruction,
check_param_count, Instruction, InstructionName, InstructionResult, ParametersError,
TryLoadInstruction,
};
use crate::machine::{Machine, RegisterOrValue};
use std::fmt::{Debug, Display};
@ -12,9 +14,10 @@ pub struct TestLessThan {
}
impl Instruction for TestLessThan {
fn execute(&self, machine: &mut Machine) {
fn execute(&self, machine: &mut Machine) -> InstructionResult {
let result = self.a.read(machine) < self.b.read(machine);
machine.registers[0].write(if result { 1 } else { 0 });
machine.registers[0].write(bool_to_isize(result));
Ok(())
}
}
@ -59,7 +62,8 @@ mod tests {
b: RegisterOrValue::Value(23),
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers[0].read(), 0);
machine.registers[0].write(-24);
@ -69,7 +73,8 @@ mod tests {
b: RegisterOrValue::Value(-23),
},
&mut machine,
);
)
.unwrap();
assert_eq!(machine.registers[0].read(), 1);
}

View file

@ -1,11 +0,0 @@
use crate::machine::Machine;
use std::fmt::{Debug, Display};
pub trait Instruction: Debug + Display {
// TODO: this needs to be able to return errors, e.g. for division by zero
fn execute(&self, machine: &mut Machine);
}
pub trait InstructionName {
const INSTRUCTION_NAME: &'static str;
}

View file

@ -5,9 +5,11 @@ pub(crate) trait TryLoadInstruction {
fn try_load(params: &[RegisterOrValue]) -> Result<Rc<Self>, ParametersError>;
}
#[derive(Debug)]
#[derive(Debug, thiserror::Error)]
pub enum ParametersError {
#[error("the instruction expects {expected} parameters, but {found} were provided")]
UnexpectedCount { expected: usize, found: usize },
#[error("unexpected type of parameter at index {index}")]
UnexpectedType { index: usize },
}

View file

@ -1,5 +1,7 @@
use crate::bool_to_isize;
use crate::instructions::{
check_param_count, Instruction, InstructionName, ParametersError, TryLoadInstruction,
check_param_count, Instruction, InstructionName, InstructionResult, ParametersError,
TryLoadInstruction,
};
use crate::machine::{Machine, RegisterOrValue};
use std::fmt::{Debug, Display};
@ -11,9 +13,10 @@ pub struct TestZero {
}
impl Instruction for TestZero {
fn execute(&self, machine: &mut Machine) {
fn execute(&self, machine: &mut Machine) -> InstructionResult {
let result = self.a.read(machine) == 0;
machine.registers[0].write(if result { 1 } else { 0 });
machine.registers[0].write(bool_to_isize(result));
Ok(())
}
}
@ -49,14 +52,15 @@ mod tests {
.unwrap();
assert_eq!(0, machine.registers[0].read());
Instruction::execute(&TestZero { a: Value(42) }, &mut machine);
Instruction::execute(&TestZero { a: Value(42) }, &mut machine).unwrap();
assert_eq!(0, machine.registers[0].read());
Instruction::execute(
&TestZero {
a: Register(RegisterIndex::R0),
},
&mut machine,
);
)
.unwrap();
assert_eq!(1, machine.registers[0].read());
}
}

View file

@ -5,3 +5,15 @@ pub mod parser;
pub(crate) mod sealed {
pub trait Sealed {}
}
pub fn isize_to_bool(value: isize) -> bool {
value != 0isize
}
pub fn bool_to_isize(value: bool) -> isize {
if value {
1isize
} else {
0isize
}
}

View file

@ -1,4 +1,4 @@
use crate::instructions::Instructions;
use crate::instructions::{ExecuteInstructionError, Instructions};
use crate::machine::Registers;
use crate::sealed::Sealed;
use std::fmt::Display;
@ -11,15 +11,15 @@ pub struct Machine {
}
impl Machine {
pub fn step(&mut self) -> bool {
pub fn step(&mut self) -> Result<(), ExecuteInstructionError> {
if self.ip >= self.instructions.len() {
return false;
return Err(ExecuteInstructionError::InvalidIp);
}
let instruction = &self.instructions[self.ip].clone();
instruction.execute(self);
instruction.execute(self)?;
self.ip += 1;
true
Ok(())
}
}
@ -115,7 +115,7 @@ pub enum MachineBuilderError {
#[cfg(test)]
mod tests {
use crate::instructions::{Addition, Instructions};
use crate::instructions::{Addition, ExecuteInstructionError, Instructions};
use crate::instructions::{Instruction, Put};
use crate::machine::register::{RegisterIndex, RegisterOrValue};
use crate::machine::MachineBuilder;
@ -145,15 +145,21 @@ mod tests {
assert_eq!(machine.ip, 0);
assert_eq!(reg_a.read(&machine), 0);
assert!(machine.step());
assert!(matches!(machine.step(), Ok(())));
assert_eq!(machine.ip, 1);
assert_eq!(reg_a.read(&machine), 42);
assert!(machine.step());
assert!(matches!(machine.step(), Ok(())));
assert_eq!(machine.ip, 2);
assert_eq!(reg_a.read(&machine), 65);
assert!(!machine.step());
assert!(!machine.step());
assert!(matches!(
machine.step(),
Err(ExecuteInstructionError::InvalidIp)
));
assert!(matches!(
machine.step(),
Err(ExecuteInstructionError::InvalidIp)
));
}
}

View file

@ -4,10 +4,10 @@ use crate::instructions::{
TryLoadInstruction,
};
use crate::parser;
use crate::parser::ei_ast::{Line};
use crate::parser::ei_ast::Line;
use crate::parser::ParseError;
use std::io::Write;
use std::rc::Rc;
use crate::parser::ParseError;
pub struct EiParser {
file_name: String,
@ -84,10 +84,7 @@ impl EiParser {
illegal => return Err(TryLoadErrorReason::IllegalInstruction(illegal.to_string())),
})
}
}
#[cfg(feature = "error-report")]
impl EiParser {
pub fn write_error_report<W: Write>(
&self,
ei_parser_error: EiParserError,
@ -115,7 +112,7 @@ impl EiParser {
use ariadne::{Color, Label, Report, ReportKind, Source};
let report = Report::build(ReportKind::Error, (&self.file_name, e.line.span.clone()))
.with_message("Loading of instructions failed")
.with_message(format!("{e}"))
.with_config(Self::get_ariadne_config());
let report = match e.reason {
@ -142,14 +139,12 @@ impl EiParser {
.with_color(Color::Red)
.with_message("not enough parameters")
};
report.with_label(label).with_note(format!(
"the instruction expects {expected} parameters, but {found} were provided."
))
report.with_label(label)
}
ParametersError::UnexpectedType { index } => {
let label = Label::new((&self.file_name, e.line.parameters[index].1.clone()))
.with_color(Color::Red)
.with_message("unexpected type of parameter");
.with_message(format!("{param_err}"));
report.with_label(label).with_help(
"check the parameter order and which ones can values and/or registers",
)
@ -162,7 +157,6 @@ impl EiParser {
.write((&self.file_name, Source::from(&self.file_content)), w)
}
#[cfg(feature = "error-report")]
pub fn write_parse_error_report<W: std::io::Write>(
&self,
e: ParseError,
@ -246,21 +240,19 @@ pub enum EiParserError {
InstructionLoadError(TryLoadError),
}
#[derive(Debug)]
#[derive(Debug, thiserror::Error)]
#[error("The loading of the instruction at index {index} failed: {reason}")]
pub struct TryLoadError {
pub index: usize,
pub line: Line,
#[source]
pub reason: TryLoadErrorReason,
}
#[derive(Debug)]
#[derive(Debug, thiserror::Error)]
pub enum TryLoadErrorReason {
#[error("Illegal instruction \"{0}\"")]
IllegalInstruction(String),
InvalidParameters(ParametersError),
}
impl From<ParametersError> for TryLoadErrorReason {
fn from(reason: ParametersError) -> Self {
TryLoadErrorReason::InvalidParameters(reason)
}
#[error(transparent)]
InvalidParameters(#[from] ParametersError),
}

View file

@ -14,7 +14,7 @@ struct Cli {
fn main() {
let args: Cli = clap::Parser::parse();
let parser = EiParser::from_file(&args.ei_file).unwrap();
let parser = EiParser::from_file(&args.ei_file).expect("failed to parse input file");
let instructions = match parser.parse() {
Ok(i) => i,
Err(e) => {
@ -33,11 +33,19 @@ fn main() {
println!("done building machine: \n{machine}");
while machine.step() {
if args.verbose {
println!("executed instruction: {machine:?}");
loop {
match machine.step() {
Ok(_) => {
if args.verbose {
println!("executed instruction: {machine:?}");
}
}
Err(e) => {
eprintln!("{e}");
break;
}
}
}
println!("final state: \n{machine}");
println!("final state: \n{}", machine.registers);
}

View file

@ -1,3 +1,4 @@
use echse::instructions::ExecuteInstructionError;
use echse::machine::{Machine, MachineBuilder};
use echse::parser::EiParser;
use std::collections::HashSet;
@ -129,7 +130,10 @@ impl App {
"{:03}: {}\n",
self.machine.ip, self.machine.instructions[self.machine.ip]
);
self.machine.step();
match self.machine.step() {
Ok(_) => {}
Err(e) => Self::print_exec_err(e),
}
} else {
println!("reached end of program\n");
}
@ -153,7 +157,12 @@ impl App {
}
DebugCommand::Continue => {
let mut count = 0;
while self.machine.step() {
loop {
match self.machine.step() {
Ok(_) => {}
Err(e) => eprintln!("{e}"),
}
count += 1;
if self.breakpoints.contains(&self.machine.ip) {
println!(
@ -163,6 +172,7 @@ impl App {
break;
}
}
println!(
"stepped {count} instructions, ip now at {}\n",
self.machine.ip
@ -188,7 +198,7 @@ impl App {
fn main() {
let args: Cli = clap::Parser::parse();
let parser = EiParser::from_file(&args.ei_file).unwrap();
let parser = EiParser::from_file(&args.ei_file).expect("failed to parse input file");
let instructions = match parser.parse() {
Ok(i) => i,
Err(e) => {