packages feed

covenant-1.3.0: test/primops/Main.hs

{-# LANGUAGE PatternSynonyms #-}

module Main (main) where

import Covenant.ASG (defaultDatatypes)
import Covenant.Prim
  ( OneArgFunc
      ( BData,
        FstPair,
        HeadList,
        IData,
        ListData,
        MapData,
        NullList,
        SerialiseData,
        SndPair,
        TailList,
        UnBData,
        UnConstrData,
        UnIData,
        UnListData,
        UnMapData
      ),
    SixArgFunc (ChooseData),
    ThreeArgFunc (ChooseList),
    TwoArgFunc (ConstrData, EqualsData, MkCons, MkPairData),
    typeOneArgFunc,
    typeSixArgFunc,
    typeThreeArgFunc,
    typeTwoArgFunc,
  )
import Covenant.Test
  ( checkApp,
    renameCompT,
    renameValT,
    runRenameM,
  )
import Covenant.Type
  ( AbstractTy (BoundAt),
    CompT,
    Renamed (Unifiable),
    ValT (Datatype),
    arity,
    boolT,
    byteStringT,
    integerT,
  )
import Data.Functor.Classes (liftEq)
import Data.Functor.Identity (Identity (Identity))
import Data.Kind (Type)
import Data.Vector qualified as Vector
import Test.QuickCheck
  ( Arbitrary (arbitrary),
    Property,
    counterexample,
    forAll,
    property,
    (===),
  )
import Test.Tasty (defaultMain, testGroup)
import Test.Tasty.HUnit (assertEqual, assertFailure, testCase)
import Test.Tasty.QuickCheck (testProperty)

main :: IO ()
main =
  defaultMain . testGroup "Primops" $
    [ -- Since there are so few primops, we don't increase the test count
      -- beyond the default 100, as it would just be redundant.
      testGroup
        "Arity"
        [ testProperty "One-argument primops take one argument" prop1Arg,
          testProperty "Two-argument primops take two arguments" prop2Args,
          testProperty "Three-argument primops take three arguments" prop3Args,
          testProperty "Six-argument primops take six arguments" prop6Args
        ],
      testGroup
        "Renaming"
        [ testProperty "One-argument primops rename correctly" prop1Rename,
          testProperty "Two-argument primops rename correctly" prop2Rename,
          testProperty "Three-argument primops rename correctly" prop3Rename,
          testProperty "Six-argument primops rename correctly" prop6Rename
        ],
      testGroup
        "Application with data types"
        [ testGroup
            "One argument"
            [ testCase "FstPair" unitFstPair,
              testCase "SndPair" unitSndPair,
              testCase "HeadList" unitHeadList,
              testCase "TailList" unitTailList,
              testCase "NullList" unitNullList,
              testCase "MapData" unitMapData,
              testCase "ListData" unitListData,
              testCase "IData" unitIData,
              testCase "BData" unitBData,
              testCase "UnConstrData" unitUnConstrData,
              testCase "UnMapData" unitUnMapData,
              testCase "UnListData" unitUnListData,
              testCase "UnIData" unitUnIData,
              testCase "UnBData" unitUnBData,
              testCase "SerialiseData" unitSerialiseData
            ],
          testGroup
            "Two arguments"
            [ testCase "MkCons" unitMkCons,
              testCase "ConstrData" unitConstrData,
              testCase "EqualsData" unitEqualsData,
              testCase "MkPairData" unitMkPairData
            ],
          testGroup
            "Three arguments"
            [ testCase "ChooseList" unitChooseList
            ],
          testGroup
            "Six arguments"
            [ testCase "ChooseData" unitChooseData
            ]
        ]
    ]

-- Test cases and properties

prop1Arg :: Property
prop1Arg = mkArgProp typeOneArgFunc 1

prop1Rename :: Property
prop1Rename = mkRenameProp typeOneArgFunc

prop2Args :: Property
prop2Args = mkArgProp typeTwoArgFunc 2

prop2Rename :: Property
prop2Rename = mkRenameProp typeTwoArgFunc

prop3Args :: Property
prop3Args = mkArgProp typeThreeArgFunc 3

prop3Rename :: Property
prop3Rename = mkRenameProp typeThreeArgFunc

prop6Args :: Property
prop6Args = mkArgProp typeSixArgFunc 6

prop6Rename :: Property
prop6Rename = mkRenameProp typeSixArgFunc

unitFstPair :: IO ()
unitFstPair = withRenamedComp (typeOneArgFunc FstPair) $ \renamedFunT ->
  withRenamedVals [Datatype "Pair" . Vector.fromList $ [integerT, byteStringT]] $
    tryAndApply integerT renamedFunT

unitSndPair :: IO ()
unitSndPair = withRenamedComp (typeOneArgFunc SndPair) $ \renamedFunT ->
  withRenamedVals [Datatype "Pair" . Vector.fromList $ [integerT, byteStringT]] $
    tryAndApply byteStringT renamedFunT

unitHeadList :: IO ()
unitHeadList = withRenamedComp (typeOneArgFunc HeadList) $ \renamedFunT ->
  withRenamedVals [Datatype "List" . Vector.singleton $ integerT] $
    tryAndApply integerT renamedFunT

unitTailList :: IO ()
unitTailList = withRenamedComp (typeOneArgFunc TailList) $ \renamedFunT ->
  withRenamedVals (Identity . Datatype "List" . Vector.singleton $ integerT) $ \(Identity renamedArgT) ->
    tryAndApply renamedArgT renamedFunT [renamedArgT]

unitNullList :: IO ()
unitNullList = withRenamedComp (typeOneArgFunc NullList) $ \renamedFunT ->
  withRenamedVals [Datatype "List" . Vector.singleton $ integerT] $
    tryAndApply boolT renamedFunT

unitMapData :: IO ()
unitMapData = withRenamedComp (typeOneArgFunc MapData) $ \renamedFunT ->
  let pairDataT = Datatype "Pair" . Vector.fromList $ [dataT, dataT]
   in withRenamedVals [Datatype "List" . Vector.singleton $ pairDataT] $
        tryAndApply dataT renamedFunT

unitListData :: IO ()
unitListData = withRenamedComp (typeOneArgFunc ListData) $ \renamedFunT ->
  withRenamedVals [Datatype "List" . Vector.singleton $ dataT] $
    tryAndApply dataT renamedFunT

unitIData :: IO ()
unitIData = withRenamedComp (typeOneArgFunc IData) $ \renamedFunT ->
  withRenamedVals [integerT] $ tryAndApply dataT renamedFunT

unitBData :: IO ()
unitBData = withRenamedComp (typeOneArgFunc BData) $ \renamedFunT ->
  withRenamedVals [byteStringT] $ tryAndApply dataT renamedFunT

unitUnConstrData :: IO ()
unitUnConstrData = withRenamedComp (typeOneArgFunc UnConstrData) $ \renamedFunT ->
  withRenamedVals (Identity dataT) $ \(Identity renamedArgT) ->
    let listDataT = Datatype "List" . Vector.singleton $ dataT
        returnT = Datatype "Pair" . Vector.fromList $ [integerT, listDataT]
     in tryAndApply returnT renamedFunT [renamedArgT]

unitUnMapData :: IO ()
unitUnMapData = withRenamedComp (typeOneArgFunc UnMapData) $ \renamedFunT ->
  withRenamedVals (Identity dataT) $ \(Identity renamedArgT) ->
    let pairDataT = Datatype "Pair" . Vector.fromList $ [dataT, dataT]
        listPairDataT = Datatype "List" . Vector.singleton $ pairDataT
     in tryAndApply listPairDataT renamedFunT [renamedArgT]

unitUnListData :: IO ()
unitUnListData = withRenamedComp (typeOneArgFunc UnListData) $ \renamedFunT ->
  withRenamedVals [dataT] $
    tryAndApply (Datatype "List" . Vector.singleton $ dataT) renamedFunT

unitUnIData :: IO ()
unitUnIData = withRenamedComp (typeOneArgFunc UnIData) $ \renamedFunT ->
  withRenamedVals [dataT] $ tryAndApply integerT renamedFunT

unitUnBData :: IO ()
unitUnBData = withRenamedComp (typeOneArgFunc UnBData) $ \renamedFunT ->
  withRenamedVals [dataT] $ tryAndApply byteStringT renamedFunT

unitSerialiseData :: IO ()
unitSerialiseData = withRenamedComp (typeOneArgFunc SerialiseData) $ \renamedFunT ->
  withRenamedVals [dataT] $ tryAndApply byteStringT renamedFunT

unitMkCons :: IO ()
unitMkCons = withRenamedComp (typeTwoArgFunc MkCons) $ \renamedFunT ->
  let listT = Datatype "List" . Vector.singleton $ integerT
   in withRenamedVals [integerT, listT] $ tryAndApply listT renamedFunT

unitConstrData :: IO ()
unitConstrData = withRenamedComp (typeTwoArgFunc ConstrData) $ \renamedFunT ->
  let listT = Datatype "List" . Vector.singleton $ dataT
   in withRenamedVals [integerT, listT] $ tryAndApply dataT renamedFunT

unitEqualsData :: IO ()
unitEqualsData = withRenamedComp (typeTwoArgFunc EqualsData) $ \renamedFunT ->
  withRenamedVals [dataT, dataT] $ tryAndApply boolT renamedFunT

unitMkPairData :: IO ()
unitMkPairData = withRenamedComp (typeTwoArgFunc MkPairData) $ \renamedFunT ->
  let pairDataT = Datatype "Pair" . Vector.fromList $ [dataT, dataT]
   in withRenamedVals [dataT, dataT] $ tryAndApply pairDataT renamedFunT

unitChooseList :: IO ()
unitChooseList = withRenamedComp (typeThreeArgFunc ChooseList) $ \renamedFunT ->
  let listT = Datatype "List" . Vector.singleton $ integerT
   in withRenamedVals [listT, byteStringT, byteStringT] $
        tryAndApply byteStringT renamedFunT

unitChooseData :: IO ()
unitChooseData = withRenamedComp (typeSixArgFunc ChooseData) $ \renamedFunT ->
  withRenamedVals [dataT, integerT, integerT, integerT, integerT, integerT] $
    tryAndApply integerT renamedFunT

-- Helpers

mkArgProp ::
  forall (a :: Type).
  (Show a, Arbitrary a) =>
  (a -> CompT AbstractTy) ->
  Int ->
  Property
mkArgProp typingFun targetArity = forAll arbitrary $ \f ->
  let t = typingFun f
   in arity t === targetArity

mkRenameProp ::
  forall (a :: Type).
  (Show a, Arbitrary a) =>
  (a -> CompT AbstractTy) ->
  Property
mkRenameProp typingFun = forAll arbitrary $ \f ->
  let t = typingFun f
      result = runRenameM mempty . renameCompT $ t
   in case result of
        Left err -> counterexample (show err) False
        Right renamed -> property $ liftEq eqRenamedVar t renamed

-- In our context, the _only_ variables we have are unifiable. If we see
-- anything else, we know we've messed up somewhere. Furthermore, the indexes
-- should 'line up'.
eqRenamedVar :: AbstractTy -> Renamed -> Bool
eqRenamedVar (BoundAt _ ix) = \case
  Unifiable ix' -> ix == ix'
  _ -> False

withRenamedComp ::
  CompT AbstractTy ->
  (CompT Renamed -> IO ()) ->
  IO ()
withRenamedComp t f = case runRenameM mempty . renameCompT $ t of
  Left err -> assertFailure $ "Could not rename: " <> show err
  Right t' -> f t'

withRenamedVals ::
  forall (t :: Type -> Type).
  (Traversable t) =>
  t (ValT AbstractTy) ->
  (t (ValT Renamed) -> IO ()) ->
  IO ()
withRenamedVals vals f = case runRenameM mempty . traverse renameValT $ vals of
  Left err -> assertFailure $ "Could not rename: " <> show err
  Right vals' -> f vals'

tryAndApply ::
  ValT Renamed ->
  CompT Renamed ->
  [ValT Renamed] ->
  IO ()
tryAndApply expected f xs = case checkApp defaultDatatypes f . fmap Just $ xs of
  Left err -> assertFailure $ "Could not apply: " <> show err
  Right res -> assertEqual "" expected res

dataT :: forall (a :: Type). ValT a
dataT = Datatype "Data" Vector.empty