🧪 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 was merged in pull request #2.
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
use internment::ArcIntern;
|
||||
use pretty::{DocAllocator, Pretty};
|
||||
use proptest::{
|
||||
prelude::Arbitrary,
|
||||
strategy::{BoxedStrategy, Strategy},
|
||||
};
|
||||
|
||||
use crate::syntax::Location;
|
||||
|
||||
type Variable = ArcIntern<String>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Program {
|
||||
pub statements: Vec<Statement>,
|
||||
}
|
||||
@@ -28,6 +33,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for Program {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
|
||||
crate::syntax::Program::arbitrary_with(args)
|
||||
.prop_map(|x| Program::from(x.simplify()))
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Statement {
|
||||
Binding(Location, Variable, Expression),
|
||||
Print(Location, Variable),
|
||||
@@ -54,6 +71,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Expression {
|
||||
Value(Location, Value),
|
||||
Reference(Location, Variable),
|
||||
@@ -126,6 +144,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ValueOrRef {
|
||||
Value(Location, Value),
|
||||
Ref(Location, ArcIntern<String>),
|
||||
@@ -144,6 +163,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Value {
|
||||
Number(Option<u8>, i64),
|
||||
}
|
||||
|
||||
77
src/ir/eval.rs
Normal file
77
src/ir/eval.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use crate::eval::{EvalEnvironment, EvalError, Value};
|
||||
use crate::ir::{Expression, Program, Statement};
|
||||
|
||||
use super::{Primitive, ValueOrRef};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user