From c3b394d2c973be7fcd03cd400f73e6cbbcff86a0 Mon Sep 17 00:00:00 2001 From: Adam Wick Date: Thu, 27 Apr 2023 21:06:03 -0700 Subject: [PATCH] Add some documentation, and clean up EvalError a bit. --- src/backend/eval.rs | 9 ++---- src/eval.rs | 72 ++++++++++++++++++++++++++++++++++++++++----- src/eval/primop.rs | 6 +++- 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/src/backend/eval.rs b/src/backend/eval.rs index 6df2f5e..e9c88f1 100644 --- a/src/backend/eval.rs +++ b/src/backend/eval.rs @@ -67,13 +67,10 @@ impl Backend { if output.status.success() { Ok(std::string::String::from_utf8_lossy(&output.stdout).to_string()) } else { - Err(EvalError::IO(format!( - "Exitted with error code {}", - output.status - ))) + Err(EvalError::ExitCode(output.status)) } } else { - Err(EvalError::IO( + Err(EvalError::RuntimeOutput( std::string::String::from_utf8_lossy(&output.stderr).to_string(), )) } @@ -105,7 +102,7 @@ impl Backend { .output()?; if !output.stderr.is_empty() { - return Err(EvalError::IO( + return Err(EvalError::Linker( std::string::String::from_utf8_lossy(&output.stderr).to_string(), )); } diff --git a/src/eval.rs b/src/eval.rs index 017c9b6..145b9e1 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,4 +1,38 @@ //! Helpful functions for evaluating NGR programs. +//! +//! Look, this is a compiler, and so you might be asking why it has a bunch of +//! stuff in it to help with writing interpreters. Well, the answer is simple: +//! testing. It's really nice to know that if you start with a program that +//! does a thing, and then you muck with it, you end up with a program that does +//! the exact same thing. If you talk to people who think about language +//! semantics, they'll call this "observational equivalence": maybe the two +//! programs don't do 100% the same things in the same order, but you shouldn't +//! be able to observe the difference ... at least, not without a stopwatch, +//! memory profilers, etc. +//! +//! The actual evaluators for our various syntaxes are hidden in `eval` functions +//! of the various ASTs. It's nice to have them "next to" the syntax that way, so +//! that we just edit stuff in one part of the source tree at a time. This module, +//! then, just contains some things that are generally helpful across all the +//! interpreters we've written. +//! +//! In particular, this module helps with: +//! +//! * Defining a common error type -- [`EvalError`] -- that we can reasonably +//! compare. It's nice to compare errors, here, because we want to know that +//! if a program used to fail, it will still fail after we change it, and +//! fail in the exact same way. +//! * Defining a notion of a binding environment: [`EvalEnvironment`]. This +//! will help us keep track of variables bound in our program, as we run it. +//! * Defining a notion of a runtime value: [`Value`]. Yes, this is the +//! umpteenth time that we're re-defining basically the same enumeration +//! with exactly the same name, but it's nice to have it separated so that +//! we don't confuse them. +//! * Finally, this module implements all of our primitive functions, as the +//! [`Value::calculate`] function. This is just a nice abstraction boundary, +//! because the implementation of some parts of these primitives is really +//! awful to look at. +//! mod env; mod primop; mod value; @@ -10,6 +44,13 @@ pub use value::Value; use crate::backend::BackendError; +/// All of the errors that can happen trying to evaluate an NGR program. +/// +/// This is yet another standard [`thiserror::Error`] type, but with the +/// caveat that it implements [`PartialEq`] even though some of its +/// constituent members don't. It does so through the very sketchy mechanism +/// of converting those errors to strings and then seeing if they're the +/// same. #[derive(Debug, thiserror::Error)] pub enum EvalError { #[error(transparent)] @@ -19,15 +60,15 @@ pub enum EvalError { #[error(transparent)] Backend(#[from] BackendError), #[error("IO error: {0}")] - IO(String), + IO(#[from] std::io::Error), #[error(transparent)] Module(#[from] ModuleError), -} - -impl From for EvalError { - fn from(value: std::io::Error) -> Self { - EvalError::IO(value.to_string()) - } + #[error("Linker error: {0}")] + Linker(String), + #[error("Program exitted with status {0}")] + ExitCode(std::process::ExitStatus), + #[error("Unexpected output at runtime: {0}")] + RuntimeOutput(String), } impl PartialEq for EvalError { @@ -49,7 +90,7 @@ impl PartialEq for EvalError { }, EvalError::IO(a) => match other { - EvalError::IO(b) => a == b, + EvalError::IO(b) => a.to_string() == b.to_string(), _ => false, }, @@ -57,6 +98,21 @@ impl PartialEq for EvalError { EvalError::Module(b) => a.to_string() == b.to_string(), _ => false, }, + + EvalError::Linker(a) => match other { + EvalError::Linker(b) => a == b, + _ => false, + } + + EvalError::ExitCode(a) => match other { + EvalError::ExitCode(b) => a == b, + _ => false, + } + + EvalError::RuntimeOutput(a) => match other { + EvalError::RuntimeOutput(b) => a == b, + _ => false, + } } } } diff --git a/src/eval/primop.rs b/src/eval/primop.rs index 3540db3..e60d39c 100644 --- a/src/eval/primop.rs +++ b/src/eval/primop.rs @@ -68,7 +68,11 @@ impl Value { /// Calculate the result of running the given primitive on the given arguments. /// /// This can cause errors in a whole mess of ways, so be careful about your - /// inputs. + /// inputs. For example, addition only works when the two values have the exact + /// same type, so expect an error if you try to do so. In addition, this + /// implementation catches and raises an error on overflow or underflow, so + /// its worth being careful to make sure that your inputs won't cause either + /// condition. pub fn calculate(operation: &str, values: Vec) -> Result { if values.len() == 2 { Value::binary_op(operation, &values[0], &values[1])