Skip to content

Commit ca56de7

Browse files
Merge branch 'master' into nightly
2 parents 1349f51 + 1beae75 commit ca56de7

File tree

21 files changed

+322
-106
lines changed

21 files changed

+322
-106
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
12
# constrained-generators
3+
[![Coverage Status](https://coveralls.io/repos/github/input-output-hk/constrained-generators/badge.svg?branch=master)](https://coveralls.io/github/input-output-hk/constrained-generators?branch=master)
4+
25
Framework for generating constrained random data using a subset of first order logic

constrained-generators.cabal

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ library
7979
-Wunused-packages
8080

8181
build-depends:
82-
QuickCheck >=2.14 && <2.18,
82+
QuickCheck >=2.15.0.1 && <2.18,
8383
base >=4.18 && <5,
8484
base-orphans,
8585
containers,
@@ -114,7 +114,7 @@ library examples
114114
-Wunused-packages
115115

116116
build-depends:
117-
QuickCheck >=2.14,
117+
QuickCheck >=2.15.0.1,
118118
base >=4.18 && <5,
119119
constrained-generators,
120120
containers,
@@ -125,7 +125,9 @@ test-suite constrained-tests
125125
type: exitcode-stdio-1.0
126126
main-is: Tests.hs
127127
hs-source-dirs: test
128-
other-modules: Constrained.Tests
128+
other-modules:
129+
Constrained.Tests
130+
Constrained.GraphSpec
129131
default-language: Haskell2010
130132
ghc-options:
131133
-Wall
@@ -138,7 +140,7 @@ test-suite constrained-tests
138140
-rtsopts
139141

140142
build-depends:
141-
QuickCheck,
143+
QuickCheck >= 2.15.0.1,
142144
base,
143145
constrained-generators,
144146
containers,

examples/Constrained/Examples/BinTree.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ instance HasSpec a => HasSpec (BinTree a) where
7676
: (BinNode left a <$> shrinkWithTypeSpec s right)
7777
++ ((\l -> BinNode l a right) <$> shrinkWithTypeSpec s left)
7878

79+
fixupWithTypeSpec _ _ = Nothing
80+
7981
cardinalTypeSpec _ = mempty
8082

8183
toPreds t (BinTreeSpec msz s) =

examples/Constrained/Examples/CheatSheet.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ tightFit0 = constrained' $ \x y ->
364364
-- TypeSpec (Cartesian TrueSpec (MemberSpec [0])) []
365365
-- ---
366366
-- assert $ Equal (Fst (ToGeneric v_3)) v_1
367-
-- Env {unEnv = fromList [(v_0,EnvValue 0)]}
367+
-- Env (fromList [(v_0,EnvValue 0)])
368368
-- genFromSpecT ErrorSpec{} with explanation:
369369
-- [1..-1]
370370

src/Constrained/Base.hs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,6 @@ import Constrained.GenT
101101
import Constrained.Generic
102102
import Constrained.List hiding (toList)
103103
import Constrained.TypeErrors
104-
import Control.Monad.Writer (
105-
Writer,
106-
tell,
107-
)
108104
import Data.Foldable (
109105
toList,
110106
)
@@ -492,6 +488,12 @@ class
492488
-- | Shrink an `a` with the aide of a `TypeSpec`
493489
shrinkWithTypeSpec :: TypeSpec a -> a -> [a]
494490

491+
-- | Try to make an `a` conform to `TypeSpec` with minimal changes. When
492+
-- `fixupWithSpec ts a` returns `Just a'`, it should be the case that
493+
-- `conformsTo a' ts`. There are no constraints in the `Nothing` case. A
494+
-- non-trivial implementation of this function is important for shrinking.
495+
fixupWithTypeSpec :: TypeSpec a -> a -> Maybe a
496+
495497
-- | Convert a spec to predicates:
496498
-- The key property here is:
497499
-- ∀ a. a `conformsTo` spec == a `conformsTo` constrained (\t -> toPreds t spec)
@@ -524,12 +526,6 @@ class
524526
alternateShow :: TypeSpec a -> BinaryShow
525527
alternateShow _ = NonBinary
526528

527-
monadConformsTo :: a -> TypeSpec a -> Writer [String] Bool
528-
monadConformsTo x spec =
529-
if conformsTo @a x spec
530-
then pure True
531-
else tell ["Fails by " ++ show spec] >> pure False
532-
533529
-- | For some types (especially finite ones) there may be much better ways to construct
534530
-- a Specification than the default method of just adding a large 'bad' list to TypSpec. This
535531
-- function allows each HasSpec instance to decide.
@@ -608,6 +604,13 @@ class
608604
[a]
609605
shrinkWithTypeSpec spec a = map fromSimpleRep $ shrinkWithTypeSpec spec (toSimpleRep a)
610606

607+
default fixupWithTypeSpec ::
608+
GenericallyInstantiated a =>
609+
TypeSpec a ->
610+
a ->
611+
Maybe a
612+
fixupWithTypeSpec spec a = fromSimpleRep <$> fixupWithTypeSpec spec (toSimpleRep a)
613+
611614
default cardinalTypeSpec ::
612615
GenericallyInstantiated a =>
613616
TypeSpec a ->
@@ -628,6 +631,7 @@ instance HasSpec Bool where
628631
cardinalTypeSpec _ = equalSpec 2
629632
cardinalTrueSpec = equalSpec 2
630633
shrinkWithTypeSpec _ = shrink
634+
fixupWithTypeSpec _ = Just
631635
conformsTo _ _ = True
632636
toPreds _ _ = TruePred
633637

@@ -637,6 +641,7 @@ instance HasSpec () where
637641
combineSpec _ _ = typeSpec ()
638642
_ `conformsTo` _ = True
639643
shrinkWithTypeSpec _ _ = []
644+
fixupWithTypeSpec _ _ = pure ()
640645
genFromTypeSpec _ = pure ()
641646
toPreds _ _ = TruePred
642647
cardinalTypeSpec _ = MemberSpec (pure 1)

src/Constrained/Conformance.hs

Lines changed: 5 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ module Constrained.Conformance (
1414
conformsToSpec,
1515
conformsToSpecE,
1616
satisfies,
17-
checkPred,
17+
checkPredE,
1818
checkPredsE,
1919
) where
2020

@@ -34,59 +34,9 @@ import Data.Semigroup (sconcat)
3434
import Prettyprinter hiding (cat)
3535
import Test.QuickCheck (Property, Testable, property)
3636

37-
-- =========================================================================
38-
39-
-- | Does the Pred evaluate to true under the given Env.
40-
-- If it doesn't, some explanation appears in the failure of the monad 'm'
41-
checkPred :: forall m. MonadGenError m => Env -> Pred -> m Bool
42-
checkPred env = \case
43-
p@(ElemPred bool term xs) -> do
44-
v <- runTerm env term
45-
case (elem v xs, bool) of
46-
(True, True) -> pure True
47-
(True, False) -> fatalErrorNE ("notElemPred reduces to True" :| [show p])
48-
(False, True) -> fatalErrorNE ("elemPred reduces to False" :| [show p])
49-
(False, False) -> pure True
50-
Monitor {} -> pure True
51-
Subst x t p -> checkPred env $ substitutePred x t p
52-
Assert t -> runTerm env t
53-
GenHint {} -> pure True
54-
p@(Reifies t' t f) -> do
55-
val <- runTerm env t
56-
val' <- runTerm env t'
57-
explainNE (NE.fromList ["Reification:", " " ++ show p]) $ pure (f val == val')
58-
ForAll t (x :-> p) -> do
59-
set <- runTerm env t
60-
and
61-
<$> sequence
62-
[ checkPred env' p
63-
| v <- forAllToList set
64-
, let env' = Env.extend x v env
65-
]
66-
Case t bs -> do
67-
v <- runTerm env t
68-
runCaseOn v (mapList thing bs) (\x val ps -> checkPred (Env.extend x val env) ps)
69-
When bt p -> do
70-
b <- runTerm env bt
71-
if b then checkPred env p else pure True
72-
TruePred -> pure True
73-
FalsePred es -> explainNE es $ pure False
74-
DependsOn {} -> pure True
75-
And ps -> checkPreds env ps
76-
Let t (x :-> p) -> do
77-
val <- runTerm env t
78-
checkPred (Env.extend x val env) p
79-
Exists k (x :-> p) -> do
80-
a <- runGE $ k (errorGE . explain "checkPred: Exists" . runTerm env)
81-
checkPred (Env.extend x a env) p
82-
Explain es p -> explainNE es $ checkPred env p
83-
84-
checkPreds :: (MonadGenError m, Traversable t) => Env -> t Pred -> m Bool
85-
checkPreds env ps = and <$> mapM (checkPred env) ps
86-
8737
-- ==========================================================
8838

89-
-- | Like checkPred, But it takes [Pred] rather than a single Pred,
39+
-- | Like checkPredE, But it takes [Pred] rather than a single Pred,
9040
-- and it builds a much more involved explanation if it fails.
9141
-- Does the Pred evaluate to True under the given Env?
9242
-- If it doesn't, an involved explanation appears in the (Just message)
@@ -101,8 +51,9 @@ checkPredsE msgs env ps =
10151
[] -> Nothing
10252
(x : xs) -> Just (NE.nub (sconcat (x NE.:| xs)))
10353

104-
-- | An involved explanation for a single Pred
105-
-- The most important explanations come when an assertion fails.
54+
-- | Does the Pred evaluate to true under the given Env. An involved
55+
-- explanation for a single Pred in case of failure and `Nothing` otherwise.
56+
-- The most important explanations come when an assertion fails.
10657
checkPredE :: Env -> NE.NonEmpty String -> Pred -> Maybe (NE.NonEmpty String)
10758
checkPredE env msgs = \case
10859
p@(ElemPred bool t xs) ->

src/Constrained/Env.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import Prettyprinter
2727
import Prelude hiding (lookup)
2828

2929
-- | Typed environments for mapping @t`Var` a@ to @a@
30-
newtype Env = Env {unEnv :: Map EnvKey EnvValue}
30+
newtype Env = Env (Map EnvKey EnvValue)
3131
deriving newtype (Semigroup, Monoid)
3232
deriving stock (Show)
3333

src/Constrained/Generation.hs

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module Constrained.Generation (
2525
genFromSpecT,
2626
genFromSpecWithSeed,
2727
shrinkWithSpec,
28+
fixupWithSpec,
2829
simplifySpec,
2930

3031
-- ** Debugging
@@ -155,15 +156,71 @@ shrinkWithSpec (simplifySpec -> spec) a = filter (`conformsToSpec` spec) $ case
155156
ExplainSpec _ s -> shrinkWithSpec s a
156157
-- TODO: filter on can't if we have a known to be sound shrinker
157158
TypeSpec s _ -> shrinkWithTypeSpec s a
158-
-- TODO: The better way of doing this is to compute the dependency graph,
159-
-- shrink one variable at a time, and fixup the rest of the variables
160-
SuspendedSpec {} -> shr a
159+
SuspendedSpec x p -> shrinkFromPreds p x a ++ shr a
161160
MemberSpec {} -> shr a
162161
TrueSpec -> shr a
163162
ErrorSpec {} -> []
164163
where
165164
shr = shrinkWithTypeSpec (emptySpec @a)
166165

166+
shrinkFromPreds :: HasSpec a => Pred -> Var a -> a -> [a]
167+
shrinkFromPreds p
168+
| Result plan <- prepareLinearization p = \x a -> listFromGE $ do
169+
-- NOTE: we do this to e.g. guard against bad construction functions in Exists
170+
case checkPredE (Env.singleton x a) (NE.fromList []) p of
171+
Nothing -> pure ()
172+
Just err -> explainNE err $ fatalError "Trying to shrink a bad value, don't do that!"
173+
-- Get an `env` for the original value
174+
initialEnv <- envFromPred (Env.singleton x a) p
175+
return
176+
[ a'
177+
| -- Shrink the initialEnv
178+
env' <- shrinkEnvFromPlan initialEnv plan
179+
, -- Get the value of the constrained variable `x` in the shrunk env
180+
Just a' <- [Env.lookup env' x]
181+
, -- NOTE: this is necessary because it's possible that changing
182+
-- a particular value in the env during shrinking might not result
183+
-- in the value of `x` changing and there is no better way to know than
184+
-- to do this.
185+
a' /= a
186+
]
187+
| otherwise = error "Bad pred"
188+
189+
-- Start with a valid Env for the plan and try to shrink it
190+
shrinkEnvFromPlan :: Env -> SolverPlan -> [Env]
191+
shrinkEnvFromPlan initialEnv SolverPlan {..} = go mempty solverPlan
192+
where
193+
go :: Env -> [SolverStage] -> [Env]
194+
go _ [] = [] -- In this case we decided to keep every variable the same so nothing to return
195+
go env ((unsafeSubstStage env -> SolverStage {..}) : plan) = do
196+
Just a <- [Env.lookup initialEnv stageVar]
197+
-- Two cases:
198+
-- - either we shrink this value and try to fixup every value later on in the plan or
199+
[ fixedEnv
200+
| a' <- shrinkWithSpec stageSpec a
201+
, let env' = Env.extend stageVar a' env
202+
, Just fixedEnv <- [fixupPlan env' plan]
203+
]
204+
-- - we keep this value the way it is and try to shrink some later value
205+
++ go (Env.extend stageVar a env) plan
206+
207+
-- Fix the rest of the plan given an environment `env` for the plan so far
208+
fixupPlan :: Env -> [SolverStage] -> Maybe Env
209+
fixupPlan env [] = pure env
210+
fixupPlan env ((unsafeSubstStage env -> SolverStage {..}) : plan) =
211+
case Env.lookup (env <> initialEnv) stageVar >>= fixupWithSpec stageSpec of
212+
Nothing -> Nothing
213+
Just a -> fixupPlan (Env.extend stageVar a env) plan
214+
215+
-- Try to fix a value w.r.t a specification
216+
fixupWithSpec :: forall a. HasSpec a => Specification a -> a -> Maybe a
217+
fixupWithSpec spec a
218+
| a `conformsToSpec` spec = Just a
219+
| otherwise = case spec of
220+
MemberSpec (a' :| _) -> Just a'
221+
TypeSpec ts _ -> fixupWithTypeSpec ts a >>= \ a' -> a' <$ guard (conformsToSpec a' spec)
222+
_ -> listToMaybe $ filter (`conformsToSpec` spec) (shrinkWithSpec TrueSpec a)
223+
167224
-- Debugging --------------------------------------------------------------
168225

169226
-- | A version of `genFromSpecT` that runs in the IO monad. Good for debugging.
@@ -197,6 +254,10 @@ prettyPlan (simplifySpec -> spec)
197254

198255
-- ---------------------- Building a plan -----------------------------------
199256

257+
unsafeSubstStage :: Env -> SolverStage -> SolverStage
258+
unsafeSubstStage env (SolverStage y ps spec relevant) =
259+
normalizeSolverStage $ SolverStage y (substPred env <$> ps) spec relevant
260+
200261
substStage :: HasSpec a => Set Name -> Var a -> a -> SolverStage -> SolverStage
201262
substStage rel' x val (SolverStage y ps spec relevant) =
202263
normalizeSolverStage $ SolverStage y (substPred env <$> ps) spec relevant'
@@ -1105,6 +1166,9 @@ instance (HasSpec a, HasSpec b, KnownNat (CountCases b)) => HasSpec (Sum a b) wh
11051166
shrinkWithTypeSpec (SumSpec _ sa _) (SumLeft a) = SumLeft <$> shrinkWithSpec sa a
11061167
shrinkWithTypeSpec (SumSpec _ _ sb) (SumRight b) = SumRight <$> shrinkWithSpec sb b
11071168

1169+
fixupWithTypeSpec (SumSpec _ sa _) (SumLeft a) = SumLeft <$> fixupWithSpec sa a
1170+
fixupWithTypeSpec (SumSpec _ _ sb) (SumRight b) = SumRight <$> fixupWithSpec sb b
1171+
11081172
toPreds ct (SumSpec h sa sb) =
11091173
Case
11101174
ct

0 commit comments

Comments
 (0)