Test that evaluation returns the same results in syntax and IR.
This commit is contained in:
15
src/eval.rs
Normal file
15
src/eval.rs
Normal 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
92
src/eval/env.rs
Normal 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
46
src/eval/primop.rs
Normal 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
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mod ast;
|
mod ast;
|
||||||
|
mod eval;
|
||||||
mod from_syntax;
|
mod from_syntax;
|
||||||
mod strings;
|
mod strings;
|
||||||
|
|
||||||
|
|||||||
76
src/ir/eval.rs
Normal file
76
src/ir/eval.rs
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod backend;
|
pub mod backend;
|
||||||
|
pub mod eval;
|
||||||
pub mod ir;
|
pub mod ir;
|
||||||
pub mod syntax;
|
pub mod syntax;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use logos::Logos;
|
|||||||
|
|
||||||
mod arbitrary;
|
mod arbitrary;
|
||||||
pub mod ast;
|
pub mod ast;
|
||||||
|
mod eval;
|
||||||
mod location;
|
mod location;
|
||||||
mod simplify;
|
mod simplify;
|
||||||
mod tokens;
|
mod tokens;
|
||||||
@@ -17,7 +18,7 @@ mod validate;
|
|||||||
|
|
||||||
pub use crate::syntax::ast::*;
|
pub use crate::syntax::ast::*;
|
||||||
pub use crate::syntax::location::Location;
|
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};
|
pub use crate::syntax::tokens::{LexerError, Token};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use ::pretty::{Arena, Pretty};
|
use ::pretty::{Arena, Pretty};
|
||||||
@@ -269,4 +270,10 @@ proptest::proptest! {
|
|||||||
let (errors, _) = program.validate();
|
let (errors, _) = program.validate();
|
||||||
prop_assert!(errors.is_empty());
|
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
64
src/syntax/eval.rs
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user