Proptest testing!
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ Cargo.lock
|
||||
test
|
||||
*.dSYM
|
||||
.vscode
|
||||
proptest-regressions/
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
let name = format!("<string_constant>{}", s);
|
||||
let global_id = self
|
||||
|
||||
@@ -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<BackendError> for Diagnostic<usize> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ArcIntern<String>, GlobalValue>;
|
||||
|
||||
@@ -21,15 +21,16 @@ impl<M: Module> Backend<M> {
|
||||
&mut self,
|
||||
function_name: &str,
|
||||
mut program: Program,
|
||||
) -> Result<FuncId, BackendError>
|
||||
{
|
||||
) -> Result<FuncId, BackendError> {
|
||||
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<M: Module> Backend<M> {
|
||||
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<String, GlobalValue> = self
|
||||
.defined_symbols
|
||||
.iter()
|
||||
|
||||
@@ -66,5 +66,4 @@ impl RuntimeFunctions {
|
||||
pub fn register_jit_implementations(builder: &mut JITBuilder) {
|
||||
builder.symbol("print", runtime_print as *const u8);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
159
src/syntax/arbitrary.rs
Normal file
159
src/syntax/arbitrary.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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<Statement>,
|
||||
}
|
||||
|
||||
#[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<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 {
|
||||
Number(Option<u8>, i64),
|
||||
}
|
||||
@@ -12,6 +12,8 @@ extern {
|
||||
enum Token {
|
||||
"=" => Token::Equals,
|
||||
";" => Token::Semi,
|
||||
"(" => Token::LeftParen,
|
||||
")" => Token::RightParen,
|
||||
|
||||
"print" => Token::Print,
|
||||
|
||||
@@ -67,5 +69,10 @@ AtomicExpression: Expression = {
|
||||
<l:@L> <n:"<num>"> => {
|
||||
let val = Value::Number(n.0, n.1);
|
||||
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,
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user