Skip to content

Commit ad75dc5

Browse files
authored
feat: add a backtrack combinator to Gen. (#53)
* feat: add a backtrack combinator to Gen. * fix: make backtrack correctly universe polymorphic.
1 parent 714fefe commit ad75dc5

File tree

2 files changed

+50
-12
lines changed

2 files changed

+50
-12
lines changed

Plausible/ArbitraryFueled.lean

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,4 @@ class ArbitraryFueled (α : Type) where
2929
meta instance [ArbitraryFueled α] : Arbitrary α where
3030
arbitrary := Gen.sized ArbitraryFueled.arbitraryFueled
3131

32-
/-- Raised when a fueled generator fails due to insufficient fuel. -/
33-
meta def Gen.outOfFuel : GenError :=
34-
.genError "out of fuel"
35-
3632
end Plausible

Plausible/Gen.lean

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

248290
end Plausible

0 commit comments

Comments
 (0)