• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

input-output-hk / constrained-generators / 441

26 Nov 2025 03:43PM UTC coverage: 77.265% (+0.2%) from 77.09%
441

push

github

web-flow
cache more things in set generation and be more lazy about `explain` (#67)

7 of 7 new or added lines in 2 files covered. (100.0%)

7 existing lines in 2 files now uncovered.

3983 of 5155 relevant lines covered (77.26%)

1.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.42
/src/Constrained/SumList.hs
1
{-# LANGUAGE AllowAmbiguousTypes #-}
2
{-# LANGUAGE ConstrainedClassMethods #-}
3
{-# LANGUAGE DataKinds #-}
4
{-# LANGUAGE ExistentialQuantification #-}
5
{-# LANGUAGE FlexibleContexts #-}
6
{-# LANGUAGE GADTs #-}
7
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8
{-# LANGUAGE LambdaCase #-}
9
{-# LANGUAGE OverloadedStrings #-}
10
{-# LANGUAGE ScopedTypeVariables #-}
11
{-# LANGUAGE TypeApplications #-}
12
{-# LANGUAGE TypeOperators #-}
13
{-# LANGUAGE UndecidableSuperClasses #-}
14
{-# LANGUAGE ViewPatterns #-}
15

16
-- | Operations for generating random elements of Num like types, that sum to a particular total.
17
--   The class `Foldy` (defined in the TheKnot.hs) gives the operations necessary to do this.
18
--   In this module we define the helper functions necessary to define the methods of the Foldy class.
19
--   The helper functions do not need to know about the Foldy class, and are not dependent upon any of
20
--   the mutually recursive operations defined in TheKnot, except the operations defined in the Complete class.
21
--   That class is defined in this module, but the instance for that class is made in TheKnot.
22
module Constrained.SumList (
23
  genNumList,
24
  pickAll,
25
  knownUpperBound,
26
  knownLowerBound,
27
  genListWithSize,
28
  Complete (..),
29
  maxFromSpec,
30
  Solution (..),
31
  logRange,
32
  logish,
33
  Cost (..),
34
  predSpecPair,
35
  narrowByFuelAndSize,
36
) where
37

38
import Constrained.AbstractSyntax
39
import Constrained.Base
40
import Constrained.Conformance (conformsToSpec)
41
import Constrained.Core (Value (..))
42
import Constrained.GenT (
43
  GE (..),
44
  GenT,
45
  MonadGenError (..),
46
  oneofT,
47
  pureGen,
48
  push,
49
  scaleT,
50
  sizeT,
51
  suchThatT,
52
  tryGenT,
53
 )
54
import Constrained.List (List (..), ListCtx (..))
55
import Constrained.NumOrd (
56
  IntW (..),
57
  MaybeBounded (..),
58
  NumSpec (..),
59
  Numeric,
60
  geqSpec,
61
  gtSpec,
62
  leqSpec,
63
  ltSpec,
64
  nubOrd,
65
 )
66
import Constrained.PrettyUtils
67
import Control.Applicative ((<|>))
68
import Control.Monad (guard)
69
import Data.List ((\\))
70
import Data.List.NonEmpty (NonEmpty (..))
71
import qualified Data.List.NonEmpty as NE
72
import Data.Maybe (fromMaybe, isNothing, listToMaybe)
73
import qualified Data.Set as Set
74
import GHC.Stack
75
import Prettyprinter hiding (cat)
76
import System.Random (Random (..))
77
import Test.QuickCheck (Arbitrary, Gen, choose, shuffle, vectorOf)
78

79
-- ====================================================================
80
-- What we need to know, that can only be defined in TheKnot module, is
81
-- abstracted into this class, which will be a precondition on the `Foldy` class
82

83
-- | Dependency-trick
84
class HasSpec a => Complete a where
85
  -- method standing for `simplifySpec`
86
  simplifyA :: Specification a -> Specification a
87

88
  -- method standing for `genFromSpecT`
89
  genFromSpecA :: forall m. (HasCallStack, HasSpec a, MonadGenError m) => Specification a -> GenT m a
90

91
  -- method standing for method `theAddFn` from the `Foldy` class
92
  theAddA :: Numeric a => IntW '[a, a] a
93
  theAddA = AddW
2✔
94

95
-- ==========================================================
96
-- helpers
97

98
-- ===================================================================
99

100
-- | Try to find an upper-bound for the values admitted by a `Specification`
101
knownUpperBound ::
102
  (TypeSpec a ~ NumSpec a, Ord a, Enum a, Num a, MaybeBounded a) =>
103
  Specification a ->
104
  Maybe a
105
knownUpperBound (ExplainSpec _ s) = knownUpperBound s
2✔
106
knownUpperBound TrueSpec = upperBound
2✔
107
knownUpperBound (MemberSpec as) = Just $ maximum as
2✔
108
knownUpperBound ErrorSpec {} = Nothing
2✔
109
knownUpperBound SuspendedSpec {} = upperBound
2✔
110
knownUpperBound (TypeSpec (NumSpecInterval lo hi) cant) = upper (lo <|> lowerBound) (hi <|> upperBound)
2✔
111
  where
112
    upper _ Nothing = Nothing
2✔
113
    upper Nothing (Just b) = listToMaybe $ [b, b - 1 ..] \\ cant
2✔
114
    upper (Just a) (Just b)
115
      | a == b = a <$ guard (a `notElem` cant)
1✔
116
      | otherwise = listToMaybe $ [b, b - 1 .. a] \\ cant
1✔
117

118
-- | Try to find a lower-bound for the values admitted by a `Specification`
119
knownLowerBound ::
120
  (TypeSpec a ~ NumSpec a, Ord a, Enum a, Num a, MaybeBounded a) =>
121
  Specification a ->
122
  Maybe a
123
knownLowerBound (ExplainSpec _ s) = knownLowerBound s
2✔
124
knownLowerBound TrueSpec = lowerBound
2✔
125
knownLowerBound (MemberSpec as) = Just $ minimum as
2✔
126
knownLowerBound ErrorSpec {} = Nothing
2✔
127
knownLowerBound SuspendedSpec {} = lowerBound
2✔
128
knownLowerBound (TypeSpec (NumSpecInterval lo hi) cant) =
129
  lower (lo <|> lowerBound) (hi <|> upperBound)
2✔
130
  where
131
    lower Nothing _ = Nothing
2✔
132
    lower (Just a) Nothing = listToMaybe $ [a, a + 1 ..] \\ cant
2✔
133
    lower (Just a) (Just b)
134
      | a == b = a <$ guard (a `notElem` cant)
1✔
135
      | otherwise = listToMaybe $ [a, a + 1 .. b] \\ cant
1✔
136

137
isEmptyNumSpec ::
138
  (TypeSpec a ~ NumSpec a, Ord a, Enum a, Num a, MaybeBounded a) => Specification a -> Bool
139
isEmptyNumSpec = \case
2✔
140
  ExplainSpec _ s -> isEmptyNumSpec s
2✔
141
  ErrorSpec {} -> True
2✔
142
  TrueSpec -> False
2✔
143
  MemberSpec _ -> False -- MemberSpec always has at least one element (NE.NonEmpty)
2✔
144
  SuspendedSpec {} -> False
2✔
145
  TypeSpec i cant -> null $ enumerateInterval i \\ cant
2✔
146

147
-- | Note: potentially infinite list
148
enumerateInterval :: (Enum a, Num a, MaybeBounded a) => NumSpec a -> [a]
149
enumerateInterval (NumSpecInterval lo hi) =
2✔
150
  case (lo <|> lowerBound, hi <|> upperBound) of
2✔
151
    (Nothing, Nothing) -> interleave [0 ..] [-1, -2 ..]
1✔
152
    (Nothing, Just b) -> [b, b - 1 ..]
1✔
153
    (Just a, Nothing) -> [a ..]
2✔
154
    (Just a, Just b) -> [a .. b]
2✔
155
  where
156
    interleave [] ys = ys
1✔
157
    interleave (x : xs) ys = x : interleave ys xs
1✔
158

159
-- ========================================================================
160
-- Operations to complete the Foldy instances genNumList, genListWithSize
161

162
-- | Generate a list of values subject to a constraint on both the elements and
163
-- the result
164
genNumList ::
165
  forall a m.
166
  ( MonadGenError m
167
  , Arbitrary a
168
  , Integral a
169
  , Numeric a
170
  , Random a
171
  , Complete a
172
  ) =>
173
  Specification a ->
174
  Specification a ->
175
  GenT m [a]
176
genNumList elemSIn foldSIn = do
2✔
177
  let extraElemConstraints
2✔
178
        | Just l <- knownLowerBound elemSIn
2✔
179
        , 0 <= l
2✔
180
        , Just u <- knownUpperBound foldSIn =
2✔
181
            leqSpec u
2✔
182
        | otherwise = TrueSpec
1✔
183
      elemSIn' = elemSIn <> extraElemConstraints
2✔
184
  normElemS <- normalize elemSIn'
2✔
185
  normFoldS <- normalize foldSIn
2✔
186
  let narrowedSpecs = narrowFoldSpecs (normElemS, normFoldS)
2✔
187
  explainNE
2✔
188
    ( NE.fromList
×
189
        [ "Can't generate list of ints with fold constraint"
×
190
        , "  elemSpec = " ++ show elemSIn
×
191
        , "  normElemSpec = " ++ show normElemS
×
192
        , "  foldSpec = " ++ show foldSIn
×
193
        ]
194
    )
195
    $ gen narrowedSpecs 50 [] >>= pureGen . shuffle
2✔
196
  where
197
    normalize (ExplainSpec es x) = explainSpec es <$> normalize x
1✔
198
    normalize spec@SuspendedSpec {} = do
2✔
199
      sz <- sizeT
2✔
200
      spec' <- buildMemberSpec sz (100 :: Int) mempty spec
1✔
201
      normalize $ spec'
2✔
202
    normalize spec =
203
      pure $
2✔
204
        maybe mempty geqSpec lowerBound
2✔
205
          <> maybe mempty leqSpec upperBound
2✔
206
          <> spec
2✔
207

208
    buildMemberSpec _ 0 es _ =
2✔
209
      pure
2✔
210
        ( memberSpec
2✔
211
            (Set.toList es)
2✔
212
            (pure "In genNumList, in buildMemberSpec 'es' is the empty list, can't make a MemberSpec from that")
×
213
        )
214
    buildMemberSpec sz fuel es spec = do
2✔
215
      me <- scaleT (const sz) $ tryGenT (genFromSpecA @a spec)
1✔
UNCOV
216
      let sz'
×
UNCOV
217
            | sz > 100 = sz
×
UNCOV
218
            | isNothing me = 2 * sz + 1
×
UNCOV
219
            | Just e <- me, Set.member e es = 2 * sz + 1
×
UNCOV
220
            | otherwise = sz
×
221
      buildMemberSpec
2✔
UNCOV
222
        sz'
×
223
        (fuel - 1)
2✔
224
        (maybe es (flip Set.insert es) me)
2✔
225
        spec
2✔
226

227
    gen ::
228
      forall m'. MonadGenError m' => (Specification a, Specification a) -> Int -> [a] -> GenT m' [a]
229
    gen (elemS, foldS) fuel lst
2✔
230
      | fuel <= 0
2✔
231
      , not $ 0 `conformsToSpec` foldS =
2✔
232
          genErrorNE $
2✔
233
            NE.fromList
×
234
              [ "Ran out of fuel in genNumList"
×
235
              , "  elemSpec =" ++ show elemSIn
×
236
              , "  foldSpec = " ++ show foldSIn
×
237
              , "  lst = " ++ show (reverse lst)
×
238
              ]
239
      | ErrorSpec err <- foldS = genErrorNE err
1✔
240
      | ErrorSpec {} <- elemS = pure lst -- At this point we know that foldS admits 0 (also this should be redundant)
2✔
241
      | 0 `conformsToSpec` foldS = oneofT [pure lst, nonemptyList @GE] -- TODO: distribution
2✔
242
      | otherwise = nonemptyList
1✔
243
      where
244
        isUnsat (elemSpec, foldSpec) = isEmptyNumSpec foldSpec || not (0 `conformsToSpec` foldSpec) && isEmptyNumSpec elemSpec
2✔
245
        nonemptyList :: forall m''. MonadGenError m'' => GenT m'' [a]
246
        nonemptyList = do
2✔
247
          (x, specs') <-
248
            explainNE
2✔
249
              ( NE.fromList
×
250
                  [ "Generating an element:"
×
251
                  , "  elemS = " ++ show elemS
×
252
                  , "  foldS = " ++ show foldS
×
253
                  , "  fuel  = " ++ show fuel
×
254
                  , "  lst   = " ++ show (reverse lst)
×
255
                  ]
256
              )
257
              $ do
2✔
258
                sz <- sizeT
2✔
259
                x <- genFromSpecA @a elemS
2✔
260
                let foldS' = propagate theAddA (HOLE :? Value x :> Nil) foldS
2✔
261
                    specs' = narrowByFuelAndSize (fromIntegral $ fuel - 1) sz (elemS, foldS')
2✔
262
                pure (x, specs')
2✔
263
                `suchThatT` not
2✔
264
                . isUnsat
2✔
265
                . snd
2✔
266
          gen specs' (fuel - 1) (x : lst)
2✔
267

268
narrowFoldSpecs ::
269
  forall a.
270
  ( TypeSpec a ~ NumSpec a
271
  , Arbitrary a
272
  , Integral a
273
  , Random a
274
  , MaybeBounded a
275
  , Complete a
276
  ) =>
277
  (Specification a, Specification a) ->
278
  (Specification a, Specification a)
279
narrowFoldSpecs specs = maybe specs narrowFoldSpecs (go specs)
2✔
280
  where
281
    -- Note: make sure there is some progress when returning Just or this will loop forever
282
    go :: (Specification a, Specification a) -> Maybe (Specification a, Specification a)
283
    go (simplifyA -> elemS, simplifyA -> foldS) = case (elemS, foldS) of
2✔
284
      -- Empty foldSpec
285
      (_, ErrorSpec {}) -> Nothing
2✔
286
      _ | isEmptyNumSpec foldS -> Just (elemS, ErrorSpec (NE.fromList ["Empty foldSpec:", show foldS]))
1✔
287
      -- Empty elemSpec
288
      (ErrorSpec {}, MemberSpec ys) | NE.toList ys == [0] -> Nothing
2✔
289
      (ErrorSpec {}, _)
290
        | 0 `conformsToSpec` foldS -> Just (elemS, MemberSpec (pure 0))
2✔
291
        | otherwise ->
×
292
            Just
2✔
293
              ( elemS
2✔
294
              , ErrorSpec $
2✔
295
                  NE.fromList
×
296
                    [ "Empty elemSpec and non-zero foldSpec"
×
297
                    , show $ indent 2 $ "elemSpec =" /> pretty elemS
×
298
                    , show $ indent 2 $ "foldSpec =" /> pretty foldS
×
299
                    ]
300
              )
301
      -- We can reduce the size of the `elemS` interval when it is
302
      -- `[l, u]` or `[l, ∞)` given that `0 <= l` and we have
303
      -- an upper bound on the sum - we can't pick things bigger than the
304
      -- upper bound.
305
      _
306
        | Just lo <- knownLowerBound elemS
2✔
307
        , 0 <= lo
2✔
308
        , Just hi <- knownUpperBound foldS
2✔
309
        , -- Check that we will actually be making the set smaller
310
          fromMaybe True ((hi <) <$> knownUpperBound elemS) ->
1✔
311
            Just (elemS <> typeSpec (NumSpecInterval (Just lo) (Just hi)), foldS)
2✔
312
      -- We can reduce the size of the foldS set by bumping the lower bound when
313
      -- there is a positive lower bound on the elemS, we can't generate things smaller
314
      -- than the lower bound on `elemS`.
315
      _
316
        | Just lo <- knownLowerBound elemS
2✔
317
        , 0 <= lo
2✔
318
        , not $ 0 `conformsToSpec` foldS
2✔
319
        , -- Check that we will actually be making the set smaller
320
          fromMaybe True ((lo >) <$> knownLowerBound foldS) ->
1✔
321
            Just (elemS, foldS <> typeSpec (NumSpecInterval (Just lo) Nothing))
2✔
322
      -- NOTE: this is far from sufficient, but it's good enough of an approximation
323
      -- to avoid the worst failures.
324
      _
325
        | Just lo <- knownLowerBound elemS
2✔
326
        , Just loS <- knownLowerBound foldS
2✔
327
        , Just hi <- knownUpperBound elemS
2✔
328
        , Just hiS <- knownUpperBound foldS
2✔
329
        , hi < loS
2✔
330
        , lo > hiS - lo ->
2✔
331
            Just
2✔
332
              ( ErrorSpec $ NE.fromList ["Can't solve diophantine equation"]
1✔
333
              , ErrorSpec $ NE.fromList ["Can't solve diophantine equation"]
1✔
334
              )
335
      _ -> Nothing
2✔
336

337
-- | Try to narrow down a specification for the elems and fold of a list
338
narrowByFuelAndSize ::
339
  forall a.
340
  ( TypeSpec a ~ NumSpec a
341
  , Arbitrary a
342
  , Integral a
343
  , Random a
344
  , MaybeBounded a
345
  , Complete a
346
  ) =>
347
  -- | Fuel
348
  a ->
349
  -- | Integer
350
  Int ->
351
  (Specification a, Specification a) ->
352
  (Specification a, Specification a)
353
narrowByFuelAndSize fuel size specpair =
2✔
354
  loop (100 :: Int) (onlyOnceTransformations $ (narrowFoldSpecs specpair))
2✔
355
  where
356
    loop 0 specs =
2✔
357
      error $
×
358
        unlines
×
359
          [ "narrowByFuelAndSize loops:"
×
360
          , "  fuel = " ++ show fuel
×
361
          , "  size = " ++ show size
×
362
          , "  specs = " ++ show specs
×
363
          , "  narrowFoldSpecs spec = " ++ show (narrowFoldSpecs specs)
×
364
          , "  go (narrowFoldSpecs specs) = " ++ show (go (narrowFoldSpecs specs))
×
365
          ]
366
    loop n specs = case go specs of
2✔
367
      Nothing -> specs
2✔
368
      Just specs' -> loop (n - 1) (narrowFoldSpecs specs')
2✔
369

370
    -- Transformations only applied once. It's annoying to check if you're
371
    -- going to change the spec with these so easier to just make sure you only apply
372
    -- these once
373
    onlyOnceTransformations (elemS, foldS)
2✔
374
      | fuel == 1 = (elemS <> foldS, foldS)
2✔
375
      | otherwise = (elemS, foldS)
1✔
376

377
    canReach _ 0 s = s == 0
2✔
378
    canReach e currentfuel s
379
      -- You can reach it in one step
380
      | s <= e = 0 < currentfuel
2✔
381
      | otherwise = canReach e (currentfuel - 1) (s - e)
1✔
382

383
    -- Precondition:
384
    --   a is negative
385
    --   the type has more negative numbers than positive ones
386
    safeNegate a
2✔
387
      | Just u <- upperBound
2✔
388
      , a < negate u =
2✔
389
          u
2✔
390
      | otherwise = negate a
1✔
391

392
    divCeil a b
2✔
393
      | b * d < a = d + 1
2✔
394
      | otherwise = d
1✔
395
      where
396
        d = a `div` b
2✔
397

398
    go :: (Specification a, Specification a) -> Maybe (Specification a, Specification a)
399
    go (simplifyA -> elemS, simplifyA -> foldS)
2✔
400
      -- There is nothing we can do
401
      | fuel == 0 = Nothing
2✔
402
      | ErrorSpec {} <- elemS = Nothing
2✔
403
      | ErrorSpec {} <- foldS = Nothing
2✔
404
      -- Give up as early as possible
405
      | Just 0 <- knownUpperBound elemS
2✔
406
      , Just 0 <- knownLowerBound elemS
2✔
407
      , not $ 0 `conformsToSpec` foldS =
1✔
408
          Just (ErrorSpec (NE.fromList ["only 0 left"]), foldS)
×
409
      -- Make sure we try to generate the smallest possible list
410
      -- that gives you the right result - don't put a bunch of zeroes in
411
      -- a _small_ (size 0) list.
412
      | size == 0
2✔
413
      , 0 `conformsToSpec` elemS =
2✔
414
          Just (elemS <> notEqualSpec 0, foldS)
2✔
415
      -- Member specs with non-zero elements, TODO: explain
416
      | MemberSpec ys <- elemS
2✔
417
      , let xs = NE.toList ys
2✔
418
      , Just u <- knownUpperBound foldS
2✔
419
      , all (0 <=) xs
2✔
420
      , any (0 <) xs
2✔
421
      , let xMinP = minimum $ filter (0 <) xs
2✔
422
            possible x = x == u || xMinP <= u - x
2✔
423
            xs' = filter possible xs
2✔
424
      , xs' /= xs =
2✔
425
          Just (memberSpec (nubOrd xs') (pure ("None of " ++ show xs ++ " are possible")), foldS)
1✔
426
      -- The lower bound on the number of elements is too low
427
      | Just e <- knownLowerBound elemS
2✔
428
      , e > 0
2✔
429
      , Just s <- knownLowerBound foldS
2✔
430
      , s > 0
2✔
431
      , let c = divCeil s fuel
2✔
432
      , e < c =
2✔
433
          Just (elemS <> geqSpec c, foldS)
2✔
434
      -- The upper bound on the number of elements is too high
435
      | Just e <- knownUpperBound elemS
2✔
436
      , e < 0
2✔
437
      , Just s <- knownUpperBound foldS
2✔
438
      , s < 0
2✔
439
      , let c = divCeil (safeNegate s) fuel
2✔
440
      , negate c < e
2✔
441
      , maybe True (c <) (knownUpperBound elemS) =
1✔
442
          Just (elemS <> leqSpec c, foldS)
×
443
      -- It's time to stop generating negative numbers
444
      | Just s <- knownLowerBound foldS
2✔
445
      , s > 0
2✔
446
      , Just e <- knownUpperBound elemS
2✔
447
      , e > 0
2✔
448
      , not $ canReach e (fuel `div` 2 + 1) s
2✔
449
      , maybe True (<= 0) (knownLowerBound elemS) =
2✔
450
          Just (elemS <> gtSpec 0, foldS)
2✔
451
      -- It's time to stop generating positive numbers
452
      | Just s <- knownUpperBound foldS
2✔
453
      , s < 0
2✔
454
      , Just e <- knownLowerBound elemS
2✔
455
      , e < 0
1✔
456
      , not $ canReach (safeNegate e) (fuel `div` 2 + 1) (safeNegate s)
2✔
457
      , maybe True (0 <=) (knownUpperBound elemS) =
1✔
458
          Just (elemS <> ltSpec 0, foldS)
2✔
459
      -- There is nothing we need to do
460
      | otherwise = Nothing
1✔
461

462
-- =====================================================================================
463
-- Like genList, but generate a list whose size conforms to s SizeSpec
464
-- =====================================================================================
465

466
-- | Generate a list with 'sizeSpec' elements, that add up to a total that conforms
467
--   to 'foldSpec'. Every element in the list should conform to 'elemSpec'
468
genListWithSize ::
469
  forall a m.
470
  ( Complete a
471
  , MonadGenError m
472
  , Random a
473
  , Integral a
474
  , Arbitrary a
475
  , Numeric a
476
  , Complete Integer
477
  ) =>
478
  Specification Integer ->
479
  Specification a ->
480
  Specification a ->
481
  GenT m [a]
482
genListWithSize sizeSpec elemSpec foldSpec
2✔
483
  | TrueSpec <- sizeSpec = genNumList elemSpec foldSpec
1✔
484
  | ErrorSpec _ <- sizeSpec <> geqSpec 0 =
2✔
485
      fatalErrorNE
2✔
486
        ( NE.fromList
×
487
            [ "genListWithSize called with possible negative size"
×
488
            , "  sizeSpec = " ++ specName sizeSpec
×
489
            , "  elemSpec = " ++ specName elemSpec
×
490
            , "  foldSpec = " ++ specName foldSpec
×
491
            ]
492
        )
493
  | otherwise = do
1✔
494
      total <- genFromSpecA @a foldSpec
2✔
495
      -- The compatible sizes for the list, for a given choice of total
496
      let sizeAdjusted =
2✔
497
            if total /= 0
2✔
498
              then sizeSpec <> gtSpec 0 -- if total is not zero, we better not pick a 0 size
2✔
499
              else
500
                if lowerBound @a == Just 0 -- Type `a` has no negative numbers (Natural, Word8, Word16, Word 32, Word64)
2✔
501
                  then sizeSpec <> equalSpec 0 -- if it is zero, and negative numbers not allowed, then only possible size is 0
2✔
502
                  else sizeSpec <> gtSpec 0
2✔
503
          message =
2✔
504
            [ "\nGenSizedList fails"
1✔
505
            , "sizespec = " ++ specName sizeSpec
×
506
            , "elemSpec = " ++ specName elemSpec
×
507
            , "foldSpec = " ++ specName foldSpec
×
508
            , "total choosen from foldSpec = " ++ show total
×
509
            , "size adjusted for total = " ++ show sizeAdjusted
×
510
            ]
511
      push message $ do
2✔
512
        count <- genFromSpecA @Integer sizeAdjusted
2✔
513
        case compare total 0 of
2✔
514
          EQ ->
515
            if count == 0
2✔
516
              then pure []
1✔
517
              else pickPositive elemSpec total count
2✔
518
          GT -> pickPositive elemSpec total count
2✔
519
          LT -> pickNegative elemSpec total count
2✔
520

521
pickPositive ::
522
  forall t m.
523
  (Integral t, Random t, MonadGenError m, TypeSpec t ~ NumSpec t, Complete t) =>
524
  Specification t ->
525
  t ->
526
  Integer ->
527
  GenT m [t]
528
pickPositive elemspec total count = do
2✔
529
  sol <-
530
    pureGen $
2✔
531
      pickAll
2✔
532
        (minFromSpec 0 elemspec) -- Search from [0..total] unless elemspec says otherwise
2✔
533
        (maxFromSpec total elemspec)
2✔
534
        (predSpecPair elemspec)
2✔
535
        total
2✔
536
        (fromInteger count)
2✔
537
        (Cost 0)
2✔
538
  case snd sol of
2✔
539
    No msgs -> fatalErrorNE (NE.fromList msgs)
1✔
540
    Yes (x :| _) -> pure x
2✔
541

542
pickNegative ::
543
  forall t m.
544
  (Integral t, Complete t, Random t, MonadGenError m, TypeSpec t ~ NumSpec t) =>
545
  Specification t ->
546
  t ->
547
  Integer ->
548
  GenT m [t]
549

550
-- | total can be either negative, or 0. If it is 0, we want `count` numbers that add to `zero`
551
pickNegative elemspec total count = do
2✔
552
  sol <-
553
    pureGen $
2✔
554
      pickAll
2✔
555
        -- Recall 'total' is negative here.
556
        -- Here is a graphic of the range we search in (smallest .. largest)
557
        -- [(total+n) .. total .. 0 .. (0-n)],  where n = (total `div` 4) which is negative.
558
        (minFromSpec (total + (total `div` 4)) elemspec)
2✔
559
        (maxFromSpec (0 - (total `div` 4)) elemspec)
2✔
560
        (predSpecPair elemspec)
2✔
561
        total
2✔
562
        (fromInteger count)
2✔
563
        (Cost 0)
2✔
564
  case snd sol of
2✔
565
    No msgs -> fatalErrorNE (NE.fromList msgs)
1✔
566
    Yes (x :| _) -> pure x
2✔
567

568
specName :: forall a. HasSpec a => Specification a -> String
569
specName (ExplainSpec [x] _) = x
×
570
specName x = show x
×
571

572
-- | Name (?!) and semantics of a spec
573
predSpecPair :: forall a. HasSpec a => Specification a -> (String, a -> Bool)
574
predSpecPair spec = (specName spec, (`conformsToSpec` spec))
1✔
575

576
-- | The smallest number admitted by the spec, if we can find one.
577
--   if not return the defaultValue 'dv'
578
minFromSpec ::
579
  forall n.
580
  (Ord n, Complete n, TypeSpec n ~ NumSpec n) =>
581
  n ->
582
  Specification n ->
583
  n
584
minFromSpec dv (ExplainSpec _ spec) = minFromSpec @n dv spec
2✔
585
minFromSpec dv TrueSpec = dv
2✔
586
minFromSpec dv s@(SuspendedSpec _ _) =
587
  case simplifyA s of
2✔
588
    SuspendedSpec {} -> dv
2✔
589
    x -> minFromSpec @n dv x
×
590
minFromSpec dv (ErrorSpec _) = dv
×
591
minFromSpec _ (MemberSpec xs) = minimum xs
2✔
592
minFromSpec dv (TypeSpec (NumSpecInterval lo _) _) = maybe dv id lo
2✔
593

594
-- | The largest number admitted by the spec, if we can find one.
595
--   if not return the defaultValue 'dv'
596
maxFromSpec ::
597
  forall n.
598
  (Ord n, Complete n, TypeSpec n ~ NumSpec n) =>
599
  n ->
600
  Specification n ->
601
  n
602
maxFromSpec dv (ExplainSpec _ spec) = maxFromSpec @n dv spec
2✔
603
maxFromSpec dv TrueSpec = dv
2✔
604
maxFromSpec dv s@(SuspendedSpec _ _) =
605
  case simplifyA s of
2✔
606
    SuspendedSpec {} -> dv
2✔
607
    x -> maxFromSpec @n dv x
×
608
maxFromSpec dv (ErrorSpec _) = dv
×
609
maxFromSpec _ (MemberSpec xs) = maximum xs
2✔
610
maxFromSpec dv (TypeSpec (NumSpecInterval _ hi) _) = maybe dv id hi
2✔
611

612
-- =======================================================
613
-- Helper functions for genSizedList
614

615
-- | Either a list of possible answers of an explanation of why there is no
616
-- solution
617
data Solution t = Yes (NonEmpty [t]) | No [String]
618
  deriving (Eq)
×
619

620
instance Show t => Show (Solution t) where
×
621
  show (No xs) = "No" ++ "\n" ++ unlines xs
×
622
  show (Yes xs) = "Yes " ++ show xs
×
623

624
-- | Special case Int for keeping track of "fuel" to find solutions
625
newtype Cost = Cost Int deriving (Eq, Show, Num, Ord)
×
626

627
firstYesG ::
628
  Monad m => Solution t -> (x -> Cost -> m (Cost, Solution t)) -> [x] -> Cost -> m (Cost, Solution t)
629
firstYesG nullSolution f xs c = go xs c
2✔
630
  where
631
    go [] cost = pure (cost, nullSolution)
1✔
632
    go [x] cost = f x (cost + 1)
2✔
633
    go (x : more) cost = do
2✔
634
      ans <- f x (cost + 1)
2✔
635
      case ans of
2✔
636
        (cost1, No _) -> go more cost1
2✔
637
        (_, Yes _) -> pure ans
2✔
638

639
noChoices :: Show t => Cost -> String -> t -> t -> t -> Int -> [(t, t)] -> Solution t
640
noChoices cost p smallest largest total count samp =
2✔
641
  No
2✔
642
    [ "\nNo legal choice can be found, where for each sample (x,y)"
×
643
    , "x+y = total && predicate x && predicate y"
×
644
    , "  predicate = " ++ p
×
645
    , "  smallest = " ++ show smallest
×
646
    , "  largest = " ++ show largest
×
647
    , "  total = " ++ show total
×
648
    , "  count = " ++ show count
×
649
    , "  cost = " ++ show cost
×
650
    , "Small sample of what was explored"
×
651
    , show samp
×
652
    ]
653

654
-- =====================================================
655

656
-- | Given 'count', return a list of pairs, that add to 'count'
657
--   splitsOf 6 --> [(1,5),(2,4),(3,3)].
658
--   Note we don't return reflections like (5,1) and (4,2),
659
--   as they have the same information as (1,5) and (2,4).
660
splitsOf :: Integral b => b -> [(b, b)]
661
splitsOf count = [(i, j) | i <- [1 .. div count 2], let j = count - i]
2✔
662
{-# SPECIALIZE splitsOf :: Int -> [(Int, Int)] #-}
663

664
-- | Given a Path, find a representative solution, 'ans', for that path, such that
665
--   1) (length ans) == 'count',
666
--   2) (sum ans) == 'total'
667
--   3) (all p ans) is True
668
--   What is a path?
669
--   Suppose i==5, then we recursively explore every way to split 5 into
670
--   split pairs that add to 5. I.e. (1,4) (2,3), then we split each of those.
671
--   Here is a picture of the graph of all paths for i==5. A path goes from the root '5'
672
--   to one of the leaves. Note all leaves are count == '1 (where the solution is '[total]').
673
--   To solve for 5, we could solve either of the sub problems rooted at 5: [1,4] or [2,3].
674
--   5
675
--   |
676
--   [1,4]
677
--   |  |
678
--   |  [1,3]
679
--   |  |  |
680
--   |  |  [1,2]
681
--   |  |     |
682
--   |  |     [1,1]
683
--   |  |
684
--   |  [2,2]
685
--   |   | |
686
--   |   | [1,1]
687
--   |   |
688
--   |   [1,1]
689
--   |
690
--   [2,3]
691
--    | |
692
--    | [1,2]
693
--    |    |
694
--    |    [1,1]
695
--    [1,1]
696
--  In 'pickAll' will explore a path for every split of 'count'
697
--  so if it returns (No _), we can be somewhat confidant that no solution exists.
698
--  Note that count of 1 and 2, are base cases.
699
--  When 'count' is greater than 1, we need to sample from [smallest..total],
700
--  so 'smallest' better be less that or equal to 'total'
701
pickAll ::
702
  forall t.
703
  (Show t, Integral t, Random t) =>
704
  t ->
705
  t ->
706
  (String, t -> Bool) ->
707
  t ->
708
  Int ->
709
  Cost ->
710
  Gen (Cost, Solution t)
711
pickAll smallest largest (pName, _) total count cost
2✔
712
  | cost > 1000 =
2✔
713
      pure $
2✔
714
        ( cost
2✔
715
        , No
2✔
716
            [ "\nPickAll exceeds cost limit " ++ show cost
×
717
            , "  predicate = " ++ pName
×
718
            , "  smallest = " ++ show smallest
×
719
            , "  largest = " ++ show largest
×
720
            , "  total = " ++ show total
×
721
            , "  count = " ++ show count
×
722
            ]
723
        )
724
pickAll smallest largest (pName, p) total 0 cost =
725
  if total == 0 && p total
×
726
    then pure (cost, Yes $ pure [])
×
727
    else
728
      pure
×
729
        ( cost
×
730
        , No
×
731
            [ "We are trying to find list of length 0."
×
732
            , "  Whose sum is " ++ show total ++ "."
×
733
            , "  That is only possible if the sum == 0."
×
734
            , "  All elements have to satisfy " ++ pName
×
735
            , "  smallest = " ++ show smallest
×
736
            , "  largest = " ++ show largest
×
737
            ]
738
        )
739
pickAll smallest largest (pName, p) total 1 cost =
740
  if p total
2✔
741
    then pure (cost, Yes $ pure [total])
2✔
742
    else pure (cost, noChoices cost pName smallest largest total 1 [(total, 0)])
1✔
743
pickAll smallest largest (pName, _) total count cost
744
  | smallest > largest =
2✔
745
      pure $
2✔
746
        ( cost
1✔
747
        , No
2✔
748
            [ "\nThe feasible range to pickAll ["
×
749
                ++ show smallest
×
750
                ++ " .. "
×
751
                ++ show (div total 2)
×
752
                ++ "] was empty"
×
753
            , "  predicate = " ++ pName
×
754
            , "  smallest = " ++ show smallest
×
755
            , "  largest = " ++ show largest
×
756
            , "  total = " ++ show total
×
757
            , "  count = " ++ show count
×
758
            , "  cost = " ++ show cost
×
759
            ]
760
        )
761
pickAll smallest largest (pName, p) total 2 cost = do
2✔
762
  -- for large things, use a fair sample.
763
  choices <- smallSample smallest largest total 1000 100
2✔
764
  case filter (\(x, y) -> p x && p y) choices of
2✔
765
    [] -> pure $ (cost + 1, noChoices cost pName smallest largest total 2 (take 10 choices))
1✔
766
    zs -> pure $ (cost + 1, Yes $ NE.fromList (fmap (\(x, y) -> [x, y]) zs))
2✔
767
pickAll smallest largest (pName, p) total count cost = do
2✔
768
  -- Compute a representative sample of the choices between smallest and total.
769
  -- E.g. when smallest = -2, and total = 5, the complete set of values is:
770
  -- [(-2,7),(-1,6),(0,5),(1,4),(2,3),(3,2),(4,1),(5,0)]  Note they all add to 5
771
  -- We could explore the whole set of values, but that can be millions of choices.
772
  -- so we choose to explore a representative subset. See the function 'fairSample', for details.
773
  -- Remember this is just 1 step on one path. So if this step fails, there are many more
774
  -- paths to explore. In fact there are usually many many solutions. We need to find just 1.
775
  choices <- smallSample smallest largest total 1000 20
2✔
776
  -- The choice of splits is crucial. If total >> count, we want the larger splits first
777
  -- if count >> total , we want smaller splits first
778
  splits <-
779
    if count >= 20
2✔
780
      then shuffle $ take 10 (splitsOf count)
2✔
781
      else
782
        if total > fromIntegral count
2✔
783
          then pure (reverse (splitsOf count))
2✔
784
          else pure (splitsOf count)
2✔
785

786
  firstYesG
2✔
787
    (No ["\nNo split has a solution", "cost = " ++ show cost])
×
788
    (doSplit smallest largest (pName, p) total choices)
1✔
789
    splits
2✔
790
    cost
2✔
791

792
-- TODO run some tests to see if this is a better solution than firstYesG
793
-- concatSolution smallest pName total count
794
--  <$> mapM  (doSplit smallest largest total (pName, p) choices (pickAll (depth +1) smallest)) splits
795

796
-- {-# SPECIALIZE pickAll::Int -> (String, Int -> Bool) -> Int -> Int -> Cost -> Gen (Cost, Solution Int) #-}
797

798
doSplit ::
799
  (Random t, Show t, Integral t) =>
800
  t ->
801
  t ->
802
  (String, t -> Bool) ->
803
  t ->
804
  [(t, t)] ->
805
  -- (t -> (String, t -> Bool) -> t -> Int -> Cost -> Gen (Cost, Solution t)) ->
806
  (Int, Int) ->
807
  Cost ->
808
  Gen (Cost, Solution t)
809
doSplit smallest largest (pName, p) total sample (i, j) c = go sample c
2✔
810
  where
811
    -- The 'sample' is a list of pairs (x,y), where we know (x+y) == total.
812
    -- We will search for the first good solution in the given sample
813
    -- to build a representative value for this path, with split (i,j).
814
    go ((x, y) : more) cost0 = do
2✔
815
      -- Note (i+j) = current length of the ans we are looking for
816
      --      (x+y) = total
817
      -- pick 'ans1' such that (sum ans1 == x) and (length ans1 == i)
818
      (cost1, ans1) <- pickAll smallest largest (pName, p) x i cost0
1✔
819
      -- pick 'ans2' such that (sum ans2 == y) and (length ans2 == j)
820
      (cost2, ans2) <- pickAll smallest largest (pName, p) y j cost1
1✔
821
      case (ans1, ans2) of
2✔
822
        (Yes ys, Yes zs) -> pure $ (cost2, Yes (NE.fromList [a <> b | a <- NE.toList ys, b <- NE.toList zs]))
2✔
823
        _ -> go more cost2
2✔
824
    go [] cost =
825
      case sample of
2✔
826
        [] ->
827
          pure $
2✔
828
            ( cost
2✔
829
            , No
2✔
830
                [ "\nThe sample passed to doSplit [" ++ show smallest ++ " .. " ++ show (div total 2) ++ "] was empty"
×
831
                , "  predicate = " ++ pName
×
832
                , "  smallest = " ++ show smallest
×
833
                , "  largest = " ++ show largest
×
834
                , "  total " ++ show total
×
835
                , "  count = " ++ show (i + j)
×
836
                , "  split of count = " ++ show (i, j)
×
837
                ]
838
            )
839
        ((left, right) : _) ->
840
          pure $
2✔
841
            ( cost
2✔
842
            , No
2✔
843
                [ "\nAll choices in (genSizedList " ++ show (i + j) ++ " 'p' " ++ show total ++ ") have failed."
×
844
                , "Here is 1 example failure."
×
845
                , "  smallest = " ++ show smallest
×
846
                , "  largest = " ++ show largest
×
847
                , "  total " ++ show total ++ " = " ++ show left ++ " + " ++ show right
×
848
                , "  count = " ++ show (i + j) ++ ", split of count = " ++ show (i, j)
×
849
                , "We are trying to solve sub-problems like:"
×
850
                , "  split " ++ show left ++ " into " ++ show i ++ " parts, where all parts meet 'p'"
×
851
                , "  split " ++ show right ++ " into " ++ show j ++ " parts, where all parts meet 'p'"
×
852
                , "Predicate 'p' = " ++ pName
×
853
                , "A small prefix of the sample, elements (x,y) where x+y = " ++ show total
×
854
                , unlines (map (("  " ++) . show) (take 10 sample))
×
855
                ]
856
            )
857
{-# INLINE doSplit #-}
858

859
-- | If the sample is small enough, then enumerate all of it, otherwise take a fair sample.
860
smallSample :: (Random t, Integral t) => t -> t -> t -> t -> Int -> Gen [(t, t)]
861
smallSample smallest largest total bound size
2✔
862
  | largest - smallest <= bound = do
2✔
863
      shuffle $ takeWhile (uncurry (<=)) [(x, total - x) | x <- [smallest .. total]]
2✔
864
  | otherwise = do
1✔
865
      choices <- fair smallest largest size 5 True
2✔
866
      shuffle [(x, total - x) | x <- choices]
2✔
867
{-# INLINE smallSample #-}
868

869
-- | Generates a fair sample of numbers between 'smallest' and 'largest'.
870
--   makes sure there are numbers of all sizes. Controls both the size of the sample
871
--   and the precision (how many powers of 10 are covered)
872
--   Here is how we generate one sample when we call (fair (-3455) (10234) 12 3 True)
873
--   raw = [(-9999,-1000),(-999,-100),(-99,-10),(-9,-1),(0,9),(10,99),(100,999),(1000,9999),(10000,99999)]
874
--   ranges = [(-3455,-1000),(-999,-100),(-99,-10),(-9,-1),(0,9),(10,99),(100,999),(1000,9999),(10000,10234)]
875
--   count = 4
876
--   largePrecision = [(10000,10234),(1000,9999),(100,999)]
877
--   smallPrecision = [(-3455,-1000),(-999,-100),(-99,-10)]
878
--   answer generated = [10128,10104,10027,10048,4911,7821,5585,2157,448,630,802,889]
879
--   isLarge==True   means be biased towards the large end of the range,
880
--   isLArge==False  means be biased towards the small end of the range,
881
fair :: (Random a, Integral a) => a -> a -> Int -> Int -> Bool -> Gen [a]
882
fair smallest largest size precision isLarge =
2✔
883
  concat <$> mapM oneRange (if isLarge then largePrecision else smallPrecision)
1✔
884
  where
885
    raw = map logRange [logish smallest .. logish largest]
2✔
886
    fixEnds (x, y) = (max smallest x, min largest y)
2✔
887
    ranges = map fixEnds raw
2✔
888
    count = div size precision
2✔
889
    largePrecision = take precision (reverse ranges)
2✔
890
    smallPrecision = take precision ranges
×
891
    oneRange (x, y) = vectorOf count (choose (x, y))
2✔
892

893
-- | Get the bucket a number is in, i.e. @0-9, 10-99@, etc.
894
logRange :: Integral a => a -> (a, a)
895
logRange 1 = (10, 99)
2✔
896
logRange (-1) = (-9, -1)
2✔
897
logRange n = case compare n 0 of
2✔
898
  EQ -> (0, 9)
2✔
899
  LT -> (negate (div b 10), negate (div a 10))
2✔
900
  GT -> (10 ^ n, 10 ^ (n + 1) - 1)
2✔
901
  where
902
    (a, b) = logRange (negate n)
2✔
903

904
-- | like (logBase10 n), except negative answers mean negative numbers, rather than fractions less than 1.
905
logish :: Integral t => t -> t
906
logish n
2✔
907
  | 0 <= n && n <= 9 = 0
2✔
908
  | n > 9 = 1 + logish (n `div` 10)
2✔
909
  | (-9) <= n && n <= (-1) = -1
2✔
910
  | True = negate (1 + logish (negate n))
1✔
911

912
-- =====================================================================
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc