packages feed

disco-0.2: src/Disco/Property.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      :  Disco.Property
-- Copyright   :  disco team and contributors
-- Maintainer  :  byorgey@gmail.com
--
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Properties of disco functions.
module Disco.Property (
  -- * Generation
  generateSamples,

  -- * Utility
  invertMotive,
  invertPropResult,

  -- * Pretty-printing
  prettyTestResult,
)
where

import Prelude hiding ((<>))

import Data.Char (toLower)
import qualified Data.Enumeration.Invertible as E

import Polysemy

import Disco.AST.Typed
import Disco.Effects.Input
import Disco.Effects.LFresh
import Disco.Error
import Disco.Pretty
import Disco.Syntax.Prims
import Disco.Typecheck.Erase (eraseProperty)
import Disco.Types (TyDefCtx)
import Disco.Value
import Polysemy.Random
import Polysemy.Reader

-- | Toggles which outcome (finding or not finding the thing being
--   searched for) qualifies as success, without changing the thing
--   being searched for.
invertMotive :: SearchMotive -> SearchMotive
invertMotive (SearchMotive (a, b)) = SearchMotive (not a, b)

-- | Flips the success or failure status of a @PropResult@, leaving
--   the explanation unchanged.
invertPropResult :: TestResult -> TestResult
invertPropResult res@(TestResult b r env)
  | TestRuntimeError {} <- r = res
  | otherwise = TestResult (not b) r env

randomLarge :: Member Random r => [Integer] -> Sem r [Integer]
randomLarge [] = return []
randomLarge [_] = return []
randomLarge (x : y : xs) = (:) <$> randomR (x, y) <*> randomLarge (y : xs)

-- | Select samples from an enumeration according to a search type. Also returns
--   a 'SearchType' describing the results, which may be 'Exhaustive' if the
--   enumeration is no larger than the number of samples requested.
generateSamples :: Member Random r => SearchType -> E.IEnumeration a -> Sem r ([a], SearchType)
generateSamples Exhaustive e = return (E.enumerate e, Exhaustive)
generateSamples (Randomized n m) e
  | E.Finite k <- E.card e, k <= n + m = return (E.enumerate e, Exhaustive)
  | otherwise = do
      let small = [0 .. n]
      rs <- randomLarge [100, 1000, 10000, 100000, 1000000]
      let samples = map (E.select e) $ small ++ rs
      return (samples, Randomized n m)

-- XXX do shrinking for randomly generated test cases?

------------------------------------------------------------
-- Pretty-printing for test results
------------------------------------------------------------

prettyResultCertainty :: Members '[LFresh, Reader PA] r => TestReason -> AProperty -> String -> Sem r (Doc ann)
prettyResultCertainty r prop res =
  (if resultIsCertain r then "Certainly" else "Possibly") <+> text res <> ":" <+> pretty (eraseProperty prop)

prettyTestReason ::
  Members '[Input TyDefCtx, LFresh, Reader PA] r =>
  Bool ->
  AProperty ->
  TestReason ->
  TestEnv ->
  Sem r (Doc ann)
prettyTestReason _ _ TestBool _ = empty
prettyTestReason b (ATAbs _ _ body) (TestFound (TestResult b' r' env')) env = do
  lunbind body $ \(_, p) ->
    prettyTestEnv ("Found " ++ if b then "example:" else "counterexample:") env
      $+$ prettyTestReason b' p r' env'
prettyTestReason b _ (TestNotFound Exhaustive) _
  | b = "No counterexamples exist; all possible values were checked."
  | otherwise = "No example exists; all possible values were checked."
prettyTestReason b _ (TestNotFound (Randomized n m)) _
  | b = "Checked" <+> text (show (n + m)) <+> "possibilities without finding a counterexample."
  | otherwise = "No example was found; checked" <+> text (show (n + m)) <+> "possibilities."
prettyTestReason _ _ (TestCmp _ t a1 a2) _ =
  bulletList
    "-"
    [ "Left side:  " <> prettyValue t a1
    , "Right side: " <> prettyValue t a2
    ]
prettyTestReason _ _ (TestRuntimeError ee) env =
  nest 2 $
    "Test failed with an error:"
      $+$ pretty (EvalErr ee)
      $+$ prettyTestEnv "Example inputs that caused the error:" env
prettyTestReason _ (ATApp _ (ATPrim _ (PrimBOp _)) (ATTup _ [p1, p2])) (TestBin _ tr1 tr2) _ =
  bulletList
    "-"
    [ nest 2 $ prettyTestResult p1 tr1
    , nest 2 $ prettyTestResult p2 tr2
    ]
-- See Note [prettyTestReason fallback]
prettyTestReason _ _ _ _ = empty

-- ~~~~ Note [prettyTestReason fallback]
--
-- prettyTestReason can do a decent job printing out reasons for a
-- test result when operators like /\, \/, etc. are written
-- explicitly; then it can structurally recurse on the original Prop
-- expression in parllel with the TestReason.  However, it is possible
-- to e.g. write a function which returns a Prop, making the structure
-- of the Prop expression opaque.  For example, consider this example
-- (from test/prop-higher-order):
--
-- !!! all [true, true, true, false, true]
-- all : List(Prop) -> Prop
-- all ps = reduce(~/\~, true, ps)
--
-- This test is false, and the TestReason ends up with a bunch of
-- nested TestBin LAnd.  However, the proposition is literally a
-- function application so we cannot see that it matches the structure
-- of the test result.  So we just give up and decline to print a
-- reason.

prettyTestResult ::
  Members '[Input TyDefCtx, LFresh, Reader PA] r =>
  AProperty ->
  TestResult ->
  Sem r (Doc ann)
prettyTestResult prop (TestResult bool tr env) =
  prettyResultCertainty tr prop (map toLower (show bool))
    $+$ prettyTestReason bool prop tr env

prettyTestEnv ::
  Members '[Input TyDefCtx, LFresh, Reader PA] r =>
  String ->
  TestEnv ->
  Sem r (Doc ann)
prettyTestEnv _ (TestEnv []) = empty
prettyTestEnv s (TestEnv vs) = nest 2 $ text s $+$ vcat (map prettyBind vs)
 where
  maxNameLen = maximum . map (\(n, _, _) -> length n) $ vs
  prettyBind (x, ty, v) =
    text x <> text (replicate (maxNameLen - length x) ' ') <+> "=" <+> prettyValue ty v