Skip to content

Commit ae36aa4

Browse files
Merge branch 'master' into remove-cruft-core
2 parents 70165ea + d20193c commit ae36aa4

File tree

12 files changed

+183
-52
lines changed

12 files changed

+183
-52
lines changed

.github/workflows/haskell-ci.yml

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ on:
1111
- 'master'
1212
schedule:
1313
- cron: 0 0 * * *
14+
workflow_dispatch:
15+
inputs:
16+
nightly:
17+
description: Run with the same settings as a nightly build
18+
type: boolean
19+
default: false
1420

1521
concurrency:
1622
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -29,6 +35,14 @@ jobs:
2935
runs-on: ${{ matrix.os }}
3036

3137
steps:
38+
- name: Set NIGHTLY environment variable if the job was triggered by the scheduler
39+
if: "${{ github.event_name == 'schedule'
40+
|| contains(github.event.pull_request.title, 'nightly')
41+
|| contains(github.event.pull_request.title, 'NIGHTLY')
42+
|| github.event_name == 'workflow_dispatch' && github.event.inputs.nightly }}"
43+
run: |
44+
echo "NIGHTLY=true" >> $GITHUB_ENV
45+
3246
- uses: actions/checkout@v4
3347
- uses: haskell-actions/setup@v2
3448
with:
@@ -50,40 +64,48 @@ jobs:
5064
name: Stack
5165
runs-on: ubuntu-latest
5266
steps:
53-
- uses: actions/checkout@v4
67+
- name: Set NIGHTLY environment variable if the job was triggered by the scheduler
68+
if: "${{ github.event_name == 'schedule'
69+
|| contains(github.event.pull_request.title, 'nightly')
70+
|| contains(github.event.pull_request.title, 'NIGHTLY')
71+
|| github.event_name == 'workflow_dispatch' && github.event.inputs.nightly }}"
72+
run: |
73+
echo "NIGHTLY=true" >> $GITHUB_ENV
74+
75+
- uses: actions/checkout@v4
5476

55-
- uses: haskell-actions/setup@v2
56-
id: setup-haskell-stack
57-
name: Setup Haskell
58-
with:
59-
enable-stack: true
60-
stack-version: latest
61-
ghc-version: 9.6.7
77+
- uses: haskell-actions/setup@v2
78+
id: setup-haskell-stack
79+
name: Setup Haskell
80+
with:
81+
enable-stack: true
82+
stack-version: latest
83+
ghc-version: 9.6.7
6284

63-
- name: Cache
64-
id: cache
65-
uses: actions/cache@v4
66-
with:
67-
path: |
68-
${{ steps.setup-haskell-stack.outputs.stack-root }}
69-
.stack-work
70-
key: ${{ runner.os }}-stack-${{ github.sha }}
71-
restore-keys: ${{ runner.os }}-stack
85+
- name: Cache
86+
id: cache
87+
uses: actions/cache@v4
88+
with:
89+
path: |
90+
${{ steps.setup-haskell-stack.outputs.stack-root }}
91+
.stack-work
92+
key: ${{ runner.os }}-stack-${{ github.sha }}
93+
restore-keys: ${{ runner.os }}-stack
7294

73-
- name: Test
74-
run: stack test --coverage --flag constrained-generators:dev
95+
- name: Test
96+
run: stack test --coverage --flag constrained-generators:dev
7597

76-
- uses: actions/cache/save@v4
77-
with:
78-
path: |
79-
${{ steps.setup-haskell-stack.outputs.stack-root }}
80-
.stack-work
81-
key: ${{ runner.os }}-stack-${{ github.sha }}
98+
- uses: actions/cache/save@v4
99+
with:
100+
path: |
101+
${{ steps.setup-haskell-stack.outputs.stack-root }}
102+
.stack-work
103+
key: ${{ runner.os }}-stack-${{ github.sha }}
82104

83-
- name: Upload coverage report
84-
env:
85-
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
86-
run: |
87-
[ -n "${COVERALLS_REPO_TOKEN}" ]
88-
curl -L https://github.com/rubik/stack-hpc-coveralls/releases/download/v0.0.7.0/shc-Linux-X64.tar.bz2 | tar xj shc
89-
./shc --repo-token="$COVERALLS_REPO_TOKEN" --partial-coverage --fetch-coverage combined all
105+
- name: Upload coverage report
106+
env:
107+
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
108+
run: |
109+
[ -n "${COVERALLS_REPO_TOKEN}" ]
110+
curl -L https://github.com/rubik/stack-hpc-coveralls/releases/download/v0.0.7.0/shc-Linux-X64.tar.bz2 | tar xj shc
111+
./shc --repo-token="$COVERALLS_REPO_TOKEN" --partial-coverage --fetch-coverage combined all

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) =

src/Constrained/Base.hs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,12 @@ class
488488
-- | Shrink an `a` with the aide of a `TypeSpec`
489489
shrinkWithTypeSpec :: TypeSpec a -> a -> [a]
490490

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+
491497
-- | Convert a spec to predicates:
492498
-- The key property here is:
493499
-- ∀ a. a `conformsTo` spec == a `conformsTo` constrained (\t -> toPreds t spec)
@@ -598,6 +604,13 @@ class
598604
[a]
599605
shrinkWithTypeSpec spec a = map fromSimpleRep $ shrinkWithTypeSpec spec (toSimpleRep a)
600606

607+
default fixupWithTypeSpec ::
608+
GenericallyInstantiated a =>
609+
TypeSpec a ->
610+
a ->
611+
Maybe a
612+
fixupWithTypeSpec spec a = fromSimpleRep <$> fixupWithTypeSpec spec (toSimpleRep a)
613+
601614
default cardinalTypeSpec ::
602615
GenericallyInstantiated a =>
603616
TypeSpec a ->
@@ -618,6 +631,7 @@ instance HasSpec Bool where
618631
cardinalTypeSpec _ = equalSpec 2
619632
cardinalTrueSpec = equalSpec 2
620633
shrinkWithTypeSpec _ = shrink
634+
fixupWithTypeSpec _ = Just
621635
conformsTo _ _ = True
622636
toPreds _ _ = TruePred
623637

@@ -627,6 +641,7 @@ instance HasSpec () where
627641
combineSpec _ _ = typeSpec ()
628642
_ `conformsTo` _ = True
629643
shrinkWithTypeSpec _ _ = []
644+
fixupWithTypeSpec _ _ = pure ()
630645
genFromTypeSpec _ = pure ()
631646
toPreds _ _ = TruePred
632647
cardinalTypeSpec _ = MemberSpec (pure 1)

src/Constrained/Conformance.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module Constrained.Conformance (
1414
conformsToSpec,
1515
conformsToSpecE,
1616
satisfies,
17+
checkPredE,
1718
checkPredsE,
1819
) where
1920

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)