Add some documentation, and clean up EvalError a bit.
This commit is contained in:
@@ -67,13 +67,10 @@ impl Backend<ObjectModule> {
|
|||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
Ok(std::string::String::from_utf8_lossy(&output.stdout).to_string())
|
Ok(std::string::String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
} else {
|
} else {
|
||||||
Err(EvalError::IO(format!(
|
Err(EvalError::ExitCode(output.status))
|
||||||
"Exitted with error code {}",
|
|
||||||
output.status
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(EvalError::IO(
|
Err(EvalError::RuntimeOutput(
|
||||||
std::string::String::from_utf8_lossy(&output.stderr).to_string(),
|
std::string::String::from_utf8_lossy(&output.stderr).to_string(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -105,7 +102,7 @@ impl Backend<ObjectModule> {
|
|||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.stderr.is_empty() {
|
if !output.stderr.is_empty() {
|
||||||
return Err(EvalError::IO(
|
return Err(EvalError::Linker(
|
||||||
std::string::String::from_utf8_lossy(&output.stderr).to_string(),
|
std::string::String::from_utf8_lossy(&output.stderr).to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
72
src/eval.rs
72
src/eval.rs
@@ -1,4 +1,38 @@
|
|||||||
//! Helpful functions for evaluating NGR programs.
|
//! 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 env;
|
||||||
mod primop;
|
mod primop;
|
||||||
mod value;
|
mod value;
|
||||||
@@ -10,6 +44,13 @@ pub use value::Value;
|
|||||||
|
|
||||||
use crate::backend::BackendError;
|
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)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum EvalError {
|
pub enum EvalError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
@@ -19,15 +60,15 @@ pub enum EvalError {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Backend(#[from] BackendError),
|
Backend(#[from] BackendError),
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
IO(String),
|
IO(#[from] std::io::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Module(#[from] ModuleError),
|
Module(#[from] ModuleError),
|
||||||
}
|
#[error("Linker error: {0}")]
|
||||||
|
Linker(String),
|
||||||
impl From<std::io::Error> for EvalError {
|
#[error("Program exitted with status {0}")]
|
||||||
fn from(value: std::io::Error) -> Self {
|
ExitCode(std::process::ExitStatus),
|
||||||
EvalError::IO(value.to_string())
|
#[error("Unexpected output at runtime: {0}")]
|
||||||
}
|
RuntimeOutput(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for EvalError {
|
impl PartialEq for EvalError {
|
||||||
@@ -49,7 +90,7 @@ impl PartialEq for EvalError {
|
|||||||
},
|
},
|
||||||
|
|
||||||
EvalError::IO(a) => match other {
|
EvalError::IO(a) => match other {
|
||||||
EvalError::IO(b) => a == b,
|
EvalError::IO(b) => a.to_string() == b.to_string(),
|
||||||
_ => false,
|
_ => false,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -57,6 +98,21 @@ impl PartialEq for EvalError {
|
|||||||
EvalError::Module(b) => a.to_string() == b.to_string(),
|
EvalError::Module(b) => a.to_string() == b.to_string(),
|
||||||
_ => false,
|
_ => 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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,11 @@ impl Value {
|
|||||||
/// Calculate the result of running the given primitive on the given arguments.
|
/// 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
|
/// 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<Value>) -> Result<Value, PrimOpError> {
|
pub fn calculate(operation: &str, values: Vec<Value>) -> Result<Value, PrimOpError> {
|
||||||
if values.len() == 2 {
|
if values.len() == 2 {
|
||||||
Value::binary_op(operation, &values[0], &values[1])
|
Value::binary_op(operation, &values[0], &values[1])
|
||||||
|
|||||||
Reference in New Issue
Block a user