From 92e68f8c26970de5604633a3f23b88310c5929e6 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Thu, 29 Jun 2023 23:01:22 -0700 Subject: [PATCH 01/21] Useless Builder struct --- src/builder.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++ src/facts.rs | 2 + src/facts/lambda.rs | 39 ++++++++++++------- src/lib.rs | 2 + 4 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 src/builder.rs diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..65b0f8f --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,91 @@ +use std::{marker::PhantomData, sync::Arc}; + +use crate::{facts::LambdaFact, *}; + +pub fn build<'a, S, T, Mutator>(mutator: Mutator) -> Build<'a, S, T, Mutator> +where + S: Clone + Send + Sync, + T: Bounds<'a>, + Mutator: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, + // Labeler: Clone + Send + Sync + Fn(String) -> String, +{ + Build { + mutator, + // labeler: None, + _phantom: PhantomData, + } +} + +pub type Labeler = Arc String + Send + Sync + 'static>; + +#[derive(Clone)] +pub struct Build<'a, S, T, Mutator> +where + S: Clone + Send + Sync, + T: Bounds<'a>, + Mutator: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, + // Labeler: Clone + Send + Sync + Fn(String) -> String, +{ + mutator: Mutator, + // labeler: Option, + _phantom: PhantomData<&'a (S, T)>, +} + +// impl<'a, S, T, Mutator> Fact<'a, T> for Build<'a, S, T, Mutator> +// where +// S: Clone + Send + Sync, +// T: Bounds<'a>, +// Mutator: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, +// // Labeler: Clone + Send + Sync + Fn(String) -> String, +// { +// fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { +// (self.mutator)(g, &mut self.state, obj) +// } +// } + +impl<'a, S, T, Mutator> Build<'a, S, T, Mutator> +where + S: Clone + Send + Sync, + T: Bounds<'a>, + Mutator: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, + // Labeler: Clone + Send + Sync + Fn(String) -> String, +{ + // pub fn label(mut self, labeler: impl Fn(String) -> String + Send + Sync + 'static) -> Self { + // self.labeler = Some(Arc::new(labeler)); + // self + // } + + // pub fn labeled(mut self, label: String) -> Self { + // self.labeler = Some(Arc::new(move |_| label.clone())); + // self + // } + + pub fn state(&self, state: S) -> LambdaFact<'a, S, T, Mutator> { + lambda(state, self.mutator.clone()) + } +} + +// #[test] +// fn test_builder() { +// use crate::facts::*; +// let mut g = utils::random_generator(); + +// let geom = |k|build(move |g, s, mut v| { +// g.set(&mut v, s, "value is not geometrically increasing by 2")?; +// *s *= 2; +// Ok(v) +// }); + +// let from2 = geom.state(2); +// let from3 = geom.state(3); + +// let geom2 = vec_of_length(4, from2); +// let geom3 = vec_of_length(4, from3); + +// let list2 = geom2.clone().build(&mut g); +// let list3 = geom3.clone().build(&mut g); +// geom2.check(&list2).unwrap(); +// geom3.check(&list3).unwrap(); +// assert_eq!(list2, vec![2, 4, 8, 16]); +// assert_eq!(list3, vec![3, 6, 12, 24]); +// } diff --git a/src/facts.rs b/src/facts.rs index a263960..0244f8c 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -31,6 +31,8 @@ pub use mapped::{mapped, mapped_fallible}; pub use prism::{prism, PrismFact}; pub use seq::{vec, vec_len, vec_of_length}; +pub(crate) use lambda::LambdaFact; + #[cfg(feature = "optics")] mod optical; #[cfg(feature = "optics")] diff --git a/src/facts/lambda.rs b/src/facts/lambda.rs index deebec4..7ad8a9c 100644 --- a/src/facts/lambda.rs +++ b/src/facts/lambda.rs @@ -23,7 +23,7 @@ use crate::*; /// fact.check(&list).unwrap(); /// assert_eq!(list, vec![2, 4, 8, 16]); /// ``` -pub fn lambda<'a, T, S, F>(state: S, f: F) -> LambdaFact<'a, T, S, F> +pub fn lambda<'a, S, T, F>(state: S, f: F) -> LambdaFact<'a, S, T, F> where S: Clone + Send + Sync, T: Bounds<'a>, @@ -37,7 +37,7 @@ where } #[derive(Clone)] -pub struct LambdaFact<'a, T, S, F> +pub struct LambdaFact<'a, S, T, F> where S: Clone + Send + Sync, T: Bounds<'a>, @@ -48,7 +48,7 @@ where _phantom: PhantomData<&'a T>, } -impl<'a, T, S, F> Fact<'a, T> for LambdaFact<'a, T, S, F> +impl<'a, S, T, F> Fact<'a, T> for LambdaFact<'a, S, T, F> where S: Clone + Send + Sync, T: Bounds<'a>, @@ -64,16 +64,29 @@ fn test_lambda_fact() { use crate::facts::*; let mut g = utils::random_generator(); - let fact = vec_of_length( - 4, - lambda(2, move |g, s, mut v| { - g.set(&mut v, s, "value is not geometrically increasing by 2")?; - *s *= 2; + let geom = |k, s| { + lambda(s, move |g, s, mut v| { + g.set( + &mut v, + s, + format!("value is not geometrically increasing by {k} starting from {s}"), + )?; + *s *= k; Ok(v) - }), - ); + }) + }; - let list = fact.clone().build(&mut g); - fact.check(&list).unwrap(); - assert_eq!(list, vec![2, 4, 8, 16]); + let fact = |k, s| vec_of_length(4, geom(k, s)); + + { + let list = fact(2, 2).build(&mut g); + assert_eq!(list, vec![2, 4, 8, 16]); + fact(2, 2).check(&list).unwrap(); + } + + { + let list = fact(3, 4).build(&mut g); + assert_eq!(list, vec![4, 12, 36, 108]); + fact(3, 4).check(&list).unwrap(); + } } diff --git a/src/lib.rs b/src/lib.rs index b93ed71..33a82c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,8 @@ mod generator; pub mod facts; pub use facts::*; +mod builder; + #[cfg(feature = "utils")] pub mod utils; From 1763308a7bd39ecaeb66aa8169d7f0bde7c7b856 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Thu, 29 Jun 2023 23:06:26 -0700 Subject: [PATCH 02/21] Remove F param from lambda --- src/builder.rs | 91 --------------------------------------------- src/facts.rs | 2 +- src/facts/lambda.rs | 21 ++++++----- src/lib.rs | 2 - 4 files changed, 13 insertions(+), 103 deletions(-) delete mode 100644 src/builder.rs diff --git a/src/builder.rs b/src/builder.rs deleted file mode 100644 index 65b0f8f..0000000 --- a/src/builder.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::{marker::PhantomData, sync::Arc}; - -use crate::{facts::LambdaFact, *}; - -pub fn build<'a, S, T, Mutator>(mutator: Mutator) -> Build<'a, S, T, Mutator> -where - S: Clone + Send + Sync, - T: Bounds<'a>, - Mutator: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, - // Labeler: Clone + Send + Sync + Fn(String) -> String, -{ - Build { - mutator, - // labeler: None, - _phantom: PhantomData, - } -} - -pub type Labeler = Arc String + Send + Sync + 'static>; - -#[derive(Clone)] -pub struct Build<'a, S, T, Mutator> -where - S: Clone + Send + Sync, - T: Bounds<'a>, - Mutator: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, - // Labeler: Clone + Send + Sync + Fn(String) -> String, -{ - mutator: Mutator, - // labeler: Option, - _phantom: PhantomData<&'a (S, T)>, -} - -// impl<'a, S, T, Mutator> Fact<'a, T> for Build<'a, S, T, Mutator> -// where -// S: Clone + Send + Sync, -// T: Bounds<'a>, -// Mutator: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, -// // Labeler: Clone + Send + Sync + Fn(String) -> String, -// { -// fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { -// (self.mutator)(g, &mut self.state, obj) -// } -// } - -impl<'a, S, T, Mutator> Build<'a, S, T, Mutator> -where - S: Clone + Send + Sync, - T: Bounds<'a>, - Mutator: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, - // Labeler: Clone + Send + Sync + Fn(String) -> String, -{ - // pub fn label(mut self, labeler: impl Fn(String) -> String + Send + Sync + 'static) -> Self { - // self.labeler = Some(Arc::new(labeler)); - // self - // } - - // pub fn labeled(mut self, label: String) -> Self { - // self.labeler = Some(Arc::new(move |_| label.clone())); - // self - // } - - pub fn state(&self, state: S) -> LambdaFact<'a, S, T, Mutator> { - lambda(state, self.mutator.clone()) - } -} - -// #[test] -// fn test_builder() { -// use crate::facts::*; -// let mut g = utils::random_generator(); - -// let geom = |k|build(move |g, s, mut v| { -// g.set(&mut v, s, "value is not geometrically increasing by 2")?; -// *s *= 2; -// Ok(v) -// }); - -// let from2 = geom.state(2); -// let from3 = geom.state(3); - -// let geom2 = vec_of_length(4, from2); -// let geom3 = vec_of_length(4, from3); - -// let list2 = geom2.clone().build(&mut g); -// let list3 = geom3.clone().build(&mut g); -// geom2.check(&list2).unwrap(); -// geom3.check(&list3).unwrap(); -// assert_eq!(list2, vec![2, 4, 8, 16]); -// assert_eq!(list3, vec![3, 6, 12, 24]); -// } diff --git a/src/facts.rs b/src/facts.rs index 0244f8c..eede32a 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -31,7 +31,7 @@ pub use mapped::{mapped, mapped_fallible}; pub use prism::{prism, PrismFact}; pub use seq::{vec, vec_len, vec_of_length}; -pub(crate) use lambda::LambdaFact; +// pub(crate) use lambda::LambdaFact; #[cfg(feature = "optics")] mod optical; diff --git a/src/facts/lambda.rs b/src/facts/lambda.rs index 7ad8a9c..dc317c1 100644 --- a/src/facts/lambda.rs +++ b/src/facts/lambda.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{marker::PhantomData, sync::Arc}; use crate::*; @@ -23,36 +23,39 @@ use crate::*; /// fact.check(&list).unwrap(); /// assert_eq!(list, vec![2, 4, 8, 16]); /// ``` -pub fn lambda<'a, S, T, F>(state: S, f: F) -> LambdaFact<'a, S, T, F> +pub fn lambda<'a, S, T>( + state: S, + f: impl 'static + Send + Sync + Fn(&mut Generator, &mut S, T) -> Mutation, +) -> LambdaFact<'a, S, T> where S: Clone + Send + Sync, T: Bounds<'a>, - F: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, { LambdaFact { state, - fun: f, + fun: Arc::new(f), _phantom: PhantomData, } } +pub type Lambda = + Arc Mutation>; + #[derive(Clone)] -pub struct LambdaFact<'a, S, T, F> +pub struct LambdaFact<'a, S, T> where S: Clone + Send + Sync, T: Bounds<'a>, - F: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, { state: S, - fun: F, + fun: Lambda, _phantom: PhantomData<&'a T>, } -impl<'a, S, T, F> Fact<'a, T> for LambdaFact<'a, S, T, F> +impl<'a, S, T> Fact<'a, T> for LambdaFact<'a, S, T> where S: Clone + Send + Sync, T: Bounds<'a>, - F: Clone + Send + Sync + FnMut(&mut Generator, &mut S, T) -> Mutation, { fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { (self.fun)(g, &mut self.state, obj) diff --git a/src/lib.rs b/src/lib.rs index 33a82c2..b93ed71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,8 +67,6 @@ mod generator; pub mod facts; pub use facts::*; -mod builder; - #[cfg(feature = "utils")] pub mod utils; From 7f038dff7a6f8c59d0bcf6df86d42a92751938b3 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Thu, 29 Jun 2023 23:23:04 -0700 Subject: [PATCH 03/21] Rewrite eq() as lambda --- src/facts.rs | 2 +- src/facts/eq.rs | 24 +++++++++++++----------- src/facts/lambda.rs | 18 ++++++++++++++---- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/facts.rs b/src/facts.rs index eede32a..1153193 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -25,7 +25,7 @@ pub use same::{different, same}; pub use and::and; pub use brute::{brute, brute_fallible}; -pub use lambda::lambda; +pub use lambda::{lambda, lambda_unit}; pub use lens::{lens, LensFact}; pub use mapped::{mapped, mapped_fallible}; pub use prism::{prism, PrismFact}; diff --git a/src/facts/eq.rs b/src/facts/eq.rs index 0f57e53..3d8b8f2 100644 --- a/src/facts/eq.rs +++ b/src/facts/eq.rs @@ -1,23 +1,25 @@ -use super::*; +use super::{lambda::LambdaFact, *}; /// Specifies an equality constraint -pub fn eq(context: S, constant: T) -> EqFact +pub fn eq<'a, S, T>(context: S, constant: T) -> LambdaFact<'a, (), T> where S: ToString, - T: std::fmt::Debug + PartialEq, + T: Bounds<'a> + PartialEq + Clone, { - EqFact { - context: context.to_string(), - constant, - op: EqOp::Equal, - _phantom: PhantomData, - } + let ctx = context.to_string(); + lambda_unit(move |g, mut obj| { + if obj != constant { + g.fail(format!("{}: expected {:?} == {:?}", ctx, obj, constant))?; + obj = constant.clone(); + } + Ok(obj) + }) } /// Specifies an equality constraint with no context -pub fn eq_(constant: T) -> EqFact +pub fn eq_<'a, T>(constant: T) -> LambdaFact<'a, (), T> where - T: std::fmt::Debug + PartialEq, + T: Bounds<'a> + PartialEq + Clone, { eq("eq", constant) } diff --git a/src/facts/lambda.rs b/src/facts/lambda.rs index dc317c1..407d7e2 100644 --- a/src/facts/lambda.rs +++ b/src/facts/lambda.rs @@ -25,7 +25,7 @@ use crate::*; /// ``` pub fn lambda<'a, S, T>( state: S, - f: impl 'static + Send + Sync + Fn(&mut Generator, &mut S, T) -> Mutation, + f: impl 'a + Send + Sync + Fn(&mut Generator, &mut S, T) -> Mutation, ) -> LambdaFact<'a, S, T> where S: Clone + Send + Sync, @@ -38,8 +38,18 @@ where } } -pub type Lambda = - Arc Mutation>; +/// Create a lambda with unit state +pub fn lambda_unit<'a, T>( + f: impl 'a + Send + Sync + Fn(&mut Generator, T) -> Mutation, +) -> LambdaFact<'a, (), T> +where + T: Bounds<'a>, +{ + lambda((), move |g, (), obj| f(g, obj)) +} + +pub type Lambda<'a, S, T> = + Arc Mutation>; #[derive(Clone)] pub struct LambdaFact<'a, S, T> @@ -48,7 +58,7 @@ where T: Bounds<'a>, { state: S, - fun: Lambda, + fun: Lambda<'a, S, T>, _phantom: PhantomData<&'a T>, } From 16a2456041fd5bc15be5d4180699a59b092c814a Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Thu, 29 Jun 2023 23:47:56 -0700 Subject: [PATCH 04/21] Rewrite not() as lambda --- src/check.rs | 8 +++++- src/error.rs | 10 ++++++-- src/facts.rs | 2 +- src/facts/brute.rs | 60 ++++++++++++++------------------------------- src/facts/lambda.rs | 6 ++--- src/facts/not.rs | 45 +++++++++------------------------- src/generator.rs | 20 +++++++-------- 7 files changed, 58 insertions(+), 93 deletions(-) diff --git a/src/check.rs b/src/check.rs index 80c9a5e..05a482c 100644 --- a/src/check.rs +++ b/src/check.rs @@ -91,6 +91,11 @@ impl Check { } } + /// If Failures, return all failures joined together in a single string + pub fn result_joined(self) -> ContrafactResult> { + self.result().map(|r| r.map_err(|es| es.join(";"))) + } + /// Create a single-error failure if predicate is false, otherwise pass /// /// ``` @@ -118,7 +123,8 @@ impl Check { Ok(_) => Self::pass(), Err(MutationError::Check(err)) => Self::fail(err), Err(MutationError::Arbitrary(err)) => Self::Error(err.to_string()), - Err(MutationError::Exception(err)) => Self::Error(format!("{:?}", err)), + Err(MutationError::Internal(err)) => Self::Error(format!("{:?}", err)), + Err(MutationError::User(err)) => Self::Error(format!("{:?}", err)), } } diff --git a/src/error.rs b/src/error.rs index 9e05622..8917ddf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,8 +29,13 @@ pub enum MutationError { /// arbitrary failed to produce new data, which means we can't go on #[from] Arbitrary(arbitrary::Error), + + /// Contrafact experienced a problem + #[from] + Internal(ContrafactError), + /// There was some other bug in the Fact implementation - Exception(String), + User(String), } impl PartialEq for MutationError { @@ -38,7 +43,8 @@ impl PartialEq for MutationError { match (self, other) { (Self::Check(s), Self::Check(o)) => s == o, (Self::Arbitrary(s), Self::Arbitrary(o)) => s.to_string() == o.to_string(), - (Self::Exception(s), Self::Exception(o)) => s == o, + (Self::Internal(s), Self::Internal(o)) => s == o, + (Self::User(s), Self::User(o)) => s == o, _ => false, } } diff --git a/src/facts.rs b/src/facts.rs index 1153193..1d94dd2 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -24,7 +24,7 @@ pub use or::or; pub use same::{different, same}; pub use and::and; -pub use brute::{brute, brute_fallible}; +pub use brute::brute; pub use lambda::{lambda, lambda_unit}; pub use lens::{lens, LensFact}; pub use mapped::{mapped, mapped_fallible}; diff --git a/src/facts/brute.rs b/src/facts/brute.rs index 9cdc5e2..48a454c 100644 --- a/src/facts/brute.rs +++ b/src/facts/brute.rs @@ -2,17 +2,9 @@ use std::sync::Arc; use crate::{fact::Bounds, Fact, BRUTE_ITERATION_LIMIT}; -use crate::{Generator, Mutation}; +use crate::{lambda_unit, ContrafactResult, Generator, Mutation}; -/// A version of [`brute`] whose closure returns a Result -pub fn brute_fallible<'a, T, F, S>(reason: S, f: F) -> BruteFact<'a, T> -where - S: ToString, - T: Bounds<'a>, - F: 'a + Send + Sync + Fn(&T) -> Mutation, -{ - BruteFact::::new(reason.to_string(), f) -} +use super::lambda::LambdaFact; /// A constraint defined only by a predicate closure. Mutation occurs by brute /// force, randomly trying values until one matches the constraint. @@ -45,53 +37,37 @@ where /// let mut g = utils::random_generator(); /// assert!(div_by(3).build(&mut g) % 3 == 0); /// ``` -pub fn brute<'a, T, F, S>(reason: S, f: F) -> BruteFact<'a, T> +pub fn brute<'a, T, F>(reason: impl ToString, f: F) -> LambdaFact<'a, (), T> where - S: ToString, T: Bounds<'a>, F: 'a + Send + Sync + Fn(&T) -> bool, { - BruteFact::::new(reason.to_string(), move |x| Ok(f(x))) + let reason = reason.to_string(); + brute_labeled(move |v| Ok(f(v).then_some(()).ok_or_else(|| reason.clone()))) } -type BruteFn<'a, T> = Arc Mutation>; - -/// A brute-force fact. Use [`brute()`] to construct. -#[derive(Clone)] -pub struct BruteFact<'a, T> { - label: String, - f: BruteFn<'a, T>, -} - -impl<'a, T> Fact<'a, T> for BruteFact<'a, T> +/// A version of [`brute`] which allows the closure to return the reason for failure +pub fn brute_labeled<'a, T, F>(f: F) -> LambdaFact<'a, (), T> where T: Bounds<'a>, + F: 'a + Send + Sync + Fn(&T) -> ContrafactResult, { - #[tracing::instrument(fields(fact = "brute"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: T) -> Mutation { - tracing::trace!("brute"); + lambda_unit(move |g, mut obj| { + let mut last_reason = "".to_string(); for _ in 0..=BRUTE_ITERATION_LIMIT { - if (self.f)(&obj)? { + if let Err(reason) = f(&obj)? { + obj = g.arbitrary(&reason)?; + last_reason = reason; + } else { return Ok(obj); } - obj = g.arbitrary(&self.label)?; } panic!( - "Exceeded iteration limit of {} while attempting to meet a BruteFact. Context: {}", - BRUTE_ITERATION_LIMIT, self.label + "Exceeded iteration limit of {} while attempting to meet a BruteFact. Last failure reason: {}", + BRUTE_ITERATION_LIMIT, last_reason ); - } + }) } -impl<'a, T> BruteFact<'a, T> { - pub(crate) fn new Mutation>( - reason: String, - f: F, - ) -> Self { - Self { - label: reason, - f: Arc::new(f), - } - } -} +type BruteResult = Result<(), String>; diff --git a/src/facts/lambda.rs b/src/facts/lambda.rs index 407d7e2..f5de8b4 100644 --- a/src/facts/lambda.rs +++ b/src/facts/lambda.rs @@ -25,7 +25,7 @@ use crate::*; /// ``` pub fn lambda<'a, S, T>( state: S, - f: impl 'a + Send + Sync + Fn(&mut Generator, &mut S, T) -> Mutation, + f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, &mut S, T) -> Mutation, ) -> LambdaFact<'a, S, T> where S: Clone + Send + Sync, @@ -40,7 +40,7 @@ where /// Create a lambda with unit state pub fn lambda_unit<'a, T>( - f: impl 'a + Send + Sync + Fn(&mut Generator, T) -> Mutation, + f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, T) -> Mutation, ) -> LambdaFact<'a, (), T> where T: Bounds<'a>, @@ -49,7 +49,7 @@ where } pub type Lambda<'a, S, T> = - Arc Mutation>; + Arc, &mut S, T) -> Mutation>; #[derive(Clone)] pub struct LambdaFact<'a, S, T> diff --git a/src/facts/not.rs b/src/facts/not.rs index ff4dd2c..23fd596 100644 --- a/src/facts/not.rs +++ b/src/facts/not.rs @@ -1,50 +1,27 @@ -use super::*; +use super::{brute::brute_labeled, lambda::LambdaFact, *}; /// Negates a fact // TODO: `not` in particular would really benefit from Facts having accessible // labels, since currently you can only get context about why a `not` fact passed, // not why it fails. -pub fn not<'a, F, S, T>(context: S, fact: F) -> NotFact<'a, F, T> +pub fn not<'a, F, T>(context: impl ToString, fact: F) -> LambdaFact<'a, (), T> where - S: ToString, - F: Fact<'a, T>, + F: 'a + Fact<'a, T>, T: Bounds<'a>, { - NotFact { - context: context.to_string(), - fact, - _phantom: PhantomData, - } + let context = context.to_string(); + lambda_unit(move |g, obj| { + let label = format!("not({})", context.clone()); + let fact = fact.clone(); + brute(label, move |o| fact.clone().check(o).is_err()).mutate(g, obj) + }) } /// Negates a fact, with no context given -pub fn not_<'a, F, T>(fact: F) -> NotFact<'a, F, T> +pub fn not_<'a, F, T>(fact: F) -> LambdaFact<'a, (), T> where - F: Fact<'a, T>, + F: 'a + Fact<'a, T>, T: Bounds<'a>, { not("not", fact) } - -#[derive(Debug, Clone)] -pub struct NotFact<'a, F, T> -where - F: Fact<'a, T>, - T: Bounds<'a>, -{ - context: String, - fact: F, - _phantom: PhantomData<&'a T>, -} - -impl<'a, F, T> Fact<'a, T> for NotFact<'a, F, T> -where - F: Fact<'a, T> + 'a, - T: Bounds<'a>, -{ - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - let label = format!("not({})", self.context.clone()); - let fact = self.fact.clone(); - brute(label, move |o| fact.clone().check(o).is_err()).mutate(g, obj) - } -} diff --git a/src/generator.rs b/src/generator.rs index 0208551..4d24763 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -94,13 +94,13 @@ impl<'a> Generator<'a> { err: impl ToString, ) -> Mutation<&T> { if choices.is_empty() { - return Err(MutationError::Exception("Empty choices".to_string())).into(); + return Err(MutationError::User("Empty choices".to_string())).into(); } if choices.len() == 1 { return Ok(&choices[0]).into(); } if !self.check && self.arb.is_empty() { - return Err(MutationError::Exception("Ran out of entropy".to_string())).into(); + return Err(MutationError::User("Ran out of entropy".to_string())).into(); } self.with(err, |u| u.choose(choices)) } @@ -116,12 +116,12 @@ impl<'a> Generator<'a> { T: Arbitrary<'a> + PartialOrd + Copy + Int, { if range.start() > range.end() { - return Err(MutationError::Exception("Invalid range".to_string())).into(); + return Err(MutationError::User("Invalid range".to_string())).into(); } else if range.start() == range.end() { return Ok(*range.start()).into(); } if !self.check && self.arb.is_empty() { - return Err(MutationError::Exception("Ran out of entropy".to_string())).into(); + return Err(MutationError::User("Ran out of entropy".to_string())).into(); } self.with(err, |u| u.int_in_range(range)) } @@ -152,7 +152,7 @@ pub mod test { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); assert_eq!( gen.int_in_range(5..=4, "error"), - Err(MutationError::Exception("Invalid range".to_string())) + Err(MutationError::User("Invalid range".to_string())) ); } @@ -178,7 +178,7 @@ pub mod test { assert_eq!(gen.len(), 0); assert_eq!( gen.int_in_range(0..=3, "error"), - Err(MutationError::Exception("Ran out of entropy".to_string())) + Err(MutationError::User("Ran out of entropy".to_string())) ); } @@ -189,7 +189,7 @@ pub mod test { let choices: [usize; 0] = []; assert_eq!( gen.choose(&choices, "error"), - Err(MutationError::Exception("Empty choices".to_string())) + Err(MutationError::User("Empty choices".to_string())) ); } @@ -216,7 +216,7 @@ pub mod test { // This is the only case where we can't choose a value, because we have 2 choices and 6 bytes. assert_eq!( gen.choose(&choices, "error"), - Err(MutationError::Exception("Ran out of entropy".to_string())) + Err(MutationError::User("Ran out of entropy".to_string())) ); } @@ -235,7 +235,7 @@ pub mod test { // This is the only case where we can't choose a value, because we have 3 choices and 6 bytes. assert_eq!( gen.choose(&choices, "error"), - Err(MutationError::Exception("Ran out of entropy".to_string())) + Err(MutationError::User("Ran out of entropy".to_string())) ); } @@ -262,7 +262,7 @@ pub mod test { // This is the only case where we can't choose a value, because we have 3 choices and 6 bytes. assert_eq!( gen.choose(&choices, "error"), - Err(MutationError::Exception("Ran out of entropy".to_string())) + Err(MutationError::User("Ran out of entropy".to_string())) ); } From 85d76ea2afae5378a421e4981162f2a7c4dbc242 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Thu, 29 Jun 2023 23:52:22 -0700 Subject: [PATCH 05/21] Rewrite mapped --- src/facts/mapped.rs | 52 ++++++++++++++------------------------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/src/facts/mapped.rs b/src/facts/mapped.rs index 2717199..0a8dc32 100644 --- a/src/facts/mapped.rs +++ b/src/facts/mapped.rs @@ -2,15 +2,21 @@ use std::sync::Arc; use crate::{fact::Bounds, *}; +use super::lambda::LambdaFact; + /// A version of [`mapped`] whose closure returns a Result -pub fn mapped_fallible<'a, T, F, O, S>(reason: S, f: F) -> MappedFact<'a, T, O> +pub fn mapped_fallible<'a, T, F, O, S>(reason: impl ToString, f: F) -> LambdaFact<'a, (), T> where - S: ToString, T: Bounds<'a>, O: Fact<'a, T>, - F: 'a + Send + Sync + Fn(&T) -> Mutation, + F: 'a + Send + Sync + Fn(&T) -> ContrafactResult, { - MappedFact::new(reason.to_string(), f) + let reason = reason.to_string(); + lambda_unit(move |g, obj| { + f(&obj)? + .mutate(g, obj) + .map_check_err(|err| format!("mapped({}) > {}", reason, err)) + }) } /// A fact which is defined based on the data to which it is applied. It maps @@ -47,44 +53,18 @@ where /// assert!(fact.clone().check(&9009).is_ok()); /// assert!(fact.clone().check(&9010).is_err()); /// ``` -pub fn mapped<'a, T, F, O, S>(reason: S, f: F) -> MappedFact<'a, T, O> +pub fn mapped<'a, T, F, O>(reason: impl ToString, f: F) -> LambdaFact<'a, (), T> where - S: ToString, T: Bounds<'a>, O: Fact<'a, T>, F: 'a + Send + Sync + Fn(&T) -> O, { - MappedFact::new(reason.to_string(), move |x| Ok(f(x))) -} - -/// A fact which is mapped from the data to be checked/mutated. -/// Use [`mapped`] to construct. -#[derive(Clone)] -pub struct MappedFact<'a, T, O> { - reason: String, - f: Arc Mutation>, -} - -impl<'a, T, O> Fact<'a, T> for MappedFact<'a, T, O> -where - T: Bounds<'a>, - O: Fact<'a, T>, -{ - #[tracing::instrument(fields(fact = "mapped"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - (self.f)(&obj)? + let reason = reason.to_string(); + lambda_unit(move |g, obj| { + f(&obj) .mutate(g, obj) - .map_check_err(|err| format!("mapped({}) > {}", self.reason, err)) - } -} - -impl<'a, T, O> MappedFact<'a, T, O> { - pub(crate) fn new Mutation>(reason: String, f: F) -> Self { - Self { - reason, - f: Arc::new(f), - } - } + .map_check_err(|err| format!("mapped({}) > {}", reason, err)) + }) } #[test] From 58596899e54696ccf09aa7235152089c7af3b4f9 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 00:26:29 -0700 Subject: [PATCH 06/21] Rename LambdaFact to Fact; rewrite consecutive_int --- src/fact.rs | 200 ++++++++++++++--------------------- src/facts.rs | 4 +- src/facts/and.rs | 18 ++-- src/facts/brute.rs | 14 +-- src/facts/consecutive_int.rs | 43 +++----- src/facts/constant.rs | 2 +- src/facts/eq.rs | 10 +- src/facts/in_range.rs | 2 +- src/facts/in_slice.rs | 2 +- src/facts/lambda.rs | 105 ------------------ src/facts/lens.rs | 12 +-- src/facts/mapped.rs | 18 ++-- src/facts/not.rs | 12 +-- src/facts/or.rs | 14 +-- src/facts/prism.rs | 12 +-- src/facts/same.rs | 2 +- src/facts/seq.rs | 20 ++-- src/factual.rs | 141 ++++++++++++++++++++++++ src/lib.rs | 7 +- tests/chainlink.rs | 4 +- tests/complex.rs | 16 +-- 21 files changed, 315 insertions(+), 343 deletions(-) delete mode 100644 src/facts/lambda.rs create mode 100644 src/factual.rs diff --git a/src/fact.rs b/src/fact.rs index 3858ac8..f163c66 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -1,141 +1,105 @@ -use arbitrary::*; -use either::Either; +use std::{marker::PhantomData, sync::Arc}; use crate::*; -// TODO: we can remove the Clone requirement if: -// - make `Mutate` track list of errors so that it can know if a mutation occurred. -// - make `mutate()` take a mut ref -// - make `check()` take a mut ref -// then `check()` can know if a mutation occurred -// -/// The trait bounds for the subject of a Fact -pub trait Bounds<'a>: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> {} -impl<'a, T> Bounds<'a> for T where - T: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> -{ -} - -// /// Type alias for a boxed Fact. Implements [`Fact`] itself. -// pub type BoxFact<'a, T> = Box>; - -// pub trait Facts>: Fact<'static, T> {} -// impl, F: Facts> Fact<'static, T> for F {} - -/// A declarative representation of a constraint on some data, which can be -/// used to both make an assertion (check) or to mold some arbitrary existing -/// data into a shape which passes that same assertion (mutate) -pub trait Fact<'a, T>: Send + Sync + Clone +/// Create a fact from a bare function which specifies the mutation. +/// Can be quicker to experiment with ideas this way than to have to directly implement +/// the [`Fact`] trait +/// +/// ``` +/// use contrafact::*; +/// let mut g = utils::random_generator(); +/// +/// let mut fact = vec_of_length( +/// 4, +/// lambda(2, move |g, s, mut v| { +/// g.set(&mut v, s, "value is not geometrically increasing by 2")?; +/// *s *= 2; +/// Ok(v) +/// }), +/// ); +/// +/// let list = fact.clone().build(&mut g); +/// fact.check(&list).unwrap(); +/// assert_eq!(list, vec![2, 4, 8, 16]); +/// ``` +pub fn stateful<'a, S, T>( + state: S, + f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, &mut S, T) -> Mutation, +) -> Fact<'a, S, T> where + S: Clone + Send + Sync, T: Bounds<'a>, { - /// Assert that the constraint is satisfied for given data. - /// - /// If the mutation function is written properly, we get a check for free - /// by using a special Generator which fails upon mutation. If this is for - /// some reason unreasonable, a check function can be written by hand, but - /// care must be taken to make sure it perfectly lines up with the mutation function. - #[tracing::instrument(fields(fact_impl = "Fact"), skip(self))] - fn check(mut self, obj: &T) -> Check { - check_raw(&mut self, obj) - } - - /// Apply a mutation which moves the obj closer to satisfying the overall - /// constraint. - // #[tracing::instrument(skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation; - - /// Make this many attempts to satisfy a constraint before giving up and panicking. - /// - /// If you are combining highly contentious facts together and relying on randomness - /// to find a solution, this limit may need to be higher. In general, you should try - /// to write facts that don't interfere with each other so that the constraint can be - /// met on the first attempt, or perhaps the second or third. If necessary, this can - /// be raised to lean more on random search. - fn satisfy_attempts(&self) -> usize { - SATISFY_ATTEMPTS - } - - /// Mutate a value such that it satisfies the constraint. - /// If the constraint cannot be satisfied, panic. - #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] - fn satisfy(&mut self, g: &mut Generator<'a>, obj: T) -> ContrafactResult { - tracing::trace!("satisfy"); - let mut last_failure: Vec = vec![]; - let mut next = obj.clone(); - for _i in 0..self.satisfy_attempts() { - let mut m = self.clone(); - let mut c = self.clone(); - next = m.mutate(g, next).unwrap(); - if let Err(errs) = check_raw(&mut c, &next).result()? { - last_failure = errs; - } else { - *self = m; - return Ok(next); - } - } - panic!( - "Could not satisfy a constraint even after {} attempts. Last check failure: {:?}", - SATISFY_ATTEMPTS, last_failure - ); - } - - #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] - /// Build a new value such that it satisfies the constraint - fn build_fallible(mut self, g: &mut Generator<'a>) -> ContrafactResult { - let obj = T::arbitrary(g).unwrap(); - self.satisfy(g, obj) - } - - /// Build a new value such that it satisfies the constraint, panicking on error - #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] - fn build(self, g: &mut Generator<'a>) -> T { - self.build_fallible(g).unwrap() + Fact { + state, + fun: Arc::new(f), + _phantom: PhantomData, } } -impl<'a, T, F1, F2> Fact<'a, T> for Either +/// Create a lambda with unit state +pub fn stateless<'a, T>( + f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, T) -> Mutation, +) -> Fact<'a, (), T> where T: Bounds<'a>, - F1: Fact<'a, T> + ?Sized, - F2: Fact<'a, T> + ?Sized, { - #[tracing::instrument(fields(fact_impl = "Either"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - match self { - Either::Left(f) => f.mutate(g, obj), - Either::Right(f) => f.mutate(g, obj), - } - } + stateful((), move |g, (), obj| f(g, obj)) } -#[tracing::instrument(skip(fact))] -pub(crate) fn check_raw<'a, T, F: Fact<'a, T>>(fact: &mut F, obj: &T) -> Check +pub type Lambda<'a, S, T> = + Arc, &mut S, T) -> Mutation>; + +#[derive(Clone)] +pub struct Fact<'a, S, T> where - T: Bounds<'a> + ?Sized, - F: Fact<'a, T> + ?Sized, + S: Clone + Send + Sync, + T: Bounds<'a>, { - let mut g = Generator::checker(); - Check::from_mutation(fact.mutate(&mut g, obj.clone())) + state: S, + fun: Lambda<'a, S, T>, + _phantom: PhantomData<&'a T>, } -#[tracing::instrument(skip(facts))] -fn collect_checks<'a, T, F>(facts: Vec, obj: &T) -> Check +impl<'a, S, T> Factual<'a, T> for Fact<'a, S, T> where + S: Clone + Send + Sync, T: Bounds<'a>, - F: Fact<'a, T>, { - let checks = facts - .into_iter() - .enumerate() - .map(|(i, f)| { - Ok(f.check(obj) - .failures()? - .iter() - .map(|e| format!("fact {}: {}", i, e)) - .collect()) + fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { + (self.fun)(g, &mut self.state, obj) + } +} + +#[test] +fn test_lambda_fact() { + use crate::facts::*; + let mut g = utils::random_generator(); + + let geom = |k, s| { + stateful(s, move |g, s, mut v| { + g.set( + &mut v, + s, + format!("value is not geometrically increasing by {k} starting from {s}"), + )?; + *s *= k; + Ok(v) }) - .collect::>>>() - .map(|fs| fs.into_iter().flatten().collect()); - Check::from_result(checks) + }; + + let fact = |k, s| vec_of_length(4, geom(k, s)); + + { + let list = fact(2, 2).build(&mut g); + assert_eq!(list, vec![2, 4, 8, 16]); + fact(2, 2).check(&list).unwrap(); + } + + { + let list = fact(3, 4).build(&mut g); + assert_eq!(list, vec![4, 12, 36, 108]); + fact(3, 4).check(&list).unwrap(); + } } diff --git a/src/facts.rs b/src/facts.rs index 1d94dd2..3cfed16 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -5,7 +5,6 @@ mod constant; mod eq; mod in_range; mod in_slice; -mod lambda; mod lens; mod mapped; mod not; @@ -25,7 +24,6 @@ pub use same::{different, same}; pub use and::and; pub use brute::brute; -pub use lambda::{lambda, lambda_unit}; pub use lens::{lens, LensFact}; pub use mapped::{mapped, mapped_fallible}; pub use prism::{prism, PrismFact}; @@ -40,7 +38,7 @@ pub use optical::*; pub(crate) use eq::EqOp; -use crate::fact::check_raw; +use crate::factual::check_raw; use crate::*; use std::marker::PhantomData; diff --git a/src/facts/and.rs b/src/facts/and.rs index 375417c..dc66e7a 100644 --- a/src/facts/and.rs +++ b/src/facts/and.rs @@ -6,8 +6,8 @@ use crate::*; pub fn and<'a, F1, F2, T>(a: F1, b: F2) -> AndFact<'a, F1, F2, T> where T: Bounds<'a>, - F1: Fact<'a, T>, - F2: Fact<'a, T>, + F1: Factual<'a, T>, + F2: Factual<'a, T>, { AndFact::new(a, b) } @@ -19,8 +19,8 @@ where #[derive(Debug, Clone, PartialEq, Eq)] pub struct AndFact<'a, F1, F2, T> where - F1: Fact<'a, T>, - F2: Fact<'a, T>, + F1: Factual<'a, T>, + F2: Factual<'a, T>, T: ?Sized + Bounds<'a>, { pub(crate) a: F1, @@ -30,8 +30,8 @@ where impl<'a, F1, F2, T> AndFact<'a, F1, F2, T> where - F1: Fact<'a, T>, - F2: Fact<'a, T>, + F1: Factual<'a, T>, + F2: Factual<'a, T>, T: ?Sized + Bounds<'a>, { /// Constructor @@ -44,10 +44,10 @@ where } } -impl<'a, F1, F2, T> Fact<'a, T> for AndFact<'a, F1, F2, T> +impl<'a, F1, F2, T> Factual<'a, T> for AndFact<'a, F1, F2, T> where - F1: Fact<'a, T> + Fact<'a, T>, - F2: Fact<'a, T> + Fact<'a, T>, + F1: Factual<'a, T> + Factual<'a, T>, + F2: Factual<'a, T> + Factual<'a, T>, T: Bounds<'a>, { fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { diff --git a/src/facts/brute.rs b/src/facts/brute.rs index 48a454c..96c5c91 100644 --- a/src/facts/brute.rs +++ b/src/facts/brute.rs @@ -1,10 +1,4 @@ -use std::sync::Arc; - -use crate::{fact::Bounds, Fact, BRUTE_ITERATION_LIMIT}; - -use crate::{lambda_unit, ContrafactResult, Generator, Mutation}; - -use super::lambda::LambdaFact; +use crate::*; /// A constraint defined only by a predicate closure. Mutation occurs by brute /// force, randomly trying values until one matches the constraint. @@ -37,7 +31,7 @@ use super::lambda::LambdaFact; /// let mut g = utils::random_generator(); /// assert!(div_by(3).build(&mut g) % 3 == 0); /// ``` -pub fn brute<'a, T, F>(reason: impl ToString, f: F) -> LambdaFact<'a, (), T> +pub fn brute<'a, T, F>(reason: impl ToString, f: F) -> Fact<'a, (), T> where T: Bounds<'a>, F: 'a + Send + Sync + Fn(&T) -> bool, @@ -47,12 +41,12 @@ where } /// A version of [`brute`] which allows the closure to return the reason for failure -pub fn brute_labeled<'a, T, F>(f: F) -> LambdaFact<'a, (), T> +pub fn brute_labeled<'a, T, F>(f: F) -> Fact<'a, (), T> where T: Bounds<'a>, F: 'a + Send + Sync + Fn(&T) -> ContrafactResult, { - lambda_unit(move |g, mut obj| { + stateless(move |g, mut obj| { let mut last_reason = "".to_string(); for _ in 0..=BRUTE_ITERATION_LIMIT { if let Err(reason) = f(&obj)? { diff --git a/src/facts/consecutive_int.rs b/src/facts/consecutive_int.rs index d3ceca7..57b78a9 100644 --- a/src/facts/consecutive_int.rs +++ b/src/facts/consecutive_int.rs @@ -1,43 +1,26 @@ use super::*; /// Specifies that a value should be increasing by 1 at every check/mutation -pub fn consecutive_int(context: S, initial: T) -> ConsecutiveIntFact +pub fn consecutive_int<'a, S>(context: impl ToString, initial: S) -> Fact<'a, S, S> where - S: ToString, - T: std::fmt::Debug + PartialEq + num::PrimInt, + S: Bounds<'a> + std::fmt::Debug + PartialEq + num::PrimInt, { - ConsecutiveIntFact { - context: context.to_string(), - counter: initial, - } + let context = context.to_string(); + stateful(initial, move |g, counter, mut obj| { + if obj != *counter { + g.fail(&context)?; + obj = counter.clone(); + } + *counter = counter.checked_add(&S::from(1).unwrap()).unwrap(); + Ok(obj) + }) } /// Specifies that a value should be increasing by 1 at every check/mutation, /// with no context given -pub fn consecutive_int_(initial: T) -> ConsecutiveIntFact +pub fn consecutive_int_<'a, T>(initial: T) -> Fact<'a, T, T> where - T: std::fmt::Debug + PartialEq + num::PrimInt, + T: Bounds<'a> + PartialEq + num::PrimInt, { consecutive_int("consecutive_int", initial) } - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ConsecutiveIntFact { - context: String, - counter: T, -} - -impl<'a, T> Fact<'a, T> for ConsecutiveIntFact -where - T: Bounds<'a> + num::PrimInt, -{ - #[tracing::instrument(fields(fact = "consecutive_int"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: T) -> Mutation { - if obj != self.counter { - g.fail(&self.context)?; - obj = self.counter.clone(); - } - self.counter = self.counter.checked_add(&T::from(1).unwrap()).unwrap(); - Ok(obj) - } -} diff --git a/src/facts/constant.rs b/src/facts/constant.rs index 424e8bc..d29bb1f 100644 --- a/src/facts/constant.rs +++ b/src/facts/constant.rs @@ -13,7 +13,7 @@ pub fn never(context: S) -> ConstantFact { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ConstantFact(bool, String); -impl<'a, T> Fact<'a, T> for ConstantFact +impl<'a, T> Factual<'a, T> for ConstantFact where T: Bounds<'a> + PartialEq + Clone, { diff --git a/src/facts/eq.rs b/src/facts/eq.rs index 3d8b8f2..d644705 100644 --- a/src/facts/eq.rs +++ b/src/facts/eq.rs @@ -1,13 +1,13 @@ -use super::{lambda::LambdaFact, *}; +use super::*; /// Specifies an equality constraint -pub fn eq<'a, S, T>(context: S, constant: T) -> LambdaFact<'a, (), T> +pub fn eq<'a, S, T>(context: S, constant: T) -> Fact<'a, (), T> where S: ToString, T: Bounds<'a> + PartialEq + Clone, { let ctx = context.to_string(); - lambda_unit(move |g, mut obj| { + stateless(move |g, mut obj| { if obj != constant { g.fail(format!("{}: expected {:?} == {:?}", ctx, obj, constant))?; obj = constant.clone(); @@ -17,7 +17,7 @@ where } /// Specifies an equality constraint with no context -pub fn eq_<'a, T>(constant: T) -> LambdaFact<'a, (), T> +pub fn eq_<'a, T>(constant: T) -> Fact<'a, (), T> where T: Bounds<'a> + PartialEq + Clone, { @@ -60,7 +60,7 @@ pub enum EqOp { NotEqual, } -impl<'a, T> Fact<'a, T> for EqFact +impl<'a, T> Factual<'a, T> for EqFact where T: Bounds<'a> + PartialEq + Clone, { diff --git a/src/facts/in_range.rs b/src/facts/in_range.rs index c3b4e3a..6488000 100644 --- a/src/facts/in_range.rs +++ b/src/facts/in_range.rs @@ -63,7 +63,7 @@ where phantom: PhantomData, } -impl<'a, R, T> Fact<'a, T> for InRangeFact +impl<'a, R, T> Factual<'a, T> for InRangeFact where R: Send + Sync + RangeBounds + std::fmt::Debug + Clone, T: Bounds<'a> diff --git a/src/facts/in_slice.rs b/src/facts/in_slice.rs index 92cd417..ce53535 100644 --- a/src/facts/in_slice.rs +++ b/src/facts/in_slice.rs @@ -30,7 +30,7 @@ where slice: &'a [T], } -impl<'a, 'b: 'a, T> Fact<'a, T> for InSliceFact<'b, T> +impl<'a, 'b: 'a, T> Factual<'a, T> for InSliceFact<'b, T> where T: 'b + Bounds<'a> + Clone, // I: Iterator, diff --git a/src/facts/lambda.rs b/src/facts/lambda.rs deleted file mode 100644 index f5de8b4..0000000 --- a/src/facts/lambda.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::{marker::PhantomData, sync::Arc}; - -use crate::*; - -/// Create a fact from a bare function which specifies the mutation. -/// Can be quicker to experiment with ideas this way than to have to directly implement -/// the [`Fact`] trait -/// -/// ``` -/// use contrafact::*; -/// let mut g = utils::random_generator(); -/// -/// let mut fact = vec_of_length( -/// 4, -/// lambda(2, move |g, s, mut v| { -/// g.set(&mut v, s, "value is not geometrically increasing by 2")?; -/// *s *= 2; -/// Ok(v) -/// }), -/// ); -/// -/// let list = fact.clone().build(&mut g); -/// fact.check(&list).unwrap(); -/// assert_eq!(list, vec![2, 4, 8, 16]); -/// ``` -pub fn lambda<'a, S, T>( - state: S, - f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, &mut S, T) -> Mutation, -) -> LambdaFact<'a, S, T> -where - S: Clone + Send + Sync, - T: Bounds<'a>, -{ - LambdaFact { - state, - fun: Arc::new(f), - _phantom: PhantomData, - } -} - -/// Create a lambda with unit state -pub fn lambda_unit<'a, T>( - f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, T) -> Mutation, -) -> LambdaFact<'a, (), T> -where - T: Bounds<'a>, -{ - lambda((), move |g, (), obj| f(g, obj)) -} - -pub type Lambda<'a, S, T> = - Arc, &mut S, T) -> Mutation>; - -#[derive(Clone)] -pub struct LambdaFact<'a, S, T> -where - S: Clone + Send + Sync, - T: Bounds<'a>, -{ - state: S, - fun: Lambda<'a, S, T>, - _phantom: PhantomData<&'a T>, -} - -impl<'a, S, T> Fact<'a, T> for LambdaFact<'a, S, T> -where - S: Clone + Send + Sync, - T: Bounds<'a>, -{ - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - (self.fun)(g, &mut self.state, obj) - } -} - -#[test] -fn test_lambda_fact() { - use crate::facts::*; - let mut g = utils::random_generator(); - - let geom = |k, s| { - lambda(s, move |g, s, mut v| { - g.set( - &mut v, - s, - format!("value is not geometrically increasing by {k} starting from {s}"), - )?; - *s *= k; - Ok(v) - }) - }; - - let fact = |k, s| vec_of_length(4, geom(k, s)); - - { - let list = fact(2, 2).build(&mut g); - assert_eq!(list, vec![2, 4, 8, 16]); - fact(2, 2).check(&list).unwrap(); - } - - { - let list = fact(3, 4).build(&mut g); - assert_eq!(list, vec![4, 12, 36, 108]); - fact(3, 4).check(&list).unwrap(); - } -} diff --git a/src/facts/lens.rs b/src/facts/lens.rs index cd926f4..e7eb31f 100644 --- a/src/facts/lens.rs +++ b/src/facts/lens.rs @@ -40,7 +40,7 @@ where O: Bounds<'a>, T: Bounds<'a> + Clone, S: ToString, - F: Fact<'a, T>, + F: Factual<'a, T>, L: 'a + Clone + Send + Sync + Fn(&mut O) -> &mut T, { let lens2 = lens.clone(); @@ -59,7 +59,7 @@ pub struct LensFact<'a, O, T, F> where T: Bounds<'a>, O: Bounds<'a>, - F: Fact<'a, T>, + F: Factual<'a, T>, { label: String, @@ -76,14 +76,14 @@ impl<'a, O, T, F> LensFact<'a, O, T, F> where T: Bounds<'a>, O: Bounds<'a>, - F: Fact<'a, T>, + F: Factual<'a, T>, { /// Constructor. Supply a lens and an existing Fact to create a new Fact. pub fn new(label: L, getter: G, setter: S, inner_fact: F) -> Self where T: Bounds<'a>, O: Bounds<'a>, - F: Fact<'a, T>, + F: Factual<'a, T>, L: ToString, G: 'a + Send + Sync + Fn(O) -> T, S: 'a + Send + Sync + Fn(O, T) -> O, @@ -98,11 +98,11 @@ where } } -impl<'a, O, T, F> Fact<'a, O> for LensFact<'a, O, T, F> +impl<'a, O, T, F> Factual<'a, O> for LensFact<'a, O, T, F> where T: Bounds<'a>, O: Bounds<'a> + Clone, - F: Fact<'a, T>, + F: Factual<'a, T>, { #[tracing::instrument(fields(fact = "lens"), skip(self, g))] fn mutate(&mut self, g: &mut Generator<'a>, obj: O) -> Mutation { diff --git a/src/facts/mapped.rs b/src/facts/mapped.rs index 0a8dc32..18d2c76 100644 --- a/src/facts/mapped.rs +++ b/src/facts/mapped.rs @@ -1,18 +1,14 @@ -use std::sync::Arc; - -use crate::{fact::Bounds, *}; - -use super::lambda::LambdaFact; +use crate::{factual::Bounds, *}; /// A version of [`mapped`] whose closure returns a Result -pub fn mapped_fallible<'a, T, F, O, S>(reason: impl ToString, f: F) -> LambdaFact<'a, (), T> +pub fn mapped_fallible<'a, T, F, O, S>(reason: impl ToString, f: F) -> Fact<'a, (), T> where T: Bounds<'a>, - O: Fact<'a, T>, + O: Factual<'a, T>, F: 'a + Send + Sync + Fn(&T) -> ContrafactResult, { let reason = reason.to_string(); - lambda_unit(move |g, obj| { + stateless(move |g, obj| { f(&obj)? .mutate(g, obj) .map_check_err(|err| format!("mapped({}) > {}", reason, err)) @@ -53,14 +49,14 @@ where /// assert!(fact.clone().check(&9009).is_ok()); /// assert!(fact.clone().check(&9010).is_err()); /// ``` -pub fn mapped<'a, T, F, O>(reason: impl ToString, f: F) -> LambdaFact<'a, (), T> +pub fn mapped<'a, T, F, O>(reason: impl ToString, f: F) -> Fact<'a, (), T> where T: Bounds<'a>, - O: Fact<'a, T>, + O: Factual<'a, T>, F: 'a + Send + Sync + Fn(&T) -> O, { let reason = reason.to_string(); - lambda_unit(move |g, obj| { + stateless(move |g, obj| { f(&obj) .mutate(g, obj) .map_check_err(|err| format!("mapped({}) > {}", reason, err)) diff --git a/src/facts/not.rs b/src/facts/not.rs index 23fd596..caaa745 100644 --- a/src/facts/not.rs +++ b/src/facts/not.rs @@ -1,16 +1,16 @@ -use super::{brute::brute_labeled, lambda::LambdaFact, *}; +use super::*; /// Negates a fact // TODO: `not` in particular would really benefit from Facts having accessible // labels, since currently you can only get context about why a `not` fact passed, // not why it fails. -pub fn not<'a, F, T>(context: impl ToString, fact: F) -> LambdaFact<'a, (), T> +pub fn not<'a, F, T>(context: impl ToString, fact: F) -> Fact<'a, (), T> where - F: 'a + Fact<'a, T>, + F: 'a + Factual<'a, T>, T: Bounds<'a>, { let context = context.to_string(); - lambda_unit(move |g, obj| { + stateless(move |g, obj| { let label = format!("not({})", context.clone()); let fact = fact.clone(); brute(label, move |o| fact.clone().check(o).is_err()).mutate(g, obj) @@ -18,9 +18,9 @@ where } /// Negates a fact, with no context given -pub fn not_<'a, F, T>(fact: F) -> LambdaFact<'a, (), T> +pub fn not_<'a, F, T>(fact: F) -> Fact<'a, (), T> where - F: 'a + Fact<'a, T>, + F: 'a + Factual<'a, T>, T: Bounds<'a>, { not("not", fact) diff --git a/src/facts/or.rs b/src/facts/or.rs index 8d2693b..8c4cb92 100644 --- a/src/facts/or.rs +++ b/src/facts/or.rs @@ -4,8 +4,8 @@ use super::*; pub fn or<'a, A, T, S, Item>(context: S, a: A, b: T) -> OrFact<'a, A, T, Item> where S: ToString, - A: Fact<'a, Item>, - T: Fact<'a, Item>, + A: Factual<'a, Item>, + T: Factual<'a, Item>, Item: Bounds<'a>, { OrFact { @@ -22,8 +22,8 @@ where #[derive(Debug, Clone, PartialEq, Eq)] pub struct OrFact<'a, M1, M2, Item> where - M1: Fact<'a, Item>, - M2: Fact<'a, Item>, + M1: Factual<'a, Item>, + M2: Factual<'a, Item>, Item: ?Sized + Bounds<'a>, { context: String, @@ -32,10 +32,10 @@ where _phantom: PhantomData<&'a Item>, } -impl<'a, P1, P2, T> Fact<'a, T> for OrFact<'a, P1, P2, T> +impl<'a, P1, P2, T> Factual<'a, T> for OrFact<'a, P1, P2, T> where - P1: Fact<'a, T> + Fact<'a, T>, - P2: Fact<'a, T> + Fact<'a, T>, + P1: Factual<'a, T> + Factual<'a, T>, + P2: Factual<'a, T> + Factual<'a, T>, T: Bounds<'a>, { fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { diff --git a/src/facts/prism.rs b/src/facts/prism.rs index 123dee1..0a1b236 100644 --- a/src/facts/prism.rs +++ b/src/facts/prism.rs @@ -64,7 +64,7 @@ where O: Bounds<'a>, S: ToString, T: Bounds<'a> + Clone, - F: Fact<'a, T>, + F: Factual<'a, T>, P: 'a + Send + Sync + Fn(&mut O) -> Option<&mut T>, { // let getter = |o| prism(&mut o).cloned(); @@ -86,7 +86,7 @@ pub struct PrismFact<'a, O, T, F> where T: Bounds<'a>, O: Bounds<'a>, - F: Fact<'a, T>, + F: Factual<'a, T>, { label: String, // getter: Arc Option>, @@ -100,14 +100,14 @@ impl<'a, O, T, F> PrismFact<'a, O, T, F> where T: Bounds<'a>, O: Bounds<'a>, - F: Fact<'a, T>, + F: Factual<'a, T>, { /// Constructor. Supply a prism and an existing Fact to create a new Fact. pub fn new

