Proptest testing!

This commit is contained in:
2023-04-07 10:04:57 -07:00
parent 1d7ca05d73
commit 7a1d22da2d
13 changed files with 279 additions and 32 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ Cargo.lock
test test
*.dSYM *.dSYM
.vscode .vscode
proptest-regressions/

View File

@@ -23,6 +23,7 @@ lalrpop-util = "^0.19.7"
lazy_static = "^1.4.0" lazy_static = "^1.4.0"
logos = "^0.12.0" logos = "^0.12.0"
pretty = { version = "^0.11.2", features = ["termcolor"] } pretty = { version = "^0.11.2", features = ["termcolor"] }
proptest = "^1.0.0"
rustyline = "^11.0.0" rustyline = "^11.0.0"
target-lexicon = "^0.12.5" target-lexicon = "^0.12.5"
thiserror = "^1.0.30" thiserror = "^1.0.30"

View File

@@ -6,10 +6,10 @@ use std::collections::HashMap;
pub use self::error::BackendError; pub use self::error::BackendError;
pub use self::runtime::{RuntimeFunctionError, RuntimeFunctions}; pub use self::runtime::{RuntimeFunctionError, RuntimeFunctions};
use cranelift_codegen::settings::{Configurable}; use cranelift_codegen::settings::Configurable;
use cranelift_codegen::{isa, settings}; use cranelift_codegen::{isa, settings};
use cranelift_jit::{JITModule, JITBuilder}; use cranelift_jit::{JITBuilder, JITModule};
use cranelift_module::{default_libcall_names, DataContext, DataId, Module, Linkage, FuncId}; use cranelift_module::{default_libcall_names, DataContext, DataId, FuncId, Linkage, Module};
use cranelift_object::{object, ObjectBuilder, ObjectModule}; use cranelift_object::{object, ObjectBuilder, ObjectModule};
use target_lexicon::Triple; use target_lexicon::Triple;
@@ -49,7 +49,7 @@ impl Backend<JITModule> {
pub fn bytes(&self, function_id: FuncId) -> *const u8 { pub fn bytes(&self, function_id: FuncId) -> *const u8 {
self.module.get_finalized_function(function_id) self.module.get_finalized_function(function_id)
} }
} }
impl Backend<ObjectModule> { impl Backend<ObjectModule> {
@@ -77,8 +77,7 @@ impl Backend<ObjectModule> {
} }
} }
impl<M: Module> Backend<M> impl<M: Module> Backend<M> {
{
pub fn define_string(&mut self, s: &str) -> Result<DataId, BackendError> { pub fn define_string(&mut self, s: &str) -> Result<DataId, BackendError> {
let name = format!("<string_constant>{}", s); let name = format!("<string_constant>{}", s);
let global_id = self let global_id = self

View File

@@ -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 crate::backend::runtime::RuntimeFunctionError;
use codespan_reporting::diagnostic::Diagnostic;
use cranelift_codegen::{isa::LookupError, settings::SetError, CodegenError};
use cranelift_module::ModuleError;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -44,4 +44,3 @@ impl From<BackendError> for Diagnostic<usize> {
} }
} }
} }

View File

