Skip to content

Commit

Permalink
Merge pull request #7 from maackle/lambda
Browse files Browse the repository at this point in the history
Rewrite lots of facts in terms of lambdas
  • Loading branch information
maackle authored Jul 31, 2023
2 parents 759fce0 + 6217967 commit 2a21352
Show file tree
Hide file tree
Showing 27 changed files with 641 additions and 1,228 deletions.
13 changes: 6 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "contrafact"
version = "0.2.0-rc.0"
version = "0.2.0-rc.1"
authors = ["Michael Dougherty <[email protected]>"]
repository = "https://github.com/maackle/contrafact-rs/"
license = "MIT"
Expand All @@ -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"
Expand All @@ -34,7 +33,7 @@ default = ["utils"]

utils = ["once_cell", "rand"]

optics = ["lens-rs"]
# optics = ["lens-rs"]

[package.metadata.inwelling]
lens-rs_generator = true
2 changes: 0 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 7 additions & 1 deletion src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ impl Check {
}
}

/// If Failures, return all failures joined together in a single string
pub fn result_joined(self) -> ContrafactResult<std::result::Result<(), String>> {
self.result().map(|r| r.map_err(|es| es.join(";")))
}

/// Create a single-error failure if predicate is false, otherwise pass
///
/// ```
Expand Down Expand Up @@ -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)),
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,22 @@ 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 {
fn eq(&self, other: &Self) -> bool {
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,
}
}
Expand Down
91 changes: 48 additions & 43 deletions src/fact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,49 @@ 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>
/// 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>
{
}

// /// Type alias for a boxed Fact. Implements [`Fact`] itself.
// pub type BoxFact<'a, T> = Box<dyn 'a + Fact<'a, T>>;

// pub trait Facts<T: Bounds<'static>>: Fact<'static, T> {}
// impl<T: Bounds<'static>, F: Facts<T>> Fact<'static, T> for F {}
/// The trait bounds for the State of a Fact
pub trait State: std::fmt::Debug + Clone + Send + Sync {}
impl<T> 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 Fact<'a, T>: Send + Sync + Clone
pub trait Fact<'a, T>: Send + Sync + Clone + std::fmt::Debug
where
T: Bounds<'a>,
T: Target<'a>,
{
/// Change the label
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.
///
/// 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)
fn check(mut self, t: &T) -> Check {
let mut g = Generator::checker();
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<T>;
fn mutate(&mut self, g: &mut Generator<'a>, t: T) -> Mutation<T>;

/// Make this many attempts to satisfy a constraint before giving up and panicking.
///
Expand All @@ -59,15 +61,14 @@ 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<T> {
fn satisfy(&mut self, g: &mut Generator<'a>, t: T) -> ContrafactResult<T> {
tracing::trace!("satisfy");
let mut last_failure: Vec<String> = vec![];
let mut next = obj.clone();
let mut next = t.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()? {
if let Err(errs) = self.clone().check(&next).result()? {
last_failure = errs;
} else {
*self = m;
Expand All @@ -83,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<T> {
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
Expand All @@ -96,40 +97,44 @@ where

impl<'a, T, F1, F2> Fact<'a, T> for Either<F1, F2>
where
T: Bounds<'a>,
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<T> {
fn mutate(&mut self, g: &mut Generator<'a>, t: T) -> Mutation<T> {
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),
}
}
}

#[tracing::instrument(skip(fact))]
pub(crate) fn check_raw<'a, T, F: Fact<'a, T>>(fact: &mut F, obj: &T) -> Check
where
T: Bounds<'a> + ?Sized,
F: Fact<'a, T> + ?Sized,
{
let mut g = Generator::checker();
Check::from_mutation(fact.mutate(&mut g, obj.clone()))
fn label(&self) -> String {
match self {
Either::Left(f) => f.label(),
Either::Right(f) => f.label(),
}
}

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<F>, obj: &T) -> Check
fn collect_checks<'a, T, F>(facts: Vec<F>, t: &T) -> Check
where
T: Bounds<'a>,
T: Target<'a>,
F: Fact<'a, T>,
{
let checks = facts
.into_iter()
.enumerate()
.map(|(i, f)| {
Ok(f.check(obj)
Ok(f.check(t)
.failures()?
.iter()
.map(|e| format!("fact {}: {}", i, e))
Expand Down
Loading

0 comments on commit 2a21352

Please sign in to comment.