Skip to content

Commit f9bb7ad

Browse files
feat: accept a configuration for property tests with SlimCheck (#57)
Extra: * Adjust code to a more common formatting * Remove a workaround for a Lean 4 bug that has already been solved
1 parent 0f9008e commit f9bb7ad

File tree

4 files changed

+98
-115
lines changed

4 files changed

+98
-115
lines changed

LSpec/LSpec.lean

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import Lean
22
import LSpec.SlimCheck.Checkable
33

44
/-!
5-
65
# The core `LSpec` framework
76
87
## Add all relavent documentation
@@ -37,15 +36,14 @@ instance (priority := 25) (p : Prop) [d : Decidable p] : Testable p :=
3736
| isFalse h => .isFalse h "Evaluated to false"
3837
| isTrue h => .isTrue h
3938

40-
open SlimCheck Decorations in
41-
instance (priority := 25) (p : Prop) [Checkable p] : Testable p :=
42-
let (res, _) := ReaderT.run (Checkable.runSuite p) (.up mkStdGen)
39+
open SlimCheck in
40+
instance instTestableOfCheckable (p : Prop) (cfg : Configuration) [Checkable p] : Testable p :=
41+
let (res, _) := ReaderT.run (Checkable.runSuite p cfg) (.up mkStdGen)
4342
match res with
4443
| .success (.inr h) => .isTrue h
4544
| .success (.inl _) => .isMaybe
4645
| .gaveUp n => .isFailure s!"Gave up {n} times"
47-
| .failure h xs n =>
48-
.isFalse h $ Checkable.formatFailure "Found problems!" xs n
46+
| .failure h xs n => .isFalse h $ Checkable.formatFailure "Found problems!" xs n
4947

5048
/-- Formats the extra error message from `Testable` failures. -/
5149
def formatErrorMsg : Option String → String
@@ -81,10 +79,7 @@ def test (descr : String) (p : Prop) [Testable p]
8179
(next : TestSeq := .done) : TestSeq :=
8280
.individual descr p inferInstance next
8381

84-
/-
85-
Allows collecting a `TestSeq` into a test group to print results
86-
in an indented group.
87-
-/
82+
/-- Allows collecting a `TestSeq` into a test group to print results in a group. -/
8883
def group (descr : String) (groupTests : TestSeq)
8984
(next : TestSeq := .done) : TestSeq :=
9085
.group descr groupTests next
@@ -94,8 +89,9 @@ open SlimCheck Decorations in
9489
Checks a `Checkable` prop. Note that `mk_decorations` is here simply to improve error messages
9590
and if `p` is Checkable, then so is `p'`.
9691
-/
97-
def check (descr : String) (p : Prop) (next : TestSeq := .done)
92+
def check (descr : String) (p : Prop) (next : TestSeq := .done) (cfg : Configuration := {})
9893
(p' : DecorationsOf p := by mk_decorations) [Checkable p'] : TestSeq :=
94+
haveI : Testable p' := instTestableOfCheckable p' cfg
9995
test descr p' next
10096

10197
inductive ExpectationFailure (exp got : String) : Prop

LSpec/SlimCheck/Checkable.lean

Lines changed: 77 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ property as is will give us an error because we do not have an instance
3737
of `Shrinkable MyType` and `SampleableExt MyType`. We can define one as follows:
3838
```lean
3939
instance : Shrinkable MyType where
40-
shrink := λ ⟨x,y,h⟩ =>
40+
shrink := fun ⟨x,y,h⟩ =>
4141
let proxy := Shrinkable.shrink (x, y - x)
42-
proxy.map (λ ⟨⟨fst, snd⟩, ha⟩ => ⟨⟨fst, fst + snd, sorry⟩, sorry⟩)
42+
proxy.map (fun ⟨⟨fst, snd⟩, ha⟩ => ⟨⟨fst, fst + snd, sorry⟩, sorry⟩)
4343
instance : SampleableExt MyType :=
4444
SampleableExt.mkSelfContained do
4545
let x ← SampleableExt.interpSample Nat
@@ -98,7 +98,17 @@ structure Configuration where
9898
traceShrinkCandidates : Bool := false
9999
randomSeed : Option Nat := none
100100
quiet : Bool := false
101-
deriving Inhabited
101+
102+
namespace Configuration
103+
104+
/-- A configuration with all the trace options enabled, useful for debugging. -/
105+
def verbose : Configuration where
106+
traceDiscarded := true
107+
traceSuccesses := true
108+
traceShrink := true
109+
traceShrinkCandidates := true
110+
111+
end Configuration
102112

103113
/--
104114
`PrintableProp p` allows one to print a proposition so that
@@ -136,8 +146,8 @@ def combine {p q : Prop} : PSum Unit (p → q) → PSum Unit p → PSum Unit q
136146

137147
/-- Combine the test result for properties `p` and `q` to create a test for their conjunction. -/
138148
def and : TestResult p → TestResult q → TestResult (p ∧ q)
139-
| failure h xs n, _ => failure (λ h2 => h h2.left) xs n
140-
| _, failure h xs n => failure (λ h2 => h h2.right) xs n
149+
| failure h xs n, _ => failure (fun h2 => h h2.left) xs n
150+
| _, failure h xs n => failure (fun h2 => h h2.right) xs n
141151
| success h1, success h2 => success $ combine (combine (PSum.inr And.intro) h1) h2
142152
| gaveUp n, gaveUp m => gaveUp $ n + m
143153
| gaveUp n, _ => gaveUp n
@@ -146,7 +156,7 @@ def and : TestResult p → TestResult q → TestResult (p ∧ q)
146156
/-- Combine the test result for properties `p` and `q` to create a test for their disjunction. -/
147157
def or : TestResult p → TestResult q → TestResult (p ∨ q)
148158
| failure h1 xs n, failure h2 ys m =>
149-
let h3 := λ h =>
159+
let h3 := fun h =>
150160
match h with
151161
| Or.inl h3 => h1 h3
152162
| Or.inr h3 => h2 h3
@@ -186,39 +196,29 @@ def addVarInfo [Repr γ] (var : String) (x : γ) (h : q → p) (r : TestResult p
186196
addInfo s!"{var} := {repr x}" h r p
187197

188198
def isFailure : TestResult p → Bool
189-
| failure _ _ _ => true
190-
| _ => false
199+
| failure .. => true
200+
| _ => false
191201

192202
end TestResult
193203

194-
namespace Configuration
195-
196-
/-- A configuration with all the trace options enabled, useful for debugging. -/
197-
def verbose : Configuration where
198-
traceDiscarded := true
199-
traceSuccesses := true
200-
traceShrink := true
201-
traceShrinkCandidates := true
202-
203-
end Configuration
204-
205204
namespace Checkable
206205

207206
open TestResult
208207

209208
def runProp (p : Prop) [Checkable p] : Configuration → Bool → Gen (TestResult p) := Checkable.run
210209

211210
/-- A `dbgTrace` with special formatting -/
212-
def slimTrace [Pure m] (s : String) : m PUnit := dbgTrace s!"[SlimCheck: {s}]" (λ _ => pure ())
211+
def slimTrace [Pure m] (s : String) : m Unit :=
212+
dbgTrace s!"[SlimCheck: {s}]" fun _ => pure ()
213213

214214
instance andCheckable [Checkable p] [Checkable q] : Checkable (p ∧ q) where
215-
run := λ cfg min => do
215+
run := fun cfg min => do
216216
let xp ← runProp p cfg min
217217
let xq ← runProp q cfg min
218218
pure $ and xp xq
219219

220220
instance orCheckable [Checkable p] [Checkable q] : Checkable (p ∨ q) where
221-
run := λ cfg min => do
221+
run := fun cfg min => do
222222
let xp ← runProp p cfg min
223223
-- As a little performance optimization we can just not run the second
224224
-- test if the first succeeds
@@ -229,44 +229,40 @@ instance orCheckable [Checkable p] [Checkable q] : Checkable (p ∨ q) where
229229
let xq ← runProp q cfg min
230230
pure $ or xp xq
231231

232-
-- TODO(Winston): Move
233-
protected theorem key : (a ↔ b) ↔ (a ∧ b) ∨ (¬ a ∧ ¬ b) :=
234-
by constructor
235-
· intro h; rw [h]
236-
by_cases h : b
237-
· exact Or.inl <| And.intro h h
238-
· exact Or.inr <| And.intro h h
239-
· intro h
240-
match h with
241-
| Or.inl h => exact Iff.intro (λ _ => h.2) (λ _ => h.1)
242-
| Or.inr h => exact Iff.intro (λ a => False.elim $ h.1 a) (λ b => False.elim $ h.2 b)
243-
244232
instance iffCheckable [Checkable ((p ∧ q) ∨ (¬ p ∧ ¬ q))] : Checkable (p ↔ q) where
245-
run := λ cfg min => do
233+
run := fun cfg min => do
246234
let h ← runProp ((p ∧ q) ∨ (¬ p ∧ ¬ q)) cfg min
247-
pure $ iff Checkable.key h
235+
have key {a b} : (a ↔ b) ↔ (a ∧ b) ∨ (¬ a ∧ ¬ b) := by
236+
constructor
237+
· intro h; rw [h]
238+
by_cases h : b
239+
· exact .inl $ .intro h h
240+
· exact .inr $ .intro h h
241+
· intro h
242+
match h with
243+
| .inl h => exact Iff.intro (fun _ => h.2) (fun _ => h.1)
244+
| .inr h => exact Iff.intro (fun a => False.elim $ h.1 a) (fun b => False.elim $ h.2 b)
245+
pure $ iff key h
248246

249247
instance decGuardCheckable [PrintableProp p] [Decidable p] {β : p → Prop} [∀ h, Checkable (β h)] : Checkable (NamedBinder var $ ∀ h, β h) where
250-
run := λ cfg min => do
248+
run := fun cfg min => do
251249
if h : p then
252250
let res := (runProp (β h) cfg min)
253251
let s := printProp p
254-
(λ r => addInfo s!"guard: {s}" (· $ h) r (PSum.inr $ λ q _ => q)) <$> res
252+
(fun r => addInfo s!"guard: {s}" (· $ h) r (PSum.inr $ fun q _ => q)) <$> res
255253
else if cfg.traceDiscarded || cfg.traceSuccesses then
256-
let res := (λ _ => pure $ gaveUp 1)
254+
let res := (fun _ => pure $ gaveUp 1)
257255
let s := printProp p
258256
slimTrace s!"discard: Guard {s} does not hold"; res
259257
else
260258
pure $ gaveUp 1
261259

262260
instance forallTypesCheckable {f : TypeProp} [Checkable (f Int)] : Checkable (NamedBinder var $ ∀ x, f x) where
263-
run := λ cfg min => do
261+
run := fun cfg min => do
264262
let r ← runProp (f Int) cfg min
265263
pure $ addVarInfo var "ℤ" (· $ Int) r
266264

267-
/--
268-
Format the counter-examples found in a test failure.
269-
-/
265+
/-- Format the counter-examples found in a test failure. -/
270266
def formatFailure (s : String) (xs : List String) (n : Nat) : String :=
271267
let counter := "\n".intercalate xs
272268
let parts := [
@@ -278,24 +274,19 @@ def formatFailure (s : String) (xs : List String) (n : Nat) : String :=
278274
]
279275
"\n".intercalate parts
280276

281-
/--
282-
Increase the number of shrinking steps in a test result.
283-
-/
277+
/-- Increase the number of shrinking steps in a test result. -/
284278
def addShrinks (n : Nat) : TestResult p → TestResult p
285279
| TestResult.failure p xs m => TestResult.failure p xs (m + n)
286280
| p => p
287281

288-
-- TODO(Winston): Move
289-
instance [Inhabited (m (Option α))]: Inhabited (OptionT m α) where
290-
default := .mk default
291-
292282
/-- Shrink a counter-example `x` by using `Shrinkable.shrink x`, picking the first
293283
candidate that falsifies a property and recursively shrinking that one.
294284
The process is guaranteed to terminate because `shrink x` produces
295285
a proof that all the values it produces are smaller (according to `SizeOf`)
296286
than `x`. -/
297-
partial def minimizeAux [SampleableExt α] {β : α → Prop} [∀ x, Checkable (β x)] (cfg : Configuration) (var : String)
298-
(x : SampleableExt.proxy α) (n : Nat) : OptionT Gen (Σ x, TestResult (β (SampleableExt.interp x))) := do
287+
def minimizeAux [SampleableExt α] {β : α → Prop} [∀ x, Checkable (β x)]
288+
(cfg : Configuration) (var : String) (x : SampleableExt.proxy α) (n : Nat) :
289+
OptionT Gen (Σ x, TestResult (β (SampleableExt.interp x))) := do
299290
let candidates := SampleableExt.shrink.shrink x
300291
if cfg.traceShrinkCandidates then
301292
slimTrace s!"Candidates for {var} := {repr x}:\n {repr candidates}"
@@ -327,12 +318,12 @@ def minimize [SampleableExt α] {β : α → Prop} [∀ x, Checkable (β x)] (cf
327318
/-- Test a universal property by creating a sample of the right type and instantiating the
328319
bound variable with it. -/
329320
instance varCheckable [SampleableExt α] {β : α → Prop} [∀ x, Checkable (β x)] : Checkable (NamedBinder var $ ∀ x : α, β x) where
330-
run := λ cfg min => do
321+
run := fun cfg min => do
331322
let x ← SampleableExt.sample
332323
if cfg.traceSuccesses || cfg.traceDiscarded then
333324
slimTrace s!"{var} := {repr x}"
334325
let r ← Checkable.runProp (β $ SampleableExt.interp x) cfg false
335-
let ⟨finalX, finalR⟩ ←
326+
let ⟨finalX, finalR⟩ ←
336327
if isFailure r then
337328
if cfg.traceSuccesses then
338329
slimTrace s!"{var} := {repr x} is a failure"
@@ -348,21 +339,21 @@ instance varCheckable [SampleableExt α] {β : α → Prop} [∀ x, Checkable (
348339
instance propVarCheckable {β : PropProp} [∀ b : Bool, Checkable (β b)] :
349340
Checkable (NamedBinder var $ ∀ p : Prop, β p)
350341
where
351-
run := λ cfg min =>
352-
imp (λ h (b : Bool) => h b) <$> Checkable.runProp (NamedBinder var $ ∀ b : Bool, β b) cfg min
342+
run := fun cfg min =>
343+
imp (fun h (b : Bool) => h b) <$> Checkable.runProp (NamedBinder var $ ∀ b : Bool, β b) cfg min
353344

354345
instance (priority := high) unusedVarCheckable [Nonempty α] [Checkable β] :
355346
Checkable (NamedBinder var $ ∀ _x : α, β)
356347
where
357-
run := λ cfg min => do
348+
run := fun cfg min => do
358349
if cfg.traceDiscarded || cfg.traceSuccesses then
359350
slimTrace s!"{var} is unused"
360351
let r ← Checkable.runProp β cfg min
361352
let finalR := addInfo s!"{var} is irrelevant (unused)" id r
362-
pure $ imp (· $ Classical.ofNonempty) finalR (PSum.inr $ λ x _ => x)
353+
pure $ imp (· $ Classical.ofNonempty) finalR (PSum.inr $ fun x _ => x)
363354

364355
instance (priority := low) decidableCheckable {p : Prop} [PrintableProp p] [Decidable p] : Checkable p where
365-
run := λ _ _ =>
356+
run := fun _ _ =>
366357
if h : p then
367358
pure $ success (PSum.inr h)
368359
else
@@ -416,34 +407,33 @@ open TestResult
416407

417408
/-- Execute `cmd` and repeat every time the result is `gave_up` (at most `n` times). -/
418409
def retry (cmd : Rand (TestResult p)) : Nat → Rand (TestResult p)
419-
| 0 => pure $ TestResult.gaveUp 1
420-
| n+1 => do
421-
let r ← cmd
422-
match r with
423-
| success hp => pure $ success hp
424-
| TestResult.failure h xs n => pure $ failure h xs n
425-
| gaveUp _ => retry cmd n
410+
| 0 => pure $ TestResult.gaveUp 1
411+
| n + 1 => do match ← cmd with
412+
| success hp => pure $ success hp
413+
| TestResult.failure h xs n => pure $ failure h xs n
414+
| gaveUp _ => retry cmd n
426415

427416
/-- Count the number of times the test procedure gave up. -/
428417
def giveUp (x : Nat) : TestResult p → TestResult p
429-
| success (PSum.inl ()) => gaveUp x
430-
| success (PSum.inr p) => success $ (PSum.inr p)
431-
| gaveUp n => gaveUp $ n + x
432-
| TestResult.failure h xs n => failure h xs n
418+
| success (PSum.inl ()) => gaveUp x
419+
| success (PSum.inr p) => success $ (PSum.inr p)
420+
| gaveUp n => gaveUp $ n + x
421+
| TestResult.failure h xs n => failure h xs n
433422

434423
/-- Try `n` times to find a counter-example for `p`. -/
435-
def Checkable.runSuiteAux (p : Prop) [Checkable p] (cfg : Configuration) : TestResult p → Nat → Rand (TestResult p)
436-
| r, 0 => pure r
437-
| r, n+1 => do
438-
let size := (cfg.numInst - n - 1) * cfg.maxSize / cfg.numInst
439-
if cfg.traceSuccesses then
440-
slimTrace s!"New sample"
441-
slimTrace s!"Retrying up to {cfg.numRetries} times until guards hold"
442-
let x ← retry (ReaderT.run (Checkable.runProp p cfg true) ⟨size⟩) cfg.numRetries
443-
match x with
444-
| (success (PSum.inl ())) => runSuiteAux p cfg r n
445-
| (gaveUp g) => runSuiteAux p cfg (giveUp g r) n
446-
| _ => pure $ x
424+
def Checkable.runSuiteAux (p : Prop) [Checkable p] (cfg : Configuration) (r : TestResult p) :
425+
Nat → Rand (TestResult p)
426+
| 0 => pure r
427+
| n + 1 => do
428+
let size := (cfg.numInst - n - 1) * cfg.maxSize / cfg.numInst
429+
if cfg.traceSuccesses then
430+
slimTrace s!"New sample"
431+
slimTrace s!"Retrying up to {cfg.numRetries} times until guards hold"
432+
let x ← retry (ReaderT.run (Checkable.runProp p cfg true) ⟨size⟩) cfg.numRetries
433+
match x with
434+
| (success (PSum.inl ())) => runSuiteAux p cfg r n
435+
| (gaveUp g) => runSuiteAux p cfg (giveUp g r) n
436+
| _ => pure $ x
447437

448438
/-- Try to find a counter-example of `p`. -/
449439
def Checkable.runSuite (p : Prop) [Checkable p] (cfg : Configuration := {}) : Rand (TestResult p) :=
@@ -464,8 +454,7 @@ open Lean
464454
/-- Traverse the syntax of a proposition to find universal quantifiers
465455
quantifiers and add `NamedBinder` annotations next to them. -/
466456
partial def addDecorations (e : Expr) : Expr :=
467-
e.replace $ λ expr =>
468-
match expr with
457+
e.replace fun expr => match expr with
469458
| Expr.forallE name type body data =>
470459
let n := name.toString
471460
let newType := addDecorations type
@@ -479,9 +468,7 @@ that the goal should be satisfied with a proposition equivalent to `p`
479468
with added annotations. -/
480469
abbrev DecorationsOf (_p : Prop) := Prop
481470

482-
open Elab.Tactic
483-
open Meta
484-
471+
open Elab.Tactic in
485472
/-- In a goal of the shape `⊢ DecorationsOf p`, `mk_decoration` examines
486473
the syntax of `p` and adds `NamedBinder` around universal quantifications
487474
to improve error messages. This tool can be used in the declaration of a
@@ -501,10 +488,9 @@ end Decorations
501488

502489
open Decorations in
503490
/-- Run a test suite for `p` and throw an exception if `p` does not not hold.-/
504-
def Checkable.check (p : Prop) (cfg : Configuration := {}) (p' : Decorations.DecorationsOf p := by mk_decorations) [Checkable p'] : IO PUnit := do
505-
let x ← Checkable.checkIO p' cfg
506-
go p' x where /-- HACK: https://github.com/leanprover/lean4/issues/1247 -/ go p' (x : TestResult p') : IO PUnit := do
507-
match x with
491+
def Checkable.check (p : Prop) (cfg : Configuration := {})
492+
(p' : DecorationsOf p := by mk_decorations) [Checkable p'] : IO Unit := do
493+
match ← Checkable.checkIO p' cfg with
508494
| TestResult.success _ => if !cfg.quiet then IO.println "Success" else pure ()
509495
| TestResult.gaveUp n => if !cfg.quiet then IO.println s!"Gave up {n} times"
510496
| TestResult.failure _ xs n => throw (IO.userError $ formatFailure "Found problems!" xs n)

LSpec/SlimCheck/Gen.lean

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ namespace Gen
3131

3232
/-- Lift `Random.random` to the `Gen` monad. -/
3333
def chooseAny (α : Type u) [Random α] [DefaultRange α] : Gen α :=
34-
λ _ => rand α
34+
fun _ => rand α
3535

3636
/-- Lift `BoundedRandom.randomR` to the `Gen` monad. -/
3737
def choose (α : Type u) [Random α] (lo hi : α) : Gen α :=
38-
λ _ => randBound α lo hi
38+
fun _ => randBound α lo hi
3939

4040
/-- Get access to the size parameter of the `Gen` monad. -/
4141
def getSize : Gen Nat :=

0 commit comments

Comments
 (0)