(label: String, prism: P, /*getter: G, setter: S,*/ inner_fact: F) -> Self where T: Bounds<'a>, O: Bounds<'a>, - F: Fact<'a, T>, + F: Factual<'a, T>, P: 'a + Send + Sync + Fn(&mut O) -> Option<&mut T>, // G: Fn(O) -> Option, // S: Fn(O, T) -> Option, @@ -123,11 +123,11 @@ where } } -impl<'a, O, T, F> Fact<'a, O> for PrismFact<'a, O, T, F> +impl<'a, O, T, F> Factual<'a, O> for PrismFact<'a, O, T, F> where T: Bounds<'a> + Clone, O: Bounds<'a>, - F: Fact<'a, T>, + F: Factual<'a, T>, { fn mutate(&mut self, g: &mut Generator<'a>, mut obj: O) -> Mutation { if let Some(t) = (self.prism)(&mut obj) { diff --git a/src/facts/same.rs b/src/facts/same.rs index 4fc2c1f..583b144 100644 --- a/src/facts/same.rs +++ b/src/facts/same.rs @@ -28,7 +28,7 @@ pub struct SameFact { _phantom: PhantomData, } -impl<'a, T> Fact<'a, (T, T)> for SameFact +impl<'a, T> Factual<'a, (T, T)> for SameFact where T: Bounds<'a> + PartialEq + Clone, { diff --git a/src/facts/seq.rs b/src/facts/seq.rs index 41d1b60..a3c31a5 100644 --- a/src/facts/seq.rs +++ b/src/facts/seq.rs @@ -40,7 +40,7 @@ use super::and; pub fn vec<'a, T, F>(inner_fact: F) -> VecFact<'a, T, F> where T: Bounds<'a> + Clone, - F: Fact<'a, T>, + F: Factual<'a, T>, { VecFact::new(inner_fact) } @@ -54,10 +54,10 @@ where } /// Combines a LenFact with a VecFact to ensure that the vector is of a given length -pub fn vec_of_length<'a, T, F>(len: usize, inner_fact: F) -> impl Fact<'a, Vec> +pub fn vec_of_length<'a, T, F>(len: usize, inner_fact: F) -> impl Factual<'a, Vec> where T: Bounds<'a> + Clone + 'a, - F: Fact<'a, T> + 'a, + F: Factual<'a, T> + 'a, { and(vec_len(len), vec(inner_fact)) } @@ -67,7 +67,7 @@ where pub struct VecFact<'a, T, F> where T: Bounds<'a>, - F: Fact<'a, T>, + F: Factual<'a, T>, { /// The inner_fact about the inner substructure inner_fact: F, @@ -78,13 +78,13 @@ where impl<'a, T, F> VecFact<'a, T, F> where T: Bounds<'a>, - F: Fact<'a, T>, + F: Factual<'a, T>, { /// Constructor. Supply a seq and an existing Fact to create a new Fact. pub fn new(inner_fact: F) -> Self where T: Bounds<'a>, - F: Fact<'a, T>, + F: Factual<'a, T>, { Self { inner_fact, @@ -93,10 +93,10 @@ where } } -impl<'a, T, F> Fact<'a, Vec> for VecFact<'a, T, F> +impl<'a, T, F> Factual<'a, Vec> for VecFact<'a, T, F> where T: Bounds<'a>, - F: Fact<'a, T>, + F: Factual<'a, T>, { #[tracing::instrument(fields(fact = "seq"), skip(self, g))] fn mutate(&mut self, g: &mut Generator<'a>, obj: Vec) -> Mutation> { @@ -138,7 +138,7 @@ where } } -impl<'a, T> Fact<'a, Vec> for SeqLenFact<'a, T> +impl<'a, T> Factual<'a, Vec> for SeqLenFact<'a, T> where T: Bounds<'a>, { @@ -221,7 +221,7 @@ mod tests { let piecewise = move || { let count = Arc::new(AtomicU8::new(0)); - lambda((), move |g, (), mut obj| { + stateful((), move |g, (), mut obj| { let c = count.fetch_add(1, Ordering::SeqCst); if c < 3 { g.set(&mut obj, &999, "i'm being difficult, haha")?; diff --git a/src/factual.rs b/src/factual.rs new file mode 100644 index 0000000..2c11ada --- /dev/null +++ b/src/factual.rs @@ -0,0 +1,141 @@ +use arbitrary::*; +use either::Either; + +use crate::*; + +// TODO: we can remove the Clone requirement if: +// - make `Mutate` track list of errors so that it can know if a mutation occurred. +// - make `mutate()` take a mut ref +// - make `check()` take a mut ref +// then `check()` can know if a mutation occurred +// +/// The trait bounds for the subject of a Fact +pub trait Bounds<'a>: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> {} +impl<'a, T> Bounds<'a> for T where + T: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> +{ +} + +// /// Type alias for a boxed Fact. Implements [`Fact`] itself. +// pub type BoxFact<'a, T> = Box>; + +// pub trait Facts>: Fact<'static, T> {} +// impl, F: Facts> Fact<'static, T> for F {} + +/// A declarative representation of a constraint on some data, which can be +/// used to both make an assertion (check) or to mold some arbitrary existing +/// data into a shape which passes that same assertion (mutate) +pub trait Factual<'a, T>: Send + Sync + Clone +where + T: Bounds<'a>, +{ + /// Assert that the constraint is satisfied for given data. + /// + /// If the mutation function is written properly, we get a check for free + /// by using a special Generator which fails upon mutation. If this is for + /// some reason unreasonable, a check function can be written by hand, but + /// care must be taken to make sure it perfectly lines up with the mutation function. + #[tracing::instrument(fields(fact_impl = "Fact"), skip(self))] + fn check(mut self, obj: &T) -> Check { + check_raw(&mut self, obj) + } + + /// Apply a mutation which moves the obj closer to satisfying the overall + /// constraint. + // #[tracing::instrument(skip(self, g))] + fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation; + + /// Make this many attempts to satisfy a constraint before giving up and panicking. + /// + /// If you are combining highly contentious facts together and relying on randomness + /// to find a solution, this limit may need to be higher. In general, you should try + /// to write facts that don't interfere with each other so that the constraint can be + /// met on the first attempt, or perhaps the second or third. If necessary, this can + /// be raised to lean more on random search. + fn satisfy_attempts(&self) -> usize { + SATISFY_ATTEMPTS + } + + /// Mutate a value such that it satisfies the constraint. + /// If the constraint cannot be satisfied, panic. + #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] + fn satisfy(&mut self, g: &mut Generator<'a>, obj: T) -> ContrafactResult { + tracing::trace!("satisfy"); + let mut last_failure: Vec = vec![]; + let mut next = obj.clone(); + for _i in 0..self.satisfy_attempts() { + let mut m = self.clone(); + let mut c = self.clone(); + next = m.mutate(g, next).unwrap(); + if let Err(errs) = check_raw(&mut c, &next).result()? { + last_failure = errs; + } else { + *self = m; + return Ok(next); + } + } + panic!( + "Could not satisfy a constraint even after {} attempts. Last check failure: {:?}", + SATISFY_ATTEMPTS, last_failure + ); + } + + #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] + /// Build a new value such that it satisfies the constraint + fn build_fallible(mut self, g: &mut Generator<'a>) -> ContrafactResult { + let obj = T::arbitrary(g).unwrap(); + self.satisfy(g, obj) + } + + /// Build a new value such that it satisfies the constraint, panicking on error + #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] + fn build(self, g: &mut Generator<'a>) -> T { + self.build_fallible(g).unwrap() + } +} + +impl<'a, T, F1, F2> Factual<'a, T> for Either +where + T: Bounds<'a>, + F1: Factual<'a, T> + ?Sized, + F2: Factual<'a, T> + ?Sized, +{ + #[tracing::instrument(fields(fact_impl = "Either"), skip(self, g))] + fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { + match self { + Either::Left(f) => f.mutate(g, obj), + Either::Right(f) => f.mutate(g, obj), + } + } +} + +#[tracing::instrument(skip(fact))] +pub(crate) fn check_raw<'a, T, F: Factual<'a, T>>(fact: &mut F, obj: &T) -> Check +where + T: Bounds<'a> + ?Sized, + F: Factual<'a, T> + ?Sized, +{ + let mut g = Generator::checker(); + Check::from_mutation(fact.mutate(&mut g, obj.clone())) +} + +#[tracing::instrument(skip(facts))] +fn collect_checks<'a, T, F>(facts: Vec, obj: &T) -> Check +where + T: Bounds<'a>, + F: Factual<'a, T>, +{ + let checks = facts + .into_iter() + .enumerate() + .map(|(i, f)| { + Ok(f.check(obj) + .failures()? + .iter() + .map(|e| format!("fact {}: {}", i, e)) + .collect()) + }) + .collect::>>>() + .map(|fs| fs.into_iter().flatten().collect()); + Check::from_result(checks) +} diff --git a/src/lib.rs b/src/lib.rs index b93ed71..9153cec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,10 +61,10 @@ mod check; mod error; mod fact; -mod generator; - /// Some built-in implementations of some useful facts pub mod facts; +mod factual; +mod generator; pub use facts::*; #[cfg(feature = "utils")] @@ -74,7 +74,8 @@ pub use arbitrary; pub use check::Check; pub use error::*; -pub use fact::{Bounds, Fact}; +pub use fact::{stateful, stateless, Fact}; +pub use factual::{Bounds, Factual}; pub use generator::*; pub use either; diff --git a/tests/chainlink.rs b/tests/chainlink.rs index 82f2449..06ef61a 100644 --- a/tests/chainlink.rs +++ b/tests/chainlink.rs @@ -23,7 +23,7 @@ struct Wrapper { /// Fact: all Links in a chain are by the same `author`, and any chain link has /// consecutive `prev` values starting with 0. -fn chain_fact<'a>(author: String) -> impl Fact<'a, Link> { +fn chain_fact<'a>(author: String) -> impl Factual<'a, Link> { facts![ lens( "Link::author", @@ -40,7 +40,7 @@ fn chain_fact<'a>(author: String) -> impl Fact<'a, Link> { /// Fact: the Links within each wrapper form a valid chain, and the color /// of the wrapper is in the given set. -fn wrapper_fact<'a>(author: String, valid_colors: &'a [Color]) -> impl Fact<'a, Wrapper> { +fn wrapper_fact<'a>(author: String, valid_colors: &'a [Color]) -> impl Factual<'a, Wrapper> { facts![ lens( "Wrapper::color", diff --git a/tests/complex.rs b/tests/complex.rs index edbca62..eac66bc 100644 --- a/tests/complex.rs +++ b/tests/complex.rs @@ -129,11 +129,11 @@ impl AlphaSigner { } #[allow(unused)] -fn alpha_fact() -> impl Fact<'static, Alpha> { +fn alpha_fact() -> impl Factual<'static, Alpha> { facts![lens("Alpha::id", |a: &mut Alpha| a.id(), id_fact(None))] } -fn beta_fact() -> impl Fact<'static, Beta> { +fn beta_fact() -> impl Factual<'static, Beta> { facts![lens("Beta::id", |a: &mut Beta| &mut a.id, id_fact(None))] } @@ -142,7 +142,7 @@ fn beta_fact() -> impl Fact<'static, Beta> { #[derive(Clone, Debug, PartialEq, Arbitrary)] struct Pi(Alpha, Option); -fn pi_beta_match() -> impl Fact<'static, Pi> { +fn pi_beta_match() -> impl Factual<'static, Pi> { facts![brute( "Pi alpha has matching beta iff beta is Some", |p: &Pi| match p { @@ -153,7 +153,7 @@ fn pi_beta_match() -> impl Fact<'static, Pi> { )] } -fn id_fact(id: Option) -> impl Fact<'static, Id> { +fn id_fact(id: Option) -> impl Factual<'static, Id> { let le = brute("< u32::MAX", |id: &Id| *id < Id::MAX / 2); if let Some(id) = id { @@ -165,7 +165,7 @@ fn id_fact(id: Option) -> impl Fact<'static, Id> { /// - id must be set as specified /// - All Ids should match each other. If there is a Beta, its id should match too. -fn pi_fact(id: Id) -> impl Fact<'static, Pi> { +fn pi_fact(id: Id) -> impl Factual<'static, Pi> { let alpha_fact = facts![ lens("Alpha::id", |a: &mut Alpha| a.id(), id_fact(Some(id))), // lens("Alpha::data", |a: &mut Alpha| a.data(), eq("data", data)), @@ -183,7 +183,7 @@ fn pi_fact(id: Id) -> impl Fact<'static, Pi> { /// - If Omega::AlphaBeta, then Alpha::Beta, /// - and, the the Betas of the Alpha and the Omega should match. /// - all data must be set as specified -fn omega_fact(id: Id) -> impl Fact<'static, Omega> { +fn omega_fact(id: Id) -> impl Factual<'static, Omega> { let omega_pi = LensFact::new( "Omega -> Pi", |o| match o { @@ -207,7 +207,7 @@ fn omega_fact(id: Id) -> impl Fact<'static, Omega> { } #[allow(unused)] -fn sigma_fact() -> impl Fact<'static, Sigma> { +fn sigma_fact() -> impl Factual<'static, Sigma> { let id2_fact = LensFact::new( "Sigma::id is correct", |mut s: Sigma| (s.id2, *(s.alpha.id()) * 2), @@ -234,7 +234,7 @@ fn sigma_fact() -> impl Fact<'static, Sigma> { /// The inner Sigma is correct wrt to signature /// XXX: this is a little wonky, probably room for improvement. -fn rho_fact(id: Id, signer: AlphaSigner) -> impl Fact<'static, Rho> { +fn rho_fact(id: Id, signer: AlphaSigner) -> impl Factual<'static, Rho> { let rho_pi = LensFact::new( "Rho -> Pi", |rho: Rho| Pi(rho.sigma.alpha, rho.beta), From 8832717f6ac93f625625065c5bc04b9e296a149d Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 00:33:40 -0700 Subject: [PATCH 07/21] Rewrite always() --- src/fact.rs | 3 +++ src/facts/constant.rs | 26 +++++++------------------- src/lib.rs | 2 +- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/fact.rs b/src/fact.rs index f163c66..db1f767 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -72,6 +72,9 @@ where } } +/// A Fact with unit state +pub type StatelessFact<'a, T> = Fact<'a, (), T>; + #[test] fn test_lambda_fact() { use crate::facts::*; diff --git a/src/facts/constant.rs b/src/facts/constant.rs index d29bb1f..51727f0 100644 --- a/src/facts/constant.rs +++ b/src/facts/constant.rs @@ -1,27 +1,15 @@ use super::*; /// A constraint which is always met -pub fn always() -> ConstantFact { - ConstantFact(true, "always".to_string()) +pub fn always<'a, T: Bounds<'a>>() -> StatelessFact<'a, T> { + stateless(|g, obj| Ok(obj)) } /// A constraint which is never met -pub fn never(context: S) -> ConstantFact { - ConstantFact(false, context.to_string()) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ConstantFact(bool, String); - -impl<'a, T> Factual<'a, T> for ConstantFact -where - T: Bounds<'a> + PartialEq + Clone, -{ - #[tracing::instrument(fields(fact = "bool"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'_>, obj: T) -> Mutation { - if !self.0 { - g.fail("never() encountered.")?; - } +pub fn never<'a, T: Bounds<'a>>(context: impl ToString) -> StatelessFact<'a, T> { + let context = context.to_string(); + stateless(move |g, obj: T| { + g.fail(context.clone())?; Ok(obj) - } + }) } diff --git a/src/lib.rs b/src/lib.rs index 9153cec..2bcaf7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ pub use arbitrary; pub use check::Check; pub use error::*; -pub use fact::{stateful, stateless, Fact}; +pub use fact::{stateful, stateless, Fact, StatelessFact}; pub use factual::{Bounds, Factual}; pub use generator::*; From 7e1024ab50dd748da55698c7b31fcb6a1baec4da Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 00:35:26 -0700 Subject: [PATCH 08/21] Fix docs --- src/fact.rs | 2 +- src/facts/brute.rs | 2 +- src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fact.rs b/src/fact.rs index db1f767..5d53a6f 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -12,7 +12,7 @@ use crate::*; /// /// let mut fact = vec_of_length( /// 4, -/// lambda(2, move |g, s, mut v| { +/// stateful(2, move |g, s, mut v| { /// g.set(&mut v, s, "value is not geometrically increasing by 2")?; /// *s *= 2; /// Ok(v) diff --git a/src/facts/brute.rs b/src/facts/brute.rs index 96c5c91..1cf375a 100644 --- a/src/facts/brute.rs +++ b/src/facts/brute.rs @@ -24,7 +24,7 @@ use crate::*; /// use arbitrary::Unstructured; /// use contrafact::*; /// -/// fn div_by(n: usize) -> impl Fact<'static, usize> { +/// fn div_by(n: usize) -> impl Factual<'static, usize> { /// facts![brute(format!("Is divisible by {}", n), move |x| x % n == 0)] /// } /// diff --git a/src/lib.rs b/src/lib.rs index 2bcaf7e..ccbb9bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ //! meets the constraint, or to generate new instances of `S` which meet the constraint. //! //! ``` -//! use contrafact::{Fact, facts::{eq, lens}}; +//! use contrafact::{Factual, facts::{eq, lens}}; //! use arbitrary::{Arbitrary, Unstructured}; //! //! #[derive(Debug, Clone, PartialEq, Arbitrary)] From bae156fe849736f1d4b367b9df935cf996ffcf18 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 11:49:19 -0700 Subject: [PATCH 09/21] Make reason a closure; rewrite same() --- src/fact.rs | 8 ++-- src/facts.rs | 100 ------------------------------------------ src/facts/brute.rs | 7 +-- src/facts/constant.rs | 2 +- src/facts/eq.rs | 65 ++++++--------------------- src/facts/in_range.rs | 40 +++++++++++++++-- src/facts/in_slice.rs | 58 ++++++++---------------- src/facts/not.rs | 14 ++++++ src/facts/or.rs | 16 +++++++ src/facts/same.rs | 78 +++++++++++++++----------------- src/facts/seq.rs | 4 +- src/generator.rs | 84 +++++++++++++++++++---------------- 12 files changed, 190 insertions(+), 286 deletions(-) diff --git a/src/fact.rs b/src/fact.rs index 5d53a6f..4f723f2 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -82,11 +82,9 @@ fn test_lambda_fact() { let geom = |k, s| { stateful(s, move |g, s, mut v| { - g.set( - &mut v, - s, - format!("value is not geometrically increasing by {k} starting from {s}"), - )?; + g.set(&mut v, s, || { + format!("value is not geometrically increasing by {k} starting from {s}") + })?; *s *= k; Ok(v) }) diff --git a/src/facts.rs b/src/facts.rs index 3cfed16..251f727 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -36,106 +36,6 @@ mod optical; #[cfg(feature = "optics")] pub use optical::*; -pub(crate) use eq::EqOp; - use crate::factual::check_raw; use crate::*; use std::marker::PhantomData; - -#[cfg(test)] -mod tests { - use super::*; - use crate::utils; - - #[test] - fn test_eq() { - observability::test_run().ok(); - let mut g = utils::random_generator(); - - let eq1 = vec(eq("must be 1", 1)); - - let ones = eq1.clone().build(&mut g); - eq1.clone().check(&ones).unwrap(); - - assert!(ones.iter().all(|x| *x == 1)); - } - - #[test] - fn test_or() { - observability::test_run().ok(); - let mut g = utils::random_generator(); - - let eq1 = eq("must be 1", 1); - let eq2 = eq("must be 2", 2); - let either = or("can be 1 or 2", eq1, eq2); - - let ones = vec(either.clone()).build(&mut g); - vec(either.clone()).check(&ones).unwrap(); - assert!(ones.iter().all(|x| *x == 1 || *x == 2)); - - assert_eq!(either.check(&3).result().unwrap().unwrap_err().len(), 1); - } - - #[test] - fn test_not() { - observability::test_run().ok(); - let mut g = utils::random_generator(); - - let eq1 = eq("must be 1", 1); - let not1 = vec(not_(eq1)); - - let nums = not1.clone().build(&mut g); - not1.clone().check(&nums).unwrap(); - - assert!(nums.iter().all(|x| *x != 1)); - } - - #[test] - fn test_same() { - observability::test_run().ok(); - let mut g = utils::random_generator(); - - { - let f = vec(same::()); - let nums = f.clone().build(&mut g); - f.clone().check(&nums).unwrap(); - assert!(nums.iter().all(|(a, b)| a == b)); - } - { - let f = vec(different::()); - let nums = f.clone().build(&mut g); - f.clone().check(&nums).unwrap(); - assert!(nums.iter().all(|(a, b)| a != b)); - } - } - - #[test] - fn test_in_range() { - observability::test_run().ok(); - let mut g = utils::random_generator(); - - let positive1 = in_range("must be positive", 1..=i32::MAX); - let positive2 = in_range("must be positive", 1..); - let smallish = in_range("must be small in magnitude", -10..100); - let over9000 = in_range("must be over 9000", 9001..); - let under9000 = in_range("must be under 9000 (and no less than zero)", ..9000u32); - - let nonpositive1 = vec(not_(positive1)); - let nonpositive2 = vec(not_(positive2)); - - let smallish_nums = smallish.clone().build(&mut g); - let over9000_nums = over9000.clone().build(&mut g); - let under9000_nums = under9000.clone().build(&mut g); - let nonpositive1_nums = nonpositive1.clone().build(&mut g); - let nonpositive2_nums = nonpositive2.clone().build(&mut g); - - dbg!(&under9000_nums); - - smallish.clone().check(&smallish_nums).unwrap(); - over9000.clone().check(&over9000_nums).unwrap(); - under9000.clone().check(&under9000_nums).unwrap(); - nonpositive1.clone().check(&nonpositive1_nums).unwrap(); - nonpositive2.clone().check(&nonpositive2_nums).unwrap(); - assert!(nonpositive1_nums.iter().all(|x| *x <= 0)); - } -} diff --git a/src/facts/brute.rs b/src/facts/brute.rs index 1cf375a..495020b 100644 --- a/src/facts/brute.rs +++ b/src/facts/brute.rs @@ -16,7 +16,8 @@ use crate::*; /// ALSO **NOTE**: It is usually best to place this constraint at the beginning /// of a chain when doing mutation, because if the closure specifies a weak /// constraint, the mutation may drastically alter the data, potentially undoing -/// constraints that were met by previous mutations. +/// constraints that were met by previous mutations. It's also probably not a +/// good idea to combine two different brute facts /// /// There is a fixed iteration limit, beyond which this will panic. /// @@ -50,8 +51,8 @@ where let mut last_reason = "".to_string(); for _ in 0..=BRUTE_ITERATION_LIMIT { if let Err(reason) = f(&obj)? { - obj = g.arbitrary(&reason)?; - last_reason = reason; + last_reason = reason.clone(); + obj = g.arbitrary(|| reason)?; } else { return Ok(obj); } diff --git a/src/facts/constant.rs b/src/facts/constant.rs index 51727f0..1d4e6bd 100644 --- a/src/facts/constant.rs +++ b/src/facts/constant.rs @@ -2,7 +2,7 @@ use super::*; /// A constraint which is always met pub fn always<'a, T: Bounds<'a>>() -> StatelessFact<'a, T> { - stateless(|g, obj| Ok(obj)) + stateless(|_, obj| Ok(obj)) } /// A constraint which is never met diff --git a/src/facts/eq.rs b/src/facts/eq.rs index d644705..a2989a4 100644 --- a/src/facts/eq.rs +++ b/src/facts/eq.rs @@ -25,68 +25,31 @@ where } /// Specifies an inequality constraint -pub fn ne(context: S, constant: T) -> EqFact +pub fn ne<'a, S, T>(context: S, constant: T) -> Fact<'a, (), T> where S: ToString, - T: std::fmt::Debug + PartialEq, + T: Bounds<'a> + PartialEq, { - EqFact { - context: context.to_string(), - constant, - op: EqOp::NotEqual, - _phantom: PhantomData, - } + not(context, eq_(constant)) } /// Specifies an inequality constraint with no context -pub fn ne_(constant: T) -> EqFact +pub fn ne_<'a, T>(constant: T) -> Fact<'a, (), T> where - T: std::fmt::Debug + PartialEq, + T: Bounds<'a> + PartialEq, { ne("ne", constant) } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EqFact { - context: String, - op: EqOp, - constant: T, - _phantom: PhantomData, -} +#[test] +fn test_eq() { + observability::test_run().ok(); + let mut g = utils::random_generator(); -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EqOp { - Equal, - NotEqual, -} + let eq1 = vec(eq("must be 1", 1)); -impl<'a, T> Factual<'a, T> for EqFact -where - T: Bounds<'a> + PartialEq + Clone, -{ - #[tracing::instrument(fields(fact = "eq"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: T) -> Mutation { - let constant = self.constant.clone(); - match self.op { - EqOp::Equal => { - if obj != constant { - g.fail(format!( - "{}: expected {:?} == {:?}", - self.context, obj, constant - ))?; - obj = constant; - } - } - EqOp::NotEqual => loop { - if obj != constant { - break; - } - obj = g.arbitrary(format!( - "{}: expected {:?} != {:?}", - self.context, obj, constant - ))?; - }, - } - Ok(obj) - } + let ones = eq1.clone().build(&mut g); + eq1.clone().check(&ones).unwrap(); + + assert!(ones.iter().all(|x| *x == 1)); } diff --git a/src/facts/in_range.rs b/src/facts/in_range.rs index 6488000..e93ad42 100644 --- a/src/facts/in_range.rs +++ b/src/facts/in_range.rs @@ -80,10 +80,12 @@ where { fn mutate(&mut self, g: &mut Generator<'a>, mut obj: T) -> Mutation { if !self.range.contains(&obj) { - let rand = g.arbitrary(format!( - "{}: expected {:?} to be contained in {:?}", - self.context, obj, self.range - ))?; + let rand = g.arbitrary(|| { + format!( + "{}: expected {:?} to be contained in {:?}", + self.context, obj, self.range + ) + })?; obj = match (self.range.start_bound(), self.range.end_bound()) { (Bound::Unbounded, Bound::Unbounded) => rand, (Bound::Included(a), Bound::Included(b)) if b.clone() - a.clone() >= T::one() => { @@ -107,3 +109,33 @@ where Ok(obj) } } + +#[test] +fn test_in_range() { + observability::test_run().ok(); + let mut g = utils::random_generator(); + + let positive1 = in_range("must be positive", 1..=i32::MAX); + let positive2 = in_range("must be positive", 1..); + let smallish = in_range("must be small in magnitude", -10..100); + let over9000 = in_range("must be over 9000", 9001..); + let under9000 = in_range("must be under 9000 (and no less than zero)", ..9000u32); + + let nonpositive1 = vec(not_(positive1)); + let nonpositive2 = vec(not_(positive2)); + + let smallish_nums = smallish.clone().build(&mut g); + let over9000_nums = over9000.clone().build(&mut g); + let under9000_nums = under9000.clone().build(&mut g); + let nonpositive1_nums = nonpositive1.clone().build(&mut g); + let nonpositive2_nums = nonpositive2.clone().build(&mut g); + + dbg!(&under9000_nums); + + smallish.clone().check(&smallish_nums).unwrap(); + over9000.clone().check(&over9000_nums).unwrap(); + under9000.clone().check(&under9000_nums).unwrap(); + nonpositive1.clone().check(&nonpositive1_nums).unwrap(); + nonpositive2.clone().check(&nonpositive2_nums).unwrap(); + assert!(nonpositive1_nums.iter().all(|x| *x <= 0)); +} diff --git a/src/facts/in_slice.rs b/src/facts/in_slice.rs index ce53535..55d00d6 100644 --- a/src/facts/in_slice.rs +++ b/src/facts/in_slice.rs @@ -1,52 +1,30 @@ use super::*; /// Specifies a membership constraint -pub fn in_slice<'a, S, T>(context: S, slice: &'a [T]) -> InSliceFact<'a, T> +pub fn in_slice<'a, T>(context: impl ToString, slice: &'a [T]) -> StatelessFact<'a, T> where - S: ToString, - T: 'a + PartialEq + std::fmt::Debug + Clone, + T: Bounds<'a> + PartialEq + Clone, { - InSliceFact { - context: context.to_string(), - slice, - } + let context = context.to_string(); + stateless(move |g, obj| { + Ok(if !slice.contains(&obj) { + let reason = || { + format!( + "{}: expected {:?} to be contained in {:?}", + context, obj, slice + ) + }; + g.choose(slice, reason)?.to_owned() + } else { + obj + }) + }) } /// Specifies a membership constraint -pub fn in_slice_<'a, T>(slice: &'a [T]) -> InSliceFact<'a, T> +pub fn in_slice_<'a, T>(slice: &'a [T]) -> StatelessFact<'a, T> where - T: 'a + PartialEq + std::fmt::Debug + Clone, + T: Bounds<'a> + PartialEq + Clone, { in_slice("in_slice", slice) } - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct InSliceFact<'a, T> -where - T: 'a + PartialEq + std::fmt::Debug + Clone, - // I: Iterator, -{ - context: String, - slice: &'a [T], -} - -impl<'a, 'b: 'a, T> Factual<'a, T> for InSliceFact<'b, T> -where - T: 'b + Bounds<'a> + Clone, - // I: Iterator, -{ - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - Ok(if !self.slice.contains(&obj) { - g.choose( - self.slice, - format!( - "{}: expected {:?} to be contained in {:?}", - self.context, obj, self.slice - ), - )? - .to_owned() - } else { - obj - }) - } -} diff --git a/src/facts/not.rs b/src/facts/not.rs index caaa745..fb7360e 100644 --- a/src/facts/not.rs +++ b/src/facts/not.rs @@ -25,3 +25,17 @@ where { not("not", fact) } + +#[test] +fn test_not() { + observability::test_run().ok(); + let mut g = utils::random_generator(); + + let eq1 = eq("must be 1", 1); + let not1 = vec(not_(eq1)); + + let nums = not1.clone().build(&mut g); + not1.clone().check(&nums).unwrap(); + + assert!(nums.iter().all(|x| *x != 1)); +} diff --git a/src/facts/or.rs b/src/facts/or.rs index 8c4cb92..dc73f2f 100644 --- a/src/facts/or.rs +++ b/src/facts/or.rs @@ -62,3 +62,19 @@ where } } } + +#[test] +fn test_or() { + observability::test_run().ok(); + let mut g = utils::random_generator(); + + let eq1 = eq("must be 1", 1); + let eq2 = eq("must be 2", 2); + let either = or("can be 1 or 2", eq1, eq2); + + let ones = vec(either.clone()).build(&mut g); + vec(either.clone()).check(&ones).unwrap(); + assert!(ones.iter().all(|x| *x == 1 || *x == 2)); + + assert_eq!(either.check(&3).result().unwrap().unwrap_err().len(), 1); +} diff --git a/src/facts/same.rs b/src/facts/same.rs index 583b144..0ba4807 100644 --- a/src/facts/same.rs +++ b/src/facts/same.rs @@ -1,56 +1,50 @@ -use super::*; +use super::{brute::brute_labeled, *}; /// Specifies an equality constraint between two items in a tuple -pub fn same() -> SameFact +pub fn same<'a, T>() -> StatelessFact<'a, (T, T)> where - T: std::fmt::Debug + PartialEq, + T: Bounds<'a> + PartialEq, { - SameFact { - op: EqOp::Equal, - _phantom: PhantomData, - } + stateless(|g, mut obj: (T, T)| { + let o = obj.clone(); + let reason = move || format!("must be same: expected {:?} == {:?}", o.0.clone(), o.1); + g.set(&mut obj.0, &obj.1, reason)?; + Ok(obj) + }) } /// Specifies an inequality constraint between two items in a tuple -pub fn different() -> SameFact +pub fn different<'a, T>() -> StatelessFact<'a, (T, T)> where - T: std::fmt::Debug + PartialEq, + T: Bounds<'a> + PartialEq, { - SameFact { - op: EqOp::NotEqual, - _phantom: PhantomData, - } + brute_labeled(|(a, b)| { + if a == b { + Ok(Err(format!( + "must be different: expected {:?} != {:?}", + a, b + ))) + } else { + Ok(Ok(())) + } + }) } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SameFact { - op: EqOp, - _phantom: PhantomData, -} +#[test] +fn test_same() { + observability::test_run().ok(); + let mut g = utils::random_generator(); -impl<'a, T> Factual<'a, (T, T)> for SameFact -where - T: Bounds<'a> + PartialEq + Clone, -{ - #[tracing::instrument(fields(fact = "same"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: (T, T)) -> Mutation<(T, T)> { - match self.op { - EqOp::Equal => { - if obj.0 != obj.1 { - g.fail(format!("must be same: expected {:?} == {:?}", obj.0, obj.1))?; - obj.0 = obj.1.clone(); - } - } - EqOp::NotEqual => loop { - if obj.0 != obj.1 { - break; - } - obj.0 = g.arbitrary(format!( - "must be different: expected {:?} != {:?}", - obj.0, obj.1 - ))?; - }, - } - Ok(obj) + { + let f = vec(same::()); + let nums = f.clone().build(&mut g); + f.clone().check(&nums).unwrap(); + assert!(nums.iter().all(|(a, b)| a == b)); + } + { + let f = vec(different::()); + let nums = f.clone().build(&mut g); + f.clone().check(&nums).unwrap(); + assert!(nums.iter().all(|(a, b)| a != b)); } } diff --git a/src/facts/seq.rs b/src/facts/seq.rs index a3c31a5..a6028b1 100644 --- a/src/facts/seq.rs +++ b/src/facts/seq.rs @@ -151,7 +151,7 @@ where obj = obj[0..self.len].to_vec(); } while obj.len() < self.len { - obj.push(g.arbitrary("LenFact: vec was too short")?) + obj.push(g.arbitrary(|| "LenFact: vec was too short")?) } Ok(obj) } @@ -224,7 +224,7 @@ mod tests { stateful((), move |g, (), mut obj| { let c = count.fetch_add(1, Ordering::SeqCst); if c < 3 { - g.set(&mut obj, &999, "i'm being difficult, haha")?; + g.set(&mut obj, &999, || "i'm being difficult, haha")?; } Ok(obj) }) diff --git a/src/generator.rs b/src/generator.rs index 4d24763..8295363 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -66,15 +66,15 @@ impl<'a> Generator<'a> { /// When running a Check, fail immediately with this error if the existing value doesn't match. /// During mutation, set the value so that it does match. - pub fn set( + pub fn set( &self, source: &mut T, target: &T, - err: impl ToString, + err: impl FnOnce() -> S, ) -> Mutation<()> { if source != target { if self.check { - return Err(MutationError::Check(err.to_string())); + return Err(MutationError::Check(err().to_string())); } else { *source = target.clone(); } @@ -83,15 +83,18 @@ impl<'a> Generator<'a> { } /// Generate arbitrary data in mutation mode, or produce an error in check mode - pub fn arbitrary>(&mut self, err: impl ToString) -> Mutation { + pub fn arbitrary, S: ToString>( + &mut self, + err: impl FnOnce() -> S, + ) -> Mutation { self.with(err, |u| u.arbitrary()) } /// Choose between specified items in mutation mode, or produce an error in check mode. - pub fn choose>( + pub fn choose, S: ToString>( &mut self, choices: &'a [T], - err: impl ToString, + err: impl FnOnce() -> S, ) -> Mutation<&T> { if choices.is_empty() { return Err(MutationError::User("Empty choices".to_string())).into(); @@ -111,9 +114,14 @@ impl<'a> Generator<'a> { /// size of a collection to build, that's exactly what I will be doing with /// this, because I'm not sure exactly what the contrafact story is for /// size hints (which is what arbitrary would be using instead). - pub fn int_in_range(&mut self, range: RangeInclusive, err: impl ToString) -> Mutation + pub fn int_in_range( + &mut self, + range: RangeInclusive, + err: impl FnOnce() -> S, + ) -> Mutation where T: Arbitrary<'a> + PartialOrd + Copy + Int, + S: ToString, { if range.start() > range.end() { return Err(MutationError::User("Invalid range".to_string())).into(); @@ -127,13 +135,13 @@ impl<'a> Generator<'a> { } /// Call the specified Arbitrary function in mutation mode, or produce an error in check mode. - pub fn with( + pub fn with( &mut self, - err: impl ToString, + err: impl FnOnce() -> S, f: impl FnOnce(&mut Unstructured<'a>) -> Result, ) -> Mutation { if self.check { - Err(MutationError::Check(err.to_string())).into() + Err(MutationError::Check(err().to_string())).into() } else { f(&mut self.arb).map_err(Into::into) } @@ -151,7 +159,7 @@ pub mod test { pub fn test_generator_int_in_range_invalid_range() { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); assert_eq!( - gen.int_in_range(5..=4, "error"), + gen.int_in_range(5..=4, || "error"), Err(MutationError::User("Invalid range".to_string())) ); } @@ -161,7 +169,7 @@ pub mod test { pub fn test_generator_int_in_range_valid_range_single_option() { let mut gen = crate::generator::Generator::from(&[0][..]); for _ in 0..10 { - assert_eq!(gen.int_in_range(5..=5, "error").unwrap(), 5); + assert_eq!(gen.int_in_range(5..=5, || "error").unwrap(), 5); } // The generator has not had any entropy consumed by this point. assert_eq!(gen.len(), 1); @@ -172,12 +180,12 @@ pub mod test { pub fn test_generator_int_in_range_valid_range_multiple_options() { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); for i in 0..6 { - assert_eq!(gen.int_in_range(0..=3, "error").unwrap(), i % 4); + assert_eq!(gen.int_in_range(0..=3, || "error").unwrap(), i % 4); } // The generator has no entropy remaining at this point. assert_eq!(gen.len(), 0); assert_eq!( - gen.int_in_range(0..=3, "error"), + gen.int_in_range(0..=3, || "error"), Err(MutationError::User("Ran out of entropy".to_string())) ); } @@ -188,7 +196,7 @@ pub mod test { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); let choices: [usize; 0] = []; assert_eq!( - gen.choose(&choices, "error"), + gen.choose(&choices, || "error"), Err(MutationError::User("Empty choices".to_string())) ); } @@ -198,7 +206,7 @@ pub mod test { pub fn test_generator_one_choices() { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); let choices = [0]; - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); } /// Test that a generator can be used to choose between two values. @@ -206,16 +214,16 @@ pub mod test { pub fn test_generator_choose_two_values() { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); let choices = [0, 1]; - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); // This is the only case where we can't choose a value, because we have 2 choices and 6 bytes. assert_eq!( - gen.choose(&choices, "error"), + gen.choose(&choices, || "error"), Err(MutationError::User("Ran out of entropy".to_string())) ); } @@ -225,16 +233,16 @@ pub mod test { pub fn test_generator_choose_three_values() { let mut gen = crate::generator::Generator::from(&[0, 1, 2, 3, 4, 5][..]); let choices = [0, 1, 2]; - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); - assert_eq!(gen.choose(&choices, "error").unwrap(), &2); - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); - assert_eq!(gen.choose(&choices, "error").unwrap(), &2); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &2); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &2); // This is the only case where we can't choose a value, because we have 3 choices and 6 bytes. assert_eq!( - gen.choose(&choices, "error"), + gen.choose(&choices, || "error"), Err(MutationError::User("Ran out of entropy".to_string())) ); } @@ -252,16 +260,16 @@ pub mod test { let mut gen = crate::generator::Generator::from(&u_data[..]); let choices = [0, 1, 2]; - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); - assert_eq!(gen.choose(&choices, "error").unwrap(), &2); - assert_eq!(gen.choose(&choices, "error").unwrap(), &2); - assert_eq!(gen.choose(&choices, "error").unwrap(), &1); - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &2); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &2); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &1); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); // This is the only case where we can't choose a value, because we have 3 choices and 6 bytes. assert_eq!( - gen.choose(&choices, "error"), + gen.choose(&choices, || "error"), Err(MutationError::User("Ran out of entropy".to_string())) ); } @@ -272,7 +280,7 @@ pub mod test { let mut gen = crate::generator::Generator::from(&[0][..]); let choices = [0]; for _ in 0..10 { - assert_eq!(gen.choose(&choices, "error").unwrap(), &0); + assert_eq!(gen.choose(&choices, || "error").unwrap(), &0); } // The generator has not had any entropy consumed by this point. From 5fb047692dadc5dd0148017f1553467a89122209 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 11:55:10 -0700 Subject: [PATCH 10/21] Rewrite and() --- src/fact.rs | 2 +- src/facts/and.rs | 53 +++++------------------------------------------- 2 files changed, 6 insertions(+), 49 deletions(-) diff --git a/src/fact.rs b/src/fact.rs index 4f723f2..2524625 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -13,7 +13,7 @@ use crate::*; /// let mut fact = vec_of_length( /// 4, /// stateful(2, move |g, s, mut v| { -/// g.set(&mut v, s, "value is not geometrically increasing by 2")?; +/// g.set(&mut v, s, || "value is not geometrically increasing by 2")?; /// *s *= 2; /// Ok(v) /// }), diff --git a/src/facts/and.rs b/src/facts/and.rs index dc66e7a..fc20c7f 100644 --- a/src/facts/and.rs +++ b/src/facts/and.rs @@ -1,58 +1,15 @@ -use std::marker::PhantomData; - use crate::*; /// A Fact which applies two other facts. -pub fn and<'a, F1, F2, T>(a: F1, b: F2) -> AndFact<'a, F1, F2, T> +pub fn and<'a, F1, F2, T>(a: F1, b: F2) -> Fact<'a, (F1, F2), T> where T: Bounds<'a>, F1: Factual<'a, T>, F2: Factual<'a, T>, { - AndFact::new(a, b) -} - -/// A fact which applies two facts. -/// This is the primary way to build up a complex fact from simpler facts. -/// The [`facts!`] macro is a more convenient way to compose more than two facts -/// together using [`AndFact`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AndFact<'a, F1, F2, T> -where - F1: Factual<'a, T>, - F2: Factual<'a, T>, - T: ?Sized + Bounds<'a>, -{ - pub(crate) a: F1, - pub(crate) b: F2, - _phantom: PhantomData<&'a T>, -} - -impl<'a, F1, F2, T> AndFact<'a, F1, F2, T> -where - F1: Factual<'a, T>, - F2: Factual<'a, T>, - T: ?Sized + Bounds<'a>, -{ - /// Constructor - pub fn new(a: F1, b: F2) -> Self { - Self { - a, - b, - _phantom: PhantomData, - } - } -} - -impl<'a, F1, F2, T> Factual<'a, T> for AndFact<'a, F1, F2, T> -where - F1: Factual<'a, T> + Factual<'a, T>, - F2: Factual<'a, T> + Factual<'a, T>, - T: Bounds<'a>, -{ - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - let obj = self.a.mutate(g, obj)?; - let obj = self.b.mutate(g, obj)?; + stateful((a, b), |g, (a, b), obj| { + let obj = a.mutate(g, obj)?; + let obj = b.mutate(g, obj)?; Ok(obj) - } + }) } From 54a5ac438b2d784d12b09d73ea666de237f86f25 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 12:27:48 -0700 Subject: [PATCH 11/21] Remove mapped() --- src/facts.rs | 2 - src/facts/mapped.rs | 127 -------------------------------------------- 2 files changed, 129 deletions(-) delete mode 100644 src/facts/mapped.rs diff --git a/src/facts.rs b/src/facts.rs index 251f727..8736604 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -6,7 +6,6 @@ mod eq; mod in_range; mod in_slice; mod lens; -mod mapped; mod not; mod or; mod prism; @@ -25,7 +24,6 @@ pub use same::{different, same}; pub use and::and; pub use brute::brute; pub use lens::{lens, LensFact}; -pub use mapped::{mapped, mapped_fallible}; pub use prism::{prism, PrismFact}; pub use seq::{vec, vec_len, vec_of_length}; diff --git a/src/facts/mapped.rs b/src/facts/mapped.rs deleted file mode 100644 index 18d2c76..0000000 --- a/src/facts/mapped.rs +++ /dev/null @@ -1,127 +0,0 @@ -use crate::{factual::Bounds, *}; - -/// A version of [`mapped`] whose closure returns a Result -pub fn mapped_fallible<'a, T, F, O, S>(reason: impl ToString, f: F) -> Fact<'a, (), T> -where - T: Bounds<'a>, - O: Factual<'a, T>, - F: 'a + Send + Sync + Fn(&T) -> ContrafactResult, -{ - let reason = reason.to_string(); - stateless(move |g, obj| { - f(&obj)? - .mutate(g, obj) - .map_check_err(|err| format!("mapped({}) > {}", reason, err)) - }) -} - -/// A fact which is defined based on the data to which it is applied. It maps -/// the data into a fact to be applied. -/// -/// This can be useful for "piecewise" functions, where the -/// constraint is fundamentally different depending on the shape of the data, -/// or when wanting to set some subset of data to match some other subset of -/// data, without caring what the value actually is, and without having to -/// explicitly construct the value. -/// -/// **NOTE**: since the returned Facts are generated brand-new on-the-fly, -/// these Facts must be stateless. State changes cannot be carried over to -/// subsequent calls when running over a sequence. -/// (TODO: add `StatelessFact` trait to give type-level protection here.) -/// -/// ``` -/// use contrafact::*; -/// -/// // This contrived fact reads: -/// // "if the number is greater than 9000, -/// // ensure that it's also divisible by 9, -/// // and otherwise, ensure that it's divisible by 10" -/// let mut fact = mapped("reason", |n: &u32| { -/// if *n > 9000 { -/// facts![ brute("divisible by 9", |n| *n % 9 == 0) ] -/// } else { -/// facts![ brute("divisible by 10", |n| *n % 10 == 0) ] -/// } -/// }); -/// -/// assert!(fact.clone().check(&50).is_ok()); -/// assert!(fact.clone().check(&99).is_err()); -/// assert!(fact.clone().check(&9009).is_ok()); -/// assert!(fact.clone().check(&9010).is_err()); -/// ``` -pub fn mapped<'a, T, F, O>(reason: impl ToString, f: F) -> Fact<'a, (), T> -where - T: Bounds<'a>, - O: Factual<'a, T>, - F: 'a + Send + Sync + Fn(&T) -> O, -{ - let reason = reason.to_string(); - stateless(move |g, obj| { - f(&obj) - .mutate(g, obj) - .map_check_err(|err| format!("mapped({}) > {}", reason, err)) - }) -} - -#[test] -fn test_mapped_fact() { - use crate::facts::*; - - type T = (u8, u8); - - let numbers = vec![(1, 11), (2, 22), (3, 33), (4, 44)]; - - // This fact says: - // if the first element of the tuple is even, - // then the second element must be divisible by 3; - // and if the first element is odd, - // then the second element must be divisible by 4. - let divisibility_fact = || { - mapped("reason", |t: &T| { - lens( - "T.1", - |(_, n)| n, - if t.0 % 2 == 0 { - brute("divisible by 3", |n: &u8| n % 3 == 0) - } else { - brute("divisible by 4", |n: &u8| n % 4 == 0) - }, - ) - }) - }; - - // assert that there was a failure - vec(divisibility_fact()) - .check(&numbers) - .result() - .unwrap() - .unwrap_err(); - - // TODO: return all errors in the seq, not just the first - // assert_eq!( - // dbg!(vec(divisibility_fact()) - // .check(&numbers) - // .result() - // .unwrap() - // .unwrap_err()), - // vec![ - // "item 0: mapped(reason) > lens(T.1) > divisible by 4".to_string(), - // "item 1: mapped(reason) > lens(T.1) > divisible by 3".to_string(), - // "item 2: mapped(reason) > lens(T.1) > divisible by 4".to_string(), - // "item 3: mapped(reason) > lens(T.1) > divisible by 3".to_string(), - // ] - // ); - - let mut g = utils::random_generator(); - - let composite_fact = || { - vec(facts![ - lens("T.0", |(i, _)| i, consecutive_int("increasing", 0)), - divisibility_fact(), - ]) - }; - - let built = composite_fact().build(&mut g); - dbg!(&built); - composite_fact().check(&built).unwrap(); -} From b48ff5c9e11f2a18a336a7aa2fa3217cbb91b587 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 12:53:45 -0700 Subject: [PATCH 12/21] Rewrite vec(); require Debug for facts --- src/fact.rs | 37 ++++++++-- src/facts.rs | 1 - src/facts/and.rs | 2 +- src/facts/brute.rs | 4 +- src/facts/consecutive_int.rs | 2 +- src/facts/constant.rs | 4 +- src/facts/eq.rs | 4 +- src/facts/in_slice.rs | 2 +- src/facts/lens.rs | 14 ++++ src/facts/not.rs | 2 +- src/facts/or.rs | 49 +++---------- src/facts/prism.rs | 14 ++++ src/facts/same.rs | 2 +- src/facts/seq.rs | 132 +++++++++-------------------------- src/factual.rs | 25 +++---- 15 files changed, 126 insertions(+), 168 deletions(-) diff --git a/src/fact.rs b/src/fact.rs index 2524625..c56a677 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -1,4 +1,4 @@ -use std::{marker::PhantomData, sync::Arc}; +use std::{fmt::Debug, marker::PhantomData, sync::Arc}; use crate::*; @@ -24,6 +24,7 @@ use crate::*; /// assert_eq!(list, vec![2, 4, 8, 16]); /// ``` pub fn stateful<'a, S, T>( + label: impl ToString, state: S, f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, &mut S, T) -> Mutation, ) -> Fact<'a, S, T> @@ -32,6 +33,7 @@ where T: Bounds<'a>, { Fact { + label: label.to_string(), state, fun: Arc::new(f), _phantom: PhantomData, @@ -40,12 +42,13 @@ where /// Create a lambda with unit state pub fn stateless<'a, T>( + label: impl ToString, f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, T) -> Mutation, ) -> Fact<'a, (), T> where T: Bounds<'a>, { - stateful((), move |g, (), obj| f(g, obj)) + stateful(label, (), move |g, (), obj| f(g, obj)) } pub type Lambda<'a, S, T> = @@ -59,12 +62,26 @@ where { state: S, fun: Lambda<'a, S, T>, + label: String, _phantom: PhantomData<&'a T>, } +impl<'a, S, T> std::fmt::Debug for Fact<'a, S, T> +where + S: Clone + Send + Sync + Debug, + T: Bounds<'a>, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Fact") + .field("label", &self.label) + .field("state", &self.state) + .finish() + } +} + impl<'a, S, T> Factual<'a, T> for Fact<'a, S, T> where - S: Clone + Send + Sync, + S: Clone + Send + Sync + Debug, T: Bounds<'a>, { fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { @@ -72,6 +89,18 @@ where } } +impl<'a, S, T> Fact<'a, S, T> +where + S: Clone + Send + Sync + Debug, + T: Bounds<'a>, +{ + /// Change the label + pub fn label(mut self, label: impl ToString) -> Self { + self.label = label.to_string(); + self + } +} + /// A Fact with unit state pub type StatelessFact<'a, T> = Fact<'a, (), T>; @@ -81,7 +110,7 @@ fn test_lambda_fact() { let mut g = utils::random_generator(); let geom = |k, s| { - stateful(s, move |g, s, mut v| { + stateful("geom", s, move |g, s, mut v| { g.set(&mut v, s, || { format!("value is not geometrically increasing by {k} starting from {s}") })?; diff --git a/src/facts.rs b/src/facts.rs index 8736604..4a719c9 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -34,6 +34,5 @@ mod optical; #[cfg(feature = "optics")] pub use optical::*; -use crate::factual::check_raw; use crate::*; use std::marker::PhantomData; diff --git a/src/facts/and.rs b/src/facts/and.rs index fc20c7f..f8a4200 100644 --- a/src/facts/and.rs +++ b/src/facts/and.rs @@ -7,7 +7,7 @@ where F1: Factual<'a, T>, F2: Factual<'a, T>, { - stateful((a, b), |g, (a, b), obj| { + stateful("and", (a, b), |g, (a, b), obj| { let obj = a.mutate(g, obj)?; let obj = b.mutate(g, obj)?; Ok(obj) diff --git a/src/facts/brute.rs b/src/facts/brute.rs index 495020b..de05269 100644 --- a/src/facts/brute.rs +++ b/src/facts/brute.rs @@ -38,7 +38,7 @@ where F: 'a + Send + Sync + Fn(&T) -> bool, { let reason = reason.to_string(); - brute_labeled(move |v| Ok(f(v).then_some(()).ok_or_else(|| reason.clone()))) + brute_labeled(move |v| Ok(f(v).then_some(()).ok_or_else(|| reason.clone()))).label("brute") } /// A version of [`brute`] which allows the closure to return the reason for failure @@ -47,7 +47,7 @@ where T: Bounds<'a>, F: 'a + Send + Sync + Fn(&T) -> ContrafactResult, { - stateless(move |g, mut obj| { + stateless("brute_labeled", move |g, mut obj| { let mut last_reason = "".to_string(); for _ in 0..=BRUTE_ITERATION_LIMIT { if let Err(reason) = f(&obj)? { diff --git a/src/facts/consecutive_int.rs b/src/facts/consecutive_int.rs index 57b78a9..50aa60b 100644 --- a/src/facts/consecutive_int.rs +++ b/src/facts/consecutive_int.rs @@ -6,7 +6,7 @@ where S: Bounds<'a> + std::fmt::Debug + PartialEq + num::PrimInt, { let context = context.to_string(); - stateful(initial, move |g, counter, mut obj| { + stateful("consecutive_int", initial, move |g, counter, mut obj| { if obj != *counter { g.fail(&context)?; obj = counter.clone(); diff --git a/src/facts/constant.rs b/src/facts/constant.rs index 1d4e6bd..f8b124c 100644 --- a/src/facts/constant.rs +++ b/src/facts/constant.rs @@ -2,13 +2,13 @@ use super::*; /// A constraint which is always met pub fn always<'a, T: Bounds<'a>>() -> StatelessFact<'a, T> { - stateless(|_, obj| Ok(obj)) + stateless("always", |_, obj| Ok(obj)) } /// A constraint which is never met pub fn never<'a, T: Bounds<'a>>(context: impl ToString) -> StatelessFact<'a, T> { let context = context.to_string(); - stateless(move |g, obj: T| { + stateless("never", move |g, obj: T| { g.fail(context.clone())?; Ok(obj) }) diff --git a/src/facts/eq.rs b/src/facts/eq.rs index a2989a4..20f8ba8 100644 --- a/src/facts/eq.rs +++ b/src/facts/eq.rs @@ -7,7 +7,7 @@ where T: Bounds<'a> + PartialEq + Clone, { let ctx = context.to_string(); - stateless(move |g, mut obj| { + stateless("eq", move |g, mut obj| { if obj != constant { g.fail(format!("{}: expected {:?} == {:?}", ctx, obj, constant))?; obj = constant.clone(); @@ -30,7 +30,7 @@ where S: ToString, T: Bounds<'a> + PartialEq, { - not(context, eq_(constant)) + not(context, eq_(constant)).label("ne") } /// Specifies an inequality constraint with no context diff --git a/src/facts/in_slice.rs b/src/facts/in_slice.rs index 55d00d6..c0eda50 100644 --- a/src/facts/in_slice.rs +++ b/src/facts/in_slice.rs @@ -6,7 +6,7 @@ where T: Bounds<'a> + PartialEq + Clone, { let context = context.to_string(); - stateless(move |g, obj| { + stateless("in_slice", move |g, obj| { Ok(if !slice.contains(&obj) { let reason = || { format!( diff --git a/src/facts/lens.rs b/src/facts/lens.rs index e7eb31f..5ce33d2 100644 --- a/src/facts/lens.rs +++ b/src/facts/lens.rs @@ -72,6 +72,20 @@ where __phantom: PhantomData<&'a F>, } +impl<'a, O, T, F> std::fmt::Debug for LensFact<'a, O, T, F> +where + T: Bounds<'a>, + O: Bounds<'a>, + F: Factual<'a, T>, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LensFact") + .field("label", &self.label) + .field("fact", &self.inner_fact) + .finish() + } +} + impl<'a, O, T, F> LensFact<'a, O, T, F> where T: Bounds<'a>, diff --git a/src/facts/not.rs b/src/facts/not.rs index fb7360e..aa16af0 100644 --- a/src/facts/not.rs +++ b/src/facts/not.rs @@ -10,7 +10,7 @@ where T: Bounds<'a>, { let context = context.to_string(); - stateless(move |g, obj| { + stateless("not", move |g, obj| { let label = format!("not({})", context.clone()); let fact = fact.clone(); brute(label, move |o| fact.clone().check(o).is_err()).mutate(g, obj) diff --git a/src/facts/or.rs b/src/facts/or.rs index dc73f2f..2f21497 100644 --- a/src/facts/or.rs +++ b/src/facts/or.rs @@ -1,49 +1,18 @@ use super::*; /// Combines two constraints so that either one may be satisfied -pub fn or<'a, A, T, S, Item>(context: S, a: A, b: T) -> OrFact<'a, A, T, Item> +pub fn or<'a, A, B, Item>(context: impl ToString, a: A, b: B) -> Fact<'a, (A, B), Item> where - S: ToString, A: Factual<'a, Item>, - T: Factual<'a, Item>, + B: Factual<'a, Item>, Item: Bounds<'a>, { - OrFact { - context: context.to_string(), - a, - b, - _phantom: PhantomData, - } -} - -/// Fact that combines two `Fact`s, returning the OR of the results. -/// -/// This is created by the `or` function. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct OrFact<'a, M1, M2, Item> -where - M1: Factual<'a, Item>, - M2: Factual<'a, Item>, - Item: ?Sized + Bounds<'a>, -{ - context: String, - pub(crate) a: M1, - pub(crate) b: M2, - _phantom: PhantomData<&'a Item>, -} - -impl<'a, P1, P2, T> Factual<'a, T> for OrFact<'a, P1, P2, T> -where - P1: Factual<'a, T> + Factual<'a, T>, - P2: Factual<'a, T> + Factual<'a, T>, - T: Bounds<'a>, -{ - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { + stateful("or", (a, b), |g, (a, b), obj| { use rand::{thread_rng, Rng}; - let a = check_raw(&mut self.a, &obj).is_ok(); - let b = check_raw(&mut self.b, &obj).is_ok(); - match (a, b) { + let a_ok = a.clone().check(&obj).is_ok(); + let b_ok = b.clone().check(&obj).is_ok(); + match (a_ok, b_ok) { (true, _) => Ok(obj), (_, true) => Ok(obj), (false, false) => { @@ -54,13 +23,13 @@ where a, b ))?; if thread_rng().gen::() { - self.a.mutate(g, obj) + a.mutate(g, obj) } else { - self.b.mutate(g, obj) + b.mutate(g, obj) } } } - } + }) } #[test] diff --git a/src/facts/prism.rs b/src/facts/prism.rs index 0a1b236..e85fcba 100644 --- a/src/facts/prism.rs +++ b/src/facts/prism.rs @@ -123,6 +123,20 @@ where } } +impl<'a, O, T, F> std::fmt::Debug for PrismFact<'a, O, T, F> +where + T: Bounds<'a>, + O: Bounds<'a>, + F: Factual<'a, T>, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PrismFact") + .field("label", &self.label) + .field("fact", &self.inner_fact) + .finish() + } +} + impl<'a, O, T, F> Factual<'a, O> for PrismFact<'a, O, T, F> where T: Bounds<'a> + Clone, diff --git a/src/facts/same.rs b/src/facts/same.rs index 0ba4807..12b1308 100644 --- a/src/facts/same.rs +++ b/src/facts/same.rs @@ -5,7 +5,7 @@ pub fn same<'a, T>() -> StatelessFact<'a, (T, T)> where T: Bounds<'a> + PartialEq, { - stateless(|g, mut obj: (T, T)| { + stateless("same", |g, mut obj: (T, T)| { let o = obj.clone(); let reason = move || format!("must be same: expected {:?} == {:?}", o.0.clone(), o.1); g.set(&mut obj.0, &obj.1, reason)?; diff --git a/src/facts/seq.rs b/src/facts/seq.rs index a6028b1..69891ee 100644 --- a/src/facts/seq.rs +++ b/src/facts/seq.rs @@ -37,124 +37,56 @@ use super::and; /// let list = fact.clone().satisfy(&mut g, vec![0; 5]).unwrap(); /// assert_eq!(list, vec![0, 1, 2, 3, 4]); /// ``` -pub fn vec<'a, T, F>(inner_fact: F) -> VecFact<'a, T, F> +pub fn vec<'a, T, F>(inner_fact: F) -> Fact<'a, F, Vec> where T: Bounds<'a> + Clone, F: Factual<'a, T>, { - VecFact::new(inner_fact) -} - -/// Checks that a Vec is of a given length -pub fn vec_len<'a, T>(len: usize) -> SeqLenFact<'a, T> -where - T: Bounds<'a> + Clone + 'a, -{ - SeqLenFact::new(len) -} - -/// Combines a LenFact with a VecFact to ensure that the vector is of a given length -pub fn vec_of_length<'a, T, F>(len: usize, inner_fact: F) -> impl Factual<'a, Vec> -where - T: Bounds<'a> + Clone + 'a, - F: Factual<'a, T> + 'a, -{ - and(vec_len(len), vec(inner_fact)) -} - -/// A fact which uses a seq to apply another fact. Use [`vec()`] to construct. -#[derive(Clone)] -pub struct VecFact<'a, T, F> -where - T: Bounds<'a>, - F: Factual<'a, T>, -{ - /// The inner_fact about the inner substructure - inner_fact: F, - - __phantom: PhantomData<&'a T>, -} - -impl<'a, T, F> VecFact<'a, T, F> -where - T: Bounds<'a>, - F: Factual<'a, T>, -{ - /// Constructor. Supply a seq and an existing Fact to create a new Fact. - pub fn new(inner_fact: F) -> Self - where - T: Bounds<'a>, - F: Factual<'a, T>, - { - Self { - inner_fact, - __phantom: PhantomData, - } - } -} - -impl<'a, T, F> Factual<'a, Vec> for VecFact<'a, T, F> -where - T: Bounds<'a>, - F: Factual<'a, T>, -{ - #[tracing::instrument(fields(fact = "seq"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: Vec) -> Mutation> { - tracing::trace!(""); + stateful("vec", inner_fact, |g, f, obj: Vec| { obj.into_iter() .enumerate() .map(|(i, o)| { - self.inner_fact - .mutate(g, o) + f.mutate(g, o) .map_check_err(|e| format!("seq[{}]: {}", i, e)) }) .collect::, _>>() - } + }) } -/// A fact which uses a seq to apply another fact. Use [`vec()`] to construct. -#[derive(Clone)] -pub struct SeqLenFact<'a, T> -where - T: Bounds<'a>, -{ - len: usize, - __phantom: PhantomData<&'a T>, -} - -impl<'a, T> SeqLenFact<'a, T> +/// Checks that a Vec is of a given length +pub fn vec_len<'a, T>(len: usize) -> StatelessFact<'a, Vec> where - T: Bounds<'a>, + T: Bounds<'a> + Clone + 'a, { - /// Constructor. Supply a seq and an existing Fact to create a new Fact. - pub fn new(len: usize) -> Self - where - T: Bounds<'a>, - { - Self { - len, - __phantom: PhantomData, + stateless("vec_len", move |g, mut obj: Vec| { + if obj.len() > len { + g.fail(format!( + "vec should be of length {} but is actually of length {}", + len, + obj.len() + ))?; + obj = obj[0..len].to_vec(); } - } + while obj.len() < len { + obj.push(g.arbitrary(|| { + format!( + "vec should be of length {} but is actually of length {}", + len, + obj.len() + ) + })?) + } + Ok(obj) + }) } -impl<'a, T> Factual<'a, Vec> for SeqLenFact<'a, T> +/// Combines a LenFact with a VecFact to ensure that the vector is of a given length +pub fn vec_of_length<'a, T, F>(len: usize, inner_fact: F) -> impl Factual<'a, Vec> where - T: Bounds<'a>, + T: Bounds<'a> + Clone + 'a, + F: Factual<'a, T> + 'a, { - #[tracing::instrument(fields(fact = "len"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: Vec) -> Mutation> { - tracing::trace!(""); - - if obj.len() > self.len { - g.fail("LenFact: vec was too long")?; - obj = obj[0..self.len].to_vec(); - } - while obj.len() < self.len { - obj.push(g.arbitrary(|| "LenFact: vec was too short")?) - } - Ok(obj) - } + and(vec_len(len), vec(inner_fact)) } #[cfg(test)] @@ -221,7 +153,7 @@ mod tests { let piecewise = move || { let count = Arc::new(AtomicU8::new(0)); - stateful((), move |g, (), mut obj| { + stateful("piecewise", (), move |g, (), mut obj| { let c = count.fetch_add(1, Ordering::SeqCst); if c < 3 { g.set(&mut obj, &999, || "i'm being difficult, haha")?; diff --git a/src/factual.rs b/src/factual.rs index 2c11ada..cbec94f 100644 --- a/src/factual.rs +++ b/src/factual.rs @@ -25,7 +25,7 @@ impl<'a, T> Bounds<'a> for T where /// A declarative representation of a constraint on some data, which can be /// used to both make an assertion (check) or to mold some arbitrary existing /// data into a shape which passes that same assertion (mutate) -pub trait Factual<'a, T>: Send + Sync + Clone +pub trait Factual<'a, T>: Send + Sync + Clone + std::fmt::Debug where T: Bounds<'a>, { @@ -37,7 +37,8 @@ where /// care must be taken to make sure it perfectly lines up with the mutation function. #[tracing::instrument(fields(fact_impl = "Fact"), skip(self))] fn check(mut self, obj: &T) -> Check { - check_raw(&mut self, obj) + let mut g = Generator::checker(); + Check::from_mutation(self.mutate(&mut g, obj.clone())) } /// Apply a mutation which moves the obj closer to satisfying the overall @@ -67,7 +68,7 @@ where let mut m = self.clone(); let mut c = self.clone(); next = m.mutate(g, next).unwrap(); - if let Err(errs) = check_raw(&mut c, &next).result()? { + if let Err(errs) = self.clone().check(&next).result()? { last_failure = errs; } else { *self = m; @@ -109,15 +110,15 @@ where } } -#[tracing::instrument(skip(fact))] -pub(crate) fn check_raw<'a, T, F: Factual<'a, T>>(fact: &mut F, obj: &T) -> Check -where - T: Bounds<'a> + ?Sized, - F: Factual<'a, T> + ?Sized, -{ - let mut g = Generator::checker(); - Check::from_mutation(fact.mutate(&mut g, obj.clone())) -} +// #[tracing::instrument(skip(fact))] +// pub(crate) fn check_raw<'a, T, F: Factual<'a, T>>(fact: &mut F, obj: &T) -> Check +// where +// T: Bounds<'a> + ?Sized, +// F: Factual<'a, T> + ?Sized, +// { +// let mut g = Generator::checker(); +// Check::from_mutation(fact.mutate(&mut g, obj.clone())) +// } #[tracing::instrument(skip(facts))] fn collect_checks<'a, T, F>(facts: Vec, obj: &T) -> Check From 025a71620a8eb8e34dfb9800840ff5bd0657d9b7 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 13:05:51 -0700 Subject: [PATCH 13/21] Rework labels --- src/fact.rs | 2 +- src/facts.rs | 4 ++-- src/facts/brute.rs | 8 +++++--- src/facts/eq.rs | 37 +++++++++++-------------------------- src/facts/in_range.rs | 4 ++-- src/facts/lens.rs | 4 ++-- src/facts/not.rs | 25 ++++++++++++------------- src/facts/optical.rs | 4 ++-- src/facts/or.rs | 17 +++++++++-------- src/facts/prism.rs | 6 +++--- src/facts/seq.rs | 6 +++--- src/lib.rs | 6 +++--- tests/chainlink.rs | 6 +----- tests/complex.rs | 4 ++-- 14 files changed, 58 insertions(+), 75 deletions(-) diff --git a/src/fact.rs b/src/fact.rs index c56a677..eb7242a 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -12,7 +12,7 @@ use crate::*; /// /// let mut fact = vec_of_length( /// 4, -/// stateful(2, move |g, s, mut v| { +/// stateful("geometric series", 2, move |g, s, mut v| { /// g.set(&mut v, s, || "value is not geometrically increasing by 2")?; /// *s *= 2; /// Ok(v) diff --git a/src/facts.rs b/src/facts.rs index 4a719c9..6433964 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -14,10 +14,10 @@ mod seq; pub use consecutive_int::{consecutive_int, consecutive_int_}; pub use constant::{always, never}; -pub use eq::{eq, eq_, ne, ne_}; +pub use eq::{eq, ne}; pub use in_range::{in_range, in_range_}; pub use in_slice::{in_slice, in_slice_}; -pub use not::{not, not_}; +pub use not::not; pub use or::or; pub use same::{different, same}; diff --git a/src/facts/brute.rs b/src/facts/brute.rs index de05269..813c34b 100644 --- a/src/facts/brute.rs +++ b/src/facts/brute.rs @@ -32,13 +32,15 @@ use crate::*; /// let mut g = utils::random_generator(); /// assert!(div_by(3).build(&mut g) % 3 == 0); /// ``` -pub fn brute<'a, T, F>(reason: impl ToString, f: F) -> Fact<'a, (), T> +pub fn brute<'a, T, F>(label: impl ToString, f: F) -> Fact<'a, (), T> where T: Bounds<'a>, F: 'a + Send + Sync + Fn(&T) -> bool, { - let reason = reason.to_string(); - brute_labeled(move |v| Ok(f(v).then_some(()).ok_or_else(|| reason.clone()))).label("brute") + let label = label.to_string(); + // TODO figure out this label stuff + let label2 = label.clone(); + brute_labeled(move |v| Ok(f(v).then_some(()).ok_or_else(|| label.clone()))).label(label2) } /// A version of [`brute`] which allows the closure to return the reason for failure diff --git a/src/facts/eq.rs b/src/facts/eq.rs index 20f8ba8..c927d98 100644 --- a/src/facts/eq.rs +++ b/src/facts/eq.rs @@ -1,44 +1,29 @@ +use std::fmt::Display; + use super::*; /// Specifies an equality constraint -pub fn eq<'a, S, T>(context: S, constant: T) -> Fact<'a, (), T> +pub fn eq<'a, T>(constant: T) -> Fact<'a, (), T> where - S: ToString, - T: Bounds<'a> + PartialEq + Clone, + T: Bounds<'a> + PartialEq + Clone + Display, { - let ctx = context.to_string(); - stateless("eq", move |g, mut obj| { + let label = format!("eq({})", constant); + stateless(label, move |g, mut obj| { if obj != constant { - g.fail(format!("{}: expected {:?} == {:?}", ctx, obj, constant))?; + g.fail(format!("expected {:?} == {:?}", obj, constant))?; obj = constant.clone(); } Ok(obj) }) } -/// Specifies an equality constraint with no context -pub fn eq_<'a, T>(constant: T) -> Fact<'a, (), T> -where - T: Bounds<'a> + PartialEq + Clone, -{ - eq("eq", constant) -} - /// Specifies an inequality constraint -pub fn ne<'a, S, T>(context: S, constant: T) -> Fact<'a, (), T> +pub fn ne<'a, S, T>(constant: T) -> Fact<'a, (), T> where S: ToString, - T: Bounds<'a> + PartialEq, -{ - not(context, eq_(constant)).label("ne") -} - -/// Specifies an inequality constraint with no context -pub fn ne_<'a, T>(constant: T) -> Fact<'a, (), T> -where - T: Bounds<'a> + PartialEq, + T: Bounds<'a> + PartialEq + Display, { - ne("ne", constant) + not(eq(constant)).label("ne") } #[test] @@ -46,7 +31,7 @@ fn test_eq() { observability::test_run().ok(); let mut g = utils::random_generator(); - let eq1 = vec(eq("must be 1", 1)); + let eq1 = vec(eq(1)); let ones = eq1.clone().build(&mut g); eq1.clone().check(&ones).unwrap(); diff --git a/src/facts/in_range.rs b/src/facts/in_range.rs index e93ad42..2b32864 100644 --- a/src/facts/in_range.rs +++ b/src/facts/in_range.rs @@ -121,8 +121,8 @@ fn test_in_range() { let over9000 = in_range("must be over 9000", 9001..); let under9000 = in_range("must be under 9000 (and no less than zero)", ..9000u32); - let nonpositive1 = vec(not_(positive1)); - let nonpositive2 = vec(not_(positive2)); + let nonpositive1 = vec(not(positive1)); + let nonpositive2 = vec(not(positive2)); let smallish_nums = smallish.clone().build(&mut g); let over9000_nums = over9000.clone().build(&mut g); diff --git a/src/facts/lens.rs b/src/facts/lens.rs index 5ce33d2..dd73110 100644 --- a/src/facts/lens.rs +++ b/src/facts/lens.rs @@ -24,7 +24,7 @@ use crate::*; /// y: u32, /// } /// -/// let mut fact = lens("S::x", |s: &mut S| &mut s.x, eq("must be 1", 1)); +/// let mut fact = lens("S::x", |s: &mut S| &mut s.x, eq(1)); /// /// assert!(fact.clone().check(&S {x: 1, y: 333}).is_ok()); /// assert!(fact.clone().check(&S {x: 2, y: 333}).is_err()); @@ -146,7 +146,7 @@ mod tests { observability::test_run().ok(); let mut g = utils::random_generator(); - let f = || vec(lens("S::x", |s: &mut S| &mut s.x, eq("must be 1", 1))); + let f = || vec(lens("S::x", |s: &mut S| &mut s.x, eq(1))); let ones = f().build(&mut g); f().check(&ones).unwrap(); diff --git a/src/facts/not.rs b/src/facts/not.rs index aa16af0..99ccb1e 100644 --- a/src/facts/not.rs +++ b/src/facts/not.rs @@ -4,35 +4,34 @@ use super::*; // TODO: `not` in particular would really benefit from Facts having accessible // labels, since currently you can only get context about why a `not` fact passed, // not why it fails. -pub fn not<'a, F, T>(context: impl ToString, fact: F) -> Fact<'a, (), T> +pub fn not<'a, F, T>(fact: F) -> Fact<'a, (), T> where F: 'a + Factual<'a, T>, T: Bounds<'a>, { - let context = context.to_string(); stateless("not", move |g, obj| { - let label = format!("not({})", context.clone()); + let label = format!("not({:?})", fact); let fact = fact.clone(); brute(label, move |o| fact.clone().check(o).is_err()).mutate(g, obj) }) } -/// Negates a fact, with no context given -pub fn not_<'a, F, T>(fact: F) -> Fact<'a, (), T> -where - F: 'a + Factual<'a, T>, - T: Bounds<'a>, -{ - not("not", fact) -} +// /// Negates a fact, with no context given +// pub fn not<'a, F, T>(fact: F) -> Fact<'a, (), T> +// where +// F: 'a + Factual<'a, T>, +// T: Bounds<'a>, +// { +// not("not", fact) +// } #[test] fn test_not() { observability::test_run().ok(); let mut g = utils::random_generator(); - let eq1 = eq("must be 1", 1); - let not1 = vec(not_(eq1)); + let eq1 = eq(1); + let not1 = vec(not(eq1)); let nums = not1.clone().build(&mut g); not1.clone().check(&nums).unwrap(); diff --git a/src/facts/optical.rs b/src/facts/optical.rs index 18cd12b..e46a50e 100644 --- a/src/facts/optical.rs +++ b/src/facts/optical.rs @@ -106,12 +106,12 @@ fn test_lens() { let mut fact = OpticalFact { label: "".into(), optics: optics!(_1._1._1), - inner_fact: eq_(3), + inner_fact: eq(3), __phantom: PhantomData::<&((u8, (u8, (u8, u8))), u8)>, }; assert_eq!(fact.check(&x).errors().len(), 1); - fact.inner_fact = eq_(4); + fact.inner_fact = eq(4); assert!(fact.check(&x).is_ok()); } diff --git a/src/facts/or.rs b/src/facts/or.rs index 2f21497..686fd0e 100644 --- a/src/facts/or.rs +++ b/src/facts/or.rs @@ -1,7 +1,7 @@ use super::*; /// Combines two constraints so that either one may be satisfied -pub fn or<'a, A, B, Item>(context: impl ToString, a: A, b: B) -> Fact<'a, (A, B), Item> +pub fn or<'a, A, B, Item>(a: A, b: B) -> Fact<'a, (A, B), Item> where A: Factual<'a, Item>, B: Factual<'a, Item>, @@ -17,9 +17,7 @@ where (_, true) => Ok(obj), (false, false) => { g.fail(format!( - "expected either one of the following conditions to be met: - condition 1: {:#?} - condition 2: {:#?}", + "expected either one of the following conditions to be met: {:?} OR {:?}", a, b ))?; if thread_rng().gen::() { @@ -37,13 +35,16 @@ fn test_or() { observability::test_run().ok(); let mut g = utils::random_generator(); - let eq1 = eq("must be 1", 1); - let eq2 = eq("must be 2", 2); - let either = or("can be 1 or 2", eq1, eq2); + let eq1 = eq(1); + let eq2 = eq(2); + let either = or(eq1, eq2); let ones = vec(either.clone()).build(&mut g); vec(either.clone()).check(&ones).unwrap(); assert!(ones.iter().all(|x| *x == 1 || *x == 2)); - assert_eq!(either.check(&3).result().unwrap().unwrap_err().len(), 1); + assert_eq!( + dbg!(either.check(&3)).result().unwrap().unwrap_err().len(), + 1 + ); } diff --git a/src/facts/prism.rs b/src/facts/prism.rs index e85fcba..e49b652 100644 --- a/src/facts/prism.rs +++ b/src/facts/prism.rs @@ -42,7 +42,7 @@ use crate::*; /// } /// } /// -/// let mut fact = prism("E::x", E::x, eq("must be 1", 1)); +/// let mut fact = prism("E::x", E::x, eq(1)); /// /// assert!(fact.clone().check(&E::X(1)).is_ok()); /// assert!(fact.clone().check(&E::X(2)).is_err()); @@ -188,8 +188,8 @@ mod tests { let f = || { facts::vec(facts![ - prism("E::x", E::x, facts::eq("must be 1", 1)), - prism("E::y", E::y, facts::eq("must be 2", 2)), + prism("E::x", E::x, facts::eq(1)), + prism("E::y", E::y, facts::eq(2)), ]) }; diff --git a/src/facts/seq.rs b/src/facts/seq.rs index 69891ee..39af294 100644 --- a/src/facts/seq.rs +++ b/src/facts/seq.rs @@ -20,7 +20,7 @@ use super::and; /// let mut g = utils::random_generator(); /// /// // `consecutive_int` -/// let fact = facts::vec(facts::eq_(1)); +/// let fact = facts::vec(facts::eq(1)); /// let list = fact.clone().satisfy(&mut g, vec![0; 5]).unwrap(); /// assert_eq!(list, vec![1, 1, 1, 1, 1]); /// ``` @@ -112,7 +112,7 @@ mod tests { let f = facts![ brute("len must be >= 3", |v: &Vec<_>| v.len() >= 3), - vec(eq("must be 1", 1)), + vec(eq(1)), ]; let ones = f.clone().build(&mut g); f.check(&ones).unwrap(); @@ -173,7 +173,7 @@ mod tests { // Assert that piecewise() messes everything up during the // first 3 mutations, and cooperates afterwards { - let mut f = facts!(eq_(0), piecewise()); + let mut f = facts!(eq(0), piecewise()); for _ in 0..3 { let val = f.mutate(&mut g, 0).unwrap(); assert!(f.clone().check(&val).is_err()); diff --git a/src/lib.rs b/src/lib.rs index ccbb9bc..2a0b9af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ //! y: u32, //! } //! -//! let mut fact = lens("S::x", |s: &mut S| &mut s.x, eq("must be 1", 1)); +//! let mut fact = lens("S::x", |s: &mut S| &mut s.x, eq(1)); //! //! assert!(fact.clone().check(&S {x: 1, y: 333}).is_ok()); //! assert!(fact.clone().check(&S {x: 2, y: 333}).is_err()); @@ -95,8 +95,8 @@ pub(crate) const SATISFY_ATTEMPTS: usize = 100; /// ``` /// use contrafact::*; /// -/// let eq1 = eq_(1); -/// let not2 = not_(eq_(2)); +/// let eq1 = eq(1); +/// let not2 = not(eq(2)); /// let mut fact = facts![eq1, not2]; /// assert!(fact.check(&1).is_ok()); /// ``` diff --git a/tests/chainlink.rs b/tests/chainlink.rs index 06ef61a..7edbab4 100644 --- a/tests/chainlink.rs +++ b/tests/chainlink.rs @@ -25,11 +25,7 @@ struct Wrapper { /// consecutive `prev` values starting with 0. fn chain_fact<'a>(author: String) -> impl Factual<'a, Link> { facts![ - lens( - "Link::author", - |o: &mut Link| &mut o.author, - eq("same author", author), - ), + lens("Link::author", |o: &mut Link| &mut o.author, eq(author),), lens( "Link::prev", |o: &mut Link| &mut o.prev, diff --git a/tests/complex.rs b/tests/complex.rs index eac66bc..475fbc7 100644 --- a/tests/complex.rs +++ b/tests/complex.rs @@ -157,7 +157,7 @@ fn id_fact(id: Option) -> impl Factual<'static, Id> { let le = brute("< u32::MAX", |id: &Id| *id < Id::MAX / 2); if let Some(id) = id { - Either::Left(facts![le, eq("id", id)]) + Either::Left(facts![le, eq(id)]) } else { Either::Right(facts![le]) } @@ -168,7 +168,7 @@ fn id_fact(id: Option) -> impl Factual<'static, Id> { fn pi_fact(id: Id) -> impl Factual<'static, Pi> { let alpha_fact = facts![ lens("Alpha::id", |a: &mut Alpha| a.id(), id_fact(Some(id))), - // lens("Alpha::data", |a: &mut Alpha| a.data(), eq("data", data)), + // lens("Alpha::data", |a: &mut Alpha| a.data(), eq(data)), ]; let beta_fact = lens("Beta::id", |b: &mut Beta| &mut b.id, id_fact(Some(id))); facts![ From 71f91a05e1d6076621670db45bb06600aeafbcda Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 13:48:02 -0700 Subject: [PATCH 14/21] Remove Factual completely --- src/fact.rs | 102 +++++++++++++++++++++---- src/facts.rs | 7 +- src/facts/and.rs | 11 ++- src/facts/brute.rs | 4 +- src/facts/consecutive_int.rs | 4 +- src/facts/constant.rs | 4 +- src/facts/eq.rs | 4 +- src/facts/in_range.rs | 82 +++----------------- src/facts/in_slice.rs | 4 +- src/facts/lens.rs | 129 ++++++++++++------------------- src/facts/not.rs | 5 +- src/facts/optical.rs | 6 +- src/facts/or.rs | 8 +- src/facts/prism.rs | 101 ++++--------------------- src/facts/same.rs | 4 +- src/facts/seq.rs | 17 +++-- src/factual.rs | 142 ----------------------------------- src/lib.rs | 6 +- tests/chainlink.rs | 8 +- tests/complex.rs | 18 ++--- 20 files changed, 214 insertions(+), 452 deletions(-) delete mode 100644 src/factual.rs diff --git a/src/fact.rs b/src/fact.rs index eb7242a..6861375 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -1,7 +1,20 @@ use std::{fmt::Debug, marker::PhantomData, sync::Arc}; +use arbitrary::Arbitrary; + use crate::*; +/// The trait bounds for the target of a Fact +pub trait Target<'a>: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> {} +impl<'a, T> Target<'a> for T where + T: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> +{ +} + +/// The trait bounds for the State of a Fact +pub trait State: std::fmt::Debug + Clone + Send + Sync {} +impl State for T where T: std::fmt::Debug + Clone + Send + Sync {} + /// Create a fact from a bare function which specifies the mutation. /// Can be quicker to experiment with ideas this way than to have to directly implement /// the [`Fact`] trait @@ -30,7 +43,7 @@ pub fn stateful<'a, S, T>( ) -> Fact<'a, S, T> where S: Clone + Send + Sync, - T: Bounds<'a>, + T: Target<'a>, { Fact { label: label.to_string(), @@ -46,7 +59,7 @@ pub fn stateless<'a, T>( f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, T) -> Mutation, ) -> Fact<'a, (), T> where - T: Bounds<'a>, + T: Target<'a>, { stateful(label, (), move |g, (), obj| f(g, obj)) } @@ -58,7 +71,7 @@ pub type Lambda<'a, S, T> = pub struct Fact<'a, S, T> where S: Clone + Send + Sync, - T: Bounds<'a>, + T: Target<'a>, { state: S, fun: Lambda<'a, S, T>, @@ -66,10 +79,13 @@ where _phantom: PhantomData<&'a T>, } +/// Two facts about the same target with different states +pub type Fact2<'a, A, B, T> = (Fact<'a, A, T>, Fact<'a, B, T>); + impl<'a, S, T> std::fmt::Debug for Fact<'a, S, T> where S: Clone + Send + Sync + Debug, - T: Bounds<'a>, + T: Target<'a>, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Fact") @@ -79,26 +95,82 @@ where } } -impl<'a, S, T> Factual<'a, T> for Fact<'a, S, T> -where - S: Clone + Send + Sync + Debug, - T: Bounds<'a>, -{ - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - (self.fun)(g, &mut self.state, obj) - } -} - impl<'a, S, T> Fact<'a, S, T> where S: Clone + Send + Sync + Debug, - T: Bounds<'a>, + T: Target<'a>, { /// Change the label pub fn label(mut self, label: impl ToString) -> Self { self.label = label.to_string(); self } + + /// Assert that the constraint is satisfied for given data. + /// + /// If the mutation function is written properly, we get a check for free + /// by using a special Generator which fails upon mutation. If this is for + /// some reason unreasonable, a check function can be written by hand, but + /// care must be taken to make sure it perfectly lines up with the mutation function. + #[tracing::instrument(fields(fact_impl = "Fact"), skip(self))] + pub fn check(mut self, obj: &T) -> Check { + let mut g = Generator::checker(); + Check::from_mutation(self.mutate(&mut g, obj.clone())) + } + + /// Apply a mutation which moves the obj closer to satisfying the overall + /// constraint. + // #[tracing::instrument(skip(self, g))] + pub fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { + (self.fun)(g, &mut self.state, obj) + } + + /// Make this many attempts to satisfy a constraint before giving up and panicking. + /// + /// If you are combining highly contentious facts together and relying on randomness + /// to find a solution, this limit may need to be higher. In general, you should try + /// to write facts that don't interfere with each other so that the constraint can be + /// met on the first attempt, or perhaps the second or third. If necessary, this can + /// be raised to lean more on random search. + pub fn satisfy_attempts(&self) -> usize { + SATISFY_ATTEMPTS + } + + /// Mutate a value such that it satisfies the constraint. + /// If the constraint cannot be satisfied, panic. + pub fn satisfy(&mut self, g: &mut Generator<'a>, obj: T) -> ContrafactResult { + tracing::trace!("satisfy"); + let mut last_failure: Vec = vec![]; + let mut next = obj.clone(); + for _i in 0..self.satisfy_attempts() { + let mut m = self.clone(); + let mut c = self.clone(); + next = m.mutate(g, next).unwrap(); + if let Err(errs) = self.clone().check(&next).result()? { + last_failure = errs; + } else { + *self = m; + return Ok(next); + } + } + panic!( + "Could not satisfy a constraint even after {} attempts. Last check failure: {:?}", + SATISFY_ATTEMPTS, last_failure + ); + } + + #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] + /// Build a new value such that it satisfies the constraint + pub fn build_fallible(mut self, g: &mut Generator<'a>) -> ContrafactResult { + let obj = T::arbitrary(g).unwrap(); + self.satisfy(g, obj) + } + + /// Build a new value such that it satisfies the constraint, panicking on error + #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] + pub fn build(self, g: &mut Generator<'a>) -> T { + self.build_fallible(g).unwrap() + } } /// A Fact with unit state diff --git a/src/facts.rs b/src/facts.rs index 6433964..343cc00 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -15,7 +15,7 @@ mod seq; pub use consecutive_int::{consecutive_int, consecutive_int_}; pub use constant::{always, never}; pub use eq::{eq, ne}; -pub use in_range::{in_range, in_range_}; +pub use in_range::in_range; pub use in_slice::{in_slice, in_slice_}; pub use not::not; pub use or::or; @@ -23,8 +23,8 @@ pub use same::{different, same}; pub use and::and; pub use brute::brute; -pub use lens::{lens, LensFact}; -pub use prism::{prism, PrismFact}; +pub use lens::{lens1, lens2}; +pub use prism::prism; pub use seq::{vec, vec_len, vec_of_length}; // pub(crate) use lambda::LambdaFact; @@ -35,4 +35,3 @@ mod optical; pub use optical::*; use crate::*; -use std::marker::PhantomData; diff --git a/src/facts/and.rs b/src/facts/and.rs index f8a4200..e9ff331 100644 --- a/src/facts/and.rs +++ b/src/facts/and.rs @@ -1,11 +1,14 @@ use crate::*; /// A Fact which applies two other facts. -pub fn and<'a, F1, F2, T>(a: F1, b: F2) -> Fact<'a, (F1, F2), T> +pub fn and<'a, S1, S2, T>( + a: Fact<'a, S1, T>, + b: Fact<'a, S2, T>, +) -> Fact<'a, (Fact<'a, S1, T>, Fact<'a, S2, T>), T> where - T: Bounds<'a>, - F1: Factual<'a, T>, - F2: Factual<'a, T>, + T: Target<'a>, + S1: State, + S2: State, { stateful("and", (a, b), |g, (a, b), obj| { let obj = a.mutate(g, obj)?; diff --git a/src/facts/brute.rs b/src/facts/brute.rs index 813c34b..568d539 100644 --- a/src/facts/brute.rs +++ b/src/facts/brute.rs @@ -34,7 +34,7 @@ use crate::*; /// ``` pub fn brute<'a, T, F>(label: impl ToString, f: F) -> Fact<'a, (), T> where - T: Bounds<'a>, + T: Target<'a>, F: 'a + Send + Sync + Fn(&T) -> bool, { let label = label.to_string(); @@ -46,7 +46,7 @@ where /// A version of [`brute`] which allows the closure to return the reason for failure pub fn brute_labeled<'a, T, F>(f: F) -> Fact<'a, (), T> where - T: Bounds<'a>, + T: Target<'a>, F: 'a + Send + Sync + Fn(&T) -> ContrafactResult, { stateless("brute_labeled", move |g, mut obj| { diff --git a/src/facts/consecutive_int.rs b/src/facts/consecutive_int.rs index 50aa60b..5e5af6c 100644 --- a/src/facts/consecutive_int.rs +++ b/src/facts/consecutive_int.rs @@ -3,7 +3,7 @@ use super::*; /// Specifies that a value should be increasing by 1 at every check/mutation pub fn consecutive_int<'a, S>(context: impl ToString, initial: S) -> Fact<'a, S, S> where - S: Bounds<'a> + std::fmt::Debug + PartialEq + num::PrimInt, + S: Target<'a> + std::fmt::Debug + PartialEq + num::PrimInt, { let context = context.to_string(); stateful("consecutive_int", initial, move |g, counter, mut obj| { @@ -20,7 +20,7 @@ where /// with no context given pub fn consecutive_int_<'a, T>(initial: T) -> Fact<'a, T, T> where - T: Bounds<'a> + PartialEq + num::PrimInt, + T: Target<'a> + PartialEq + num::PrimInt, { consecutive_int("consecutive_int", initial) } diff --git a/src/facts/constant.rs b/src/facts/constant.rs index f8b124c..6cc7767 100644 --- a/src/facts/constant.rs +++ b/src/facts/constant.rs @@ -1,12 +1,12 @@ use super::*; /// A constraint which is always met -pub fn always<'a, T: Bounds<'a>>() -> StatelessFact<'a, T> { +pub fn always<'a, T: Target<'a>>() -> StatelessFact<'a, T> { stateless("always", |_, obj| Ok(obj)) } /// A constraint which is never met -pub fn never<'a, T: Bounds<'a>>(context: impl ToString) -> StatelessFact<'a, T> { +pub fn never<'a, T: Target<'a>>(context: impl ToString) -> StatelessFact<'a, T> { let context = context.to_string(); stateless("never", move |g, obj: T| { g.fail(context.clone())?; diff --git a/src/facts/eq.rs b/src/facts/eq.rs index c927d98..8bc2a39 100644 --- a/src/facts/eq.rs +++ b/src/facts/eq.rs @@ -5,7 +5,7 @@ use super::*; /// Specifies an equality constraint pub fn eq<'a, T>(constant: T) -> Fact<'a, (), T> where - T: Bounds<'a> + PartialEq + Clone + Display, + T: Target<'a> + PartialEq + Clone + Display, { let label = format!("eq({})", constant); stateless(label, move |g, mut obj| { @@ -21,7 +21,7 @@ where pub fn ne<'a, S, T>(constant: T) -> Fact<'a, (), T> where S: ToString, - T: Bounds<'a> + PartialEq + Display, + T: Target<'a> + PartialEq + Display, { not(eq(constant)).label("ne") } diff --git a/src/facts/in_range.rs b/src/facts/in_range.rs index 2b32864..0abd7da 100644 --- a/src/facts/in_range.rs +++ b/src/facts/in_range.rs @@ -3,90 +3,28 @@ use std::ops::{Bound, RangeBounds}; use super::*; /// Specifies a range constraint -pub fn in_range(context: S, range: R) -> InRangeFact +pub fn in_range<'a, R, T>(context: impl ToString, range: R) -> StatelessFact<'a, T> where - S: ToString, - R: RangeBounds + std::fmt::Debug, - T: PartialEq + R: 'a + Send + Sync + RangeBounds + std::fmt::Debug, + T: Target<'a> + PartialOrd + Ord - + Clone - + std::fmt::Debug + num::traits::Euclid + std::ops::Add + std::ops::Sub + num::Bounded + num::One, { - InRangeFact { - context: context.to_string(), - range, - phantom: PhantomData, - } -} - -/// Specifies a range constraint -pub fn in_range_(range: R) -> InRangeFact -where - R: RangeBounds + std::fmt::Debug, - T: PartialEq - + PartialOrd - + Ord - + Clone - + std::fmt::Debug - + num::traits::Euclid - + std::ops::Add - + std::ops::Sub - + num::Bounded - + num::One, -{ - in_range("in_range", range) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct InRangeFact -where - R: RangeBounds + std::fmt::Debug, - T: PartialEq - + PartialOrd - + Ord - + Clone - + std::fmt::Debug - + num::traits::Euclid - + std::ops::Add - + std::ops::Sub - + num::Bounded - + num::One, -{ - context: String, - range: R, - phantom: PhantomData, -} - -impl<'a, R, T> Factual<'a, T> for InRangeFact -where - R: Send + Sync + RangeBounds + std::fmt::Debug + Clone, - T: Bounds<'a> - + PartialEq - + PartialOrd - + Ord - + Clone - + std::fmt::Debug - + num::traits::Euclid - + std::ops::Add - + std::ops::Sub - + num::Bounded - + num::One, -{ - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: T) -> Mutation { - if !self.range.contains(&obj) { + let context = context.to_string(); + stateless("in_range", move |g, mut obj| { + if !range.contains(&obj) { let rand = g.arbitrary(|| { format!( "{}: expected {:?} to be contained in {:?}", - self.context, obj, self.range + context, obj, range ) })?; - obj = match (self.range.start_bound(), self.range.end_bound()) { + obj = match (range.start_bound(), range.end_bound()) { (Bound::Unbounded, Bound::Unbounded) => rand, (Bound::Included(a), Bound::Included(b)) if b.clone() - a.clone() >= T::one() => { a.clone() + rand.rem_euclid(&(b.clone() - a.clone())) @@ -103,11 +41,11 @@ where (Bound::Included(a), Bound::Unbounded) => { a.clone() + rand.rem_euclid(&(T::max_value() - a.clone())) } - _ => panic!("Range not yet supported, sorry! {:?}", self.range), + _ => panic!("Range not yet supported, sorry! {:?}", range), }; } Ok(obj) - } + }) } #[test] diff --git a/src/facts/in_slice.rs b/src/facts/in_slice.rs index c0eda50..d65d1b7 100644 --- a/src/facts/in_slice.rs +++ b/src/facts/in_slice.rs @@ -3,7 +3,7 @@ use super::*; /// Specifies a membership constraint pub fn in_slice<'a, T>(context: impl ToString, slice: &'a [T]) -> StatelessFact<'a, T> where - T: Bounds<'a> + PartialEq + Clone, + T: Target<'a> + PartialEq + Clone, { let context = context.to_string(); stateless("in_slice", move |g, obj| { @@ -24,7 +24,7 @@ where /// Specifies a membership constraint pub fn in_slice_<'a, T>(slice: &'a [T]) -> StatelessFact<'a, T> where - T: Bounds<'a> + PartialEq + Clone, + T: Target<'a> + PartialEq + Clone, { in_slice("in_slice", slice) } diff --git a/src/facts/lens.rs b/src/facts/lens.rs index dd73110..2354d06 100644 --- a/src/facts/lens.rs +++ b/src/facts/lens.rs @@ -24,7 +24,7 @@ use crate::*; /// y: u32, /// } /// -/// let mut fact = lens("S::x", |s: &mut S| &mut s.x, eq(1)); +/// let mut fact = lens1("S::x", |s: &mut S| &mut s.x, eq(1)); /// /// assert!(fact.clone().check(&S {x: 1, y: 333}).is_ok()); /// assert!(fact.clone().check(&S {x: 2, y: 333}).is_err()); @@ -35,100 +35,65 @@ use crate::*; /// ``` // // TODO: can rewrite this in terms of PrismFact for DRYness -pub fn lens<'a, O, T, F, L, S>(label: S, lens: L, inner_fact: F) -> LensFact<'a, O, T, F> +pub fn lens1<'a, O, T, L, S>( + label: impl ToString, + accessor: L, + inner_fact: Fact<'a, S, T>, +) -> Fact<'a, Fact<'a, S, T>, O> where - O: Bounds<'a>, - T: Bounds<'a> + Clone, - S: ToString, - F: Factual<'a, T>, + O: Target<'a>, + T: Target<'a>, + S: State, L: 'a + Clone + Send + Sync + Fn(&mut O) -> &mut T, { - let lens2 = lens.clone(); - let getter = move |mut o| lens(&mut o).clone(); + let accessor2 = accessor.clone(); + let getter = move |mut o| accessor(&mut o).clone(); let setter = move |mut o, t: T| { - let r = lens2(&mut o); + let r = accessor2(&mut o); *r = t; o }; - LensFact::new(label.to_string(), getter, setter, inner_fact) + lens2(label, getter, setter, inner_fact).label("lens1") } -/// A fact which uses a lens to apply another fact. Use [`lens()`] to construct. -#[derive(Clone)] -pub struct LensFact<'a, O, T, F> +pub fn lens2<'a, O, T, S>( + label: impl ToString, + getter: impl 'a + Clone + Send + Sync + Fn(O) -> T, + setter: impl 'a + Clone + Send + Sync + Fn(O, T) -> O, + inner_fact: Fact<'a, S, T>, +) -> Fact<'a, Fact<'a, S, T>, O> where - T: Bounds<'a>, - O: Bounds<'a>, - F: Factual<'a, T>, + O: Target<'a>, + T: Target<'a>, + S: State, { - label: String, - - getter: Arc T>, - setter: Arc O>, - - /// The inner_fact about the inner substructure - inner_fact: F, - - __phantom: PhantomData<&'a F>, -} - -impl<'a, O, T, F> std::fmt::Debug for LensFact<'a, O, T, F> -where - T: Bounds<'a>, - O: Bounds<'a>, - F: Factual<'a, T>, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("LensFact") - .field("label", &self.label) - .field("fact", &self.inner_fact) - .finish() - } -} - -impl<'a, O, T, F> LensFact<'a, O, T, F> -where - T: Bounds<'a>, - O: Bounds<'a>, - F: Factual<'a, T>, -{ - /// Constructor. Supply a lens and an existing Fact to create a new Fact. - pub fn new(label: L, getter: G, setter: S, inner_fact: F) -> Self - where - T: Bounds<'a>, - O: Bounds<'a>, - F: Factual<'a, T>, - L: ToString, - G: 'a + Send + Sync + Fn(O) -> T, - S: 'a + Send + Sync + Fn(O, T) -> O, - { - Self { - label: label.to_string(), - getter: Arc::new(getter), - setter: Arc::new(setter), - inner_fact, - __phantom: PhantomData, - } - } -} - -impl<'a, O, T, F> Factual<'a, O> for LensFact<'a, O, T, F> -where - T: Bounds<'a>, - O: Bounds<'a> + Clone, - F: Factual<'a, T>, -{ - #[tracing::instrument(fields(fact = "lens"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: O) -> Mutation { - let t = (self.getter)(obj.clone()); - let t = self - .inner_fact + let label = label.to_string(); + stateful("lens", inner_fact, move |g, fact, obj: O| { + let t = getter(obj.clone()); + let t = fact .mutate(g, t) - .map_check_err(|err| format!("lens({}) > {}", self.label, err))?; - Ok((self.setter)(obj, t)) - } + .map_check_err(|err| format!("lens1({}) > {}", label, err))?; + Ok(setter(obj, t)) + }) } +// impl<'a, O, T, F> Factual<'a, O> for LensFact<'a, O, T, F> +// where +// T: Bounds<'a>, +// O: Bounds<'a> + Clone, +// F: Factual<'a, T>, +// { +// #[tracing::instrument(fields(fact = "lens"), skip(self, g))] +// fn mutate(&mut self, g: &mut Generator<'a>, obj: O) -> Mutation { +// let t = (self.getter)(obj.clone()); +// let t = self +// .inner_fact +// .mutate(g, t) +// .map_check_err(|err| format!("lens1({}) > {}", self.label, err))?; +// Ok((self.setter)(obj, t)) +// } +// } + #[cfg(test)] mod tests { use super::*; @@ -146,7 +111,7 @@ mod tests { observability::test_run().ok(); let mut g = utils::random_generator(); - let f = || vec(lens("S::x", |s: &mut S| &mut s.x, eq(1))); + let f = || vec(lens1("S::x", |s: &mut S| &mut s.x, eq(1))); let ones = f().build(&mut g); f().check(&ones).unwrap(); diff --git a/src/facts/not.rs b/src/facts/not.rs index 99ccb1e..581b4fa 100644 --- a/src/facts/not.rs +++ b/src/facts/not.rs @@ -4,10 +4,9 @@ use super::*; // TODO: `not` in particular would really benefit from Facts having accessible // labels, since currently you can only get context about why a `not` fact passed, // not why it fails. -pub fn not<'a, F, T>(fact: F) -> Fact<'a, (), T> +pub fn not<'a, T>(fact: StatelessFact<'a, T>) -> StatelessFact<'a, T> where - F: 'a + Factual<'a, T>, - T: Bounds<'a>, + T: Target<'a>, { stateless("not", move |g, obj| { let label = format!("not({:?})", fact); diff --git a/src/facts/optical.rs b/src/facts/optical.rs index e46a50e..d3dafbf 100644 --- a/src/facts/optical.rs +++ b/src/facts/optical.rs @@ -20,7 +20,7 @@ where OpticalFact::new(label.to_string(), optics, inner_fact) } -/// A fact which uses a lens to apply another fact. Use [`lens()`] to construct. +/// A fact which uses a lens to apply another fact. Use [`lens1()`] to construct. #[derive(Clone)] pub struct OpticalFact<'a, Src, Img, Optics, F> where @@ -78,7 +78,7 @@ where self.inner_fact .check(img) - .map(|err| format!("lens({}){{{:?}}} > {}", label, self.optics.clone(), err)) + .map(|err| format!("lens1({}){{{:?}}} > {}", label, self.optics.clone(), err)) }) .collect::>() .into() @@ -100,7 +100,7 @@ where } #[test] -fn test_lens() { +fn test_lens1() { let x = (1u8, (2u8, (3u8, 4u8))); let mut fact = OpticalFact { diff --git a/src/facts/or.rs b/src/facts/or.rs index 686fd0e..c1bd5ff 100644 --- a/src/facts/or.rs +++ b/src/facts/or.rs @@ -1,11 +1,11 @@ use super::*; /// Combines two constraints so that either one may be satisfied -pub fn or<'a, A, B, Item>(a: A, b: B) -> Fact<'a, (A, B), Item> +pub fn or<'a, A, B, T>(a: Fact<'a, A, T>, b: Fact<'a, B, T>) -> Fact<'a, Fact2<'a, A, B, T>, T> where - A: Factual<'a, Item>, - B: Factual<'a, Item>, - Item: Bounds<'a>, + T: Target<'a>, + A: State, + B: State, { stateful("or", (a, b), |g, (a, b), obj| { use rand::{thread_rng, Rng}; diff --git a/src/facts/prism.rs b/src/facts/prism.rs index e49b652..0f76a96 100644 --- a/src/facts/prism.rs +++ b/src/facts/prism.rs @@ -59,99 +59,26 @@ use crate::*; /// The `prism` closure is a rather lazy way to provide a prism in the /// traditional optics sense. We may consider using a true lens library for /// this in the future. -pub fn prism<'a, O, T, F, P, S>(label: S, prism: P, inner_fact: F) -> PrismFact<'a, O, T, F> +pub fn prism<'a, O, T, P, S>( + label: impl ToString, + prism: P, + inner_fact: Fact<'a, S, T>, +) -> Fact<'a, Fact<'a, S, T>, O> where - O: Bounds<'a>, - S: ToString, - T: Bounds<'a> + Clone, - F: Factual<'a, T>, + O: Target<'a>, + T: Target<'a>, P: 'a + Send + Sync + Fn(&mut O) -> Option<&mut T>, + S: State, { - // let getter = |o| prism(&mut o).cloned(); - // let setter = |o, t| { - // let some = if let Some(i) = prism(&mut o) { - // *i = t; - // true - // } else { - // false - // }; - // some.then_some(o) - // }; - PrismFact::new(label.to_string(), prism, inner_fact) -} - -/// A fact which uses a prism to apply another fact. Use [`prism()`] to construct. -#[derive(Clone)] -pub struct PrismFact<'a, O, T, F> -where - T: Bounds<'a>, - O: Bounds<'a>, - F: Factual<'a, T>, -{ - label: String, - // getter: Arc Option>, - // setter: Arc Option>, - prism: Arc Option<&mut T>>, - inner_fact: F, - __phantom: PhantomData<&'a F>, -} - -impl<'a, O, T, F> PrismFact<'a, O, T, F> -where - T: Bounds<'a>, - O: Bounds<'a>, - F: Factual<'a, T>, -{ - /// Constructor. Supply a prism and an existing Fact to create a new Fact. - pub fn new

(label: String, prism: P, /*getter: G, setter: S,*/ inner_fact: F) -> Self - where - T: Bounds<'a>, - O: Bounds<'a>, - F: Factual<'a, T>, - P: 'a + Send + Sync + Fn(&mut O) -> Option<&mut T>, - // G: Fn(O) -> Option, - // S: Fn(O, T) -> Option, - { - Self { - label, - inner_fact, - prism: Arc::new(prism), - // getter: Arc::new(getter), - // setter: Arc::new(setter), - __phantom: PhantomData, - } - } -} - -impl<'a, O, T, F> std::fmt::Debug for PrismFact<'a, O, T, F> -where - T: Bounds<'a>, - O: Bounds<'a>, - F: Factual<'a, T>, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PrismFact") - .field("label", &self.label) - .field("fact", &self.inner_fact) - .finish() - } -} - -impl<'a, O, T, F> Factual<'a, O> for PrismFact<'a, O, T, F> -where - T: Bounds<'a> + Clone, - O: Bounds<'a>, - F: Factual<'a, T>, -{ - fn mutate(&mut self, g: &mut Generator<'a>, mut obj: O) -> Mutation { - if let Some(t) = (self.prism)(&mut obj) { - *t = self - .inner_fact + let label = label.to_string(); + stateful("prism", inner_fact, move |g, fact, mut obj| { + if let Some(t) = prism(&mut obj) { + *t = fact .mutate(g, t.clone()) - .map_check_err(|err| format!("prism({}) > {}", self.label, err))?; + .map_check_err(|err| format!("prism({}) > {}", label, err))?; } Ok(obj) - } + }) } #[cfg(test)] diff --git a/src/facts/same.rs b/src/facts/same.rs index 12b1308..97b9b62 100644 --- a/src/facts/same.rs +++ b/src/facts/same.rs @@ -3,7 +3,7 @@ use super::{brute::brute_labeled, *}; /// Specifies an equality constraint between two items in a tuple pub fn same<'a, T>() -> StatelessFact<'a, (T, T)> where - T: Bounds<'a> + PartialEq, + T: Target<'a> + PartialEq, { stateless("same", |g, mut obj: (T, T)| { let o = obj.clone(); @@ -16,7 +16,7 @@ where /// Specifies an inequality constraint between two items in a tuple pub fn different<'a, T>() -> StatelessFact<'a, (T, T)> where - T: Bounds<'a> + PartialEq, + T: Target<'a> + PartialEq, { brute_labeled(|(a, b)| { if a == b { diff --git a/src/facts/seq.rs b/src/facts/seq.rs index 39af294..986084b 100644 --- a/src/facts/seq.rs +++ b/src/facts/seq.rs @@ -37,10 +37,10 @@ use super::and; /// let list = fact.clone().satisfy(&mut g, vec![0; 5]).unwrap(); /// assert_eq!(list, vec![0, 1, 2, 3, 4]); /// ``` -pub fn vec<'a, T, F>(inner_fact: F) -> Fact<'a, F, Vec> +pub fn vec<'a, S, T>(inner_fact: Fact<'a, S, T>) -> Fact<'a, Fact<'a, S, T>, Vec> where - T: Bounds<'a> + Clone, - F: Factual<'a, T>, + T: Target<'a> + Clone, + S: State, { stateful("vec", inner_fact, |g, f, obj: Vec| { obj.into_iter() @@ -56,7 +56,7 @@ where /// Checks that a Vec is of a given length pub fn vec_len<'a, T>(len: usize) -> StatelessFact<'a, Vec> where - T: Bounds<'a> + Clone + 'a, + T: Target<'a> + Clone + 'a, { stateless("vec_len", move |g, mut obj: Vec| { if obj.len() > len { @@ -81,10 +81,13 @@ where } /// Combines a LenFact with a VecFact to ensure that the vector is of a given length -pub fn vec_of_length<'a, T, F>(len: usize, inner_fact: F) -> impl Factual<'a, Vec> +pub fn vec_of_length<'a, S, T>( + len: usize, + inner_fact: Fact<'a, S, T>, +) -> Fact<'a, Fact2<'a, (), Fact<'a, S, T>, Vec>, Vec> where - T: Bounds<'a> + Clone + 'a, - F: Factual<'a, T> + 'a, + S: State, + T: Target<'a> + 'a, { and(vec_len(len), vec(inner_fact)) } diff --git a/src/factual.rs b/src/factual.rs deleted file mode 100644 index cbec94f..0000000 --- a/src/factual.rs +++ /dev/null @@ -1,142 +0,0 @@ -use arbitrary::*; -use either::Either; - -use crate::*; - -// TODO: we can remove the Clone requirement if: -// - make `Mutate` track list of errors so that it can know if a mutation occurred. -// - make `mutate()` take a mut ref -// - make `check()` take a mut ref -// then `check()` can know if a mutation occurred -// -/// The trait bounds for the subject of a Fact -pub trait Bounds<'a>: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> {} -impl<'a, T> Bounds<'a> for T where - T: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> -{ -} - -// /// Type alias for a boxed Fact. Implements [`Fact`] itself. -// pub type BoxFact<'a, T> = Box>; - -// pub trait Facts>: Fact<'static, T> {} -// impl, F: Facts> Fact<'static, T> for F {} - -/// A declarative representation of a constraint on some data, which can be -/// used to both make an assertion (check) or to mold some arbitrary existing -/// data into a shape which passes that same assertion (mutate) -pub trait Factual<'a, T>: Send + Sync + Clone + std::fmt::Debug -where - T: Bounds<'a>, -{ - /// Assert that the constraint is satisfied for given data. - /// - /// If the mutation function is written properly, we get a check for free - /// by using a special Generator which fails upon mutation. If this is for - /// some reason unreasonable, a check function can be written by hand, but - /// care must be taken to make sure it perfectly lines up with the mutation function. - #[tracing::instrument(fields(fact_impl = "Fact"), skip(self))] - fn check(mut self, obj: &T) -> Check { - let mut g = Generator::checker(); - Check::from_mutation(self.mutate(&mut g, obj.clone())) - } - - /// Apply a mutation which moves the obj closer to satisfying the overall - /// constraint. - // #[tracing::instrument(skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation; - - /// Make this many attempts to satisfy a constraint before giving up and panicking. - /// - /// If you are combining highly contentious facts together and relying on randomness - /// to find a solution, this limit may need to be higher. In general, you should try - /// to write facts that don't interfere with each other so that the constraint can be - /// met on the first attempt, or perhaps the second or third. If necessary, this can - /// be raised to lean more on random search. - fn satisfy_attempts(&self) -> usize { - SATISFY_ATTEMPTS - } - - /// Mutate a value such that it satisfies the constraint. - /// If the constraint cannot be satisfied, panic. - #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] - fn satisfy(&mut self, g: &mut Generator<'a>, obj: T) -> ContrafactResult { - tracing::trace!("satisfy"); - let mut last_failure: Vec = vec![]; - let mut next = obj.clone(); - for _i in 0..self.satisfy_attempts() { - let mut m = self.clone(); - let mut c = self.clone(); - next = m.mutate(g, next).unwrap(); - if let Err(errs) = self.clone().check(&next).result()? { - last_failure = errs; - } else { - *self = m; - return Ok(next); - } - } - panic!( - "Could not satisfy a constraint even after {} attempts. Last check failure: {:?}", - SATISFY_ATTEMPTS, last_failure - ); - } - - #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] - /// Build a new value such that it satisfies the constraint - fn build_fallible(mut self, g: &mut Generator<'a>) -> ContrafactResult { - let obj = T::arbitrary(g).unwrap(); - self.satisfy(g, obj) - } - - /// Build a new value such that it satisfies the constraint, panicking on error - #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] - fn build(self, g: &mut Generator<'a>) -> T { - self.build_fallible(g).unwrap() - } -} - -impl<'a, T, F1, F2> Factual<'a, T> for Either -where - T: Bounds<'a>, - F1: Factual<'a, T> + ?Sized, - F2: Factual<'a, T> + ?Sized, -{ - #[tracing::instrument(fields(fact_impl = "Either"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - match self { - Either::Left(f) => f.mutate(g, obj), - Either::Right(f) => f.mutate(g, obj), - } - } -} - -// #[tracing::instrument(skip(fact))] -// pub(crate) fn check_raw<'a, T, F: Factual<'a, T>>(fact: &mut F, obj: &T) -> Check -// where -// T: Bounds<'a> + ?Sized, -// F: Factual<'a, T> + ?Sized, -// { -// let mut g = Generator::checker(); -// Check::from_mutation(fact.mutate(&mut g, obj.clone())) -// } - -#[tracing::instrument(skip(facts))] -fn collect_checks<'a, T, F>(facts: Vec, obj: &T) -> Check -where - T: Bounds<'a>, - F: Factual<'a, T>, -{ - let checks = facts - .into_iter() - .enumerate() - .map(|(i, f)| { - Ok(f.check(obj) - .failures()? - .iter() - .map(|e| format!("fact {}: {}", i, e)) - .collect()) - }) - .collect::>>>() - .map(|fs| fs.into_iter().flatten().collect()); - Check::from_result(checks) -} diff --git a/src/lib.rs b/src/lib.rs index 2a0b9af..0afb455 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ //! y: u32, //! } //! -//! let mut fact = lens("S::x", |s: &mut S| &mut s.x, eq(1)); +//! let mut fact = lens1("S::x", |s: &mut S| &mut s.x, eq(1)); //! //! assert!(fact.clone().check(&S {x: 1, y: 333}).is_ok()); //! assert!(fact.clone().check(&S {x: 2, y: 333}).is_err()); @@ -63,7 +63,6 @@ mod error; mod fact; /// Some built-in implementations of some useful facts pub mod facts; -mod factual; mod generator; pub use facts::*; @@ -74,8 +73,7 @@ pub use arbitrary; pub use check::Check; pub use error::*; -pub use fact::{stateful, stateless, Fact, StatelessFact}; -pub use factual::{Bounds, Factual}; +pub use fact::{stateful, stateless, Fact, Fact2, State, StatelessFact, Target}; pub use generator::*; pub use either; diff --git a/tests/chainlink.rs b/tests/chainlink.rs index 7edbab4..4ccf664 100644 --- a/tests/chainlink.rs +++ b/tests/chainlink.rs @@ -25,8 +25,8 @@ struct Wrapper { /// consecutive `prev` values starting with 0. fn chain_fact<'a>(author: String) -> impl Factual<'a, Link> { facts![ - lens("Link::author", |o: &mut Link| &mut o.author, eq(author),), - lens( + lens1("Link::author", |o: &mut Link| &mut o.author, eq(author),), + lens1( "Link::prev", |o: &mut Link| &mut o.prev, consecutive_int("increasing prev", 0), @@ -38,12 +38,12 @@ fn chain_fact<'a>(author: String) -> impl Factual<'a, Link> { /// of the wrapper is in the given set. fn wrapper_fact<'a>(author: String, valid_colors: &'a [Color]) -> impl Factual<'a, Wrapper> { facts![ - lens( + lens1( "Wrapper::color", |o: &mut Wrapper| &mut o.color, in_slice("valid color", valid_colors), ), - lens( + lens1( "Wrapper::link", |o: &mut Wrapper| &mut o.link, chain_fact(author), diff --git a/tests/complex.rs b/tests/complex.rs index 475fbc7..6e24004 100644 --- a/tests/complex.rs +++ b/tests/complex.rs @@ -130,11 +130,11 @@ impl AlphaSigner { #[allow(unused)] fn alpha_fact() -> impl Factual<'static, Alpha> { - facts![lens("Alpha::id", |a: &mut Alpha| a.id(), id_fact(None))] + facts![lens1("Alpha::id", |a: &mut Alpha| a.id(), id_fact(None))] } fn beta_fact() -> impl Factual<'static, Beta> { - facts![lens("Beta::id", |a: &mut Beta| &mut a.id, id_fact(None))] + facts![lens1("Beta::id", |a: &mut Beta| &mut a.id, id_fact(None))] } /// Just a pair of an Alpha with optional Beta. @@ -167,13 +167,13 @@ fn id_fact(id: Option) -> impl Factual<'static, Id> { /// - All Ids should match each other. If there is a Beta, its id should match too. fn pi_fact(id: Id) -> impl Factual<'static, Pi> { let alpha_fact = facts![ - lens("Alpha::id", |a: &mut Alpha| a.id(), id_fact(Some(id))), - // lens("Alpha::data", |a: &mut Alpha| a.data(), eq(data)), + lens1("Alpha::id", |a: &mut Alpha| a.id(), id_fact(Some(id))), + // lens1("Alpha::data", |a: &mut Alpha| a.data(), eq(data)), ]; - let beta_fact = lens("Beta::id", |b: &mut Beta| &mut b.id, id_fact(Some(id))); + let beta_fact = lens1("Beta::id", |b: &mut Beta| &mut b.id, id_fact(Some(id))); facts![ pi_beta_match(), - lens("Pi::alpha", |o: &mut Pi| &mut o.0, alpha_fact), + lens1("Pi::alpha", |o: &mut Pi| &mut o.0, alpha_fact), prism("Pi::beta", |o: &mut Pi| o.1.as_mut(), beta_fact), ] } @@ -202,7 +202,7 @@ fn omega_fact(id: Id) -> impl Factual<'static, Omega> { facts![ omega_pi, - lens("Omega::id", |o: &mut Omega| o.id_mut(), id_fact(Some(id))), + lens1("Omega::id", |o: &mut Omega| o.id_mut(), id_fact(Some(id))), ] } @@ -227,7 +227,7 @@ fn sigma_fact() -> impl Factual<'static, Sigma> { same(), ); facts![ - lens("Sigma::id", |o: &mut Sigma| o.alpha.id(), id_fact(None)), + lens1("Sigma::id", |o: &mut Sigma| o.alpha.id(), id_fact(None)), id2_fact ] } @@ -249,7 +249,7 @@ fn rho_fact(id: Id, signer: AlphaSigner) -> impl Factual<'static, Rho> { #[cfg(not(feature = "optics"))] { facts![ - lens("Rho -> Sigma", |rho: &mut Rho| &mut rho.sigma, sigma_fact()), + lens1("Rho -> Sigma", |rho: &mut Rho| &mut rho.sigma, sigma_fact()), rho_pi ] } From f80d0918f82f3b4630ccbe3cf0a9a91af0919aa5 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 13:51:22 -0700 Subject: [PATCH 15/21] WIP --- src/fact.rs | 11 ---- src/facts/lens.rs | 4 +- src/factual.rs | 133 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 4 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 src/factual.rs diff --git a/src/fact.rs b/src/fact.rs index 6861375..2cdd1f9 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -4,17 +4,6 @@ use arbitrary::Arbitrary; use crate::*; -/// The trait bounds for the target of a Fact -pub trait Target<'a>: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> {} -impl<'a, T> Target<'a> for T where - T: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> -{ -} - -/// The trait bounds for the State of a Fact -pub trait State: std::fmt::Debug + Clone + Send + Sync {} -impl State for T where T: std::fmt::Debug + Clone + Send + Sync {} - /// Create a fact from a bare function which specifies the mutation. /// Can be quicker to experiment with ideas this way than to have to directly implement /// the [`Fact`] trait diff --git a/src/facts/lens.rs b/src/facts/lens.rs index 2354d06..d1f3cc3 100644 --- a/src/facts/lens.rs +++ b/src/facts/lens.rs @@ -38,8 +38,8 @@ use crate::*; pub fn lens1<'a, O, T, L, S>( label: impl ToString, accessor: L, - inner_fact: Fact<'a, S, T>, -) -> Fact<'a, Fact<'a, S, T>, O> + inner_fact: impl Factual<'a, T>, +) -> impl Factual<'a, O> where O: Target<'a>, T: Target<'a>, diff --git a/src/factual.rs b/src/factual.rs new file mode 100644 index 0000000..bddbb20 --- /dev/null +++ b/src/factual.rs @@ -0,0 +1,133 @@ +use arbitrary::*; +use either::Either; + +use crate::*; + +/// The trait bounds for the target of a Fact +pub trait Target<'a>: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> {} +impl<'a, T> Target<'a> for T where + T: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> +{ +} + +/// The trait bounds for the State of a Fact +pub trait State: std::fmt::Debug + Clone + Send + Sync {} +impl State for T where T: std::fmt::Debug + Clone + Send + Sync {} + +/// A declarative representation of a constraint on some data, which can be +/// used to both make an assertion (check) or to mold some arbitrary existing +/// data into a shape which passes that same assertion (mutate) +pub trait Factual<'a, T>: Send + Sync + Clone + std::fmt::Debug +where + T: Target<'a>, +{ + /// Assert that the constraint is satisfied for given data. + /// + /// If the mutation function is written properly, we get a check for free + /// by using a special Generator which fails upon mutation. If this is for + /// some reason unreasonable, a check function can be written by hand, but + /// care must be taken to make sure it perfectly lines up with the mutation function. + #[tracing::instrument(fields(fact_impl = "Fact"), skip(self))] + fn check(mut self, obj: &T) -> Check { + let mut g = Generator::checker(); + Check::from_mutation(self.mutate(&mut g, obj.clone())) + } + + /// Apply a mutation which moves the obj closer to satisfying the overall + /// constraint. + // #[tracing::instrument(skip(self, g))] + fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation; + + /// Make this many attempts to satisfy a constraint before giving up and panicking. + /// + /// If you are combining highly contentious facts together and relying on randomness + /// to find a solution, this limit may need to be higher. In general, you should try + /// to write facts that don't interfere with each other so that the constraint can be + /// met on the first attempt, or perhaps the second or third. If necessary, this can + /// be raised to lean more on random search. + fn satisfy_attempts(&self) -> usize { + SATISFY_ATTEMPTS + } + + /// Mutate a value such that it satisfies the constraint. + /// If the constraint cannot be satisfied, panic. + #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] + fn satisfy(&mut self, g: &mut Generator<'a>, obj: T) -> ContrafactResult { + tracing::trace!("satisfy"); + let mut last_failure: Vec = vec![]; + let mut next = obj.clone(); + for _i in 0..self.satisfy_attempts() { + let mut m = self.clone(); + next = m.mutate(g, next).unwrap(); + if let Err(errs) = self.clone().check(&next).result()? { + last_failure = errs; + } else { + *self = m; + return Ok(next); + } + } + panic!( + "Could not satisfy a constraint even after {} attempts. Last check failure: {:?}", + SATISFY_ATTEMPTS, last_failure + ); + } + + #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] + /// Build a new value such that it satisfies the constraint + fn build_fallible(mut self, g: &mut Generator<'a>) -> ContrafactResult { + let obj = T::arbitrary(g).unwrap(); + self.satisfy(g, obj) + } + + /// Build a new value such that it satisfies the constraint, panicking on error + #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] + fn build(self, g: &mut Generator<'a>) -> T { + self.build_fallible(g).unwrap() + } +} + +impl<'a, T, F1, F2> Factual<'a, T> for Either +where + T: Target<'a>, + F1: Factual<'a, T> + ?Sized, + F2: Factual<'a, T> + ?Sized, +{ + #[tracing::instrument(fields(fact_impl = "Either"), skip(self, g))] + fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { + match self { + Either::Left(f) => f.mutate(g, obj), + Either::Right(f) => f.mutate(g, obj), + } + } +} + +// #[tracing::instrument(skip(fact))] +// pub(crate) fn check_raw<'a, T, F: Factual<'a, T>>(fact: &mut F, obj: &T) -> Check +// where +// T: Target<'a> + ?Sized, +// F: Factual<'a, T> + ?Sized, +// { +// let mut g = Generator::checker(); +// Check::from_mutation(fact.mutate(&mut g, obj.clone())) +// } + +#[tracing::instrument(skip(facts))] +fn collect_checks<'a, T, F>(facts: Vec, obj: &T) -> Check +where + T: Target<'a>, + F: Factual<'a, T>, +{ + let checks = facts + .into_iter() + .enumerate() + .map(|(i, f)| { + Ok(f.check(obj) + .failures()? + .iter() + .map(|e| format!("fact {}: {}", i, e)) + .collect()) + }) + .collect::>>>() + .map(|fs| fs.into_iter().flatten().collect()); + Check::from_result(checks) +} diff --git a/src/lib.rs b/src/lib.rs index 0afb455..2d7622e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,6 +63,7 @@ mod error; mod fact; /// Some built-in implementations of some useful facts pub mod facts; +mod factual; mod generator; pub use facts::*; @@ -74,6 +75,7 @@ pub use arbitrary; pub use check::Check; pub use error::*; pub use fact::{stateful, stateless, Fact, Fact2, State, StatelessFact, Target}; +pub use factual::Factual; pub use generator::*; pub use either; From d769e0d760d6fdd47c07333f19ff2e7817799e08 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 13:57:58 -0700 Subject: [PATCH 16/21] Re-add Factual trait --- src/fact.rs | 14 ++++++++++++++ src/facts/and.rs | 7 +------ src/facts/lens.rs | 10 ++++------ src/facts/prism.rs | 7 +++---- src/facts/seq.rs | 9 ++------- src/factual.rs | 16 ++++++++++++++-- src/lib.rs | 6 +++--- tests/complex.rs | 9 +++++---- 8 files changed, 46 insertions(+), 32 deletions(-) diff --git a/src/fact.rs b/src/fact.rs index 2cdd1f9..e15b53e 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -84,6 +84,20 @@ where } } +impl<'a, S, T> Factual<'a, T> for Fact<'a, S, T> +where + S: Clone + Send + Sync + Debug, + T: Target<'a>, +{ + fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { + (self.fun)(g, &mut self.state, obj) + } + + fn label(self, label: impl ToString) -> Self { + self.label(label) + } +} + impl<'a, S, T> Fact<'a, S, T> where S: Clone + Send + Sync + Debug, diff --git a/src/facts/and.rs b/src/facts/and.rs index e9ff331..d96475a 100644 --- a/src/facts/and.rs +++ b/src/facts/and.rs @@ -1,14 +1,9 @@ use crate::*; /// A Fact which applies two other facts. -pub fn and<'a, S1, S2, T>( - a: Fact<'a, S1, T>, - b: Fact<'a, S2, T>, -) -> Fact<'a, (Fact<'a, S1, T>, Fact<'a, S2, T>), T> +pub fn and<'a, T>(a: impl Factual<'a, T>, b: impl Factual<'a, T>) -> impl Factual<'a, T> where T: Target<'a>, - S1: State, - S2: State, { stateful("and", (a, b), |g, (a, b), obj| { let obj = a.mutate(g, obj)?; diff --git a/src/facts/lens.rs b/src/facts/lens.rs index d1f3cc3..48bca9c 100644 --- a/src/facts/lens.rs +++ b/src/facts/lens.rs @@ -35,7 +35,7 @@ use crate::*; /// ``` // // TODO: can rewrite this in terms of PrismFact for DRYness -pub fn lens1<'a, O, T, L, S>( +pub fn lens1<'a, O, T, L>( label: impl ToString, accessor: L, inner_fact: impl Factual<'a, T>, @@ -43,7 +43,6 @@ pub fn lens1<'a, O, T, L, S>( where O: Target<'a>, T: Target<'a>, - S: State, L: 'a + Clone + Send + Sync + Fn(&mut O) -> &mut T, { let accessor2 = accessor.clone(); @@ -56,16 +55,15 @@ where lens2(label, getter, setter, inner_fact).label("lens1") } -pub fn lens2<'a, O, T, S>( +pub fn lens2<'a, O, T>( label: impl ToString, getter: impl 'a + Clone + Send + Sync + Fn(O) -> T, setter: impl 'a + Clone + Send + Sync + Fn(O, T) -> O, - inner_fact: Fact<'a, S, T>, -) -> Fact<'a, Fact<'a, S, T>, O> + inner_fact: impl Factual<'a, T>, +) -> impl Factual<'a, O> where O: Target<'a>, T: Target<'a>, - S: State, { let label = label.to_string(); stateful("lens", inner_fact, move |g, fact, obj: O| { diff --git a/src/facts/prism.rs b/src/facts/prism.rs index 0f76a96..93c0a2e 100644 --- a/src/facts/prism.rs +++ b/src/facts/prism.rs @@ -59,16 +59,15 @@ use crate::*; /// The `prism` closure is a rather lazy way to provide a prism in the /// traditional optics sense. We may consider using a true lens library for /// this in the future. -pub fn prism<'a, O, T, P, S>( +pub fn prism<'a, O, T, P>( label: impl ToString, prism: P, - inner_fact: Fact<'a, S, T>, -) -> Fact<'a, Fact<'a, S, T>, O> + inner_fact: impl Factual<'a, T>, +) -> impl Factual<'a, O> where O: Target<'a>, T: Target<'a>, P: 'a + Send + Sync + Fn(&mut O) -> Option<&mut T>, - S: State, { let label = label.to_string(); stateful("prism", inner_fact, move |g, fact, mut obj| { diff --git a/src/facts/seq.rs b/src/facts/seq.rs index 986084b..e0facb4 100644 --- a/src/facts/seq.rs +++ b/src/facts/seq.rs @@ -37,10 +37,9 @@ use super::and; /// let list = fact.clone().satisfy(&mut g, vec![0; 5]).unwrap(); /// assert_eq!(list, vec![0, 1, 2, 3, 4]); /// ``` -pub fn vec<'a, S, T>(inner_fact: Fact<'a, S, T>) -> Fact<'a, Fact<'a, S, T>, Vec> +pub fn vec<'a, T>(inner_fact: impl Factual<'a, T>) -> impl Factual<'a, Vec> where T: Target<'a> + Clone, - S: State, { stateful("vec", inner_fact, |g, f, obj: Vec| { obj.into_iter() @@ -81,12 +80,8 @@ where } /// Combines a LenFact with a VecFact to ensure that the vector is of a given length -pub fn vec_of_length<'a, S, T>( - len: usize, - inner_fact: Fact<'a, S, T>, -) -> Fact<'a, Fact2<'a, (), Fact<'a, S, T>, Vec>, Vec> +pub fn vec_of_length<'a, T>(len: usize, inner_fact: impl Factual<'a, T>) -> impl Factual<'a, Vec> where - S: State, T: Target<'a> + 'a, { and(vec_len(len), vec(inner_fact)) diff --git a/src/factual.rs b/src/factual.rs index bddbb20..d0e9f86 100644 --- a/src/factual.rs +++ b/src/factual.rs @@ -4,9 +4,12 @@ use either::Either; use crate::*; /// The trait bounds for the target of a Fact -pub trait Target<'a>: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> {} +pub trait Target<'a>: + 'a + std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> +{ +} impl<'a, T> Target<'a> for T where - T: std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> + T: 'a + std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> { } @@ -21,6 +24,8 @@ pub trait Factual<'a, T>: Send + Sync + Clone + std::fmt::Debug where T: Target<'a>, { + fn label(self, label: impl ToString) -> Self; + /// Assert that the constraint is satisfied for given data. /// /// If the mutation function is written properly, we get a check for free @@ -99,6 +104,13 @@ where Either::Right(f) => f.mutate(g, obj), } } + + fn label(self, label: impl ToString) -> Self { + match self { + Either::Left(f) => Either::Left(f.label(label)), + Either::Right(f) => Either::Right(f.label(label)), + } + } } // #[tracing::instrument(skip(fact))] diff --git a/src/lib.rs b/src/lib.rs index 2d7622e..31e3cb9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ //! meets the constraint, or to generate new instances of `S` which meet the constraint. //! //! ``` -//! use contrafact::{Factual, facts::{eq, lens}}; +//! use contrafact::{Factual, facts::{eq, lens1}}; //! use arbitrary::{Arbitrary, Unstructured}; //! //! #[derive(Debug, Clone, PartialEq, Arbitrary)] @@ -74,8 +74,8 @@ pub use arbitrary; pub use check::Check; pub use error::*; -pub use fact::{stateful, stateless, Fact, Fact2, State, StatelessFact, Target}; -pub use factual::Factual; +pub use fact::{stateful, stateless, Fact, Fact2, StatelessFact}; +pub use factual::{Factual, State, Target}; pub use generator::*; pub use either; diff --git a/tests/complex.rs b/tests/complex.rs index 6e24004..934c38a 100644 --- a/tests/complex.rs +++ b/tests/complex.rs @@ -116,6 +116,7 @@ struct Rho { /// Some struct needed to set the values of a Sigma whenever its Alpha changes. /// Analogous to Holochain's Keystore (MetaLairClient). +#[derive(Clone)] struct AlphaSigner; impl AlphaSigner { @@ -184,7 +185,7 @@ fn pi_fact(id: Id) -> impl Factual<'static, Pi> { /// - and, the the Betas of the Alpha and the Omega should match. /// - all data must be set as specified fn omega_fact(id: Id) -> impl Factual<'static, Omega> { - let omega_pi = LensFact::new( + let omega_pi = lens2( "Omega -> Pi", |o| match o { Omega::AlphaBeta { alpha, beta, .. } => Pi(alpha, Some(beta)), @@ -208,7 +209,7 @@ fn omega_fact(id: Id) -> impl Factual<'static, Omega> { #[allow(unused)] fn sigma_fact() -> impl Factual<'static, Sigma> { - let id2_fact = LensFact::new( + let id2_fact = lens2( "Sigma::id is correct", |mut s: Sigma| (s.id2, *(s.alpha.id()) * 2), |mut s, (_, id2)| { @@ -217,7 +218,7 @@ fn sigma_fact() -> impl Factual<'static, Sigma> { }, same(), ); - let sig_fact = LensFact::new( + let sig_fact = lens2( "Sigma::sig is correct", |mut s: Sigma| (s.sig, s.alpha.id().to_string()), |mut s, (_, sig)| { @@ -235,7 +236,7 @@ fn sigma_fact() -> impl Factual<'static, Sigma> { /// The inner Sigma is correct wrt to signature /// XXX: this is a little wonky, probably room for improvement. fn rho_fact(id: Id, signer: AlphaSigner) -> impl Factual<'static, Rho> { - let rho_pi = LensFact::new( + let rho_pi = lens2( "Rho -> Pi", |rho: Rho| Pi(rho.sigma.alpha, rho.beta), move |mut rho, Pi(a, b)| { From fc98f1872eeeb7d77e071ccc31f8de6e5087e6d9 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 14:41:08 -0700 Subject: [PATCH 17/21] Reinstate Fact trait --- Cargo.toml | 11 +- src/fact.rs | 202 ++++++++++++----------------------- src/facts.rs | 11 +- src/facts/and.rs | 4 +- src/facts/brute.rs | 10 +- src/facts/consecutive_int.rs | 6 +- src/facts/constant.rs | 8 +- src/facts/eq.rs | 8 +- src/facts/in_range.rs | 4 +- src/facts/in_slice.rs | 6 +- src/facts/lens.rs | 41 +++---- src/facts/not.rs | 6 +- src/facts/optical.rs | 18 ++-- src/facts/or.rs | 6 +- src/facts/prism.rs | 8 +- src/facts/same.rs | 6 +- src/facts/seq.rs | 14 ++- src/factual.rs | 145 ------------------------- src/lambda.rs | 132 +++++++++++++++++++++++ src/lib.rs | 10 +- tests/chainlink.rs | 4 +- tests/complex.rs | 16 +-- 22 files changed, 291 insertions(+), 385 deletions(-) delete mode 100644 src/factual.rs create mode 100644 src/lambda.rs diff --git a/Cargo.toml b/Cargo.toml index 9493325..f11ced6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,19 +11,18 @@ edition = "2021" [dependencies] arbitrary = {version = "1.0", features = ["derive"]} +either = "1.5" derive_more = "0.99" itertools = "0.10" -tracing = "0.1" num = "0.4.0" -anyhow = "1.0.40" +tracing = "0.1" # utils once_cell = { version = "1.5", optional = true } rand = { version = "0.7", optional = true } -# lens -lens-rs = { version = "0.3", optional = true } -either = "1.5" +# # lens +# lens-rs = { version = "0.3", optional = true } [dev-dependencies] either = "1.5" @@ -34,7 +33,7 @@ default = ["utils"] utils = ["once_cell", "rand"] -optics = ["lens-rs"] +# optics = ["lens-rs"] [package.metadata.inwelling] lens-rs_generator = true diff --git a/src/fact.rs b/src/fact.rs index e15b53e..5c2da93 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -1,113 +1,34 @@ -use std::{fmt::Debug, marker::PhantomData, sync::Arc}; - -use arbitrary::Arbitrary; +use arbitrary::*; +use either::Either; use crate::*; -/// Create a fact from a bare function which specifies the mutation. -/// Can be quicker to experiment with ideas this way than to have to directly implement -/// the [`Fact`] trait -/// -/// ``` -/// use contrafact::*; -/// let mut g = utils::random_generator(); -/// -/// let mut fact = vec_of_length( -/// 4, -/// stateful("geometric series", 2, move |g, s, mut v| { -/// g.set(&mut v, s, || "value is not geometrically increasing by 2")?; -/// *s *= 2; -/// Ok(v) -/// }), -/// ); -/// -/// let list = fact.clone().build(&mut g); -/// fact.check(&list).unwrap(); -/// assert_eq!(list, vec![2, 4, 8, 16]); -/// ``` -pub fn stateful<'a, S, T>( - label: impl ToString, - state: S, - f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, &mut S, T) -> Mutation, -) -> Fact<'a, S, T> -where - S: Clone + Send + Sync, - T: Target<'a>, +/// The trait bounds for the target of a Fact +pub trait Target<'a>: + 'a + std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> { - Fact { - label: label.to_string(), - state, - fun: Arc::new(f), - _phantom: PhantomData, - } } - -/// Create a lambda with unit state -pub fn stateless<'a, T>( - label: impl ToString, - f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, T) -> Mutation, -) -> Fact<'a, (), T> -where - T: Target<'a>, -{ - stateful(label, (), move |g, (), obj| f(g, obj)) -} - -pub type Lambda<'a, S, T> = - Arc, &mut S, T) -> Mutation>; - -#[derive(Clone)] -pub struct Fact<'a, S, T> -where - S: Clone + Send + Sync, - T: Target<'a>, +impl<'a, T> Target<'a> for T where + T: 'a + std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> { - state: S, - fun: Lambda<'a, S, T>, - label: String, - _phantom: PhantomData<&'a T>, } -/// Two facts about the same target with different states -pub type Fact2<'a, A, B, T> = (Fact<'a, A, T>, Fact<'a, B, T>); - -impl<'a, S, T> std::fmt::Debug for Fact<'a, S, T> -where - S: Clone + Send + Sync + Debug, - T: Target<'a>, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Fact") - .field("label", &self.label) - .field("state", &self.state) - .finish() - } -} - -impl<'a, S, T> Factual<'a, T> for Fact<'a, S, T> -where - S: Clone + Send + Sync + Debug, - T: Target<'a>, -{ - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - (self.fun)(g, &mut self.state, obj) - } - - fn label(self, label: impl ToString) -> Self { - self.label(label) - } -} +/// The trait bounds for the State of a Fact +pub trait State: std::fmt::Debug + Clone + Send + Sync {} +impl State for T where T: std::fmt::Debug + Clone + Send + Sync {} -impl<'a, S, T> Fact<'a, S, T> +/// A declarative representation of a constraint on some data, which can be +/// used to both make an assertion (check) or to mold some arbitrary existing +/// data into a shape which passes that same assertion (mutate) +pub trait Fact<'a, T>: Send + Sync + Clone + std::fmt::Debug where - S: Clone + Send + Sync + Debug, T: Target<'a>, { /// Change the label - pub fn label(mut self, label: impl ToString) -> Self { - self.label = label.to_string(); - self - } + fn labeled(self, label: impl ToString) -> Self; + + /// Get the label of this fact + fn label(&self) -> String; /// Assert that the constraint is satisfied for given data. /// @@ -116,7 +37,7 @@ where /// some reason unreasonable, a check function can be written by hand, but /// care must be taken to make sure it perfectly lines up with the mutation function. #[tracing::instrument(fields(fact_impl = "Fact"), skip(self))] - pub fn check(mut self, obj: &T) -> Check { + fn check(mut self, obj: &T) -> Check { let mut g = Generator::checker(); Check::from_mutation(self.mutate(&mut g, obj.clone())) } @@ -124,9 +45,7 @@ where /// Apply a mutation which moves the obj closer to satisfying the overall /// constraint. // #[tracing::instrument(skip(self, g))] - pub fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - (self.fun)(g, &mut self.state, obj) - } + fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation; /// Make this many attempts to satisfy a constraint before giving up and panicking. /// @@ -135,19 +54,19 @@ where /// to write facts that don't interfere with each other so that the constraint can be /// met on the first attempt, or perhaps the second or third. If necessary, this can /// be raised to lean more on random search. - pub fn satisfy_attempts(&self) -> usize { + fn satisfy_attempts(&self) -> usize { SATISFY_ATTEMPTS } /// Mutate a value such that it satisfies the constraint. /// If the constraint cannot be satisfied, panic. - pub fn satisfy(&mut self, g: &mut Generator<'a>, obj: T) -> ContrafactResult { + #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] + fn satisfy(&mut self, g: &mut Generator<'a>, obj: T) -> ContrafactResult { tracing::trace!("satisfy"); let mut last_failure: Vec = vec![]; let mut next = obj.clone(); for _i in 0..self.satisfy_attempts() { let mut m = self.clone(); - let mut c = self.clone(); next = m.mutate(g, next).unwrap(); if let Err(errs) = self.clone().check(&next).result()? { last_failure = errs; @@ -164,47 +83,64 @@ where #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] /// Build a new value such that it satisfies the constraint - pub fn build_fallible(mut self, g: &mut Generator<'a>) -> ContrafactResult { + fn build_fallible(mut self, g: &mut Generator<'a>) -> ContrafactResult { let obj = T::arbitrary(g).unwrap(); self.satisfy(g, obj) } /// Build a new value such that it satisfies the constraint, panicking on error #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] - pub fn build(self, g: &mut Generator<'a>) -> T { + fn build(self, g: &mut Generator<'a>) -> T { self.build_fallible(g).unwrap() } } -/// A Fact with unit state -pub type StatelessFact<'a, T> = Fact<'a, (), T>; - -#[test] -fn test_lambda_fact() { - use crate::facts::*; - let mut g = utils::random_generator(); - - let geom = |k, s| { - stateful("geom", s, move |g, s, mut v| { - g.set(&mut v, s, || { - format!("value is not geometrically increasing by {k} starting from {s}") - })?; - *s *= k; - Ok(v) - }) - }; - - let fact = |k, s| vec_of_length(4, geom(k, s)); +impl<'a, T, F1, F2> Fact<'a, T> for Either +where + T: Target<'a>, + F1: Fact<'a, T> + ?Sized, + F2: Fact<'a, T> + ?Sized, +{ + #[tracing::instrument(fields(fact_impl = "Either"), skip(self, g))] + fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { + match self { + Either::Left(f) => f.mutate(g, obj), + Either::Right(f) => f.mutate(g, obj), + } + } - { - let list = fact(2, 2).build(&mut g); - assert_eq!(list, vec![2, 4, 8, 16]); - fact(2, 2).check(&list).unwrap(); + fn label(&self) -> String { + match self { + Either::Left(f) => f.label(), + Either::Right(f) => f.label(), + } } - { - let list = fact(3, 4).build(&mut g); - assert_eq!(list, vec![4, 12, 36, 108]); - fact(3, 4).check(&list).unwrap(); + fn labeled(self, label: impl ToString) -> Self { + match self { + Either::Left(f) => Either::Left(f.labeled(label)), + Either::Right(f) => Either::Right(f.labeled(label)), + } } } + +#[tracing::instrument(skip(facts))] +fn collect_checks<'a, T, F>(facts: Vec, obj: &T) -> Check +where + T: Target<'a>, + F: Fact<'a, T>, +{ + let checks = facts + .into_iter() + .enumerate() + .map(|(i, f)| { + Ok(f.check(obj) + .failures()? + .iter() + .map(|e| format!("fact {}: {}", i, e)) + .collect()) + }) + .collect::>>>() + .map(|fs| fs.into_iter().flatten().collect()); + Check::from_result(checks) +} diff --git a/src/facts.rs b/src/facts.rs index 343cc00..f2bfdd1 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -27,11 +27,10 @@ pub use lens::{lens1, lens2}; pub use prism::prism; pub use seq::{vec, vec_len, vec_of_length}; -// pub(crate) use lambda::LambdaFact; - -#[cfg(feature = "optics")] -mod optical; -#[cfg(feature = "optics")] -pub use optical::*; +// Optical facts are experimental and currently not supported +// #[cfg(feature = "optics")] +// mod optical; +// #[cfg(feature = "optics")] +// pub use optical::*; use crate::*; diff --git a/src/facts/and.rs b/src/facts/and.rs index d96475a..dd3a118 100644 --- a/src/facts/and.rs +++ b/src/facts/and.rs @@ -1,11 +1,11 @@ use crate::*; /// A Fact which applies two other facts. -pub fn and<'a, T>(a: impl Factual<'a, T>, b: impl Factual<'a, T>) -> impl Factual<'a, T> +pub fn and<'a, T>(a: impl Fact<'a, T>, b: impl Fact<'a, T>) -> impl Fact<'a, T> where T: Target<'a>, { - stateful("and", (a, b), |g, (a, b), obj| { + lambda("and", (a, b), |g, (a, b), obj| { let obj = a.mutate(g, obj)?; let obj = b.mutate(g, obj)?; Ok(obj) diff --git a/src/facts/brute.rs b/src/facts/brute.rs index 568d539..c0219e3 100644 --- a/src/facts/brute.rs +++ b/src/facts/brute.rs @@ -25,14 +25,14 @@ use crate::*; /// use arbitrary::Unstructured; /// use contrafact::*; /// -/// fn div_by(n: usize) -> impl Factual<'static, usize> { +/// fn div_by(n: usize) -> impl Fact<'static, usize> { /// facts![brute(format!("Is divisible by {}", n), move |x| x % n == 0)] /// } /// /// let mut g = utils::random_generator(); /// assert!(div_by(3).build(&mut g) % 3 == 0); /// ``` -pub fn brute<'a, T, F>(label: impl ToString, f: F) -> Fact<'a, (), T> +pub fn brute<'a, T, F>(label: impl ToString, f: F) -> Lambda<'a, (), T> where T: Target<'a>, F: 'a + Send + Sync + Fn(&T) -> bool, @@ -40,16 +40,16 @@ where let label = label.to_string(); // TODO figure out this label stuff let label2 = label.clone(); - brute_labeled(move |v| Ok(f(v).then_some(()).ok_or_else(|| label.clone()))).label(label2) + brute_labeled(move |v| Ok(f(v).then_some(()).ok_or_else(|| label.clone()))).labeled(label2) } /// A version of [`brute`] which allows the closure to return the reason for failure -pub fn brute_labeled<'a, T, F>(f: F) -> Fact<'a, (), T> +pub fn brute_labeled<'a, T, F>(f: F) -> Lambda<'a, (), T> where T: Target<'a>, F: 'a + Send + Sync + Fn(&T) -> ContrafactResult, { - stateless("brute_labeled", move |g, mut obj| { + lambda_unit("brute_labeled", move |g, mut obj| { let mut last_reason = "".to_string(); for _ in 0..=BRUTE_ITERATION_LIMIT { if let Err(reason) = f(&obj)? { diff --git a/src/facts/consecutive_int.rs b/src/facts/consecutive_int.rs index 5e5af6c..db36d7e 100644 --- a/src/facts/consecutive_int.rs +++ b/src/facts/consecutive_int.rs @@ -1,12 +1,12 @@ use super::*; /// Specifies that a value should be increasing by 1 at every check/mutation -pub fn consecutive_int<'a, S>(context: impl ToString, initial: S) -> Fact<'a, S, S> +pub fn consecutive_int<'a, S>(context: impl ToString, initial: S) -> Lambda<'a, S, S> where S: Target<'a> + std::fmt::Debug + PartialEq + num::PrimInt, { let context = context.to_string(); - stateful("consecutive_int", initial, move |g, counter, mut obj| { + lambda("consecutive_int", initial, move |g, counter, mut obj| { if obj != *counter { g.fail(&context)?; obj = counter.clone(); @@ -18,7 +18,7 @@ where /// Specifies that a value should be increasing by 1 at every check/mutation, /// with no context given -pub fn consecutive_int_<'a, T>(initial: T) -> Fact<'a, T, T> +pub fn consecutive_int_<'a, T>(initial: T) -> Lambda<'a, T, T> where T: Target<'a> + PartialEq + num::PrimInt, { diff --git a/src/facts/constant.rs b/src/facts/constant.rs index 6cc7767..4dea1af 100644 --- a/src/facts/constant.rs +++ b/src/facts/constant.rs @@ -1,14 +1,14 @@ use super::*; /// A constraint which is always met -pub fn always<'a, T: Target<'a>>() -> StatelessFact<'a, T> { - stateless("always", |_, obj| Ok(obj)) +pub fn always<'a, T: Target<'a>>() -> Lambda<'a, (), T> { + lambda_unit("always", |_, obj| Ok(obj)) } /// A constraint which is never met -pub fn never<'a, T: Target<'a>>(context: impl ToString) -> StatelessFact<'a, T> { +pub fn never<'a, T: Target<'a>>(context: impl ToString) -> Lambda<'a, (), T> { let context = context.to_string(); - stateless("never", move |g, obj: T| { + lambda_unit("never", move |g, obj: T| { g.fail(context.clone())?; Ok(obj) }) diff --git a/src/facts/eq.rs b/src/facts/eq.rs index 8bc2a39..bab1a45 100644 --- a/src/facts/eq.rs +++ b/src/facts/eq.rs @@ -3,12 +3,12 @@ use std::fmt::Display; use super::*; /// Specifies an equality constraint -pub fn eq<'a, T>(constant: T) -> Fact<'a, (), T> +pub fn eq<'a, T>(constant: T) -> Lambda<'a, (), T> where T: Target<'a> + PartialEq + Clone + Display, { let label = format!("eq({})", constant); - stateless(label, move |g, mut obj| { + lambda_unit(label, move |g, mut obj| { if obj != constant { g.fail(format!("expected {:?} == {:?}", obj, constant))?; obj = constant.clone(); @@ -18,12 +18,12 @@ where } /// Specifies an inequality constraint -pub fn ne<'a, S, T>(constant: T) -> Fact<'a, (), T> +pub fn ne<'a, S, T>(constant: T) -> Lambda<'a, (), T> where S: ToString, T: Target<'a> + PartialEq + Display, { - not(eq(constant)).label("ne") + not(eq(constant)).labeled("ne") } #[test] diff --git a/src/facts/in_range.rs b/src/facts/in_range.rs index 0abd7da..25a480e 100644 --- a/src/facts/in_range.rs +++ b/src/facts/in_range.rs @@ -3,7 +3,7 @@ use std::ops::{Bound, RangeBounds}; use super::*; /// Specifies a range constraint -pub fn in_range<'a, R, T>(context: impl ToString, range: R) -> StatelessFact<'a, T> +pub fn in_range<'a, R, T>(context: impl ToString, range: R) -> Lambda<'a, (), T> where R: 'a + Send + Sync + RangeBounds + std::fmt::Debug, T: Target<'a> @@ -16,7 +16,7 @@ where + num::One, { let context = context.to_string(); - stateless("in_range", move |g, mut obj| { + lambda_unit("in_range", move |g, mut obj| { if !range.contains(&obj) { let rand = g.arbitrary(|| { format!( diff --git a/src/facts/in_slice.rs b/src/facts/in_slice.rs index d65d1b7..777b807 100644 --- a/src/facts/in_slice.rs +++ b/src/facts/in_slice.rs @@ -1,12 +1,12 @@ use super::*; /// Specifies a membership constraint -pub fn in_slice<'a, T>(context: impl ToString, slice: &'a [T]) -> StatelessFact<'a, T> +pub fn in_slice<'a, T>(context: impl ToString, slice: &'a [T]) -> LambdaUnit<'a, T> where T: Target<'a> + PartialEq + Clone, { let context = context.to_string(); - stateless("in_slice", move |g, obj| { + lambda_unit("in_slice", move |g, obj| { Ok(if !slice.contains(&obj) { let reason = || { format!( @@ -22,7 +22,7 @@ where } /// Specifies a membership constraint -pub fn in_slice_<'a, T>(slice: &'a [T]) -> StatelessFact<'a, T> +pub fn in_slice_<'a, T>(slice: &'a [T]) -> LambdaUnit<'a, T> where T: Target<'a> + PartialEq + Clone, { diff --git a/src/facts/lens.rs b/src/facts/lens.rs index 48bca9c..ed6e5c5 100644 --- a/src/facts/lens.rs +++ b/src/facts/lens.rs @@ -1,8 +1,7 @@ -use std::{marker::PhantomData, sync::Arc}; - use crate::*; -/// Lifts a Fact about a subset of some data into a Fact about the superset. +/// Lifts a Fact about a subset of some data into a Fact about the superset, +/// using a single function to specify a getter/setter pair. /// /// In other words, if type `O` contains a `T`, and you have a `Fact<'a, T>`, /// `LensFact` lets you lift that fact into a `Fact<'a, O>`. @@ -38,8 +37,8 @@ use crate::*; pub fn lens1<'a, O, T, L>( label: impl ToString, accessor: L, - inner_fact: impl Factual<'a, T>, -) -> impl Factual<'a, O> + inner_fact: impl Fact<'a, T>, +) -> impl Fact<'a, O> where O: Target<'a>, T: Target<'a>, @@ -52,21 +51,28 @@ where *r = t; o }; - lens2(label, getter, setter, inner_fact).label("lens1") + lens2(label, getter, setter, inner_fact).labeled("lens1") } +/// Lifts a Fact about a subset of some data into a Fact about the superset, using +/// explicit getter and setter functions. +/// +/// This is a more general version of [`lens1`]. This can be useful particularly +/// when the setter requires modifications other than replacing the item specified +/// by the getter, for instance if your data contains some kind of digest of the data +/// being focused on, then the digest must also be recomputed when the focus is modified. pub fn lens2<'a, O, T>( label: impl ToString, getter: impl 'a + Clone + Send + Sync + Fn(O) -> T, setter: impl 'a + Clone + Send + Sync + Fn(O, T) -> O, - inner_fact: impl Factual<'a, T>, -) -> impl Factual<'a, O> + inner_fact: impl Fact<'a, T>, +) -> impl Fact<'a, O> where O: Target<'a>, T: Target<'a>, { let label = label.to_string(); - stateful("lens", inner_fact, move |g, fact, obj: O| { + lambda("lens", inner_fact, move |g, fact, obj: O| { let t = getter(obj.clone()); let t = fact .mutate(g, t) @@ -75,23 +81,6 @@ where }) } -// impl<'a, O, T, F> Factual<'a, O> for LensFact<'a, O, T, F> -// where -// T: Bounds<'a>, -// O: Bounds<'a> + Clone, -// F: Factual<'a, T>, -// { -// #[tracing::instrument(fields(fact = "lens"), skip(self, g))] -// fn mutate(&mut self, g: &mut Generator<'a>, obj: O) -> Mutation { -// let t = (self.getter)(obj.clone()); -// let t = self -// .inner_fact -// .mutate(g, t) -// .map_check_err(|err| format!("lens1({}) > {}", self.label, err))?; -// Ok((self.setter)(obj, t)) -// } -// } - #[cfg(test)] mod tests { use super::*; diff --git a/src/facts/not.rs b/src/facts/not.rs index 581b4fa..af3af83 100644 --- a/src/facts/not.rs +++ b/src/facts/not.rs @@ -4,11 +4,11 @@ use super::*; // TODO: `not` in particular would really benefit from Facts having accessible // labels, since currently you can only get context about why a `not` fact passed, // not why it fails. -pub fn not<'a, T>(fact: StatelessFact<'a, T>) -> StatelessFact<'a, T> +pub fn not<'a, T>(fact: LambdaUnit<'a, T>) -> LambdaUnit<'a, T> where T: Target<'a>, { - stateless("not", move |g, obj| { + lambda_unit("not", move |g, obj| { let label = format!("not({:?})", fact); let fact = fact.clone(); brute(label, move |o| fact.clone().check(o).is_err()).mutate(g, obj) @@ -18,7 +18,7 @@ where // /// Negates a fact, with no context given // pub fn not<'a, F, T>(fact: F) -> Fact<'a, (), T> // where -// F: 'a + Factual<'a, T>, +// F: 'a + Fact<'a, T>, // T: Bounds<'a>, // { // not("not", fact) diff --git a/src/facts/optical.rs b/src/facts/optical.rs index d3dafbf..f95754d 100644 --- a/src/facts/optical.rs +++ b/src/facts/optical.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use arbitrary::Unstructured; use lens_rs::*; -use crate::{fact::Bounds, Fact, *}; +use crate::{fact::Target, Fact, *}; pub fn optical<'a, Src, Img, Optics, F, L>( label: L, @@ -11,8 +11,8 @@ pub fn optical<'a, Src, Img, Optics, F, L>( inner_fact: F, ) -> OpticalFact<'a, Src, Img, Optics, F> where - Src: Bounds<'a> + Lens, - Img: Bounds<'a> + Clone, + Src: Target<'a> + Lens, + Img: Target<'a>, Optics: Clone + std::fmt::Debug, F: Fact<'a, Img>, L: ToString, @@ -24,8 +24,8 @@ where #[derive(Clone)] pub struct OpticalFact<'a, Src, Img, Optics, F> where - Src: Bounds<'a> + Lens, - Img: Bounds<'a> + Clone, + Src: Target<'a> + Lens, + Img: Target<'a>, Optics: Clone + std::fmt::Debug, F: Fact<'a, Img>, { @@ -41,8 +41,8 @@ where impl<'a, Src, Img, Optics, F> OpticalFact<'a, Src, Img, Optics, F> where - Src: Bounds<'a> + Lens, - Img: Bounds<'a> + Clone, + Src: Target<'a> + Lens, + Img: Target<'a>, Optics: Clone + std::fmt::Debug, F: Fact<'a, Img>, { @@ -58,8 +58,8 @@ where impl<'a, Src, Img, Optics, F> Fact<'a, Src> for OpticalFact<'a, Src, Img, Optics, F> where - Src: Bounds<'a> + Lens, - Img: Bounds<'a> + Clone, + Src: Target<'a> + Lens, + Img: Target<'a>, Optics: Clone + std::fmt::Debug, F: Fact<'a, Img>, { diff --git a/src/facts/or.rs b/src/facts/or.rs index c1bd5ff..b5ca656 100644 --- a/src/facts/or.rs +++ b/src/facts/or.rs @@ -1,13 +1,11 @@ use super::*; /// Combines two constraints so that either one may be satisfied -pub fn or<'a, A, B, T>(a: Fact<'a, A, T>, b: Fact<'a, B, T>) -> Fact<'a, Fact2<'a, A, B, T>, T> +pub fn or<'a, T>(a: impl Fact<'a, T>, b: impl Fact<'a, T>) -> impl Fact<'a, T> where T: Target<'a>, - A: State, - B: State, { - stateful("or", (a, b), |g, (a, b), obj| { + lambda("or", (a, b), |g, (a, b), obj| { use rand::{thread_rng, Rng}; let a_ok = a.clone().check(&obj).is_ok(); diff --git a/src/facts/prism.rs b/src/facts/prism.rs index 93c0a2e..528a023 100644 --- a/src/facts/prism.rs +++ b/src/facts/prism.rs @@ -1,5 +1,3 @@ -use std::{marker::PhantomData, sync::Arc}; - use crate::*; /// Lifts a Fact about some *optional* subset of data into a Fact about the @@ -62,15 +60,15 @@ use crate::*; pub fn prism<'a, O, T, P>( label: impl ToString, prism: P, - inner_fact: impl Factual<'a, T>, -) -> impl Factual<'a, O> + inner_fact: impl Fact<'a, T>, +) -> impl Fact<'a, O> where O: Target<'a>, T: Target<'a>, P: 'a + Send + Sync + Fn(&mut O) -> Option<&mut T>, { let label = label.to_string(); - stateful("prism", inner_fact, move |g, fact, mut obj| { + lambda("prism", inner_fact, move |g, fact, mut obj| { if let Some(t) = prism(&mut obj) { *t = fact .mutate(g, t.clone()) diff --git a/src/facts/same.rs b/src/facts/same.rs index 97b9b62..ea54310 100644 --- a/src/facts/same.rs +++ b/src/facts/same.rs @@ -1,11 +1,11 @@ use super::{brute::brute_labeled, *}; /// Specifies an equality constraint between two items in a tuple -pub fn same<'a, T>() -> StatelessFact<'a, (T, T)> +pub fn same<'a, T>() -> LambdaUnit<'a, (T, T)> where T: Target<'a> + PartialEq, { - stateless("same", |g, mut obj: (T, T)| { + lambda_unit("same", |g, mut obj: (T, T)| { let o = obj.clone(); let reason = move || format!("must be same: expected {:?} == {:?}", o.0.clone(), o.1); g.set(&mut obj.0, &obj.1, reason)?; @@ -14,7 +14,7 @@ where } /// Specifies an inequality constraint between two items in a tuple -pub fn different<'a, T>() -> StatelessFact<'a, (T, T)> +pub fn different<'a, T>() -> LambdaUnit<'a, (T, T)> where T: Target<'a> + PartialEq, { diff --git a/src/facts/seq.rs b/src/facts/seq.rs index e0facb4..3659eef 100644 --- a/src/facts/seq.rs +++ b/src/facts/seq.rs @@ -5,8 +5,6 @@ //! of internally inconsistent facts, then the facts must be "rolled back" for the next //! `satisfy()` attempt. -use std::marker::PhantomData; - use crate::*; use super::and; @@ -37,11 +35,11 @@ use super::and; /// let list = fact.clone().satisfy(&mut g, vec![0; 5]).unwrap(); /// assert_eq!(list, vec![0, 1, 2, 3, 4]); /// ``` -pub fn vec<'a, T>(inner_fact: impl Factual<'a, T>) -> impl Factual<'a, Vec> +pub fn vec<'a, T>(inner_fact: impl Fact<'a, T>) -> impl Fact<'a, Vec> where T: Target<'a> + Clone, { - stateful("vec", inner_fact, |g, f, obj: Vec| { + lambda("vec", inner_fact, |g, f, obj: Vec| { obj.into_iter() .enumerate() .map(|(i, o)| { @@ -53,11 +51,11 @@ where } /// Checks that a Vec is of a given length -pub fn vec_len<'a, T>(len: usize) -> StatelessFact<'a, Vec> +pub fn vec_len<'a, T>(len: usize) -> LambdaUnit<'a, Vec> where T: Target<'a> + Clone + 'a, { - stateless("vec_len", move |g, mut obj: Vec| { + lambda_unit("vec_len", move |g, mut obj: Vec| { if obj.len() > len { g.fail(format!( "vec should be of length {} but is actually of length {}", @@ -80,7 +78,7 @@ where } /// Combines a LenFact with a VecFact to ensure that the vector is of a given length -pub fn vec_of_length<'a, T>(len: usize, inner_fact: impl Factual<'a, T>) -> impl Factual<'a, Vec> +pub fn vec_of_length<'a, T>(len: usize, inner_fact: impl Fact<'a, T>) -> impl Fact<'a, Vec> where T: Target<'a> + 'a, { @@ -151,7 +149,7 @@ mod tests { let piecewise = move || { let count = Arc::new(AtomicU8::new(0)); - stateful("piecewise", (), move |g, (), mut obj| { + lambda("piecewise", (), move |g, (), mut obj| { let c = count.fetch_add(1, Ordering::SeqCst); if c < 3 { g.set(&mut obj, &999, || "i'm being difficult, haha")?; diff --git a/src/factual.rs b/src/factual.rs deleted file mode 100644 index d0e9f86..0000000 --- a/src/factual.rs +++ /dev/null @@ -1,145 +0,0 @@ -use arbitrary::*; -use either::Either; - -use crate::*; - -/// The trait bounds for the target of a Fact -pub trait Target<'a>: - 'a + std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> -{ -} -impl<'a, T> Target<'a> for T where - T: 'a + std::fmt::Debug + Clone + Send + Sync + PartialEq + Arbitrary<'a> -{ -} - -/// The trait bounds for the State of a Fact -pub trait State: std::fmt::Debug + Clone + Send + Sync {} -impl State for T where T: std::fmt::Debug + Clone + Send + Sync {} - -/// A declarative representation of a constraint on some data, which can be -/// used to both make an assertion (check) or to mold some arbitrary existing -/// data into a shape which passes that same assertion (mutate) -pub trait Factual<'a, T>: Send + Sync + Clone + std::fmt::Debug -where - T: Target<'a>, -{ - fn label(self, label: impl ToString) -> Self; - - /// Assert that the constraint is satisfied for given data. - /// - /// If the mutation function is written properly, we get a check for free - /// by using a special Generator which fails upon mutation. If this is for - /// some reason unreasonable, a check function can be written by hand, but - /// care must be taken to make sure it perfectly lines up with the mutation function. - #[tracing::instrument(fields(fact_impl = "Fact"), skip(self))] - fn check(mut self, obj: &T) -> Check { - let mut g = Generator::checker(); - Check::from_mutation(self.mutate(&mut g, obj.clone())) - } - - /// Apply a mutation which moves the obj closer to satisfying the overall - /// constraint. - // #[tracing::instrument(skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation; - - /// Make this many attempts to satisfy a constraint before giving up and panicking. - /// - /// If you are combining highly contentious facts together and relying on randomness - /// to find a solution, this limit may need to be higher. In general, you should try - /// to write facts that don't interfere with each other so that the constraint can be - /// met on the first attempt, or perhaps the second or third. If necessary, this can - /// be raised to lean more on random search. - fn satisfy_attempts(&self) -> usize { - SATISFY_ATTEMPTS - } - - /// Mutate a value such that it satisfies the constraint. - /// If the constraint cannot be satisfied, panic. - #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] - fn satisfy(&mut self, g: &mut Generator<'a>, obj: T) -> ContrafactResult { - tracing::trace!("satisfy"); - let mut last_failure: Vec = vec![]; - let mut next = obj.clone(); - for _i in 0..self.satisfy_attempts() { - let mut m = self.clone(); - next = m.mutate(g, next).unwrap(); - if let Err(errs) = self.clone().check(&next).result()? { - last_failure = errs; - } else { - *self = m; - return Ok(next); - } - } - panic!( - "Could not satisfy a constraint even after {} attempts. Last check failure: {:?}", - SATISFY_ATTEMPTS, last_failure - ); - } - - #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] - /// Build a new value such that it satisfies the constraint - fn build_fallible(mut self, g: &mut Generator<'a>) -> ContrafactResult { - let obj = T::arbitrary(g).unwrap(); - self.satisfy(g, obj) - } - - /// Build a new value such that it satisfies the constraint, panicking on error - #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] - fn build(self, g: &mut Generator<'a>) -> T { - self.build_fallible(g).unwrap() - } -} - -impl<'a, T, F1, F2> Factual<'a, T> for Either -where - T: Target<'a>, - F1: Factual<'a, T> + ?Sized, - F2: Factual<'a, T> + ?Sized, -{ - #[tracing::instrument(fields(fact_impl = "Either"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - match self { - Either::Left(f) => f.mutate(g, obj), - Either::Right(f) => f.mutate(g, obj), - } - } - - fn label(self, label: impl ToString) -> Self { - match self { - Either::Left(f) => Either::Left(f.label(label)), - Either::Right(f) => Either::Right(f.label(label)), - } - } -} - -// #[tracing::instrument(skip(fact))] -// pub(crate) fn check_raw<'a, T, F: Factual<'a, T>>(fact: &mut F, obj: &T) -> Check -// where -// T: Target<'a> + ?Sized, -// F: Factual<'a, T> + ?Sized, -// { -// let mut g = Generator::checker(); -// Check::from_mutation(fact.mutate(&mut g, obj.clone())) -// } - -#[tracing::instrument(skip(facts))] -fn collect_checks<'a, T, F>(facts: Vec, obj: &T) -> Check -where - T: Target<'a>, - F: Factual<'a, T>, -{ - let checks = facts - .into_iter() - .enumerate() - .map(|(i, f)| { - Ok(f.check(obj) - .failures()? - .iter() - .map(|e| format!("fact {}: {}", i, e)) - .collect()) - }) - .collect::>>>() - .map(|fs| fs.into_iter().flatten().collect()); - Check::from_result(checks) -} diff --git a/src/lambda.rs b/src/lambda.rs new file mode 100644 index 0000000..d27dbd9 --- /dev/null +++ b/src/lambda.rs @@ -0,0 +1,132 @@ +use std::{fmt::Debug, marker::PhantomData, sync::Arc}; + +use crate::*; + +/// Create a fact from a bare function which specifies the mutation. +/// Can be quicker to experiment with ideas this way than to have to directly implement +/// the [`Fact`] trait +/// +/// ``` +/// use contrafact::*; +/// let mut g = utils::random_generator(); +/// +/// let mut fact = vec_of_length( +/// 4, +/// lambda("geometric series", 2, move |g, s, mut v| { +/// g.set(&mut v, s, || "value is not geometrically increasing by 2")?; +/// *s *= 2; +/// Ok(v) +/// }), +/// ); +/// +/// let list = fact.clone().build(&mut g); +/// fact.check(&list).unwrap(); +/// assert_eq!(list, vec![2, 4, 8, 16]); +/// ``` +pub fn lambda<'a, S, T>( + label: impl ToString, + state: S, + f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, &mut S, T) -> Mutation, +) -> Lambda<'a, S, T> +where + S: State, + T: Target<'a>, +{ + Lambda { + label: label.to_string(), + state, + fun: Arc::new(f), + _phantom: PhantomData, + } +} + +/// Create a lambda with unit state +pub fn lambda_unit<'a, T>( + label: impl ToString, + f: impl 'a + Send + Sync + Fn(&mut Generator<'a>, T) -> Mutation, +) -> Lambda<'a, (), T> +where + T: Target<'a>, +{ + lambda(label, (), move |g, (), obj| f(g, obj)) +} + +pub type LambdaFn<'a, S, T> = + Arc, &mut S, T) -> Mutation>; + +#[derive(Clone)] +pub struct Lambda<'a, S, T> +where + S: State, + T: Target<'a>, +{ + state: S, + fun: LambdaFn<'a, S, T>, + label: String, + _phantom: PhantomData<&'a T>, +} + +/// A Lambda with unit state +pub type LambdaUnit<'a, T> = Lambda<'a, (), T>; + +impl<'a, S, T> std::fmt::Debug for Lambda<'a, S, T> +where + S: State + Debug, + T: Target<'a>, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Lambda") + .field("label", &self.label) + .field("state", &self.state) + .finish() + } +} + +impl<'a, S, T> Fact<'a, T> for Lambda<'a, S, T> +where + S: State + Debug, + T: Target<'a>, +{ + fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { + (self.fun)(g, &mut self.state, obj) + } + + fn label(&self) -> String { + self.label.clone() + } + + fn labeled(mut self, label: impl ToString) -> Self { + self.label = label.to_string(); + self + } +} + +#[test] +fn test_lambda_fact() { + use crate::facts::*; + let mut g = utils::random_generator(); + + let geom = |k, s| { + lambda("geom", s, move |g, s, mut v| { + g.set(&mut v, s, || { + format!("value is not geometrically increasing by {k} starting from {s}") + })?; + *s *= k; + Ok(v) + }) + }; + + let fact = |k, s| vec_of_length(4, geom(k, s)); + + { + let list = fact(2, 2).build(&mut g); + assert_eq!(list, vec![2, 4, 8, 16]); + fact(2, 2).check(&list).unwrap(); + } + + { + let list = fact(3, 4).build(&mut g); + assert_eq!(list, vec![4, 12, 36, 108]); + fact(3, 4).check(&list).unwrap(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 31e3cb9..54fa798 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ //! meets the constraint, or to generate new instances of `S` which meet the constraint. //! //! ``` -//! use contrafact::{Factual, facts::{eq, lens1}}; +//! use contrafact::{Fact, facts::{eq, lens1}}; //! use arbitrary::{Arbitrary, Unstructured}; //! //! #[derive(Debug, Clone, PartialEq, Arbitrary)] @@ -63,8 +63,8 @@ mod error; mod fact; /// Some built-in implementations of some useful facts pub mod facts; -mod factual; mod generator; +mod lambda; pub use facts::*; #[cfg(feature = "utils")] @@ -74,9 +74,11 @@ pub use arbitrary; pub use check::Check; pub use error::*; -pub use fact::{stateful, stateless, Fact, Fact2, StatelessFact}; -pub use factual::{Factual, State, Target}; +pub use fact::{Fact, State, Target}; pub use generator::*; +pub use lambda::{lambda, lambda_unit}; + +pub(crate) use lambda::{Lambda, LambdaUnit}; pub use either; diff --git a/tests/chainlink.rs b/tests/chainlink.rs index 4ccf664..f124bcb 100644 --- a/tests/chainlink.rs +++ b/tests/chainlink.rs @@ -23,7 +23,7 @@ struct Wrapper { /// Fact: all Links in a chain are by the same `author`, and any chain link has /// consecutive `prev` values starting with 0. -fn chain_fact<'a>(author: String) -> impl Factual<'a, Link> { +fn chain_fact<'a>(author: String) -> impl Fact<'a, Link> { facts![ lens1("Link::author", |o: &mut Link| &mut o.author, eq(author),), lens1( @@ -36,7 +36,7 @@ fn chain_fact<'a>(author: String) -> impl Factual<'a, Link> { /// Fact: the Links within each wrapper form a valid chain, and the color /// of the wrapper is in the given set. -fn wrapper_fact<'a>(author: String, valid_colors: &'a [Color]) -> impl Factual<'a, Wrapper> { +fn wrapper_fact<'a>(author: String, valid_colors: &'a [Color]) -> impl Fact<'a, Wrapper> { facts![ lens1( "Wrapper::color", diff --git a/tests/complex.rs b/tests/complex.rs index 934c38a..1742128 100644 --- a/tests/complex.rs +++ b/tests/complex.rs @@ -130,11 +130,11 @@ impl AlphaSigner { } #[allow(unused)] -fn alpha_fact() -> impl Factual<'static, Alpha> { +fn alpha_fact() -> impl Fact<'static, Alpha> { facts![lens1("Alpha::id", |a: &mut Alpha| a.id(), id_fact(None))] } -fn beta_fact() -> impl Factual<'static, Beta> { +fn beta_fact() -> impl Fact<'static, Beta> { facts![lens1("Beta::id", |a: &mut Beta| &mut a.id, id_fact(None))] } @@ -143,7 +143,7 @@ fn beta_fact() -> impl Factual<'static, Beta> { #[derive(Clone, Debug, PartialEq, Arbitrary)] struct Pi(Alpha, Option); -fn pi_beta_match() -> impl Factual<'static, Pi> { +fn pi_beta_match() -> impl Fact<'static, Pi> { facts![brute( "Pi alpha has matching beta iff beta is Some", |p: &Pi| match p { @@ -154,7 +154,7 @@ fn pi_beta_match() -> impl Factual<'static, Pi> { )] } -fn id_fact(id: Option) -> impl Factual<'static, Id> { +fn id_fact(id: Option) -> impl Fact<'static, Id> { let le = brute("< u32::MAX", |id: &Id| *id < Id::MAX / 2); if let Some(id) = id { @@ -166,7 +166,7 @@ fn id_fact(id: Option) -> impl Factual<'static, Id> { /// - id must be set as specified /// - All Ids should match each other. If there is a Beta, its id should match too. -fn pi_fact(id: Id) -> impl Factual<'static, Pi> { +fn pi_fact(id: Id) -> impl Fact<'static, Pi> { let alpha_fact = facts![ lens1("Alpha::id", |a: &mut Alpha| a.id(), id_fact(Some(id))), // lens1("Alpha::data", |a: &mut Alpha| a.data(), eq(data)), @@ -184,7 +184,7 @@ fn pi_fact(id: Id) -> impl Factual<'static, Pi> { /// - If Omega::AlphaBeta, then Alpha::Beta, /// - and, the the Betas of the Alpha and the Omega should match. /// - all data must be set as specified -fn omega_fact(id: Id) -> impl Factual<'static, Omega> { +fn omega_fact(id: Id) -> impl Fact<'static, Omega> { let omega_pi = lens2( "Omega -> Pi", |o| match o { @@ -208,7 +208,7 @@ fn omega_fact(id: Id) -> impl Factual<'static, Omega> { } #[allow(unused)] -fn sigma_fact() -> impl Factual<'static, Sigma> { +fn sigma_fact() -> impl Fact<'static, Sigma> { let id2_fact = lens2( "Sigma::id is correct", |mut s: Sigma| (s.id2, *(s.alpha.id()) * 2), @@ -235,7 +235,7 @@ fn sigma_fact() -> impl Factual<'static, Sigma> { /// The inner Sigma is correct wrt to signature /// XXX: this is a little wonky, probably room for improvement. -fn rho_fact(id: Id, signer: AlphaSigner) -> impl Factual<'static, Rho> { +fn rho_fact(id: Id, signer: AlphaSigner) -> impl Fact<'static, Rho> { let rho_pi = lens2( "Rho -> Pi", |rho: Rho| Pi(rho.sigma.alpha, rho.beta), From 410fca63c21a3c91776908dbbc20ef3f82cbed85 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 15:45:39 -0700 Subject: [PATCH 18/21] Rename obj -> t --- TODO.md | 2 -- src/fact.rs | 26 +++++++++++++------------- src/facts/and.rs | 8 ++++---- src/facts/brute.rs | 8 ++++---- src/facts/consecutive_int.rs | 8 ++++---- src/facts/constant.rs | 6 +++--- src/facts/eq.rs | 10 +++++----- src/facts/in_range.rs | 10 +++++----- src/facts/in_slice.rs | 8 ++++---- src/facts/lens.rs | 6 +++--- src/facts/not.rs | 4 ++-- src/facts/optical.rs | 14 +++++++------- src/facts/or.rs | 14 +++++++------- src/facts/prism.rs | 6 +++--- src/facts/same.rs | 8 ++++---- src/facts/seq.rs | 26 +++++++++++++------------- src/lambda.rs | 6 +++--- 17 files changed, 84 insertions(+), 86 deletions(-) diff --git a/TODO.md b/TODO.md index 67678a0..d998569 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,2 @@ -- [ ] Fix generator fns -- [ ] Make Fact a builder which takes a LambdaFact-style function, and can provide a label or label modifier - [ ] Take some methods off of the trait (satisfy, build) \ No newline at end of file diff --git a/src/fact.rs b/src/fact.rs index 5c2da93..02d3182 100644 --- a/src/fact.rs +++ b/src/fact.rs @@ -37,15 +37,15 @@ where /// some reason unreasonable, a check function can be written by hand, but /// care must be taken to make sure it perfectly lines up with the mutation function. #[tracing::instrument(fields(fact_impl = "Fact"), skip(self))] - fn check(mut self, obj: &T) -> Check { + fn check(mut self, t: &T) -> Check { let mut g = Generator::checker(); - Check::from_mutation(self.mutate(&mut g, obj.clone())) + Check::from_mutation(self.mutate(&mut g, t.clone())) } - /// Apply a mutation which moves the obj closer to satisfying the overall + /// Apply a mutation which moves the t closer to satisfying the overall /// constraint. // #[tracing::instrument(skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation; + fn mutate(&mut self, g: &mut Generator<'a>, t: T) -> Mutation; /// Make this many attempts to satisfy a constraint before giving up and panicking. /// @@ -61,10 +61,10 @@ where /// Mutate a value such that it satisfies the constraint. /// If the constraint cannot be satisfied, panic. #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] - fn satisfy(&mut self, g: &mut Generator<'a>, obj: T) -> ContrafactResult { + fn satisfy(&mut self, g: &mut Generator<'a>, t: T) -> ContrafactResult { tracing::trace!("satisfy"); let mut last_failure: Vec = vec![]; - let mut next = obj.clone(); + let mut next = t.clone(); for _i in 0..self.satisfy_attempts() { let mut m = self.clone(); next = m.mutate(g, next).unwrap(); @@ -84,8 +84,8 @@ where #[tracing::instrument(fields(fact_impl = "Fact"), skip(self, g))] /// Build a new value such that it satisfies the constraint fn build_fallible(mut self, g: &mut Generator<'a>) -> ContrafactResult { - let obj = T::arbitrary(g).unwrap(); - self.satisfy(g, obj) + let t = T::arbitrary(g).unwrap(); + self.satisfy(g, t) } /// Build a new value such that it satisfies the constraint, panicking on error @@ -102,10 +102,10 @@ where F2: Fact<'a, T> + ?Sized, { #[tracing::instrument(fields(fact_impl = "Either"), skip(self, g))] - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { + fn mutate(&mut self, g: &mut Generator<'a>, t: T) -> Mutation { match self { - Either::Left(f) => f.mutate(g, obj), - Either::Right(f) => f.mutate(g, obj), + Either::Left(f) => f.mutate(g, t), + Either::Right(f) => f.mutate(g, t), } } @@ -125,7 +125,7 @@ where } #[tracing::instrument(skip(facts))] -fn collect_checks<'a, T, F>(facts: Vec, obj: &T) -> Check +fn collect_checks<'a, T, F>(facts: Vec, t: &T) -> Check where T: Target<'a>, F: Fact<'a, T>, @@ -134,7 +134,7 @@ where .into_iter() .enumerate() .map(|(i, f)| { - Ok(f.check(obj) + Ok(f.check(t) .failures()? .iter() .map(|e| format!("fact {}: {}", i, e)) diff --git a/src/facts/and.rs b/src/facts/and.rs index dd3a118..a6f609a 100644 --- a/src/facts/and.rs +++ b/src/facts/and.rs @@ -5,9 +5,9 @@ pub fn and<'a, T>(a: impl Fact<'a, T>, b: impl Fact<'a, T>) -> impl Fact<'a, T> where T: Target<'a>, { - lambda("and", (a, b), |g, (a, b), obj| { - let obj = a.mutate(g, obj)?; - let obj = b.mutate(g, obj)?; - Ok(obj) + lambda("and", (a, b), |g, (a, b), t| { + let t = a.mutate(g, t)?; + let t = b.mutate(g, t)?; + Ok(t) }) } diff --git a/src/facts/brute.rs b/src/facts/brute.rs index c0219e3..9f4077f 100644 --- a/src/facts/brute.rs +++ b/src/facts/brute.rs @@ -49,14 +49,14 @@ where T: Target<'a>, F: 'a + Send + Sync + Fn(&T) -> ContrafactResult, { - lambda_unit("brute_labeled", move |g, mut obj| { + lambda_unit("brute_labeled", move |g, mut t| { let mut last_reason = "".to_string(); for _ in 0..=BRUTE_ITERATION_LIMIT { - if let Err(reason) = f(&obj)? { + if let Err(reason) = f(&t)? { last_reason = reason.clone(); - obj = g.arbitrary(|| reason)?; + t = g.arbitrary(|| reason)?; } else { - return Ok(obj); + return Ok(t); } } diff --git a/src/facts/consecutive_int.rs b/src/facts/consecutive_int.rs index db36d7e..156b654 100644 --- a/src/facts/consecutive_int.rs +++ b/src/facts/consecutive_int.rs @@ -6,13 +6,13 @@ where S: Target<'a> + std::fmt::Debug + PartialEq + num::PrimInt, { let context = context.to_string(); - lambda("consecutive_int", initial, move |g, counter, mut obj| { - if obj != *counter { + lambda("consecutive_int", initial, move |g, counter, mut t| { + if t != *counter { g.fail(&context)?; - obj = counter.clone(); + t = counter.clone(); } *counter = counter.checked_add(&S::from(1).unwrap()).unwrap(); - Ok(obj) + Ok(t) }) } diff --git a/src/facts/constant.rs b/src/facts/constant.rs index 4dea1af..fca0e76 100644 --- a/src/facts/constant.rs +++ b/src/facts/constant.rs @@ -2,14 +2,14 @@ use super::*; /// A constraint which is always met pub fn always<'a, T: Target<'a>>() -> Lambda<'a, (), T> { - lambda_unit("always", |_, obj| Ok(obj)) + lambda_unit("always", |_, t| Ok(t)) } /// A constraint which is never met pub fn never<'a, T: Target<'a>>(context: impl ToString) -> Lambda<'a, (), T> { let context = context.to_string(); - lambda_unit("never", move |g, obj: T| { + lambda_unit("never", move |g, t: T| { g.fail(context.clone())?; - Ok(obj) + Ok(t) }) } diff --git a/src/facts/eq.rs b/src/facts/eq.rs index bab1a45..ab4de2a 100644 --- a/src/facts/eq.rs +++ b/src/facts/eq.rs @@ -8,12 +8,12 @@ where T: Target<'a> + PartialEq + Clone + Display, { let label = format!("eq({})", constant); - lambda_unit(label, move |g, mut obj| { - if obj != constant { - g.fail(format!("expected {:?} == {:?}", obj, constant))?; - obj = constant.clone(); + lambda_unit(label, move |g, mut t| { + if t != constant { + g.fail(format!("expected {:?} == {:?}", t, constant))?; + t = constant.clone(); } - Ok(obj) + Ok(t) }) } diff --git a/src/facts/in_range.rs b/src/facts/in_range.rs index 25a480e..dbd7f0d 100644 --- a/src/facts/in_range.rs +++ b/src/facts/in_range.rs @@ -16,15 +16,15 @@ where + num::One, { let context = context.to_string(); - lambda_unit("in_range", move |g, mut obj| { - if !range.contains(&obj) { + lambda_unit("in_range", move |g, mut t| { + if !range.contains(&t) { let rand = g.arbitrary(|| { format!( "{}: expected {:?} to be contained in {:?}", - context, obj, range + context, t, range ) })?; - obj = match (range.start_bound(), range.end_bound()) { + t = match (range.start_bound(), range.end_bound()) { (Bound::Unbounded, Bound::Unbounded) => rand, (Bound::Included(a), Bound::Included(b)) if b.clone() - a.clone() >= T::one() => { a.clone() + rand.rem_euclid(&(b.clone() - a.clone())) @@ -44,7 +44,7 @@ where _ => panic!("Range not yet supported, sorry! {:?}", range), }; } - Ok(obj) + Ok(t) }) } diff --git a/src/facts/in_slice.rs b/src/facts/in_slice.rs index 777b807..e989f57 100644 --- a/src/facts/in_slice.rs +++ b/src/facts/in_slice.rs @@ -6,17 +6,17 @@ where T: Target<'a> + PartialEq + Clone, { let context = context.to_string(); - lambda_unit("in_slice", move |g, obj| { - Ok(if !slice.contains(&obj) { + lambda_unit("in_slice", move |g, t| { + Ok(if !slice.contains(&t) { let reason = || { format!( "{}: expected {:?} to be contained in {:?}", - context, obj, slice + context, t, slice ) }; g.choose(slice, reason)?.to_owned() } else { - obj + t }) }) } diff --git a/src/facts/lens.rs b/src/facts/lens.rs index ed6e5c5..77758c7 100644 --- a/src/facts/lens.rs +++ b/src/facts/lens.rs @@ -72,12 +72,12 @@ where T: Target<'a>, { let label = label.to_string(); - lambda("lens", inner_fact, move |g, fact, obj: O| { - let t = getter(obj.clone()); + lambda("lens", inner_fact, move |g, fact, o: O| { + let t = getter(o.clone()); let t = fact .mutate(g, t) .map_check_err(|err| format!("lens1({}) > {}", label, err))?; - Ok(setter(obj, t)) + Ok(setter(o, t)) }) } diff --git a/src/facts/not.rs b/src/facts/not.rs index af3af83..b71696f 100644 --- a/src/facts/not.rs +++ b/src/facts/not.rs @@ -8,10 +8,10 @@ pub fn not<'a, T>(fact: LambdaUnit<'a, T>) -> LambdaUnit<'a, T> where T: Target<'a>, { - lambda_unit("not", move |g, obj| { + lambda_unit("not", move |g, t| { let label = format!("not({:?})", fact); let fact = fact.clone(); - brute(label, move |o| fact.clone().check(o).is_err()).mutate(g, obj) + brute(label, move |o| fact.clone().check(o).is_err()).mutate(g, t) }) } diff --git a/src/facts/optical.rs b/src/facts/optical.rs index f95754d..e9e2bb3 100644 --- a/src/facts/optical.rs +++ b/src/facts/optical.rs @@ -65,8 +65,8 @@ where { // TODO: remove #[tracing::instrument(skip(self))] - fn check(&mut self, obj: &Src) -> Check { - let imgs = obj.traverse_ref(self.optics.clone()); + fn check(&mut self, t: &Src) -> Check { + let imgs = t.traverse_ref(self.optics.clone()); imgs.iter() .enumerate() .flat_map(|(i, img)| { @@ -84,16 +84,16 @@ where .into() } - fn mutate(&mut self, mut obj: Src, g: &mut Generator<'a>) -> Src { - for img in obj.traverse_mut(self.optics.clone()) { + fn mutate(&mut self, mut t: Src, g: &mut Generator<'a>) -> Src { + for img in t.traverse_mut(self.optics.clone()) { *img = self.inner_fact.mutate(img.clone(), g); } - obj + t } #[tracing::instrument(skip(self))] - fn advance(&mut self, obj: &Src) { - for img in obj.traverse_ref(self.optics.clone()) { + fn advance(&mut self, t: &Src) { + for img in t.traverse_ref(self.optics.clone()) { self.inner_fact.advance(img); } } diff --git a/src/facts/or.rs b/src/facts/or.rs index b5ca656..f42299a 100644 --- a/src/facts/or.rs +++ b/src/facts/or.rs @@ -5,23 +5,23 @@ pub fn or<'a, T>(a: impl Fact<'a, T>, b: impl Fact<'a, T>) -> impl Fact<'a, T> where T: Target<'a>, { - lambda("or", (a, b), |g, (a, b), obj| { + lambda("or", (a, b), |g, (a, b), t| { use rand::{thread_rng, Rng}; - let a_ok = a.clone().check(&obj).is_ok(); - let b_ok = b.clone().check(&obj).is_ok(); + let a_ok = a.clone().check(&t).is_ok(); + let b_ok = b.clone().check(&t).is_ok(); match (a_ok, b_ok) { - (true, _) => Ok(obj), - (_, true) => Ok(obj), + (true, _) => Ok(t), + (_, true) => Ok(t), (false, false) => { g.fail(format!( "expected either one of the following conditions to be met: {:?} OR {:?}", a, b ))?; if thread_rng().gen::() { - a.mutate(g, obj) + a.mutate(g, t) } else { - b.mutate(g, obj) + b.mutate(g, t) } } } diff --git a/src/facts/prism.rs b/src/facts/prism.rs index 528a023..8c0f982 100644 --- a/src/facts/prism.rs +++ b/src/facts/prism.rs @@ -68,13 +68,13 @@ where P: 'a + Send + Sync + Fn(&mut O) -> Option<&mut T>, { let label = label.to_string(); - lambda("prism", inner_fact, move |g, fact, mut obj| { - if let Some(t) = prism(&mut obj) { + lambda("prism", inner_fact, move |g, fact, mut t| { + if let Some(t) = prism(&mut t) { *t = fact .mutate(g, t.clone()) .map_check_err(|err| format!("prism({}) > {}", label, err))?; } - Ok(obj) + Ok(t) }) } diff --git a/src/facts/same.rs b/src/facts/same.rs index ea54310..ea19405 100644 --- a/src/facts/same.rs +++ b/src/facts/same.rs @@ -5,11 +5,11 @@ pub fn same<'a, T>() -> LambdaUnit<'a, (T, T)> where T: Target<'a> + PartialEq, { - lambda_unit("same", |g, mut obj: (T, T)| { - let o = obj.clone(); + lambda_unit("same", |g, mut t: (T, T)| { + let o = t.clone(); let reason = move || format!("must be same: expected {:?} == {:?}", o.0.clone(), o.1); - g.set(&mut obj.0, &obj.1, reason)?; - Ok(obj) + g.set(&mut t.0, &t.1, reason)?; + Ok(t) }) } diff --git a/src/facts/seq.rs b/src/facts/seq.rs index 3659eef..d5440e4 100644 --- a/src/facts/seq.rs +++ b/src/facts/seq.rs @@ -39,8 +39,8 @@ pub fn vec<'a, T>(inner_fact: impl Fact<'a, T>) -> impl Fact<'a, Vec> where T: Target<'a> + Clone, { - lambda("vec", inner_fact, |g, f, obj: Vec| { - obj.into_iter() + lambda("vec", inner_fact, |g, f, t: Vec| { + t.into_iter() .enumerate() .map(|(i, o)| { f.mutate(g, o) @@ -55,25 +55,25 @@ pub fn vec_len<'a, T>(len: usize) -> LambdaUnit<'a, Vec> where T: Target<'a> + Clone + 'a, { - lambda_unit("vec_len", move |g, mut obj: Vec| { - if obj.len() > len { + lambda_unit("vec_len", move |g, mut t: Vec| { + if t.len() > len { g.fail(format!( "vec should be of length {} but is actually of length {}", len, - obj.len() + t.len() ))?; - obj = obj[0..len].to_vec(); + t = t[0..len].to_vec(); } - while obj.len() < len { - obj.push(g.arbitrary(|| { + while t.len() < len { + t.push(g.arbitrary(|| { format!( "vec should be of length {} but is actually of length {}", len, - obj.len() + t.len() ) })?) } - Ok(obj) + Ok(t) }) } @@ -149,12 +149,12 @@ mod tests { let piecewise = move || { let count = Arc::new(AtomicU8::new(0)); - lambda("piecewise", (), move |g, (), mut obj| { + lambda("piecewise", (), move |g, (), mut t| { let c = count.fetch_add(1, Ordering::SeqCst); if c < 3 { - g.set(&mut obj, &999, || "i'm being difficult, haha")?; + g.set(&mut t, &999, || "i'm being difficult, haha")?; } - Ok(obj) + Ok(t) }) }; diff --git a/src/lambda.rs b/src/lambda.rs index d27dbd9..c97656c 100644 --- a/src/lambda.rs +++ b/src/lambda.rs @@ -48,7 +48,7 @@ pub fn lambda_unit<'a, T>( where T: Target<'a>, { - lambda(label, (), move |g, (), obj| f(g, obj)) + lambda(label, (), move |g, (), t| f(g, t)) } pub type LambdaFn<'a, S, T> = @@ -87,8 +87,8 @@ where S: State + Debug, T: Target<'a>, { - fn mutate(&mut self, g: &mut Generator<'a>, obj: T) -> Mutation { - (self.fun)(g, &mut self.state, obj) + fn mutate(&mut self, g: &mut Generator<'a>, t: T) -> Mutation { + (self.fun)(g, &mut self.state, t) } fn label(&self) -> String { From 4eaa86e4e970122b23567163c953df565d4bfc15 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 15:57:04 -0700 Subject: [PATCH 19/21] Remove Display from eq --- src/facts/eq.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/facts/eq.rs b/src/facts/eq.rs index ab4de2a..0da38a5 100644 --- a/src/facts/eq.rs +++ b/src/facts/eq.rs @@ -1,13 +1,11 @@ -use std::fmt::Display; - use super::*; /// Specifies an equality constraint pub fn eq<'a, T>(constant: T) -> Lambda<'a, (), T> where - T: Target<'a> + PartialEq + Clone + Display, + T: Target<'a> + PartialEq + Clone, { - let label = format!("eq({})", constant); + let label = format!("eq({:?})", constant); lambda_unit(label, move |g, mut t| { if t != constant { g.fail(format!("expected {:?} == {:?}", t, constant))?; @@ -21,7 +19,7 @@ where pub fn ne<'a, S, T>(constant: T) -> Lambda<'a, (), T> where S: ToString, - T: Target<'a> + PartialEq + Display, + T: Target<'a> + PartialEq, { not(eq(constant)).labeled("ne") } From 76c933b588989909f4907c53371ac64012bb5591 Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 16:05:18 -0700 Subject: [PATCH 20/21] Fix macro --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 54fa798..b1f262b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,7 +108,7 @@ macro_rules! facts { ( $fact:expr $(,)?) => { $fact }; ( $fact_0:expr, $fact_1:expr $( , $fact_n:expr )* $(,)? ) => {{ - facts![ + $crate::facts![ $crate::facts::and($fact_0, $fact_1), $( $fact_n , )* ] From 621796718ec5bea60cb044c4417aadfe5c6b80ef Mon Sep 17 00:00:00 2001 From: Michael Dougherty Date: Fri, 30 Jun 2023 16:08:05 -0700 Subject: [PATCH 21/21] 0.2.0-rc.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f11ced6..5ce3dc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "contrafact" -version = "0.2.0-rc.0" +version = "0.2.0-rc.1" authors = ["Michael Dougherty "] repository = "https://github.com/maackle/contrafact-rs/" license = "MIT"