@@ -109,9 +109,13 @@ The code for these combinators closely mirrors those used in Rocq/Coq QuickChick
109109
110110 -/
111111
112+ /-- Raised when a fueled generator fails due to insufficient fuel. -/
113+ meta def Gen.outOfFuel : GenError :=
114+ .genError "out of fuel"
115+
112116/-- `pick default xs n` chooses a weight & a generator `(k, gen)` from the list `xs` such that `n < k`.
113117 If `xs` is empty, the `default` generator with weight 0 is returned. -/
114- private def pick (default : Gen α) (xs : List (Nat × Gen α)) (n : Nat) : Nat × Gen α :=
118+ def pick (default : Gen α) (xs : List (Nat × Gen α)) (n : Nat) : Nat × Gen α :=
115119 match xs with
116120 | [] => (0 , default)
117121 | (k, x) :: xs =>
@@ -120,6 +124,40 @@ private def pick (default : Gen α) (xs : List (Nat × Gen α)) (n : Nat) : Nat
120124 else
121125 pick default xs (n - k)
122126
127+ instance inhabitedGen : Inhabited (Gen α) where
128+ default := throw <| .genError "inhabitedWitness"
129+
130+
131+ private def pickDrop (xs : List (Nat × Gen α)) (n : Nat) : Nat × Gen α × List (Nat × Gen α) :=
132+ let fail : GenError := .genError "Plausible.Chamelean.Gen.pickDrop: out of options."
133+ match xs with
134+ | [] => (0 , throw fail, [])
135+ | (k, x) :: xs =>
136+ if n < k then
137+ (k, x, xs)
138+ else
139+ let (k', x', xs') := pickDrop xs (n - k)
140+ (k', x', (k, x)::xs')
141+
142+ private def sumFst (gs : List (Nat × α)) : Nat := List.sum <| List.map Prod.fst gs
143+
144+ /- Helper function for `backtrack` which picks one out of `total` generators with some initial amount of `fuel` -/
145+ private def backtrackFuel {α : Type u} (fuel : Nat) (total : Nat) (gs : List (Nat × Gen α)) : Gen α :=
146+ match fuel with
147+ | .zero => throw Gen.outOfFuel
148+ | .succ fuel' => do
149+ let n ← (Gen.choose Nat 0 (total - 1 ) (by omega)).up
150+ let (k, g, gs') := pickDrop gs n.down
151+ -- Try to generate a value using `g`, if it fails, backtrack with `fuel'`
152+ -- and pick one out of the `total - k` remaining generators
153+ tryCatch g (fun _ => backtrackFuel fuel' (total - k) gs')
154+
155+ /-- Tries all generators until one returns a `Some` value or all the generators failed once with `None`.
156+ The generators are picked at random according to their weights (like `frequency` in Haskell QuickCheck),
157+ and each generator is run at most once. -/
158+ def backtrack (gs : List (Nat × Gen α)) : Gen α :=
159+ backtrackFuel (gs.length) (sumFst gs) gs
160+
123161/-- Picks one of the generators in `gs` at random, returning the `default` generator
124162 if `gs` is empty.
125163
@@ -220,7 +258,7 @@ partial def Gen.runUntil {α : Type} (attempts : Option Nat := .none) (x : Gen
220258 where
221259 repeatGen (attempts : Option Nat) (x : Gen α) : Gen α :=
222260 match attempts with
223- | .some 0 => throw <| GenError.genError "Gen.runUtil : Out of attempts"
261+ | .some 0 => throw <| GenError.genError "Gen.runUntil : Out of attempts"
224262 | _ =>
225263 try x catch
226264 | GenError.genError _ => do
@@ -230,19 +268,23 @@ partial def Gen.runUntil {α : Type} (attempts : Option Nat := .none) (x : Gen
230268 | .some n => .some (n-1 )
231269 | .none => .none
232270
233- private def test : Gen Nat :=
271+ private def test (n : Nat) : Gen Nat :=
234272 do
235273 let x : Nat ← Gen.choose Nat 0 (← Gen.getSize) (Nat.zero_le _)
236- if x % 10 == 0
274+ if x % n == 0
237275 then
238276 return x
239277 else
240278 throw <| .genError "uh oh"
241279
242- -- This fails 9/10 times
243- -- #eval Gen.run test 9
280+ -- -- This fails `9/10` times
281+ -- #eval Gen.run (test 10) 9
282+
283+ -- -- This succeeds almost always.
284+ -- #eval Gen.runUntil (attempts := .some 1000) (test 10) 9
285+
244286
245- -- This succeeds almost always.
246- -- #eval Gen.runUntil (attempts := .some 1000) test 9
287+ -- -- This test usually returns an even number, but sometimes `1`
288+ -- #eval Gen.run (Gen.backtrack [(10, test 2), (1, return 1)]) 4
247289
248290end Plausible
0 commit comments