Skip to content

Commit

Permalink
[PIR] [Test] Add term generators (#4949)
Browse files Browse the repository at this point in the history
  • Loading branch information
effectfully authored Dec 16, 2022
1 parent 56e893f commit febabc4
Show file tree
Hide file tree
Showing 13 changed files with 1,358 additions and 61 deletions.
15 changes: 12 additions & 3 deletions plutus-core/plutus-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,10 @@ library plutus-core-testlib
PlutusCore.Generators.QuickCheck.Utils
PlutusCore.Test
PlutusIR.Generators.AST
PlutusIR.Generators.QuickCheck
PlutusIR.Generators.QuickCheck.Common
PlutusIR.Generators.QuickCheck.GenerateTerms
PlutusIR.Generators.QuickCheck.ShrinkTerms
PlutusIR.Test
Test.Tasty.Extras

Expand All @@ -375,6 +379,7 @@ library plutus-core-testlib
, bifunctors
, bytestring
, containers
, data-default-class
, dependent-map >=0.4.0.0
, filepath
, hedgehog >=1.0
Expand Down Expand Up @@ -467,27 +472,31 @@ test-suite plutus-ir-test
GeneratorSpec
GeneratorSpec.Builtin
GeneratorSpec.Substitution
GeneratorSpec.Terms
GeneratorSpec.Types
NamesSpec
ParserSpec
TransformSpec
TypeSpec

build-depends:
, base >=4.9 && <5
, base >=4.9 && <5
, containers
, flat
, hashable
, hedgehog
, lens
, megaparsec
, plutus-core ^>=1.1
, plutus-core-testlib ^>=1.1
, mtl
, plutus-core ^>=1.1
, plutus-core-testlib ^>=1.1
, QuickCheck
, serialise
, tasty
, tasty-hedgehog
, tasty-quickcheck
, text
, unordered-containers

test-suite untyped-plutus-core-test
import: lang
Expand Down
21 changes: 16 additions & 5 deletions plutus-core/plutus-core/src/PlutusCore/Builtin/KnownKind.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ data SingKind k where
SingType :: SingKind GHC.Type
SingKindArrow :: SingKind k -> SingKind l -> SingKind (k -> l)

-- | Feed the 'SingKind' version of the given 'Kind' to the given continuation.
withSingKind :: Kind ann -> (forall k. SingKind k -> r) -> r
withSingKind (Type _) k = k SingType
withSingKind (KindArrow _ dom cod) k =
withSingKind dom $ \domS ->
withSingKind cod $ \codS ->
k $ SingKindArrow domS codS

-- | For reifying Haskell kinds representing Plutus kinds at the term level.
class KnownKind k where
knownKind :: SingKind k
Expand All @@ -34,12 +42,15 @@ instance rep ~ LiftedRep => KnownKind (TYPE rep) where
instance (KnownKind dom, KnownKind cod) => KnownKind (dom -> cod) where
knownKind = SingKindArrow (knownKind @dom) (knownKind @cod)

-- | Satisfy the 'KnownKind' constraint of a continuation using the given 'SingKind'.
bringKnownKind :: SingKind k -> (KnownKind k => r) -> r
bringKnownKind SingType r = r
bringKnownKind (SingKindArrow dom cod) r = bringKnownKind dom $ bringKnownKind cod r

withKnownKind :: Kind ann -> (forall k. KnownKind k => Proxy k -> r) -> r
withKnownKind (Type _) k = k $ Proxy @GHC.Type
withKnownKind (KindArrow _ dom cod) k =
withKnownKind dom $ \(_ :: Proxy dom) ->
withKnownKind cod $ \(_ :: Proxy cod) ->
k $ Proxy @(dom -> cod)
withKnownKind kind k =
withSingKind kind $ \(kindS :: SingKind kind) ->
bringKnownKind kindS $ k $ Proxy @kind

-- We need this for type checking Plutus, however we get Plutus types/terms/programs by either
-- producing them directly or by parsing/decoding them and in both the cases we have access to the
Expand Down
1 change: 1 addition & 0 deletions plutus-core/plutus-core/src/Universe/Core.hs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ module Universe.Core
, GShow (..)
, gshow
, GEq (..)
, defaultEq
, deriveGEq
, deriveGCompare
, (:~:)(..)
Expand Down
36 changes: 31 additions & 5 deletions plutus-core/plutus-ir/test/GeneratorSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module GeneratorSpec where

import GeneratorSpec.Builtin
import GeneratorSpec.Substitution
import GeneratorSpec.Terms
import GeneratorSpec.Types

import Test.QuickCheck
Expand All @@ -14,9 +15,34 @@ import Test.Tasty.QuickCheck
-- The default for the argument is @1@.
generators :: Int -> TestNested
generators factor = return $ testGroup "generators"
[ testProperty "prop_genData" $ withMaxSuccess (factor*10000) prop_genData
, testProperty "prop_genKindCorrect" $ withMaxSuccess (factor*100000) (prop_genKindCorrect False)
, testProperty "prop_shrinkTypeSound" $ withMaxSuccess (factor*100000) prop_shrinkTypeSound
, testProperty "prop_substType" $ withMaxSuccess (factor*10000) prop_substType
, testProperty "prop_unify" $ withMaxSuccess (factor*10000) prop_unify
[ testProperty "prop_genData" $
withMaxSuccess (factor*3000) prop_genData

, testProperty "prop_genKindCorrect" $
withMaxSuccess (factor*100000) (prop_genKindCorrect False)
, testProperty "prop_shrinkTypeSound" $
withMaxSuccess (factor*30000) prop_shrinkTypeSound

, testProperty "prop_substType" $
withMaxSuccess (factor*10000) prop_substType
, testProperty "prop_unify" $
withMaxSuccess (factor*10000) prop_unify

, testProperty "prop_genTypeCorrect" $
withMaxSuccess (factor*10000) (prop_genTypeCorrect False)
, testProperty "prop_genWellTypedFullyApplied" $
withMaxSuccess (factor*1000) prop_genWellTypedFullyApplied
, testProperty "prop_findInstantiation" $
withMaxSuccess (factor*10000) prop_findInstantiation
, testProperty "prop_inhabited" $
withMaxSuccess (factor*3000) prop_inhabited
-- These tests sometimes take a long time to run due to
-- a large number of shrinks being generated.
-- See https://github.com/input-output-hk/plutus/pull/4949#discussion_r1029985014
-- , testProperty "prop_stats_numShrink" $
-- withMaxSuccess (factor*40) prop_stats_numShrink
-- , testProperty "prop_noTermShrinkLoops" $
-- withMaxSuccess (factor*40) prop_noTermShrinkLoops
-- , testProperty "prop_shrinkTermSound" $
-- withMaxSuccess (factor*40) prop_shrinkTermSound
]
182 changes: 182 additions & 0 deletions plutus-core/plutus-ir/test/GeneratorSpec/Terms.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
-- editorconfig-checker-disable-file
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}

{-# OPTIONS_GHC -fno-warn-orphans #-}

module GeneratorSpec.Terms where

import PlutusCore.Generators.QuickCheck
import PlutusIR.Generators.QuickCheck

import PlutusCore.Default
import PlutusCore.Name
import PlutusCore.Quote
import PlutusCore.Rename
import PlutusIR
import PlutusIR.Core.Instance.Pretty.Readable

import Control.Monad.Reader
import Data.Char
import Data.Either
import Data.Function
import Data.Hashable
import Data.HashMap.Strict qualified as HashMap
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map.Strict qualified as Map
import Test.QuickCheck

-- | 'rename' a 'Term' and 'show' it afterwards.
showRenameTerm :: Term TyName Name DefaultUni DefaultFun () -> String
showRenameTerm = show . runQuote . rename

-- | Same as 'nub', but relies on 'Hashable' and is therefore asymptotically faster.
nubHashableOn :: Hashable b => (a -> b) -> [a] -> [a]
nubHashableOn f = HashMap.elems . HashMap.fromList . map (\x -> (f x, x))

-- We need this for checking the behavior of the shrinker (in particular, whether a term shrinks
-- to itself, which would be a bug, or how often a term shrinks to the same thing multiple times
-- within a single step). Should we move this to @plutus-ir@ itself? Not sure, but it's safe to
-- place it here, since nothing can depend on a test suite (apart from modules from within this test
-- suite), hence no conflicting orphans can occur.
instance Eq (Term TyName Name DefaultUni DefaultFun ()) where
-- Quick-and-dirty implementation in terms of 'Show'.
-- We generally consider equality modulo alpha, hence the call to 'rename'.
(==) = (==) `on` showRenameTerm

-- * Core properties for PIR generators

-- | Test that our generators only result in well-typed terms.
-- Note, the counterexamples from this property are not shrunk (see why below).
-- See Note [Debugging generators that don't generate well-typed/kinded terms/types]
-- and the utility properties below when this property fails.
prop_genTypeCorrect :: Bool -> Property
prop_genTypeCorrect debug = do
-- Note, we don't shrink this term here because a precondition of shrinking is that
-- the term we are shrinking is well-typed. If it is not, the counterexample we get
-- from shrinking will be nonsene.
let gen = if debug then genTypeAndTermDebug_ else genTypeAndTerm_
forAllDoc "ty,tm" gen (const []) $ \ (ty, tm) -> typeCheckTerm tm ty

-- | Test that when we generate a fully applied term we end up
-- with a well-typed term.
prop_genWellTypedFullyApplied :: Property
prop_genWellTypedFullyApplied =
forAllDoc "ty, tm" genTypeAndTerm_ shrinkClosedTypedTerm $ \ (ty, tm) ->
-- No shrinking here because if `genFullyApplied` is wrong then the shrinking
-- will be wrong too. See `prop_genTypeCorrect`.
forAllDoc "ty', tm'" (genFullyApplied ty tm) (const []) $ \ (ty', tm') ->
typeCheckTerm tm' ty'

-- | Test that shrinking a well-typed term results in a well-typed term
prop_shrinkTermSound :: Property
prop_shrinkTermSound =
forAllDoc "ty,tm" genTypeAndTerm_ shrinkClosedTypedTerm $ \ (ty, tm) ->
let shrinks = shrinkClosedTypedTerm (ty, tm) in
-- While we generate well-typed terms we still need this check here for
-- shrinking counterexamples to *this* property. If we find a term whose
-- shrinks aren't well-typed we want to find smaller *well-typed* terms
-- whose shrinks aren't well typed.
-- Importantly, this property is only interesting when
-- shrinking itself is broken, so we can only use the
-- parts of shrinking that happen to be OK.
isRight (typeCheckTerm tm ty) ==>
-- We don't want to let the shrinker get away with being empty, so we ignore empty
-- shrinks. QuickCheck will give up and print an error if the shrinker returns the empty list too
-- often.
not (null shrinks) ==>
assertNoCounterexamples $ lefts
[ ((ty', tm'), ) <$> typeCheckTerm tm' ty'
| (ty', tm') <- shrinks
]

-- * Utility tests for debugging generators that behave weirdly

-- | Test that `findInstantiation` results in a well-typed instantiation.
prop_findInstantiation :: Property
prop_findInstantiation =
forAllDoc "ctx" genCtx (const []) $ \ ctx0 ->
forAllDoc "ty" (genTypeWithCtx ctx0 $ Type ()) (shrinkType ctx0) $ \ ty0 ->
forAllDoc "target" (genTypeWithCtx ctx0 $ Type ()) (shrinkType ctx0) $ \ target ->
assertNoCounterexamples $ lefts
[ (n ,) <$> checkInst ctx0 x0 ty0 insts target
| n <- [0 .. arity ty0 + 3]
, Right insts <- [findInstantiation ctx0 n target ty0]
]
where
x0 = Name "x" (toEnum 0)
arity (TyForall _ _ _ a) = arity a
arity (TyFun _ _ b) = 1 + arity b
arity _ = 0

-- Check that building a "minimal" term that performs the instantiations in
-- `insts` produces a well-typed term.
checkInst ctx1 x1 ty1 insts1 target = typeCheckTermInContext ctx1 tmCtx1 tm1 target
where
-- Build a term and a context from `insts` that consists of
-- `tm @ty` for every `InstApp ty` in `insts` and `tm y` for
-- a fresh variable `y : ty` for every `InstArg ty` in `insts`.
(tmCtx1, tm1) = go (toEnum 1) (Map.singleton x1 ty1) (Var () x1) insts1
go _ tmCtx tm [] = (tmCtx, tm)
go i tmCtx tm (InstApp ty : insts) = go i tmCtx (TyInst () tm ty) insts
go i tmCtx tm (InstArg ty : insts) = go (succ i) (Map.insert y ty tmCtx)
(Apply () tm (Var () y)) insts
where y = Name "y" i

-- | Check what's in the leaves of the generated data
prop_stats_leaves :: Property
prop_stats_leaves =
-- No shrinking here because we are only collecting stats
forAllDoc "_,tm" genTypeAndTerm_ (const []) $ \ (_, tm) ->
tabulate "leaves" (map (filter isAlpha . show . prettyPirReadable) $ leaves tm) $ property True
where
-- Figure out what's at the leaves of the AST,
-- including variable names, error, and builtins.
leaves (Var _ x) = [x]
leaves (TyInst _ a _) = leaves a
leaves (Let _ _ _ b) = leaves b
leaves (LamAbs _ _ _ b) = leaves b
leaves (Apply _ a b) = leaves a ++ leaves b
leaves Error{} = [Name "error" $ toEnum 0]
leaves Builtin{} = [Name "builtin" $ toEnum 0]
leaves _ = []

-- | Check the ratio of duplicate shrinks
prop_stats_numShrink :: Property
prop_stats_numShrink =
-- No shrinking here because we are only collecting stats
forAllDoc "ty,tm" genTypeAndTerm_ (const []) $ \ (ty, tm) ->
let shrinks = map snd $ shrinkClosedTypedTerm (ty, tm)
n = length shrinks
u = length $ nubHashableOn showRenameTerm shrinks
r | n > 0 = 5 * ((n - u) * 20 `div` n)
| otherwise = 0
in tabulate "distribution | duplicates" [" | " ++ show r ++ "%"] True

-- | Specific test that `inhabitType` returns well-typed things
prop_inhabited :: Property
prop_inhabited =
-- No shrinking here because if the generator
-- generates nonsense shrinking will be nonsense.
forAllDoc "ty,tm" (genInhab mempty) (const []) $ \ (ty, tm) -> typeCheckTerm tm ty
where
-- Generate some datatypes and then immediately call
-- `inhabitType` to test `inhabitType` directly instead
-- of through the whole term generator. Quick-ish way
-- of debugging "clever hacks" in `inhabitType`.
genInhab ctx = runGenTm $ local (\ e -> e { geTypes = ctx }) $
genDatatypeLets $ \ dats -> do
ty <- genType $ Type ()
tm <- inhabitType ty
return (ty, foldr (\ dat -> Let () NonRec (DatatypeBind () dat :| [])) tm dats)

-- | Check that there are no one-step shrink loops
prop_noTermShrinkLoops :: Property
prop_noTermShrinkLoops =
-- Note that we need to remove x from the shrinks of x here because
-- a counterexample to this property is otherwise guaranteed to
-- go into a shrink loop.
forAllDoc "ty,tm" genTypeAndTerm_
(\(ty', tm') -> filter ((/= tm') . snd) $ shrinkClosedTypedTerm (ty', tm')) $ \(ty, tm) ->
tm `notElem` map snd (shrinkClosedTypedTerm (ty, tm))
2 changes: 1 addition & 1 deletion plutus-core/plutus-ir/test/GeneratorSpec/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ prop_shrinkTypeSound =
-- See discussion about the same trick in 'prop_shrinkTermSound'.
isRight (checkKind ctx ty k) ==>
assertNoCounterexamples $ lefts
[ (k', ty', ) <$> checkKind ctx ty k
[ (k', ty', ) <$> checkKind ctx ty' k'
| (k', ty') <- shrinkKindAndType ctx (k, ty)
]

Expand Down
Loading

0 comments on commit febabc4

Please sign in to comment.