🧪 Add evaluation tests to ensure that passes retain NGR semantics. #2

Merged
acw merged 6 commits from acw/eval-tests into develop 2023-04-16 16:07:45 -07:00
14 changed files with 215 additions and 32 deletions
Showing only changes of commit 8d8b200513 - Show all commits

View File

@@ -26,6 +26,7 @@ pretty = { version = "^0.11.2", features = ["termcolor"] }
proptest = "^1.0.0" proptest = "^1.0.0"
rustyline = "^11.0.0" rustyline = "^11.0.0"
target-lexicon = "^0.12.5" target-lexicon = "^0.12.5"
tempfile = "^3.5.0"
thiserror = "^1.0.30" thiserror = "^1.0.30"
[build-dependencies] [build-dependencies]

View File

@@ -2,7 +2,7 @@
#include <stdio.h> #include <stdio.h>
void print(char *variable_name, uint64_t value) { void print(char *variable_name, uint64_t value) {
printf("%s = %llu\n", variable_name, value); printf("%s = %llii64\n", variable_name, value);
} }
void caller() { void caller() {

View File

@@ -1,4 +1,5 @@
mod error; mod error;
mod eval;
mod into_crane; mod into_crane;
mod runtime; mod runtime;
@@ -10,7 +11,7 @@ use cranelift_codegen::settings::Configurable;
use cranelift_codegen::{isa, settings}; use cranelift_codegen::{isa, settings};
use cranelift_jit::{JITBuilder, JITModule}; use cranelift_jit::{JITBuilder, JITModule};
use cranelift_module::{default_libcall_names, DataContext, DataId, FuncId, Linkage, Module}; use cranelift_module::{default_libcall_names, DataContext, DataId, FuncId, Linkage, Module};
use cranelift_object::{object, ObjectBuilder, ObjectModule}; use cranelift_object::{ObjectBuilder, ObjectModule};
use target_lexicon::Triple; use target_lexicon::Triple;
const EMPTY_DATUM: [u8; 8] = [0; 8]; const EMPTY_DATUM: [u8; 8] = [0; 8];
@@ -72,20 +73,22 @@ impl Backend<ObjectModule> {
}) })
} }
pub fn bytes(self) -> Result<Vec<u8>, object::write::Error> { pub fn bytes(self) -> Result<Vec<u8>, BackendError> {
self.module.finish().emit() self.module.finish().emit().map_err(Into::into)
} }
} }
impl<M: Module> Backend<M> { impl<M: Module> Backend<M> {
pub fn define_string(&mut self, s: &str) -> Result<DataId, BackendError> { pub fn define_string(&mut self, s: &str) -> Result<DataId, BackendError> {
let name = format!("<string_constant>{}", s); let name = format!("<string_constant>{}", s);
let s0 = format!("{}\0", s);
let global_id = self let global_id = self
.module .module
.declare_data(&name, Linkage::Local, false, false)?; .declare_data(&name, Linkage::Local, false, false)?;
let mut data_context = DataContext::new(); let mut data_context = DataContext::new();
data_context.set_align(8); data_context.set_align(8);
data_context.define(s.to_owned().into_boxed_str().into_boxed_bytes()); data_context.define(s0.into_boxed_str().into_boxed_bytes());
self.module.define_data(global_id, &data_context)?; self.module.define_data(global_id, &data_context)?;
self.defined_strings.insert(s.to_owned(), global_id); self.defined_strings.insert(s.to_owned(), global_id);
Ok(global_id) Ok(global_id)

View File

@@ -18,6 +18,8 @@ pub enum BackendError {
SetError(#[from] SetError), SetError(#[from] SetError),
#[error(transparent)] #[error(transparent)]
LookupError(#[from] LookupError), LookupError(#[from] LookupError),
#[error(transparent)]
Write(#[from] cranelift_object::object::write::Error),
} }
impl From<BackendError> for Diagnostic<usize> { impl From<BackendError> for Diagnostic<usize> {
@@ -41,6 +43,41 @@ impl From<BackendError> for Diagnostic<usize> {
BackendError::LookupError(me) => { BackendError::LookupError(me) => {
Diagnostic::error().with_message(format!("Internal error: {}", me)) Diagnostic::error().with_message(format!("Internal error: {}", me))
} }
BackendError::Write(me) => {
Diagnostic::error().with_message(format!("Cranelift object write error: {}", me))
}
}
}
}
impl PartialEq for BackendError {
fn eq(&self, other: &Self) -> bool {
match self {
BackendError::BuiltinError(a) => match other {
BackendError::BuiltinError(b) => a == b,
_ => false,
},
BackendError::CodegenError(_) => matches!(other, BackendError::CodegenError(_)),
BackendError::Cranelift(_) => matches!(other, BackendError::Cranelift(_)),
BackendError::LookupError(a) => match other {
BackendError::LookupError(b) => a == b,
_ => false,
},
BackendError::SetError(a) => match other {
BackendError::SetError(b) => a == b,
_ => false,
},
BackendError::VariableLookupFailure => other == &BackendError::VariableLookupFailure,
BackendError::Write(a) => match other {
BackendError::Write(b) => a == b,
_ => false,
},
} }
} }
} }

89
src/backend/eval.rs Normal file
View File

@@ -0,0 +1,89 @@
use std::path::Path;
use crate::backend::Backend;
use crate::eval::EvalError;
use crate::ir::Program;
use cranelift_jit::JITModule;
use cranelift_object::ObjectModule;
use target_lexicon::Triple;
impl Backend<JITModule> {
pub fn eval(_program: Program) -> Result<String, EvalError> {
unimplemented!()
}
}
impl Backend<ObjectModule> {
pub fn eval(program: Program) -> Result<String, EvalError> {
//use pretty::{Arena, Pretty};
//let allocator = Arena::<()>::new();
//program.pretty(&allocator).render(80, &mut std::io::stdout())?;
let mut backend = Self::object_file(Triple::host())?;
let my_directory = tempfile::tempdir()?;
let object_path = my_directory.path().join("object.o");
let executable_path = my_directory.path().join("test_executable");
backend.compile_function("gogogo", program)?;
let bytes = backend.bytes()?;
std::fs::write(&object_path, bytes)?;
Self::link(&object_path, &executable_path)?;
let output = std::process::Command::new(executable_path).output()?;
if output.stderr.is_empty() {
if output.status.success() {
Ok(std::string::String::from_utf8_lossy(&output.stdout).to_string())
} else {
Err(EvalError::IO(format!(
"Exitted with error code {}",
output.status
)))
}
} else {
Err(EvalError::IO(
std::string::String::from_utf8_lossy(&output.stderr).to_string(),
))
}
}
#[cfg(target_family = "unix")]
fn link(object_file: &Path, executable_path: &Path) -> Result<(), EvalError> {
use std::{os::unix::prelude::PermissionsExt, path::PathBuf};
let output = std::process::Command::new("gcc")
.arg(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("runtime")
.join("rts.c"),
)
.arg(object_file)
.arg("-o")
.arg(executable_path)
.output()?;
if output.stderr.is_empty() {
let mut perms = std::fs::metadata(executable_path)?.permissions();
perms.set_mode(perms.mode() | 0o100); // user execute bit
std::fs::set_permissions(executable_path, perms)?;
Ok(())
} else {
Err(EvalError::IO(
std::string::String::from_utf8_lossy(&output.stderr).to_string(),
))
}
}
}
proptest::proptest! {
#[test]
fn file_backend_works(program: Program) {
use crate::eval::PrimOpError;
let basic_result = program.eval();
if !matches!(basic_result, Err(EvalError::PrimOp(PrimOpError::MathFailure(_)))) {
let compiled_result = Backend::<ObjectModule>::eval(program);
assert_eq!(basic_result, compiled_result);
}
}
}

View File

@@ -12,7 +12,7 @@ pub struct RuntimeFunctions {
_referenced_functions: Vec<String>, _referenced_functions: Vec<String>,
} }
#[derive(Debug, Error)] #[derive(Debug, Error, PartialEq)]
pub enum RuntimeFunctionError { pub enum RuntimeFunctionError {
#[error("Could not find runtime function named '{0}'")] #[error("Could not find runtime function named '{0}'")]
CannotFindRuntimeFunction(String), CannotFindRuntimeFunction(String),

View File

@@ -6,10 +6,22 @@ pub use env::{EvalEnvironment, LookupError};
pub use primop::PrimOpError; pub use primop::PrimOpError;
pub use value::Value; pub use value::Value;
#[derive(Clone, Debug, PartialEq, thiserror::Error)] use crate::backend::BackendError;
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum EvalError { pub enum EvalError {
#[error(transparent)] #[error(transparent)]
Lookup(#[from] LookupError), Lookup(#[from] LookupError),
#[error(transparent)] #[error(transparent)]
PrimOp(#[from] PrimOpError), PrimOp(#[from] PrimOpError),
#[error(transparent)]
Backend(#[from] BackendError),
#[error("IO error: {0}")]
IO(String),
}
impl From<std::io::Error> for EvalError {
fn from(value: std::io::Error) -> Self {
EvalError::IO(value.to_string())
}
} }

View File

@@ -11,7 +11,6 @@ pub enum EvalEnvInternal {
Value(ArcIntern<String>, Value, Arc<EvalEnvInternal>), Value(ArcIntern<String>, Value, Arc<EvalEnvInternal>),
} }
#[derive(Clone, Debug, PartialEq, thiserror::Error)] #[derive(Clone, Debug, PartialEq, thiserror::Error)]
pub enum LookupError { pub enum LookupError {
#[error("Could not find variable '{0}' in environment")] #[error("Could not find variable '{0}' in environment")]
@@ -26,12 +25,14 @@ impl Default for EvalEnvironment {
impl EvalEnvironment { impl EvalEnvironment {
pub fn empty() -> Self { pub fn empty() -> Self {
EvalEnvironment { inner: Arc::new(EvalEnvInternal::Empty) } EvalEnvironment {
inner: Arc::new(EvalEnvInternal::Empty),
}
} }
pub fn extend(&self, name: ArcIntern<String>, value: Value) -> Self { pub fn extend(&self, name: ArcIntern<String>, value: Value) -> Self {
EvalEnvironment { EvalEnvironment {
inner: Arc::new(EvalEnvInternal::Value(name, value, self.inner.clone())) inner: Arc::new(EvalEnvInternal::Value(name, value, self.inner.clone())),
} }
} }

View File

@@ -17,12 +17,24 @@ pub enum PrimOpError {
macro_rules! run_op { macro_rules! run_op {
($op: ident, $left: expr, $right: expr) => { ($op: ident, $left: expr, $right: expr) => {
match $op { match $op {
"+" => $left.checked_add($right).ok_or(PrimOpError::MathFailure("+")).map(Into::into), "+" => $left
"-" => $left.checked_sub($right).ok_or(PrimOpError::MathFailure("+")).map(Into::into), .checked_add($right)
"*" => $left.checked_mul($right).ok_or(PrimOpError::MathFailure("+")).map(Into::into), .ok_or(PrimOpError::MathFailure("+"))
"/" => $left.checked_div($right).ok_or(PrimOpError::MathFailure("+")).map(Into::into), .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())), _ => Err(PrimOpError::UnknownPrimOp($op.to_string())),
} }
}; };
} }
@@ -31,8 +43,12 @@ impl Value {
match left { match left {
Value::I64(x) => match right { Value::I64(x) => match right {
Value::I64(y) => run_op!(operation, x, *y), Value::I64(y) => run_op!(operation, x, *y),
_ => Err(PrimOpError::TypeMismatch(operation.to_string(), left.clone(), right.clone())) // _ => Err(PrimOpError::TypeMismatch(
} // operation.to_string(),
// left.clone(),
// right.clone(),
// )),
},
} }
} }
@@ -40,7 +56,10 @@ impl Value {
if values.len() == 2 { if values.len() == 2 {
Value::binary_op(operation, &values[0], &values[1]) Value::binary_op(operation, &values[0], &values[1])
} else { } else {
Err(PrimOpError::BadArgCount(operation.to_string(), values.len())) Err(PrimOpError::BadArgCount(
operation.to_string(),
values.len(),
))
} }
} }
} }

View File

@@ -1,10 +1,15 @@
use internment::ArcIntern; use internment::ArcIntern;
use pretty::{DocAllocator, Pretty}; use pretty::{DocAllocator, Pretty};
use proptest::{
prelude::Arbitrary,
strategy::{BoxedStrategy, Strategy},
};
use crate::syntax::Location; use crate::syntax::Location;
type Variable = ArcIntern<String>; type Variable = ArcIntern<String>;
#[derive(Debug)]
pub struct Program { pub struct Program {
pub statements: Vec<Statement>, 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 { pub enum Statement {
Binding(Location, Variable, Expression), Binding(Location, Variable, Expression),
Print(Location, Variable), Print(Location, Variable),
@@ -54,6 +71,7 @@ where
} }
} }
#[derive(Debug)]
pub enum Expression { pub enum Expression {
Value(Location, Value), Value(Location, Value),
Reference(Location, Variable), Reference(Location, Variable),
@@ -126,6 +144,7 @@ where
} }
} }
#[derive(Debug)]
pub enum ValueOrRef { pub enum ValueOrRef {
Value(Location, Value), Value(Location, Value),
Ref(Location, ArcIntern<String>), Ref(Location, ArcIntern<String>),
@@ -144,6 +163,7 @@ where
} }
} }
#[derive(Debug)]
pub enum Value { pub enum Value {
Number(Option<u8>, i64), Number(Option<u8>, i64),
} }

View File

@@ -1,9 +1,7 @@
use internment::ArcIntern;
use crate::eval::{EvalEnvironment, EvalError, Value}; use crate::eval::{EvalEnvironment, EvalError, Value};
use crate::ir::{Program, Statement, Expression}; use crate::ir::{Expression, Program, Statement};
use super::{ValueOrRef, Primitive}; use super::{Primitive, ValueOrRef};
impl Program { impl Program {
pub fn eval(&self) -> Result<String, EvalError> { pub fn eval(&self) -> Result<String, EvalError> {
@@ -34,7 +32,7 @@ impl Expression {
match self { match self {
Expression::Value(_, v) => match v { Expression::Value(_, v) => match v {
super::Value::Number(_, v) => Ok(Value::I64(*v)), super::Value::Number(_, v) => Ok(Value::I64(*v)),
} },
Expression::Reference(_, n) => Ok(env.lookup(n.clone())?), Expression::Reference(_, n) => Ok(env.lookup(n.clone())?),
@@ -44,7 +42,9 @@ impl Expression {
for arg in args.iter() { for arg in args.iter() {
match arg { match arg {
ValueOrRef::Ref(_, n) => arg_values.push(env.lookup(n.clone())?), ValueOrRef::Ref(_, n) => arg_values.push(env.lookup(n.clone())?),
ValueOrRef::Value(_, super::Value::Number(_, v)) => arg_values.push(Value::I64(*v)), ValueOrRef::Value(_, super::Value::Number(_, v)) => {
arg_values.push(Value::I64(*v))
}
} }
} }
@@ -69,8 +69,9 @@ fn two_plus_three() {
#[test] #[test]
fn lotsa_math() { fn lotsa_math() {
let input = crate::syntax::Program::parse(0, "x = 2 + 3 * 10 / 5 - 1; print x;").expect("parse works"); 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 ir = Program::from(input.simplify());
let output = ir.eval().expect("runs successfully"); let output = ir.eval().expect("runs successfully");
assert_eq!("x = 7i64\n", &output); assert_eq!("x = 7i64\n", &output);
} }

View File

@@ -80,4 +80,4 @@ proptest::proptest! {
let ir_result = ir.eval(); let ir_result = ir.eval();
assert_eq!(syntax_result, ir_result); assert_eq!(syntax_result, ir_result);
} }
} }

View File

@@ -18,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, eval::EvalError}; use crate::syntax::parser::ProgramParser;
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};
@@ -273,7 +273,7 @@ proptest::proptest! {
#[test] #[test]
fn generated_run_or_overflow(program: Program) { fn generated_run_or_overflow(program: Program) {
use crate::eval::PrimOpError; use crate::eval::{EvalError, PrimOpError};
assert!(matches!(program.eval(), Ok(_) | Err(EvalError::PrimOp(PrimOpError::MathFailure(_))))) assert!(matches!(program.eval(), Ok(_) | Err(EvalError::PrimOp(PrimOpError::MathFailure(_)))))
} }
} }

View File

@@ -1,7 +1,7 @@
use internment::ArcIntern; use internment::ArcIntern;
use crate::eval::{EvalEnvironment, EvalError, Value}; use crate::eval::{EvalEnvironment, EvalError, Value};
use crate::syntax::{Program, Statement, Expression}; use crate::syntax::{Expression, Program, Statement};
impl Program { impl Program {
pub fn eval(&self) -> Result<String, EvalError> { pub fn eval(&self) -> Result<String, EvalError> {
@@ -32,7 +32,7 @@ impl Expression {
match self { match self {
Expression::Value(_, v) => match v { Expression::Value(_, v) => match v {
super::Value::Number(_, v) => Ok(Value::I64(*v)), super::Value::Number(_, v) => Ok(Value::I64(*v)),
} },
Expression::Reference(_, n) => Ok(env.lookup(ArcIntern::new(n.clone()))?), Expression::Reference(_, n) => Ok(env.lookup(ArcIntern::new(n.clone()))?),
@@ -61,4 +61,4 @@ fn lotsa_math() {
let input = Program::parse(0, "x = 2 + 3 * 10 / 5 - 1; print x;").expect("parse works"); let input = Program::parse(0, "x = 2 + 3 * 10 / 5 - 1; print x;").expect("parse works");
let output = input.eval().expect("runs successfully"); let output = input.eval().expect("runs successfully");
assert_eq!("x = 7i64\n", &output); assert_eq!("x = 7i64\n", &output);
} }