🧪 Add evaluation tests to ensure that passes retain NGR semantics. (#2)
This change adds `Arbitrary` instances to the key IR data types (both as syntax and as native IR), as well as evaluator functions for both. This way, we can ensure that the evaluation of one version of the IR is observationally equivalent to another version of the IR, or even a later IR. It also adds a similar ability through both static file compilation and the JIT, to ensure that the translation through Cranelift and our runtime works as expected. This actually found a couple issues in its creation, and I hope is helpful extensions into more interesting programs.
This commit is contained in:
93
src/eval/env.rs
Normal file
93
src/eval/env.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use crate::eval::Value;
|
||||
use internment::ArcIntern;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct EvalEnvironment {
|
||||
inner: Arc<EvalEnvInternal>,
|
||||
}
|
||||
|
||||
pub enum EvalEnvInternal {
|
||||
Empty,
|
||||
Value(ArcIntern<String>, Value, Arc<EvalEnvInternal>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
|
||||
pub enum LookupError {
|
||||
#[error("Could not find variable '{0}' in environment")]
|
||||
CouldNotFind(ArcIntern<String>),
|
||||
}
|
||||
|
||||
impl Default for EvalEnvironment {
|
||||
fn default() -> Self {
|
||||
EvalEnvironment::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl EvalEnvironment {
|
||||
pub fn empty() -> Self {
|
||||
EvalEnvironment {
|
||||
inner: Arc::new(EvalEnvInternal::Empty),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extend(&self, name: ArcIntern<String>, value: Value) -> Self {
|
||||
EvalEnvironment {
|
||||
inner: Arc::new(EvalEnvInternal::Value(name, value, self.inner.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lookup(&self, n: ArcIntern<String>) -> Result<Value, LookupError> {
|
||||
self.inner.lookup(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl EvalEnvInternal {
|
||||
fn lookup(&self, n: ArcIntern<String>) -> Result<Value, LookupError> {
|
||||
match self {
|
||||
EvalEnvInternal::Empty => Err(LookupError::CouldNotFind(n)),
|
||||
EvalEnvInternal::Value(name, value, _) if *name == n => Ok(value.clone()),
|
||||
EvalEnvInternal::Value(_, _, rest) => rest.lookup(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use internment::ArcIntern;
|
||||
|
||||
use super::EvalEnvironment;
|
||||
|
||||
#[test]
|
||||
fn simple_lookups() {
|
||||
let tester = EvalEnvironment::default();
|
||||
let tester = tester.extend(arced("foo"), 1i64.into());
|
||||
let tester = tester.extend(arced("bar"), 2i64.into());
|
||||
let tester = tester.extend(arced("goo"), 5i64.into());
|
||||
|
||||
assert_eq!(tester.lookup(arced("foo")), Ok(1.into()));
|
||||
assert_eq!(tester.lookup(arced("bar")), Ok(2.into()));
|
||||
assert_eq!(tester.lookup(arced("goo")), Ok(5.into()));
|
||||
assert!(tester.lookup(arced("baz")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested() {
|
||||
let tester = EvalEnvironment::default();
|
||||
let tester = tester.extend(arced("foo"), 1i64.into());
|
||||
|
||||
check_nested(&tester);
|
||||
|
||||
assert_eq!(tester.lookup(arced("foo")), Ok(1.into()));
|
||||
assert!(tester.lookup(arced("bar")).is_err());
|
||||
}
|
||||
|
||||
fn check_nested(env: &EvalEnvironment) {
|
||||
let nested_env = env.extend(arced("bar"), 2i64.into());
|
||||
assert_eq!(nested_env.lookup(arced("foo")), Ok(1.into()));
|
||||
assert_eq!(nested_env.lookup(arced("bar")), Ok(2.into()));
|
||||
}
|
||||
|
||||
fn arced(s: &str) -> ArcIntern<String> {
|
||||
ArcIntern::new(s.to_string())
|
||||
}
|
||||
}
|
||||
65
src/eval/primop.rs
Normal file
65
src/eval/primop.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::eval::value::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
|
||||
pub enum PrimOpError {
|
||||
#[error("Math error (underflow or overflow) computing {0} operator")]
|
||||
MathFailure(&'static str),
|
||||
#[error("Type mismatch ({1} vs {2}) computing {0} operator")]
|
||||
TypeMismatch(String, Value, Value),
|
||||
#[error("Bad type for operator {0}: {1}")]
|
||||
BadTypeFor(&'static str, Value),
|
||||
#[error("Illegal number of arguments for {0}: {1} arguments found")]
|
||||
BadArgCount(String, usize),
|
||||
#[error("Unknown primitive operation {0}")]
|
||||
UnknownPrimOp(String),
|
||||
}
|
||||
|
||||
macro_rules! run_op {
|
||||
($op: ident, $left: expr, $right: expr) => {
|
||||
match $op {
|
||||
"+" => $left
|
||||
.checked_add($right)
|
||||
.ok_or(PrimOpError::MathFailure("+"))
|
||||
.map(Into::into),
|
||||
"-" => $left
|
||||
.checked_sub($right)
|
||||
.ok_or(PrimOpError::MathFailure("+"))
|
||||
.map(Into::into),
|
||||
"*" => $left
|
||||
.checked_mul($right)
|
||||
.ok_or(PrimOpError::MathFailure("+"))
|
||||
.map(Into::into),
|
||||
"/" => $left
|
||||
.checked_div($right)
|
||||
.ok_or(PrimOpError::MathFailure("+"))
|
||||
.map(Into::into),
|
||||
_ => Err(PrimOpError::UnknownPrimOp($op.to_string())),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Value {
|
||||
fn binary_op(operation: &str, left: &Value, right: &Value) -> Result<Value, PrimOpError> {
|
||||
match left {
|
||||
Value::I64(x) => match right {
|
||||
Value::I64(y) => run_op!(operation, x, *y),
|
||||
// _ => Err(PrimOpError::TypeMismatch(
|
||||
// operation.to_string(),
|
||||
// left.clone(),
|
||||
// right.clone(),
|
||||
// )),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate(operation: &str, values: Vec<Value>) -> Result<Value, PrimOpError> {
|
||||
if values.len() == 2 {
|
||||
Value::binary_op(operation, &values[0], &values[1])
|
||||
} else {
|
||||
Err(PrimOpError::BadArgCount(
|
||||
operation.to_string(),
|
||||
values.len(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/eval/value.rs
Normal file
20
src/eval/value.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Value {
|
||||
I64(i64),
|
||||
}
|
||||
|
||||
impl Display for Value {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Value::I64(x) => write!(f, "{}i64", x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Value {
|
||||
fn from(value: i64) -> Self {
|
||||
Value::I64(value)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user