//! 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; use cranelift_module::ModuleError; pub use env::{EvalEnvironment, LookupError}; pub use primop::PrimOpError; 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)] Lookup(#[from] LookupError), #[error(transparent)] PrimOp(#[from] PrimOpError), #[error(transparent)] Backend(#[from] BackendError), #[error("IO error: {0}")] IO(#[from] std::io::Error), #[error(transparent)] Module(#[from] ModuleError), #[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 { fn eq(&self, other: &Self) -> bool { match self { EvalError::Lookup(a) => match other { EvalError::Lookup(b) => a == b, _ => false, }, EvalError::PrimOp(a) => match other { EvalError::PrimOp(b) => a == b, _ => false, }, EvalError::Backend(a) => match other { EvalError::Backend(b) => a == b, _ => false, }, EvalError::IO(a) => match other { EvalError::IO(b) => a.to_string() == b.to_string(), _ => false, }, EvalError::Module(a) => match other { 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, }, } } }