From 7a1d22da2db71f0074f150176ce8fce6942543a1 Mon Sep 17 00:00:00 2001 From: Adam Wick Date: Fri, 7 Apr 2023 10:04:57 -0700 Subject: [PATCH] Proptest testing! --- .gitignore | 1 + Cargo.toml | 1 + src/backend.rs | 11 ++- src/backend/error.rs | 7 +- src/backend/into_crane.rs | 15 ++-- src/backend/runtime.rs | 1 - src/bin/ngrc.rs | 4 +- src/syntax.rs | 34 ++++++++ src/syntax/arbitrary.rs | 159 ++++++++++++++++++++++++++++++++++++++ src/syntax/ast.rs | 44 +++++++++-- src/syntax/parser.lalrpop | 9 ++- src/syntax/pretty.rs | 17 ++-- src/syntax/tokens.rs | 8 ++ 13 files changed, 279 insertions(+), 32 deletions(-) create mode 100644 src/syntax/arbitrary.rs diff --git a/.gitignore b/.gitignore index ce17bee..55de121 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Cargo.lock test *.dSYM .vscode +proptest-regressions/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index de0898e..41e2435 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ lalrpop-util = "^0.19.7" lazy_static = "^1.4.0" logos = "^0.12.0" pretty = { version = "^0.11.2", features = ["termcolor"] } +proptest = "^1.0.0" rustyline = "^11.0.0" target-lexicon = "^0.12.5" thiserror = "^1.0.30" diff --git a/src/backend.rs b/src/backend.rs index 756e934..54622dc 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -6,10 +6,10 @@ use std::collections::HashMap; pub use self::error::BackendError; pub use self::runtime::{RuntimeFunctionError, RuntimeFunctions}; -use cranelift_codegen::settings::{Configurable}; +use cranelift_codegen::settings::Configurable; use cranelift_codegen::{isa, settings}; -use cranelift_jit::{JITModule, JITBuilder}; -use cranelift_module::{default_libcall_names, DataContext, DataId, Module, Linkage, FuncId}; +use cranelift_jit::{JITBuilder, JITModule}; +use cranelift_module::{default_libcall_names, DataContext, DataId, FuncId, Linkage, Module}; use cranelift_object::{object, ObjectBuilder, ObjectModule}; use target_lexicon::Triple; @@ -49,7 +49,7 @@ impl Backend { pub fn bytes(&self, function_id: FuncId) -> *const u8 { self.module.get_finalized_function(function_id) - } + } } impl Backend { @@ -77,8 +77,7 @@ impl Backend { } } -impl Backend -{ +impl Backend { pub fn define_string(&mut self, s: &str) -> Result { let name = format!("{}", s); let global_id = self diff --git a/src/backend/error.rs b/src/backend/error.rs index ea55990..26b7bf0 100644 --- a/src/backend/error.rs +++ b/src/backend/error.rs @@ -1,7 +1,7 @@ -use codespan_reporting::diagnostic::Diagnostic; -use cranelift_codegen::{CodegenError, settings::SetError, isa::LookupError}; -use cranelift_module::ModuleError; use crate::backend::runtime::RuntimeFunctionError; +use codespan_reporting::diagnostic::Diagnostic; +use cranelift_codegen::{isa::LookupError, settings::SetError, CodegenError}; +use cranelift_module::ModuleError; use thiserror::Error; #[derive(Debug, Error)] @@ -44,4 +44,3 @@ impl From for Diagnostic { } } } - diff --git a/src/backend/into_crane.rs b/src/backend/into_crane.rs index 78887a0..8605feb 100644 --- a/src/backend/into_crane.rs +++ b/src/backend/into_crane.rs @@ -11,8 +11,8 @@ use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext, Variable}; use cranelift_module::{FuncId, Linkage, Module, ModuleError}; use internment::ArcIntern; -use crate::backend::Backend; use crate::backend::error::BackendError; +use crate::backend::Backend; type StringTable = HashMap, GlobalValue>; @@ -21,15 +21,16 @@ impl Backend { &mut self, function_name: &str, mut program: Program, - ) -> Result - { + ) -> Result { let basic_signature = Signature { params: vec![], returns: vec![], call_conv: CallConv::SystemV, }; - let func_id = self.module.declare_function(function_name, Linkage::Export, &basic_signature)?; + let func_id = + self.module + .declare_function(function_name, Linkage::Export, &basic_signature)?; let mut ctx = Context::new(); ctx.func = Function::with_name_signature(UserFuncName::user(0, func_id.as_u32()), basic_signature); @@ -37,7 +38,11 @@ impl Backend { let string_table = self.build_string_table(&mut ctx.func, &program)?; let mut variable_table = HashMap::new(); let mut next_var_num = 1; - let print_func_ref = self.runtime_functions.include_runtime_function("print", &mut self.module, &mut ctx.func)?; + let print_func_ref = self.runtime_functions.include_runtime_function( + "print", + &mut self.module, + &mut ctx.func, + )?; let pre_defined_symbols: HashMap = self .defined_symbols .iter() diff --git a/src/backend/runtime.rs b/src/backend/runtime.rs index 2412b6b..ecf0e7b 100644 --- a/src/backend/runtime.rs +++ b/src/backend/runtime.rs @@ -66,5 +66,4 @@ impl RuntimeFunctions { pub fn register_jit_implementations(builder: &mut JITBuilder) { builder.symbol("print", runtime_print as *const u8); } - } diff --git a/src/bin/ngrc.rs b/src/bin/ngrc.rs index d488530..23a9021 100644 --- a/src/bin/ngrc.rs +++ b/src/bin/ngrc.rs @@ -5,8 +5,8 @@ use codespan_reporting::term; use codespan_reporting::term::termcolor::{ColorChoice, StandardStream}; use cranelift_object::object; -use ngr::backend::BackendError; use ngr::backend::Backend; +use ngr::backend::BackendError; use ngr::ir::Program as IR; use ngr::syntax::{ParserError, Program as Syntax}; use target_lexicon::Triple; @@ -71,7 +71,7 @@ fn compile(file_database: &mut SimpleFiles) -> Result<(), MainEr let ir = IR::from(syntax.simplify()); let mut backend = Backend::object_file(Triple::host())?; - backend.compile_function("gogogo", ir)?; + backend.compile_function("gogogo", ir)?; let bytes = backend.bytes()?; std::fs::write(args.output.unwrap_or_else(|| "output.o".to_string()), bytes)?; Ok(()) diff --git a/src/syntax.rs b/src/syntax.rs index 9d978bf..39e992d 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -2,6 +2,7 @@ use codespan_reporting::{diagnostic::Diagnostic, files::SimpleFiles}; use lalrpop_util::lalrpop_mod; use logos::Logos; +mod arbitrary; pub mod ast; mod location; mod simplify; @@ -18,8 +19,12 @@ pub use crate::syntax::ast::*; pub use crate::syntax::location::Location; use crate::syntax::parser::ProgramParser; pub use crate::syntax::tokens::{LexerError, Token}; +#[cfg(test)] +use ::pretty::{Arena, Pretty}; use lalrpop_util::ParseError; #[cfg(test)] +use proptest::{prop_assert, prop_assert_eq}; +#[cfg(test)] use std::str::FromStr; use thiserror::Error; @@ -236,3 +241,32 @@ fn order_of_operations() { } ); } + +proptest::proptest! { + #[test] + fn random_render_parses_equal(program: Program) { + let mut file_database = SimpleFiles::new(); + let writer = ::pretty::termcolor::StandardStream::stderr(::pretty::termcolor::ColorChoice::Auto); + let config = codespan_reporting::term::Config::default(); + let allocator = Arena::<()>::new(); + + let mut out_vector = vec![]; + prop_assert!(program.pretty(&allocator).render(80, &mut out_vector).is_ok()); + let string = std::str::from_utf8(&out_vector).expect("emitted valid string"); + let file_handle = file_database.add("test", string); + let file_db_info = file_database.get(file_handle).expect("find thing just inserted"); + let parsed = Program::parse(file_handle, file_db_info.source()); + + if let Err(e) = &parsed { + eprintln!("failed to parse:\n{}", string); + codespan_reporting::term::emit(&mut writer.lock(), &config, &file_database, &e.into()).unwrap(); + } + prop_assert_eq!(program, parsed.unwrap()); + } + + #[test] + fn random_syntaxes_validate(program: Program) { + let (errors, _) = program.validate(); + prop_assert!(errors.is_empty()); + } +} diff --git a/src/syntax/arbitrary.rs b/src/syntax/arbitrary.rs new file mode 100644 index 0000000..52f43ab --- /dev/null +++ b/src/syntax/arbitrary.rs @@ -0,0 +1,159 @@ +use std::collections::HashSet; + +use crate::syntax::ast::{Expression, Program, Statement, Value}; +use crate::syntax::location::Location; +use proptest::sample::select; +use proptest::{ + prelude::{Arbitrary, BoxedStrategy, Strategy}, + strategy::{Just, Union}, +}; + +const VALID_VARIABLE_NAMES: &str = r"[a-z][a-zA-Z0-9_]*"; + +#[derive(Debug)] +struct Name(String); + +impl Arbitrary for Name { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + VALID_VARIABLE_NAMES.prop_map(Name).boxed() + } +} + +impl Arbitrary for Program { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + let optionals = Vec::>::arbitrary(); + + optionals + .prop_flat_map(|mut possible_names| { + let mut statements = Vec::new(); + let mut defined_variables: HashSet = HashSet::new(); + + for possible_name in possible_names.drain(..) { + match possible_name { + None if defined_variables.is_empty() => continue, + None => statements.push( + Union::new(defined_variables.iter().map(|name| { + Just(Statement::Print(Location::manufactured(), name.to_string())) + })) + .boxed(), + ), + Some(new_name) => { + let closures_name = new_name.0.clone(); + let retval = + Expression::arbitrary_with(Some(defined_variables.clone())) + .prop_map(move |exp| { + Statement::Binding( + Location::manufactured(), + closures_name.clone(), + exp, + ) + }) + .boxed(); + + defined_variables.insert(new_name.0); + statements.push(retval); + } + } + } + + statements + }) + .prop_map(|statements| Program { statements }) + .boxed() + } +} + +impl Arbitrary for Statement { + type Parameters = Option>; + type Strategy = BoxedStrategy; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + let duplicated_args = args.clone(); + let defined_variables = args.unwrap_or_default(); + + let binding_strategy = ( + VALID_VARIABLE_NAMES, + Expression::arbitrary_with(duplicated_args), + ) + .prop_map(|(name, exp)| Statement::Binding(Location::manufactured(), name, exp)) + .boxed(); + + if defined_variables.is_empty() { + binding_strategy + } else { + let print_strategy = Union::new( + defined_variables + .iter() + .map(|x| Just(Statement::Print(Location::manufactured(), x.to_string()))), + ) + .boxed(); + + Union::new([binding_strategy, print_strategy]).boxed() + } + } +} + +impl Arbitrary for Expression { + type Parameters = Option>; + type Strategy = BoxedStrategy; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + let defined_variables = args.unwrap_or_default(); + + let value_strategy = Value::arbitrary() + .prop_map(move |x| Expression::Value(Location::manufactured(), x)) + .boxed(); + + let leaf_strategy = if defined_variables.is_empty() { + value_strategy + } else { + let reference_strategy = Union::new(defined_variables.iter().map(|x| { + Just(Expression::Reference( + Location::manufactured(), + x.to_owned(), + )) + })) + .boxed(); + Union::new([value_strategy, reference_strategy]).boxed() + }; + + leaf_strategy + .prop_recursive(3, 64, 2, move |inner| { + ( + select(super::BINARY_OPERATORS), + proptest::collection::vec(inner, 2), + ) + .prop_map(move |(operator, exprs)| { + Expression::Primitive(Location::manufactured(), operator.to_string(), exprs) + }) + }) + .boxed() + } +} + +impl Arbitrary for Value { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + let base_strategy = Union::new([ + Just(None::), + Just(Some(2)), + Just(Some(8)), + Just(Some(10)), + Just(Some(16)), + ]); + + let value_strategy = i64::arbitrary(); + + (base_strategy, value_strategy) + .prop_map(move |(base, value)| Value::Number(base, value)) + .boxed() + } +} diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index d5acea3..ad28025 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -2,25 +2,59 @@ use crate::syntax::Location; pub static BINARY_OPERATORS: &[&str] = &["+", "-", "*", "/"]; -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Program { pub statements: Vec, } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug)] pub enum Statement { Binding(Location, String, Expression), Print(Location, String), } -#[derive(Debug, PartialEq)] +impl PartialEq for Statement { + fn eq(&self, other: &Self) -> bool { + match self { + Statement::Binding(_, name1, expr1) => match other { + Statement::Binding(_, name2, expr2) => name1 == name2 && expr1 == expr2, + _ => false, + }, + Statement::Print(_, name1) => match other { + Statement::Print(_, name2) => name1 == name2, + _ => false, + }, + } + } +} + +#[derive(Clone, Debug)] pub enum Expression { Value(Location, Value), Reference(Location, String), Primitive(Location, String, Vec), } -#[derive(Debug, PartialEq, Eq)] +impl PartialEq for Expression { + fn eq(&self, other: &Self) -> bool { + match self { + Expression::Value(_, val1) => match other { + Expression::Value(_, val2) => val1 == val2, + _ => false, + }, + Expression::Reference(_, var1) => match other { + Expression::Reference(_, var2) => var1 == var2, + _ => false, + }, + Expression::Primitive(_, prim1, args1) => match other { + Expression::Primitive(_, prim2, args2) => prim1 == prim2 && args1 == args2, + _ => false, + }, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Value { Number(Option, i64), -} \ No newline at end of file +} diff --git a/src/syntax/parser.lalrpop b/src/syntax/parser.lalrpop index 12cd2c7..85b21b3 100644 --- a/src/syntax/parser.lalrpop +++ b/src/syntax/parser.lalrpop @@ -12,6 +12,8 @@ extern { enum Token { "=" => Token::Equals, ";" => Token::Semi, + "(" => Token::LeftParen, + ")" => Token::RightParen, "print" => Token::Print, @@ -67,5 +69,10 @@ AtomicExpression: Expression = { "> => { let val = Value::Number(n.0, n.1); Expression::Value(Location::new(file_idx, l), val) - } + }, + "-" "> => { + let val = Value::Number(n.0, -n.1); + Expression::Value(Location::new(file_idx, l), val) + }, + "(" ")" => e, } \ No newline at end of file diff --git a/src/syntax/pretty.rs b/src/syntax/pretty.rs index f9d18ff..46a59fb 100644 --- a/src/syntax/pretty.rs +++ b/src/syntax/pretty.rs @@ -1,5 +1,5 @@ -use pretty::{Pretty, DocAllocator, DocBuilder}; -use crate::syntax::ast::{Program, Statement, Expression, Value, BINARY_OPERATORS}; +use crate::syntax::ast::{Expression, Program, Statement, Value, BINARY_OPERATORS}; +use pretty::{DocAllocator, DocBuilder, Pretty}; impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b Program where @@ -85,13 +85,14 @@ where fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> { match self { Value::Number(opt_base, value) => { + let sign = if *value < 0 { "-" } else { "" }; let value_str = match opt_base { None => format!("{}", value), - Some(2) => format!("0b{:b}", value), - Some(8) => format!("0o{:o}", value), - Some(10) => format!("0d{}", value), - Some(16) => format!("0x{:x}", value), - Some(_) => format!("!!{:x}!!", value), + Some(2) => format!("{}0b{:b}", sign, value.abs()), + Some(8) => format!("{}0o{:o}", sign, value.abs()), + Some(10) => format!("{}0d{}", sign, value.abs()), + Some(16) => format!("{}0x{:x}", sign, value.abs()), + Some(_) => format!("!!{}{:x}!!", sign, value.abs()), }; allocator.text(value_str) @@ -111,4 +112,4 @@ where fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> { allocator.text(",").append(allocator.space()) } -} \ No newline at end of file +} diff --git a/src/syntax/tokens.rs b/src/syntax/tokens.rs index 10818ca..78d0c8a 100644 --- a/src/syntax/tokens.rs +++ b/src/syntax/tokens.rs @@ -12,6 +12,12 @@ pub enum Token { #[token(";")] Semi, + #[token("(")] + LeftParen, + + #[token(")")] + RightParen, + #[token("print")] Print, @@ -39,6 +45,8 @@ impl fmt::Display for Token { match self { Token::Equals => write!(f, "'='"), Token::Semi => write!(f, "';'"), + Token::LeftParen => write!(f, "'('"), + Token::RightParen => write!(f, "')'"), Token::Print => write!(f, "'print'"), Token::Operator(c) => write!(f, "'{}'", c), Token::Number((None, v)) => write!(f, "'{}'", v),