packages feed

cleveland-0.1.1: morley-test/Test/Typecheck.hs

-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

-- TODO [#712]: Remove this next major release
{-# OPTIONS_GHC -Wno-deprecations #-}

module Test.Typecheck
  ( unit_Good_contracts
  , unit_Bad_contracts
  , test_srcPosition
  , unit_Unreachable_code
  , test_Roundtrip
  , test_StackRef
  , test_TCTypeError_display
  , hprop_ValueSeq_as_list
  , test_SELF
  , test_Value_contract
  , test_Nested_Sequences
  , test_Views
  ) where

import Data.Default (def)
import Data.Map qualified as M
import Data.Text.IO.Utf8 qualified as Utf8 (readFile)
import Data.Typeable (typeRep)
import Fmt (build, pretty)
import Hedgehog (Gen, Property, evalNF, forAll, property, (===))
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Test.HUnit (Assertion, assertFailure, (@?=))
import Test.Hspec (expectationFailure)
import Test.Hspec.Expectations (Expectation)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase)
import Test.Tasty.Hedgehog (testProperty)

import Hedgehog.Gen.Michelson.Typed
import Morley.Michelson.ErrorPos (InstrCallStack(..), LetName(..), Pos(..), SrcPos(..), srcPos)
import Morley.Michelson.Parser (MichelsonSource(..), utypeQ)
import Morley.Michelson.Runtime (prepareContractExt)
import Morley.Michelson.Text (MText)
import Morley.Michelson.TypeCheck
import Morley.Michelson.Typed qualified as T
import Morley.Michelson.Untyped (ParameterType(..), buildEpName, noAnn)
import Morley.Michelson.Untyped qualified as Un
import Morley.Tezos.Address (Address(..), mkContractHashHack)
import Morley.Tezos.Core (ChainId, Mutez, Timestamp)
import Morley.Tezos.Crypto (Bls12381Fr, Bls12381G1, Bls12381G2, KeyHash, PublicKey, Signature)
import Morley.Util.Interpolate (itu)
import Morley.Util.MismatchError
import Test.Cleveland.Michelson (failedTest, meanTimeUpperBoundPropNF, sec)
import Test.Cleveland.Michelson.Import (ContractReadError(..), readContract)

import Test.Util.Contracts (getIllTypedContracts, getWellTypedContracts, inContractsDir)

unit_Good_contracts :: Assertion
unit_Good_contracts
  = mapM_ (\f -> do checkFile True f def{ tcVerbose = True } (const pass)
                    checkFile True f def (const pass))
    =<< getWellTypedContracts

unit_Bad_contracts :: Assertion
unit_Bad_contracts
  = mapM_ (\f -> do checkFile False f def{ tcVerbose = True } (const pass)
                    checkFile False f def (const pass))
    =<< getIllTypedContracts

pattern IsSrcPos :: Word -> Word -> InstrCallStack
pattern IsSrcPos l c <- InstrCallStack [] (SrcPos (Pos l) (Pos c))

test_srcPosition :: [TestTree]
test_srcPosition =
  [ testCase "Verify instruction position in a typecheck error" $ do
      checkIllFile (inContractsDir "ill-typed/basic3.tz") $ \case
          TCFailedOnInstr (Un.CONS _) _ (IsSrcPos 8 6) _ (Just (AnnError _)) -> True
          _ -> False

      checkIllFile (inContractsDir "ill-typed/testassert_invalid_stack3.mtz") $ \case
          TCFailedOnInstr Un.DROP _ (IsSrcPos 10 17) _ (Just NotEnoughItemsOnStack) -> True
          _ -> False

      checkIllFile (inContractsDir "ill-typed/testassert_invalid_stack2.mtz") $ \case
          TCExtError _ (IsSrcPos 9 2) (TestAssertError _) -> True
          _ -> False

      checkIllFile (inContractsDir "ill-typed/macro_in_let_fail.mtz") $ \case
          TCFailedOnInstr (Un.COMPARE _) _ (InstrCallStack [LetName "cmpLet"] (SrcPos (Pos 7) (Pos 6))) _
                                              (Just (TypeEqError _)) -> True
          _ -> False
      checkIllFile (inContractsDir "ill-typed/compare_annotation_mismatch.tz") $ \case
          TCFailedOnInstr (Un.COMPARE _) _ _ _ (Just (AnnError _)) -> True
          _ -> False

      checkIllFile (inContractsDir "ill-typed/annotation_mismatch_map_update.tz") $ \case
          TCFailedOnInstr (Un.UPDATE _) (SomeHST (_ ::& _ ::& (T.NTMap{}, _, _) ::& SNil)) _ _ (Just (AnnError _)) -> True
          _ -> False

      checkIllFile (inContractsDir "ill-typed/annotation_mismatch_map_get.tz") $ \case
          TCFailedOnInstr (Un.GET _) (SomeHST (_ ::& (T.NTMap{}, _, _) ::& SNil)) _ _ (Just (AnnError _)) -> True
          _ -> False

      checkIllFile (inContractsDir "ill-typed/annotation_mismatch_map_mem.tz") $ \case
          TCFailedOnInstr (Un.MEM _) (SomeHST (_ ::& (T.NTMap{}, _, _) ::& SNil)) _ _ (Just (AnnError _)) -> True
          _ -> False

      checkIllFile (inContractsDir "ill-typed/annotation_mismatch_big_map_update.tz") $ \case
          TCFailedOnInstr (Un.UPDATE _) (SomeHST (_ ::& _ ::& (T.NTBigMap{}, _, _) ::& SNil)) _ _ (Just (AnnError _)) -> True
          _ -> False

      checkIllFile (inContractsDir "ill-typed/annotation_mismatch_big_map_get.tz") $ \case
          TCFailedOnInstr (Un.GET _) (SomeHST (_ ::& (T.NTBigMap{}, _, _) ::& SNil)) _ _ (Just (AnnError _)) -> True
          _ -> False

      checkIllFile (inContractsDir "ill-typed/annotation_mismatch_big_map_mem.tz") $ \case
          TCFailedOnInstr (Un.MEM _) (SomeHST (_ ::& (T.NTBigMap{}, _, _) ::& SNil)) _ _ (Just (AnnError _)) -> True
          _ -> False

      checkIllFile (inContractsDir "ill-typed/annotation_mismatch_set_update.tz") $ \case
          TCFailedOnInstr (Un.UPDATE _) (SomeHST (_ ::& _ ::& (T.NTSet{}, _, _) ::& SNil)) _ _ (Just (AnnError _)) -> True
          _ -> False

      checkIllFile (inContractsDir "ill-typed/annotation_mismatch_set_mem.tz") $ \case
          TCFailedOnInstr (Un.MEM _) (SomeHST (_ ::& (T.NTSet{}, _, _) ::& SNil)) _ _ (Just (AnnError _)) -> True
          _ -> False
  ]

checkFile
  :: HasCallStack
  => Bool
  -> FilePath
  -> TypeCheckOptions
  -> (TCError -> Expectation)
  -> Expectation
checkFile wellTyped file options onError = do
  c <- prepareContractExt (Just file)
  case typeCheckingWith options $ typeCheckContract c of
    Left err
      | wellTyped ->
        expectationFailure $
        "Typechecker unexpectedly failed on \"" <> file <>
        "\": " <> displayException err
      | otherwise -> onError err
    Right _
      | not wellTyped ->
        assertFailure $
        "Typechecker unexpectedly considered \"" <> file <> "\" well-typed."
      | otherwise -> pass

checkIllFile :: FilePath -> (TCError -> Bool) -> Expectation
checkIllFile file check = checkFile False file def
  \e -> if check e then pass else unexpected file e
  where
    unexpected f e =
      expectationFailure $ "Unexpected typecheck error: " <> displayException e <> " in file: " <> f

unit_Unreachable_code :: Assertion
unit_Unreachable_code = do
  let file = inContractsDir "ill-typed/fail_before_nop.tz"
  let ics = InstrCallStack [] (srcPos 7 13)
  econtract <- readContract @'T.TUnit @'T.TUnit (MSFile file) <$> Utf8.readFile file
  econtract @?= Left (CRETypeCheck (MSFile file) $ TCUnreachableCode ics (one $ Un.WithSrcEx ics $ Un.SeqEx []))

test_Roundtrip :: [TestTree]
test_Roundtrip =
  [ testGroup "Value"
    [ roundtripValue @Integer genValueInt
    , roundtripValue @Timestamp genValueTimestamp
    , roundtripValue @PublicKey $ genValue @'T.TKey
    , roundtripValue @Signature $ genValue @'T.TSignature
    , roundtripValue @ChainId $ genValue @'T.TChainId
    , roundtripValue @(Maybe MText) $ genValue @('T.TOption 'T.TString)
    , roundtripValue @[Maybe Integer] $ genValueList $ genValue @('T.TOption 'T.TInt)
    , roundtripValue @(Set Integer) $ genValue @('T.TSet 'T.TInt)
    , roundtripValue @(Integer, MText) $ genValuePair genValueInt $ genValue @'T.TString
    , roundtripValue @(Integer, (MText, Integer)) $
        genValuePair genValueInt $ genValuePair (genValue @'T.TString) genValueInt
    , roundtripValue @(Either MText Integer) $ genValue @('T.TOr 'T.TString 'T.TInt)
    , roundtripValue @(Map MText Bool) $ genValue @('T.TMap 'T.TString 'T.TBool)
    , roundtripValue @Natural $ genValue @'T.TNat
    , roundtripValue @MText $ genValue @'T.TString
    , roundtripValue @ByteString $ genValue @'T.TBytes
    , roundtripValue @Mutez genValueMutez
    , roundtripValue @Bool $ genValue @'T.TBool
    , roundtripValue @KeyHash genValueKeyHash
    , roundtripValue @Address $ genValue @'T.TAddress
    , roundtripValue @Bls12381Fr $ genValue @'T.TBls12381Fr
    , roundtripValue @Bls12381G1 $ genValue @'T.TBls12381G1
    , roundtripValue @Bls12381G2 $ genValue @'T.TBls12381G2
    ]
  ]
  where
  roundtripValue
    :: forall (a :: Type).
        ( Each [T.SingI, T.HasNoOp] '[T.ToT a]
        , Typeable a
        )
    => Gen (T.Value $ T.ToT a)
    -> TestTree
  roundtripValue gen = testGroup (show $ typeRep (Proxy @a))
    [ roundtripValue' @a T.untypeValue "Readable" gen
    , roundtripValue' @a T.untypeValueOptimized "Optimized" gen
    , roundtripValue' @a T.untypeValueHashable "Hashable" gen
    ]

  roundtripValue'
    :: forall (a :: Type).
       Each [T.SingI, T.HasNoOp] '[T.ToT a]
    => (T.Value $ T.ToT a -> Un.Value)
    -> String
    -> Gen (T.Value $ T.ToT a)
    -> TestTree
  roundtripValue' doUntype name genV =
    testProperty name $ property $ do
      val :: T.Value (T.ToT a) <- forAll $ genV
      let uval = doUntype val
          runTC = typeCheckingWith def . runTypeCheckIsolated . usingReaderT (def @InstrCallStack)
      case runTC $ typeCheckValue uval of
        Right got -> got === val
        Left err -> failedTest $
                    "Type check unexpectedly failed: " <> pretty err

test_StackRef :: [TestTree]
test_StackRef =
  [ testProperty "Typecheck fails when ref is out of bounds" $ property $ do
      let instr = printStRef 2
          hst = stackEl ::& stackEl ::& SNil
      case
        typeCheckingWith def . runTypeCheckIsolated $
        typeCheckList [Un.WithSrcEx def $ Un.PrimEx instr] hst
        of
          Left err -> void $ evalNF err
          Right _ -> failedTest "Typecheck unexpectedly succeded"
  , testProperty "Typecheck time is reasonably bounded" $
      let hst = stackEl ::& SNil
          run i =
            case
              typeCheckingWith def . runTypeCheckIsolated $
              typeCheckList [Un.WithSrcEx def $ Un.PrimEx (printStRef i)] hst
            of
              Left err -> err
              Right _ -> error "Typecheck unexpectedly succeded"
      -- Making code processing performance scale with code size looks like a
      -- good property, so we'd like to avoid scenario when user tries to
      -- access 100500-th element of stack and typecheck hangs as a result
      in  meanTimeUpperBoundPropNF (sec 1) run 100000000000
  ]
  where
    printStRef i = Un.EXT . Un.UPRINT $ Un.PrintComment [Right (Un.StackRef i)]
    stackEl = (T.starNotes @'T.TUnit, T.Dict, noAnn)

test_TCTypeError_display :: [TestTree]
test_TCTypeError_display =
  -- One may say that it's madness to write tests on 'Buildable' instances,
  -- but IMO (martoon) it's worth resulting duplication because tests allow
  -- avoiding silly errors like lost spaces and ensuring general sanity
  -- of used way to display content.
  [ testCase "TypeEqError" $
      build (TypeEqError MkMismatchError{meActual=T.TUnit, meExpected=T.TKey})
      @?= [itu|
        Types not equal:
        Expected: key
        Actual:   unit
        |]

  , testCase "StackEqError" $
      build (StackEqError $ MkMismatchError{meExpected=[T.TUnit, T.TBytes], meActual=[]})
      @?= [itu|
        Stacks not equal:
        Expected: [unit, bytes]
        Actual:   []
        |]

  , testCase "UnsupportedTypes" $
      build (UnsupportedTypeForScope (T.TBigMap T.TInt T.TInt) T.BtHasBigMap)
      @?= "Type 'big_map int int' is unsupported here because it has 'big_map'"

  , testCase "InvalidValueType" $
      build (InvalidValueType T.TUnit)
      @?= "Value type is never a valid `unit`"
  ]

hprop_ValueSeq_as_list :: Property
hprop_ValueSeq_as_list = property $ do
  l <- forAll $ Gen.nonEmpty (Range.linear 0 100) (Gen.integral (Range.linearFrom 0 -1000 1000))
  let
    untypedValue = Un.ValueSeq $ Un.ValueInt <$> l
    typedValue = T.VList $ T.VInt <$> toList l
    runTypeCheckInstr = typeCheckingWith def . runTypeCheckIsolated . usingReaderT def
  runTypeCheckInstr (typeCheckValue untypedValue) === Right typedValue

test_Nested_Sequences :: [TestTree]
test_Nested_Sequences =
  let
    runTC = typeCheckingWith def . runTypeCheckIsolated . usingReaderT (def @InstrCallStack)
    illTypedList = Un.ValueLambda $ Un.SeqEx [Un.SeqEx []] :| [Un.SeqEx []]
    wellTypedList = Un.ValueLambda $ Un.SeqEx [] :| [Un.SeqEx []]
    wellTypedLambda = Un.ValueSeq $ Un.ValueSeq (Un.ValueNil :| []) :| [Un.ValueNil]
  in
    [ testCase "Nested seq with different levels of nesting can't be typechecked as list" $
        case runTC $ typeCheckValue @('T.TList ('T.TList 'T.TInt)) illTypedList of
          Left _      -> pass
          Right other -> assertFailure $ "Unexpected result: " <> pretty other
    , testCase "Nested seq with the same levels of nesting can be typechecked as list" $
        case runTC $ typeCheckValue @('T.TList ('T.TList 'T.TInt)) wellTypedList of
          Right _  -> pass
          Left err -> assertFailure $ "Unexpected error: " <> pretty err
    , testCase "Nested seq with different levels of nesting can be typechecked as lambda" $
        case runTC $ typeCheckValue @('T.TLambda 'T.TInt 'T.TInt) wellTypedLambda of
          Right _  -> pass
          Left err -> assertFailure $ "Unexpected error: " <> pretty err
    ]

test_SELF :: [TestTree]
test_SELF =
  [ testCase "Entrypoint not present" $
      checkFile False (inContractsDir "ill-typed/self-bad-entrypoint.mtz") def $
      \case
        TCFailedOnInstr Un.SELF{} _ _ _ (Just EntrypointNotFound{}) -> pass
        other -> assertFailure $ "Unexpected error: " <> pretty other

  , testCase "Entrypoint type mismatch" $
      checkFile False (inContractsDir "ill-typed/self-entrypoint-type-mismatch.mtz")
        def (const pass)

  , testCase "Entrypoint can be found" $
      checkFile True (inContractsDir "entrypoints/self1.mtz") def
        (const pass)
  ]

test_Value_contract :: [TestTree]
test_Value_contract =
  [ testCase "No contract exists" $
      case typeCheckingWith def $
             typeVerifyParameter @('T.TContract 'T.TUnit) mempty addrUVal1
      of
        Left (TCFailedOnValue _ _ _ _ (Just (UnknownContract _))) -> pass
        res -> assertFailure $ "Unexpected result: " <> either pretty pretty res

  , testCase "Entrypoint does not exist" $
      case typeCheckingWith def $
             typeVerifyParameter @('T.TContract 'T.TKey) env1 addrUVal1
      of
        Left (TCFailedOnValue _ _ _ _ (Just (EntrypointNotFound _))) -> pass
        res -> assertFailure $ "Unexpected result: " <> either pretty pretty res

  , testCase "Correct contract value" $
      case typeCheckingWith def $
             typeVerifyParameter @('T.TContract 'T.TInt) env1 addrUVal2
      of
        Right _ -> pass
        res -> assertFailure $ "Unexpected result: " <> either pretty pretty res
  ]
  where
    addr1' = mkContractHashHack "123"
    addr1 = T.EpAddress (ContractAddress addr1') . unsafe $ buildEpName "a"
    addr2 = T.EpAddress (ContractAddress addr1') . unsafe $ buildEpName "q"
    addrUVal1 = Un.ValueString $ T.mformatEpAddress addr1
    addrUVal2 = Un.ValueString $ T.mformatEpAddress addr2

    env1 = M.fromList
      [ ( addr1',
          unsafe . mkSomeParamType
            $ ParameterType [utypeQ| or (nat %s) (int %q) |] noAnn
        )
      ]

test_Views :: [TestTree]
test_Views =
  [ testCase "Duplicated name" $ do
      checkIllFile (inContractsDir "ill-typed/duplicated_view_name.tz") \case
        TCContractError "Duplicated view name 'add'" _ -> True
        _ -> False

  , testCase "Fully duplicated view" $ do
      checkIllFile (inContractsDir "ill-typed/duplicated_view_full.tz") \case
        TCContractError "Duplicated view name 'add'" _ -> True
        _ -> False

  ]