🧪 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 is contained in:
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
106
src/backend/eval.rs
Normal file
106
src/backend/eval.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
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> {
|
||||
let mut jitter = Backend::jit(Some(String::new()))?;
|
||||
let function_id = jitter.compile_function("test", program)?;
|
||||
jitter.module.finalize_definitions()?;
|
||||
let compiled_bytes = jitter.bytes(function_id);
|
||||
let compiled_function = unsafe { std::mem::transmute::<_, fn() -> ()>(compiled_bytes) };
|
||||
compiled_function();
|
||||
Ok(jitter.output())
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn link(object_file: &Path, executable_path: &Path) -> Result<(), EvalError> {
|
||||
use std::path::PathBuf;
|
||||
|
||||
let output = std::process::Command::new("clang")
|
||||
.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() {
|
||||
return Err(EvalError::IO(
|
||||
std::string::String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn file_backend_works(program: Program) {
|
||||
use crate::eval::PrimOpError;
|
||||
|
||||
let basic_result = program.eval();
|
||||
|
||||
#[cfg(target_family="windows")]
|
||||
let basic_result = basic_result.map(|x| x.replace('\n', "\r\n"));
|
||||
|
||||
if !matches!(basic_result, Err(EvalError::PrimOp(PrimOpError::MathFailure(_)))) {
|
||||
let compiled_result = Backend::<ObjectModule>::eval(program);
|
||||
assert_eq!(basic_result, compiled_result);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jit_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::<JITModule>::eval(program);
|
||||
assert_eq!(basic_result, compiled_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,8 @@ impl<M: Module> Backend<M> {
|
||||
for stmt in program.statements.drain(..) {
|
||||
match stmt {
|
||||
Statement::Print(ann, var) => {
|
||||
let buffer_ptr = self.output_buffer_ptr();
|
||||
let buffer_ptr = builder.ins().iconst(types::I64, buffer_ptr as i64);
|
||||
let local_name_ref = string_table.get(&var).unwrap();
|
||||
let name_ptr = builder.ins().symbol_value(types::I64, *local_name_ref);
|
||||
let val = ValueOrRef::Ref(ann, var).into_cranelift(
|
||||
@@ -67,7 +69,9 @@ impl<M: Module> Backend<M> {
|
||||
&variable_table,
|
||||
&pre_defined_symbols,
|
||||
)?;
|
||||
builder.ins().call(print_func_ref, &[name_ptr, val]);
|
||||
builder
|
||||
.ins()
|
||||
.call(print_func_ref, &[buffer_ptr, name_ptr, val]);
|
||||
}
|
||||
|
||||
Statement::Binding(_, var_name, value) => {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
struct BackendObject {
|
||||
}
|
||||
|
||||
impl BackendObject {
|
||||
pub fn new() -> Result<Self, ()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use cranelift_jit::JITBuilder;
|
||||
use cranelift_module::{FuncId, Linkage, Module, ModuleResult};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::CStr;
|
||||
use std::fmt::Write;
|
||||
use target_lexicon::Triple;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -12,16 +13,21 @@ 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),
|
||||
}
|
||||
|
||||
extern "C" fn runtime_print(name: *const i8, value: u64) {
|
||||
extern "C" fn runtime_print(output_buffer: *mut String, name: *const i8, value: i64) {
|
||||
let cstr = unsafe { CStr::from_ptr(name) };
|
||||
let reconstituted = cstr.to_string_lossy();
|
||||
println!("{} = {}", reconstituted, value);
|
||||
|
||||
if let Some(output_buffer) = unsafe { output_buffer.as_mut() } {
|
||||
writeln!(output_buffer, "{} = {}i64", reconstituted, value).unwrap();
|
||||
} else {
|
||||
println!("{} = {}", reconstituted, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeFunctions {
|
||||
@@ -36,7 +42,7 @@ impl RuntimeFunctions {
|
||||
"print",
|
||||
Linkage::Import,
|
||||
&Signature {
|
||||
params: vec![string_param, int64_param],
|
||||
params: vec![string_param, string_param, int64_param],
|
||||
returns: vec![],
|
||||
call_conv: CallConv::triple_default(platform),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user