Test that evaluation returns the same results in syntax and IR.

This commit is contained in:
2023-04-07 17:59:25 -07:00
parent f701598e5d
commit 8dfcc67e51
10 changed files with 333 additions and 1 deletions

15
src/eval.rs Normal file
View File

@@ -0,0 +1,15 @@
mod env;
mod primop;
mod value;
pub use env::{EvalEnvironment, LookupError};
pub use primop::PrimOpError;
pub use value::Value;
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
pub enum EvalError {
#[error(transparent)]
Lookup(#[from] LookupError),
#[error(transparent)]
PrimOp(#[from] PrimOpError),
}

92
src/eval/env.rs Normal file
View File

@@ -0,0 +1,92 @@
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())
}
}

46
src/eval/primop.rs Normal file
View File

@@ -0,0 +1,46 @@
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
View 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)
}
}

View File

@@ -1,4 +1,5 @@
mod ast;
mod eval;
mod from_syntax;
mod strings;

76
src/ir/eval.rs Normal file
View File

@@ -0,0 +1,76 @@
use internment::ArcIntern;
use crate::eval::{EvalEnvironment, EvalError, Value};
use crate::ir::{Program, Statement, Expression};
use super::{ValueOrRef, Primitive};
impl Program {
pub fn eval(&self) -> Result<String, EvalError> {
let mut env = EvalEnvironment::empty();
let mut stdout = String::new();
for stmt in self.statements.iter() {
match stmt {
Statement::Binding(_, name, value) => {
let actual_value = value.eval(&env)?;
env = env.extend(name.clone(), actual_value);
}
Statement::Print(_, name) => {
let value = env.lookup(name.clone())?;
let line = format!("{} = {}\n", name, value);
stdout.push_str(&line);
}
}
}
Ok(stdout)
}
}
impl Expression {
fn eval(&self, env: &EvalEnvironment) -> Result<Value, EvalError> {
match self {
Expression::Value(_, v) => match v {
super::Value::Number(_, v) => Ok(Value::I64(*v)),
}
Expression::Reference(_, n) => Ok(env.lookup(n.clone())?),
Expression::Primitive(_, op, args) => {
let mut arg_values = Vec::with_capacity(args.len());
for arg in args.iter() {
match arg {
ValueOrRef::Ref(_, n) => arg_values.push(env.lookup(n.clone())?),
ValueOrRef::Value(_, super::Value::Number(_, v)) => arg_values.push(Value::I64(*v)),
}
}
match op {
Primitive::Plus => Ok(Value::calculate("+", arg_values)?),
Primitive::Minus => Ok(Value::calculate("-", arg_values)?),
Primitive::Times => Ok(Value::calculate("*", arg_values)?),
Primitive::Divide => Ok(Value::calculate("/", arg_values)?),
}
}
}
}
}
#[test]
fn two_plus_three() {
let input = crate::syntax::Program::parse(0, "x = 2 + 3; print x;").expect("parse works");
let ir = Program::from(input.simplify());
let output = ir.eval().expect("runs successfully");
assert_eq!("x = 5i64\n", &output);
}
#[test]
fn lotsa_math() {
let input = crate::syntax::Program::parse(0, "x = 2 + 3 * 10 / 5 - 1; print x;").expect("parse works");
let ir = Program::from(input.simplify());
let output = ir.eval().expect("runs successfully");
assert_eq!("x = 7i64\n", &output);
}

View File

@@ -71,3 +71,13 @@ impl From<syntax::Value> for ir::Value {
}
}
}
proptest::proptest! {
#[test]
fn translation_maintains_semantics(input: syntax::Program) {
let syntax_result = input.eval();
let ir = ir::Program::from(input.simplify());
let ir_result = ir.eval();
assert_eq!(syntax_result, ir_result);
}
}

View File

@@ -1,3 +1,4 @@
pub mod backend;
pub mod eval;
pub mod ir;
pub mod syntax;

View File

@@ -4,6 +4,7 @@ use logos::Logos;
mod arbitrary;
pub mod ast;
mod eval;
mod location;
mod simplify;
mod tokens;
@@ -17,7 +18,7 @@ mod validate;
pub use crate::syntax::ast::*;
pub use crate::syntax::location::Location;
use crate::syntax::parser::ProgramParser;
use crate::{syntax::parser::ProgramParser, eval::EvalError};
pub use crate::syntax::tokens::{LexerError, Token};
#[cfg(test)]
use ::pretty::{Arena, Pretty};
@@ -269,4 +270,10 @@ proptest::proptest! {
let (errors, _) = program.validate();
prop_assert!(errors.is_empty());
}
#[test]
fn generated_run_or_overflow(program: Program) {
use crate::eval::PrimOpError;
assert!(matches!(program.eval(), Ok(_) | Err(EvalError::PrimOp(PrimOpError::MathFailure(_)))))
}
}

64
src/syntax/eval.rs Normal file
View File

@@ -0,0 +1,64 @@
use internment::ArcIntern;
use crate::eval::{EvalEnvironment, EvalError, Value};
use crate::syntax::{Program, Statement, Expression};
impl Program {
pub fn eval(&self) -> Result<String, EvalError> {
let mut env = EvalEnvironment::empty();
let mut stdout = String::new();
for stmt in self.statements.iter() {
match stmt {
Statement::Binding(_, name, value) => {
let actual_value = value.eval(&env)?;
env = env.extend(ArcIntern::new(name.clone()), actual_value);
}
Statement::Print(_, name) => {
let value = env.lookup(ArcIntern::new(name.clone()))?;
let line = format!("{} = {}\n", name, value);
stdout.push_str(&line);
}
}
}
Ok(stdout)
}
}
impl Expression {
fn eval(&self, env: &EvalEnvironment) -> Result<Value, EvalError> {
match self {
Expression::Value(_, v) => match v {
super::Value::Number(_, v) => Ok(Value::I64(*v)),
}
Expression::Reference(_, n) => Ok(env.lookup(ArcIntern::new(n.clone()))?),
Expression::Primitive(_, op, args) => {
let mut arg_values = Vec::with_capacity(args.len());
for arg in args.iter() {
arg_values.push(arg.eval(env)?);
}
Ok(Value::calculate(op, arg_values)?)
}
}
}
}
#[test]
fn two_plus_three() {
let input = Program::parse(0, "x = 2 + 3; print x;").expect("parse works");
let output = input.eval().expect("runs successfully");
assert_eq!("x = 5i64\n", &output);
}
#[test]
fn lotsa_math() {
let input = Program::parse(0, "x = 2 + 3 * 10 / 5 - 1; print x;").expect("parse works");
let output = input.eval().expect("runs successfully");
assert_eq!("x = 7i64\n", &output);
}