@@ -11,8 +11,8 @@ use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext, Variable};
use cranelift_module::{FuncId, Linkage, Module, ModuleError}; use cranelift_module::{FuncId, Linkage, Module, ModuleError};
use internment::ArcIntern; use internment::ArcIntern;
use crate::backend::Backend;
use crate::backend::error::BackendError; use crate::backend::error::BackendError;
use crate::backend::Backend;
type StringTable = HashMap<ArcIntern<String>, GlobalValue>; type StringTable = HashMap<ArcIntern<String>, GlobalValue>;
@@ -21,15 +21,16 @@ impl<M: Module> Backend<M> {
&mut self, &mut self,
function_name: &str, function_name: &str,
mut program: Program, mut program: Program,
) -> Result<FuncId, BackendError> ) -> Result<FuncId, BackendError> {
{
let basic_signature = Signature { let basic_signature = Signature {
params: vec![], params: vec![],
returns: vec![], returns: vec![],
call_conv: CallConv::SystemV, 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(); let mut ctx = Context::new();
ctx.func = ctx.func =
Function::with_name_signature(UserFuncName::user(0, func_id.as_u32()), basic_signature); Function::with_name_signature(UserFuncName::user(0, func_id.as_u32()), basic_signature);
@@ -37,7 +38,11 @@ impl<M: Module> Backend<M> {
let string_table = self.build_string_table(&mut ctx.func, &program)?; let string_table = self.build_string_table(&mut ctx.func, &program)?;
let mut variable_table = HashMap::new(); let mut variable_table = HashMap::new();
let mut next_var_num = 1; 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<String, GlobalValue> = self let pre_defined_symbols: HashMap<String, GlobalValue> = self
.defined_symbols .defined_symbols
.iter() .iter()

View File

@@ -66,5 +66,4 @@ impl RuntimeFunctions {
pub fn register_jit_implementations(builder: &mut JITBuilder) { pub fn register_jit_implementations(builder: &mut JITBuilder) {
builder.symbol("print", runtime_print as *const u8); builder.symbol("print", runtime_print as *const u8);
} }
} }

View File

@@ -5,8 +5,8 @@ use codespan_reporting::term;
use codespan_reporting::term::termcolor::{ColorChoice, StandardStream}; use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
use cranelift_object::object; use cranelift_object::object;
use ngr::backend::BackendError;
use ngr::backend::Backend; use ngr::backend::Backend;
use ngr::backend::BackendError;
use ngr::ir::Program as IR; use ngr::ir::Program as IR;
use ngr::syntax::{ParserError, Program as Syntax}; use ngr::syntax::{ParserError, Program as Syntax};
use target_lexicon::Triple; use target_lexicon::Triple;
@@ -71,7 +71,7 @@ fn compile(file_database: &mut SimpleFiles<String, String>) -> Result<(), MainEr
let ir = IR::from(syntax.simplify()); let ir = IR::from(syntax.simplify());
let mut backend = Backend::object_file(Triple::host())?; let mut backend = Backend::object_file(Triple::host())?;
backend.compile_function("gogogo", ir)?; backend.compile_function("gogogo", ir)?;
let bytes = backend.bytes()?; let bytes = backend.bytes()?;
std::fs::write(args.output.unwrap_or_else(|| "output.o".to_string()), bytes)?; std::fs::write(args.output.unwrap_or_else(|| "output.o".to_string()), bytes)?;
Ok(()) Ok(())

View File

@@ -2,6 +2,7 @@ use codespan_reporting::{diagnostic::Diagnostic, files::SimpleFiles};
use lalrpop_util::lalrpop_mod; use lalrpop_util::lalrpop_mod;
use logos::Logos; use logos::Logos;
mod arbitrary;
pub mod ast; pub mod ast;
mod location; mod location;
mod simplify; mod simplify;
@@ -18,8 +19,12 @@ pub use crate::syntax::ast::*;
pub use crate::syntax::location::Location; pub use crate::syntax::location::Location;
use crate::syntax::parser::ProgramParser; use crate::syntax::parser::ProgramParser;
pub use crate::syntax::tokens::{LexerError, Token}; pub use crate::syntax::tokens::{LexerError, Token};
#[cfg(test)]
use ::pretty::{Arena, Pretty};
use lalrpop_util::ParseError; use lalrpop_util::ParseError;
#[cfg(test)] #[cfg(test)]
use proptest::{prop_assert, prop_assert_eq};
#[cfg(test)]
use std::str::FromStr; use std::str::FromStr;
use thiserror::Error; 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());
}
}

159
src/syntax/arbitrary.rs Normal file
View File

@@ -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<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
VALID_VARIABLE_NAMES.prop_map(Name).boxed()
}
}
impl Arbitrary for Program {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
let optionals = Vec::<Option<Name>>::arbitrary();
optionals
.prop_flat_map(|mut possible_names| {
let mut statements = Vec::new();
let mut defined_variables: HashSet<String> = 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<HashSet<String>>;
type Strategy = BoxedStrategy<Self>;
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<HashSet<String>>;
type Strategy = BoxedStrategy<Self>;
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<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
let base_strategy = Union::new([
Just(None::<u8>),
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()
}
}

View File

@@ -2,25 +2,59 @@ use crate::syntax::Location;
pub static BINARY_OPERATORS: &[&str] = &["+", "-", "*", "/"]; pub static BINARY_OPERATORS: &[&str] = &["+", "-", "*", "/"];
#[derive(Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Program { pub struct Program {
pub statements: Vec<Statement>, pub statements: Vec<Statement>,
} }
#[derive(Debug, PartialEq)] #[derive(Clone, Debug)]
pub enum Statement { pub enum Statement {
Binding(Location, String, Expression), Binding(Location, String, Expression),
Print(Location, String), 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 { pub enum Expression {
Value(Location, Value), Value(Location, Value),
Reference(Location, String), Reference(Location, String),
Primitive(Location, String, Vec<Expression>), Primitive(Location, String, Vec<Expression>),
} }
#[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 { pub enum Value {
Number(Option<u8>, i64), Number(Option<u8>, i64),
} }

View File

@@ -12,6 +12,8 @@ extern {
enum Token { enum Token {
"=" => Token::Equals, "=" => Token::Equals,
";" => Token::Semi, ";" => Token::Semi,
"(" => Token::LeftParen,
")" => Token::RightParen,
"print" => Token::Print, "print" => Token::Print,
@@ -67,5 +69,10 @@ AtomicExpression: Expression = {
<l:@L> <n:"<num>"> => { <l:@L> <n:"<num>"> => {
let val = Value::Number(n.0, n.1); let val = Value::Number(n.0, n.1);
Expression::Value(Location::new(file_idx, l), val) Expression::Value(Location::new(file_idx, l), val)
} },
<l:@L> "-" <n:"<num>"> => {
let val = Value::Number(n.0, -n.1);
Expression::Value(Location::new(file_idx, l), val)
},
"(" <e:Expression> ")" => e,
} }

View File

@@ -1,5 +1,5 @@
use pretty::{Pretty, DocAllocator, DocBuilder}; use crate::syntax::ast::{Expression, Program, Statement, Value, BINARY_OPERATORS};
use crate::syntax::ast::{Program, Statement, Expression, Value, BINARY_OPERATORS}; use pretty::{DocAllocator, DocBuilder, Pretty};
impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b Program impl<'a, 'b, D, A> Pretty<'a, D, A> for &'b Program
where where
@@ -85,13 +85,14 @@ where
fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> { fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> {
match self { match self {
Value::Number(opt_base, value) => { Value::Number(opt_base, value) => {
let sign = if *value < 0 { "-" } else { "" };
let value_str = match opt_base { let value_str = match opt_base {
None => format!("{}", value), None => format!("{}", value),
Some(2) => format!("0b{:b}", value), Some(2) => format!("{}0b{:b}", sign, value.abs()),
Some(8) => format!("0o{:o}", value), Some(8) => format!("{}0o{:o}", sign, value.abs()),
Some(10) => format!("0d{}", value), Some(10) => format!("{}0d{}", sign, value.abs()),
Some(16) => format!("0x{:x}", value), Some(16) => format!("{}0x{:x}", sign, value.abs()),
Some(_) => format!("!!{:x}!!", value), Some(_) => format!("!!{}{:x}!!", sign, value.abs()),
}; };
allocator.text(value_str) allocator.text(value_str)
@@ -111,4 +112,4 @@ where
fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> { fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> {
allocator.text(",").append(allocator.space()) allocator.text(",").append(allocator.space())
} }
} }

View File

@@ -12,6 +12,12 @@ pub enum Token {
#[token(";")] #[token(";")]
Semi, Semi,
#[token("(")]
LeftParen,
#[token(")")]
RightParen,
#[token("print")] #[token("print")]
Print, Print,
@@ -39,6 +45,8 @@ impl fmt::Display for Token {
match self { match self {
Token::Equals => write!(f, "'='"), Token::Equals => write!(f, "'='"),
Token::Semi => write!(f, "';'"), Token::Semi => write!(f, "';'"),
Token::LeftParen => write!(f, "'('"),
Token::RightParen => write!(f, "')'"),
Token::Print => write!(f, "'print'"), Token::Print => write!(f, "'print'"),
Token::Operator(c) => write!(f, "'{}'", c), Token::Operator(c) => write!(f, "'{}'", c),
Token::Number((None, v)) => write!(f, "'{}'", v), Token::Number((None, v)) => write!(f, "'{}'", v),