🧪 Add evaluation tests to ensure that passes retain NGR semantics. #2
@@ -26,6 +26,7 @@ pretty = { version = "^0.11.2", features = ["termcolor"] }
|
||||
proptest = "^1.0.0"
|
||||
rustyline = "^11.0.0"
|
||||
target-lexicon = "^0.12.5"
|
||||
tempfile = "^3.5.0"
|
||||
thiserror = "^1.0.30"
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <stdio.h>
|
||||
|
||||
void print(char *variable_name, uint64_t value) {
|
||||
printf("%s = %llu\n", variable_name, value);
|
||||
printf("%s = %llii64\n", variable_name, value);
|
||||
}
|
||||
|
||||
void caller() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod error;
|
||||
mod eval;
|
||||
mod into_crane;
|
||||
mod runtime;
|
||||
|
||||
@@ -10,7 +11,7 @@ use cranelift_codegen::settings::Configurable;
|
||||
use cranelift_codegen::{isa, settings};
|
||||
use cranelift_jit::{JITBuilder, JITModule};
|
||||
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;
|
||||
|
||||
const EMPTY_DATUM: [u8; 8] = [0; 8];
|
||||
@@ -72,20 +73,22 @@ impl Backend<ObjectModule> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn bytes(self) -> Result<Vec<u8>, object::write::Error> {
|
||||
self.module.finish().emit()
|
||||
pub fn bytes(self) -> Result<Vec<u8>, BackendError> {
|
||||
self.module.finish().emit().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Module> Backend<M> {
|
||||
pub fn define_string(&mut self, s: &str) -> Result<DataId, BackendError> {
|
||||
let name = format!("<string_constant>{}", s);
|
||||
let s0 = format!("{}\0", s);
|
||||
|
||||
let global_id = self
|
||||
.module
|
||||
.declare_data(&name, Linkage::Local, false, false)?;
|
||||
let mut data_context = DataContext::new();
|
||||
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.defined_strings.insert(s.to_owned(), global_id);
|
||||
Ok(global_id)
|
||||
|
||||
@@ -18,6 +18,8 @@ pub enum BackendError {
|
||||
SetError(#[from] SetError),
|
||||
#[error(transparent)]
|
||||
LookupError(#[from] LookupError),
|
||||
#[error(transparent)]
|
||||
Write(#[from] cranelift_object::object::write::Error),
|
||||
}
|
||||
|
||||
impl From<BackendError> for Diagnostic<usize> {
|
||||
@@ -41,6 +43,41 @@ impl From<BackendError> for Diagnostic<usize> {
|
||||
BackendError::LookupError(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
89
src/backend/eval.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ pub struct RuntimeFunctions {
|
||||
_referenced_functions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[derive(Debug, Error, PartialEq)]
|
||||
pub enum RuntimeFunctionError {
|
||||
#[error("Could not find runtime function named '{0}'")]
|
||||
CannotFindRuntimeFunction(String),
|
||||
|
||||
14
src/eval.rs
14
src/eval.rs
@@ -6,10 +6,22 @@ pub use env::{EvalEnvironment, LookupError};
|
||||
pub use primop::PrimOpError;
|
||||
pub use value::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
|
||||
use crate::backend::BackendError;
|
||||
|
||||
#[derive(Debug, PartialEq, thiserror::Error)]
|
||||
pub enum EvalError {
|
||||
#[error(transparent)]
|
||||
Lookup(#[from] LookupError),
|
||||
#[error(transparent)]
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ pub enum EvalEnvInternal {
|
||||
Value(ArcIntern<String>, Value, Arc<EvalEnvInternal>),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
|
||||
pub enum LookupError {
|
||||
#[error("Could not find variable '{0}' in environment")]
|
||||
@@ -26,12 +25,14 @@ impl Default for EvalEnvironment {
|
||||
|
||||
impl EvalEnvironment {
|
||||
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 {
|
||||
EvalEnvironment {
|
||||
inner: Arc::new(EvalEnvInternal::Value(name, value, self.inner.clone()))
|
||||
inner: Arc::new(EvalEnvInternal::Value(name, value, self.inner.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,24 @@ pub enum PrimOpError {
|
||||
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),
|
||||
"+" => $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())),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,8 +43,12 @@ impl Value {
|
||||
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()))
|
||||
}
|
||||
// _ => Err(PrimOpError::TypeMismatch(
|
||||
// operation.to_string(),
|
||||
// left.clone(),
|
||||
// right.clone(),
|
||||
// )),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +56,10 @@ impl Value {
|
||||
if values.len() == 2 {
|
||||
Value::binary_op(operation, &values[0], &values[1])
|
||||
} else {
|
||||
Err(PrimOpError::BadArgCount(operation.to_string(), values.len()))
|
||||
Err(PrimOpError::BadArgCount(
|
||||
operation.to_string(),
|
||||
values.len(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use internment::ArcIntern;
|
||||
|
||||
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 {
|
||||
pub fn eval(&self) -> Result<String, EvalError> {
|
||||
@@ -34,7 +32,7 @@ impl Expression {
|
||||
match self {
|
||||
Expression::Value(_, v) => match v {
|
||||
super::Value::Number(_, v) => Ok(Value::I64(*v)),
|
||||
}
|
||||
},
|
||||
|
||||
Expression::Reference(_, n) => Ok(env.lookup(n.clone())?),
|
||||
|
||||
@@ -44,7 +42,9 @@ impl Expression {
|
||||
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)),
|
||||
ValueOrRef::Value(_, super::Value::Number(_, v)) => {
|
||||
arg_values.push(Value::I64(*v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,8 +69,9 @@ fn two_plus_three() {
|
||||
|
||||
#[test]
|
||||
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 output = ir.eval().expect("runs successfully");
|
||||
assert_eq!("x = 7i64\n", &output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,4 +80,4 @@ proptest::proptest! {
|
||||
let ir_result = ir.eval();
|
||||
assert_eq!(syntax_result, ir_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ mod validate;
|
||||
|
||||
pub use crate::syntax::ast::*;
|
||||
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};
|
||||
#[cfg(test)]
|
||||
use ::pretty::{Arena, Pretty};
|
||||
@@ -273,7 +273,7 @@ proptest::proptest! {
|
||||
|
||||
#[test]
|
||||
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(_)))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use internment::ArcIntern;
|
||||
|
||||
use crate::eval::{EvalEnvironment, EvalError, Value};
|
||||
use crate::syntax::{Program, Statement, Expression};
|
||||
use crate::syntax::{Expression, Program, Statement};
|
||||
|
||||
impl Program {
|
||||
pub fn eval(&self) -> Result<String, EvalError> {
|
||||
@@ -32,7 +32,7 @@ impl Expression {
|
||||
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()))?),
|
||||
|
||||
@@ -61,4 +61,4 @@ 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