Files
ngr/src/eval.rs
Adam Wick bd3b9af469 🤔 Add a type inference engine, along with typed literals. (#4)
The typed literal formatting mirrors that of Rust. If no type can be
inferred for an untagged literal, the type inference engine will warn
the user and then assume that they meant an unsigned 64-bit number.
(This is slightly inconvenient, because there can be cases in which our
Arbitrary instance may generate a unary negation, in which we should
assume that it's a signed 64-bit number; we may want to revisit this
later.)

The type inference engine is a standard two phase one, in which we first
generate a series of type constraints, and then we solve those
constraints. In this particular implementation, we actually use a third
phase to generate a final AST.

Finally, to increase the amount of testing performed, I've removed the
overflow checking in the evaluator. The only thing we now check for is
division by zero. This does make things a trace slower in testing, but
hopefully we get more coverage this way.
2023-09-19 20:40:05 -07:00

121 lines
4.5 KiB
Rust

//! 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 primtype;
mod value;
use cranelift_module::ModuleError;
pub use env::{EvalEnvironment, LookupError};
pub use primop::PrimOpError;
pub use primtype::PrimitiveType;
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,
},
}
}
}