----------------------------------------------------------------------------------
-- The `DecOpt` typeclass (adapted from QuickChick)
----------------------------------------------------------------------------------

import Plausible.Gen

open Plausible

/-- The `DecOpt` class encodes partial decidability:
     - It takes a `nat` argument as fuel
     - It fails, if it can't decide (e.g. because it runs out of fuel)
     - It returns `ok true/false` if it can.
     - These are intended to be monotonic, in the
       sense that if they ever return `ok b` for
       some fuel, they will also do so for higher
       fuel values.
-/
class DecOpt (P : Prop) where
  decOpt : Nat → Except GenError Bool

/-- All `Prop`s that have a `Decidable` instance (this includes `DecidableEq`)
    can be automatically given a `DecOpt` instance -/
instance [Decidable P] : DecOpt P where
  decOpt := fun _ => .ok (decide P)


----------------------------------------------------------------------------------
-- Combinators for checkers (adapted from QuickChick sourcecode)
-- https://github.com/QuickChick/QuickChick/blob/master/src/Decidability.v
----------------------------------------------------------------------------------

namespace DecOpt

/-- `checkerBacktrack` takes a list of (thunked) sub-checkers  and returns:
    - `ok true` if *any* sub-checker does so
    - `ok false` if *all* sub-checkers do so
    - `error` otherwise
    (see section 2 of "Computing Correctly with Inductive Relations") -/
def checkerBacktrack (checkers : List (Unit → Except GenError Bool)) : Except GenError Bool :=
  let rec aux (l : List (Unit → Except GenError Bool)) (b : Bool) : Except GenError Bool :=
    let err := .genError "DecOpt.checkerBacktrack failure."
    match l with
    | c :: cs =>
      match c () with
      | .ok true => .ok true
      | .ok false => aux cs b
      | .error _ => aux cs true
    | [] => if b then throw err else .ok false
  aux checkers false

/-- Conjunction lifted to work over `Option Bool`
    (corresponds to the `.&&` infix operator in section 2 of "Computing Correctly with Inductive Relations") -/
def andOpt (a : Except GenError Bool) (b : Except GenError Bool) : Except GenError Bool :=
  match a with
  | .ok true => b
  | _ => a

/-- Folds an optional conjunction operation `andOpt` over a list of `Except _ Bool`s,
    returning the resultant `Except _ Bool` -/
def andOptList (bs : List (Except GenError Bool)) : Except GenError Bool :=
  List.foldl andOpt (.ok true) bs

/-- Negation lifted to `Except GenError Bool`: flips `ok true ↔ ok false`, propagates errors -/
def negOpt (a : Except GenError Bool) : Except GenError Bool :=
  match a with
  | .ok b => .ok (!b)
  | .error e => .error e

end DecOpt
