diff --git a/src/eval.rs b/src/eval.rs new file mode 100644 index 0000000..e140e4f --- /dev/null +++ b/src/eval.rs @@ -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), +} diff --git a/src/eval/env.rs b/src/eval/env.rs new file mode 100644 index 0000000..8fd16d8 --- /dev/null +++ b/src/eval/env.rs @@ -0,0 +1,92 @@ +use crate::eval::Value; +use internment::ArcIntern; +use std::sync::Arc; + +pub struct EvalEnvironment { + inner: Arc, +} + +pub enum EvalEnvInternal { + Empty, + Value(ArcIntern, Value, Arc), +} + + +#[derive(Clone, Debug, PartialEq, thiserror::Error)] +pub enum LookupError { + #[error("Could not find variable '{0}' in environment")] + CouldNotFind(ArcIntern), +} + +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, value: Value) -> Self { + EvalEnvironment { + inner: Arc::new(EvalEnvInternal::Value(name, value, self.inner.clone())) + } + } + + pub fn lookup(&self, n: ArcIntern) -> Result { + self.inner.lookup(n) + } +} + +impl EvalEnvInternal { + fn lookup(&self, n: ArcIntern) -> Result { + 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 { + ArcIntern::new(s.to_string()) + } +} diff --git a/src/eval/primop.rs b/src/eval/primop.rs new file mode 100644 index 0000000..a063554 --- /dev/null +++ b/src/eval/primop.rs @@ -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 { + 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) -> Result { + if values.len() == 2 { + Value::binary_op(operation, &values[0], &values[1]) + } else { + Err(PrimOpError::BadArgCount(operation.to_string(), values.len())) + } + } +} diff --git a/src/eval/value.rs b/src/eval/value.rs new file mode 100644 index 0000000..a158dc9 --- /dev/null +++ b/src/eval/value.rs @@ -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 for Value { + fn from(value: i64) -> Self { + Value::I64(value) + } +} diff --git a/src/ir.rs b/src/ir.rs index 9b5157d..b7cd9cf 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -1,4 +1,5 @@ mod ast; +mod eval; mod from_syntax; mod strings; diff --git a/src/ir/eval.rs b/src/ir/eval.rs new file mode 100644 index 0000000..a014000 --- /dev/null +++ b/src/ir/eval.rs @@ -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 { + 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 { + 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); +} \ No newline at end of file diff --git a/src/ir/from_syntax.rs b/src/ir/from_syntax.rs index c4d710d..1102c45 100644 --- a/src/ir/from_syntax.rs +++ b/src/ir/from_syntax.rs @@ -71,3 +71,13 @@ impl From 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); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 6ed733f..71d55e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ pub mod backend; +pub mod eval; pub mod ir; pub mod syntax; diff --git a/src/syntax.rs b/src/syntax.rs index 39e992d..d3dbc6f 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -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(_))))) + } } diff --git a/src/syntax/eval.rs b/src/syntax/eval.rs new file mode 100644 index 0000000..6473e18 --- /dev/null +++ b/src/syntax/eval.rs @@ -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 { + 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 { + 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); +} \ No newline at end of file