From 8d8b20051311b698d4b67fa3a01b561c77ab820a Mon Sep 17 00:00:00 2001 From: Adam Wick Date: Sat, 8 Apr 2023 14:20:49 -0700 Subject: [PATCH] End-to-end testing of the compile path. --- Cargo.toml | 1 + runtime/rts.c | 2 +- src/backend.rs | 11 ++++-- src/backend/error.rs | 37 ++++++++++++++++++ src/backend/eval.rs | 89 ++++++++++++++++++++++++++++++++++++++++++ src/backend/runtime.rs | 2 +- src/eval.rs | 14 ++++++- src/eval/env.rs | 7 ++-- src/eval/primop.rs | 35 +++++++++++++---- src/ir/ast.rs | 20 ++++++++++ src/ir/eval.rs | 17 ++++---- src/ir/from_syntax.rs | 2 +- src/syntax.rs | 4 +- src/syntax/eval.rs | 6 +-- 14 files changed, 215 insertions(+), 32 deletions(-) create mode 100644 src/backend/eval.rs diff --git a/Cargo.toml b/Cargo.toml index 41e2435..59b8033 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/runtime/rts.c b/runtime/rts.c index 4f955e6..c7c87fa 100644 --- a/runtime/rts.c +++ b/runtime/rts.c @@ -2,7 +2,7 @@ #include void print(char *variable_name, uint64_t value) { - printf("%s = %llu\n", variable_name, value); + printf("%s = %llii64\n", variable_name, value); } void caller() { diff --git a/src/backend.rs b/src/backend.rs index 54622dc..24d71e6 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -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 { }) } - pub fn bytes(self) -> Result, object::write::Error> { - self.module.finish().emit() + pub fn bytes(self) -> Result, BackendError> { + self.module.finish().emit().map_err(Into::into) } } impl Backend { pub fn define_string(&mut self, s: &str) -> Result { let name = format!("{}", 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) diff --git a/src/backend/error.rs b/src/backend/error.rs index 26b7bf0..3eb3118 100644 --- a/src/backend/error.rs +++ b/src/backend/error.rs @@ -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 for Diagnostic { @@ -41,6 +43,41 @@ impl From for Diagnostic { 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, + }, } } } diff --git a/src/backend/eval.rs b/src/backend/eval.rs new file mode 100644 index 0000000..fef25bc --- /dev/null +++ b/src/backend/eval.rs @@ -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 { + pub fn eval(_program: Program) -> Result { + unimplemented!() + } +} + +impl Backend { + pub fn eval(program: Program) -> Result { + //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::::eval(program); + assert_eq!(basic_result, compiled_result); + } + } +} diff --git a/src/backend/runtime.rs b/src/backend/runtime.rs index ecf0e7b..01e00bc 100644 --- a/src/backend/runtime.rs +++ b/src/backend/runtime.rs @@ -12,7 +12,7 @@ pub struct RuntimeFunctions { _referenced_functions: Vec, } -#[derive(Debug, Error)] +#[derive(Debug, Error, PartialEq)] pub enum RuntimeFunctionError { #[error("Could not find runtime function named '{0}'")] CannotFindRuntimeFunction(String), diff --git a/src/eval.rs b/src/eval.rs index e140e4f..edb2ca9 100644 --- a/src/eval.rs +++ b/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 for EvalError { + fn from(value: std::io::Error) -> Self { + EvalError::IO(value.to_string()) + } } diff --git a/src/eval/env.rs b/src/eval/env.rs index 8fd16d8..ff24834 100644 --- a/src/eval/env.rs +++ b/src/eval/env.rs @@ -11,7 +11,6 @@ pub enum EvalEnvInternal { Value(ArcIntern, Value, Arc), } - #[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, value: Value) -> Self { EvalEnvironment { - inner: Arc::new(EvalEnvInternal::Value(name, value, self.inner.clone())) + inner: Arc::new(EvalEnvInternal::Value(name, value, self.inner.clone())), } } diff --git a/src/eval/primop.rs b/src/eval/primop.rs index a063554..aef9681 100644 --- a/src/eval/primop.rs +++ b/src/eval/primop.rs @@ -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(), + )) } } } diff --git a/src/ir/ast.rs b/src/ir/ast.rs index 97cda55..ad96e95 100644 --- a/src/ir/ast.rs +++ b/src/ir/ast.rs @@ -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; +#[derive(Debug)] pub struct Program { pub statements: Vec, } @@ -28,6 +33,18 @@ where } } +impl Arbitrary for Program { + type Parameters = (); + type Strategy = BoxedStrategy; + + 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), @@ -144,6 +163,7 @@ where } } +#[derive(Debug)] pub enum Value { Number(Option, i64), } diff --git a/src/ir/eval.rs b/src/ir/eval.rs index a014000..d7c4135 100644 --- a/src/ir/eval.rs +++ b/src/ir/eval.rs @@ -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 { @@ -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); -} \ No newline at end of file +} diff --git a/src/ir/from_syntax.rs b/src/ir/from_syntax.rs index 1102c45..e5eea0e 100644 --- a/src/ir/from_syntax.rs +++ b/src/ir/from_syntax.rs @@ -80,4 +80,4 @@ proptest::proptest! { let ir_result = ir.eval(); assert_eq!(syntax_result, ir_result); } -} \ No newline at end of file +} diff --git a/src/syntax.rs b/src/syntax.rs index d3dbc6f..cbadf0c 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -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(_))))) } } diff --git a/src/syntax/eval.rs b/src/syntax/eval.rs index 6473e18..15e7b85 100644 --- a/src/syntax/eval.rs +++ b/src/syntax/eval.rs @@ -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 { @@ -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); -} \ No newline at end of file +}