🧪 Add evaluation tests to ensure that passes retain NGR semantics. #2
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)
|
||||
}
|
||||
}
|
||||
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 eval;
|
||||
pub mod ir;
|
||||
pub mod syntax;
|
||||
|
||||
@@ -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
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