From 8dfcc67e519a96e915dd4e3cdf90dc28a084fa16 Mon Sep 17 00:00:00 2001 From: Adam Wick Date: Fri, 7 Apr 2023 17:59:25 -0700 Subject: [PATCH 1/6] Test that evaluation returns the same results in syntax and IR. --- src/eval.rs | 15 +++++++ src/eval/env.rs | 92 +++++++++++++++++++++++++++++++++++++++++++ src/eval/primop.rs | 46 ++++++++++++++++++++++ src/eval/value.rs | 20 ++++++++++ src/ir.rs | 1 + src/ir/eval.rs | 76 +++++++++++++++++++++++++++++++++++ src/ir/from_syntax.rs | 10 +++++ src/lib.rs | 1 + src/syntax.rs | 9 ++++- src/syntax/eval.rs | 64 ++++++++++++++++++++++++++++++ 10 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 src/eval.rs create mode 100644 src/eval/env.rs create mode 100644 src/eval/primop.rs create mode 100644 src/eval/value.rs create mode 100644 src/ir/eval.rs create mode 100644 src/syntax/eval.rs diff --git a/src/eval.rs b/src/eval.rs new file mode 100644 index 0000000..e140e4f --- /dev/null +++ b/src/eval.rs @@ -0,0 +1,15 @@ +mod env; +mod primop; +mod value; + +pub use env::{EvalEnvironment, LookupError}; +pub use primop::PrimOpError; +pub use value::Value; + +#[derive(Clone, Debug, PartialEq, thiserror::Error)] +pub enum EvalError { + #[error(transparent)] + Lookup(#[from] LookupError), + #[error(transparent)] + PrimOp(#[from] PrimOpError), +} diff --git a/src/eval/env.rs b/src/eval/env.rs new file mode 100644 index 0000000..8fd16d8 --- /dev/null +++ b/src/eval/env.rs @@ -0,0 +1,92 @@ +use crate::eval::Value; +use internment::ArcIntern; +use std::sync::Arc; + +pub struct EvalEnvironment { + inner: Arc, +} + +pub enum EvalEnvInternal { + Empty, + Value(ArcIntern, Value, Arc), +} + + +#[derive(Clone, Debug, PartialEq, thiserror::Error)] +pub enum LookupError { + #[error("Could not find variable '{0}' in environment")] + CouldNotFind(ArcIntern), +} + +impl Default for EvalEnvironment { + fn default() -> Self { + EvalEnvironment::empty() + } +} + +impl EvalEnvironment { + pub fn empty() -> Self { + 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())) + } + } + + pub fn lookup(&self, n: ArcIntern) -> Result { + self.inner.lookup(n) + } +} + +impl EvalEnvInternal { + fn lookup(&self, n: ArcIntern) -> Result { + match self { + EvalEnvInternal::Empty => Err(LookupError::CouldNotFind(n)), + EvalEnvInternal::Value(name, value, _) if *name == n => Ok(value.clone()), + EvalEnvInternal::Value(_, _, rest) => rest.lookup(n), + } + } +} + +#[cfg(test)] +mod tests { + use internment::ArcIntern; + + use super::EvalEnvironment; + + #[test] + fn simple_lookups() { + let tester = EvalEnvironment::default(); + let tester = tester.extend(arced("foo"), 1i64.into()); + let tester = tester.extend(arced("bar"), 2i64.into()); + let tester = tester.extend(arced("goo"), 5i64.into()); + + assert_eq!(tester.lookup(arced("foo")), Ok(1.into())); + assert_eq!(tester.lookup(arced("bar")), Ok(2.into())); + assert_eq!(tester.lookup(arced("goo")), Ok(5.into())); + assert!(tester.lookup(arced("baz")).is_err()); + } + + #[test] + fn nested() { + let tester = EvalEnvironment::default(); + let tester = tester.extend(arced("foo"), 1i64.into()); + + check_nested(&tester); + + assert_eq!(tester.lookup(arced("foo")), Ok(1.into())); + assert!(tester.lookup(arced("bar")).is_err()); + } + + fn check_nested(env: &EvalEnvironment) { + let nested_env = env.extend(arced("bar"), 2i64.into()); + assert_eq!(nested_env.lookup(arced("foo")), Ok(1.into())); + assert_eq!(nested_env.lookup(arced("bar")), Ok(2.into())); + } + + fn arced(s: &str) -> ArcIntern { + ArcIntern::new(s.to_string()) + } +} diff --git a/src/eval/primop.rs b/src/eval/primop.rs new file mode 100644 index 0000000..a063554 --- /dev/null +++ b/src/eval/primop.rs @@ -0,0 +1,46 @@ +use crate::eval::value::Value; + +#[derive(Clone, Debug, PartialEq, thiserror::Error)] +pub enum PrimOpError { + #[error("Math error (underflow or overflow) computing {0} operator")] + MathFailure(&'static str), + #[error("Type mismatch ({1} vs {2}) computing {0} operator")] + TypeMismatch(String, Value, Value), + #[error("Bad type for operator {0}: {1}")] + BadTypeFor(&'static str, Value), + #[error("Illegal number of arguments for {0}: {1} arguments found")] + BadArgCount(String, usize), + #[error("Unknown primitive operation {0}")] + UnknownPrimOp(String), +} + +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), + _ => Err(PrimOpError::UnknownPrimOp($op.to_string())), + } + }; +} + +impl Value { + fn binary_op(operation: &str, left: &Value, right: &Value) -> Result { + 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())) + } + } + } + + pub fn calculate(operation: &str, values: Vec) -> Result { + if values.len() == 2 { + Value::binary_op(operation, &values[0], &values[1]) + } else { + Err(PrimOpError::BadArgCount(operation.to_string(), values.len())) + } + } +} diff --git a/src/eval/value.rs b/src/eval/value.rs new file mode 100644 index 0000000..a158dc9 --- /dev/null +++ b/src/eval/value.rs @@ -0,0 +1,20 @@ +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq)] +pub enum Value { + I64(i64), +} + +impl Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::I64(x) => write!(f, "{}i64", x), + } + } +} + +impl From for Value { + fn from(value: i64) -> Self { + Value::I64(value) + } +} diff --git a/src/ir.rs b/src/ir.rs index 9b5157d..b7cd9cf 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -1,4 +1,5 @@ mod ast; +mod eval; mod from_syntax; mod strings; diff --git a/src/ir/eval.rs b/src/ir/eval.rs new file mode 100644 index 0000000..a014000 --- /dev/null +++ b/src/ir/eval.rs @@ -0,0 +1,76 @@ +use internment::ArcIntern; + +use crate::eval::{EvalEnvironment, EvalError, Value}; +use crate::ir::{Program, Statement, Expression}; + +use super::{ValueOrRef, Primitive}; + +impl Program { + pub fn eval(&self) -> Result { + 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 { + 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); +} \ No newline at end of file diff --git a/src/ir/from_syntax.rs b/src/ir/from_syntax.rs index c4d710d..1102c45 100644 --- a/src/ir/from_syntax.rs +++ b/src/ir/from_syntax.rs @@ -71,3 +71,13 @@ impl From 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); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 6ed733f..71d55e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ pub mod backend; +pub mod eval; pub mod ir; pub mod syntax; diff --git a/src/syntax.rs b/src/syntax.rs index 39e992d..d3dbc6f 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -4,6 +4,7 @@ use logos::Logos; mod arbitrary; pub mod ast; +mod eval; mod location; mod simplify; mod tokens; @@ -17,7 +18,7 @@ mod validate; pub use crate::syntax::ast::*; pub use crate::syntax::location::Location; -use crate::syntax::parser::ProgramParser; +use crate::{syntax::parser::ProgramParser, eval::EvalError}; pub use crate::syntax::tokens::{LexerError, Token}; #[cfg(test)] use ::pretty::{Arena, Pretty}; @@ -269,4 +270,10 @@ proptest::proptest! { let (errors, _) = program.validate(); prop_assert!(errors.is_empty()); } + + #[test] + fn generated_run_or_overflow(program: Program) { + use crate::eval::PrimOpError; + assert!(matches!(program.eval(), Ok(_) | Err(EvalError::PrimOp(PrimOpError::MathFailure(_))))) + } } diff --git a/src/syntax/eval.rs b/src/syntax/eval.rs new file mode 100644 index 0000000..6473e18 --- /dev/null +++ b/src/syntax/eval.rs @@ -0,0 +1,64 @@ +use internment::ArcIntern; + +use crate::eval::{EvalEnvironment, EvalError, Value}; +use crate::syntax::{Program, Statement, Expression}; + +impl Program { + pub fn eval(&self) -> Result { + 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(ArcIntern::new(name.clone()), actual_value); + } + + Statement::Print(_, name) => { + let value = env.lookup(ArcIntern::new(name.clone()))?; + let line = format!("{} = {}\n", name, value); + stdout.push_str(&line); + } + } + } + + Ok(stdout) + } +} + +impl Expression { + fn eval(&self, env: &EvalEnvironment) -> Result { + 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()))?), + + Expression::Primitive(_, op, args) => { + let mut arg_values = Vec::with_capacity(args.len()); + + for arg in args.iter() { + arg_values.push(arg.eval(env)?); + } + + Ok(Value::calculate(op, arg_values)?) + } + } + } +} + +#[test] +fn two_plus_three() { + let input = Program::parse(0, "x = 2 + 3; print x;").expect("parse works"); + let output = input.eval().expect("runs successfully"); + assert_eq!("x = 5i64\n", &output); +} + +#[test] +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 -- 2.53.0 From 8d8b20051311b698d4b67fa3a01b561c77ab820a Mon Sep 17 00:00:00 2001 From: Adam Wick Date: Sat, 8 Apr 2023 14:20:49 -0700 Subject: [PATCH 2/6] 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 +} -- 2.53.0 From 00af4a5f05b52b0afd30aa611ca8e05949188330 Mon Sep 17 00:00:00 2001 From: Adam Wick Date: Thu, 13 Apr 2023 20:57:09 -0700 Subject: [PATCH 3/6] Remove a stray file. --- src/backend/object.rs | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/backend/object.rs diff --git a/src/backend/object.rs b/src/backend/object.rs deleted file mode 100644 index d17fd03..0000000 --- a/src/backend/object.rs +++ /dev/null @@ -1,8 +0,0 @@ -struct BackendObject { -} - -impl BackendObject { - pub fn new() -> Result { - unimplemented!() - } -} \ No newline at end of file -- 2.53.0 From d455ee87b55804a380e51c8c8dd20424a55c155e Mon Sep 17 00:00:00 2001 From: Adam Wick Date: Thu, 13 Apr 2023 21:04:43 -0700 Subject: [PATCH 4/6] Add a proptest for the JIT backend. --- runtime/rts.c | 4 ++-- src/backend.rs | 21 ++++++++++++++++++++- src/backend/eval.rs | 22 ++++++++++++++++++++-- src/backend/into_crane.rs | 6 +++++- src/backend/runtime.rs | 12 +++++++++--- src/bin/ngri.rs | 2 +- src/eval.rs | 36 +++++++++++++++++++++++++++++++++++- 7 files changed, 92 insertions(+), 11 deletions(-) diff --git a/runtime/rts.c b/runtime/rts.c index c7c87fa..0ddd90f 100644 --- a/runtime/rts.c +++ b/runtime/rts.c @@ -1,12 +1,12 @@ #include #include -void print(char *variable_name, uint64_t value) { +void print(char *_ignore, char *variable_name, uint64_t value) { printf("%s = %llii64\n", variable_name, value); } void caller() { - print("x", 4); + print(NULL, "x", 4); } extern void gogogo(); diff --git a/src/backend.rs b/src/backend.rs index 24d71e6..f70a52f 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -22,10 +22,11 @@ pub struct Backend { runtime_functions: RuntimeFunctions, defined_strings: HashMap, defined_symbols: HashMap, + output_buffer: Option, } impl Backend { - pub fn jit() -> Result { + pub fn jit(output_buffer: Option) -> Result { let platform = Triple::host(); let isa_builder = isa::lookup(platform.clone())?; let mut settings_builder = settings::builder(); @@ -45,6 +46,7 @@ impl Backend { runtime_functions, defined_strings: HashMap::new(), defined_symbols: HashMap::new(), + output_buffer, }) } @@ -70,6 +72,7 @@ impl Backend { runtime_functions, defined_strings: HashMap::new(), defined_symbols: HashMap::new(), + output_buffer: None, }) } @@ -104,4 +107,20 @@ impl Backend { self.defined_symbols.insert(name, id); Ok(id) } + + pub fn output_buffer_ptr(&mut self) -> *mut String { + if let Some(str) = self.output_buffer.as_mut() { + str as *mut String + } else { + std::ptr::null_mut() + } + } + + pub fn output(self) -> String { + if let Some(s) = self.output_buffer { + s + } else { + String::new() + } + } } diff --git a/src/backend/eval.rs b/src/backend/eval.rs index fef25bc..950a5a1 100644 --- a/src/backend/eval.rs +++ b/src/backend/eval.rs @@ -8,8 +8,14 @@ use cranelift_object::ObjectModule; use target_lexicon::Triple; impl Backend { - pub fn eval(_program: Program) -> Result { - unimplemented!() + pub fn eval(program: Program) -> Result { + 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()) } } @@ -86,4 +92,16 @@ proptest::proptest! { 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::::eval(program); + assert_eq!(basic_result, compiled_result); + } + } } diff --git a/src/backend/into_crane.rs b/src/backend/into_crane.rs index 8605feb..6ceff02 100644 --- a/src/backend/into_crane.rs +++ b/src/backend/into_crane.rs @@ -60,6 +60,8 @@ impl Backend { 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 Backend { &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) => { diff --git a/src/backend/runtime.rs b/src/backend/runtime.rs index 01e00bc..1338a73 100644 --- a/src/backend/runtime.rs +++ b/src/backend/runtime.rs @@ -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; @@ -18,10 +19,15 @@ pub enum RuntimeFunctionError { 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), }, diff --git a/src/bin/ngri.rs b/src/bin/ngri.rs index c78179f..b1d74de 100644 --- a/src/bin/ngri.rs +++ b/src/bin/ngri.rs @@ -48,7 +48,7 @@ impl<'a> RunLoop<'a> { pub fn new(writer: &'a mut dyn WriteColor, config: Config) -> Result { Ok(RunLoop { file_database: SimpleFiles::new(), - jitter: Backend::jit()?, + jitter: Backend::jit(None)?, variable_binding_sites: HashMap::new(), gensym_index: 1, writer, diff --git a/src/eval.rs b/src/eval.rs index edb2ca9..b764eb5 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -2,13 +2,14 @@ mod env; mod primop; mod value; +use cranelift_module::ModuleError; pub use env::{EvalEnvironment, LookupError}; pub use primop::PrimOpError; pub use value::Value; use crate::backend::BackendError; -#[derive(Debug, PartialEq, thiserror::Error)] +#[derive(Debug, thiserror::Error)] pub enum EvalError { #[error(transparent)] Lookup(#[from] LookupError), @@ -18,6 +19,8 @@ pub enum EvalError { Backend(#[from] BackendError), #[error("IO error: {0}")] IO(String), + #[error(transparent)] + Module(#[from] ModuleError), } impl From for EvalError { @@ -25,3 +28,34 @@ impl From for EvalError { EvalError::IO(value.to_string()) } } + +impl PartialEq for EvalError { + fn eq(&self, other: &Self) -> bool { + match self { + EvalError::Lookup(a) => match other { + EvalError::Lookup(b) => a == b, + _ => false, + }, + + EvalError::PrimOp(a) => match other { + EvalError::PrimOp(b) => a == b, + _ => false, + }, + + EvalError::Backend(a) => match other { + EvalError::Backend(b) => a == b, + _ => false, + }, + + EvalError::IO(a) => match other { + EvalError::IO(b) => a == b, + _ => false, + }, + + EvalError::Module(a) => match other { + EvalError::Module(b) => a.to_string() == b.to_string(), + _ => false, + }, + } + } +} -- 2.53.0 From c2c2cbd02ab6b4e5f69161d77788b865a456cecd Mon Sep 17 00:00:00 2001 From: Adam Wick Date: Sun, 16 Apr 2023 15:32:34 -0700 Subject: [PATCH 5/6] Use clang instead of gcc, which seems easier to install. --- src/backend/eval.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/backend/eval.rs b/src/backend/eval.rs index 950a5a1..1218507 100644 --- a/src/backend/eval.rs +++ b/src/backend/eval.rs @@ -52,11 +52,10 @@ impl Backend { } } - #[cfg(target_family = "unix")] fn link(object_file: &Path, executable_path: &Path) -> Result<(), EvalError> { - use std::{os::unix::prelude::PermissionsExt, path::PathBuf}; + use std::path::PathBuf; - let output = std::process::Command::new("gcc") + let output = std::process::Command::new("clang") .arg( PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("runtime") @@ -67,16 +66,13 @@ impl Backend { .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( + if !output.stderr.is_empty() { + return Err(EvalError::IO( std::string::String::from_utf8_lossy(&output.stderr).to_string(), - )) + )); } + + Ok(()) } } -- 2.53.0 From 74848d84f820354fdce5b60f0452cffbe27de675 Mon Sep 17 00:00:00 2001 From: Adam Wick Date: Sun, 16 Apr 2023 15:59:14 -0700 Subject: [PATCH 6/6] Clean up output printing. --- runtime/rts.c | 5 +++-- src/backend/eval.rs | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/runtime/rts.c b/runtime/rts.c index 0ddd90f..4cf41e5 100644 --- a/runtime/rts.c +++ b/runtime/rts.c @@ -1,8 +1,9 @@ #include #include +#include -void print(char *_ignore, char *variable_name, uint64_t value) { - printf("%s = %llii64\n", variable_name, value); +void print(char *_ignore, char *variable_name, int64_t value) { + printf("%s = %" PRId64 "i64\n", variable_name, value); } void caller() { diff --git a/src/backend/eval.rs b/src/backend/eval.rs index 1218507..a57c625 100644 --- a/src/backend/eval.rs +++ b/src/backend/eval.rs @@ -83,6 +83,9 @@ proptest::proptest! { 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::::eval(program); assert_eq!(basic_result, compiled_result); -- 2.53.0