-- |
-- Module      :  Cryptol.TypeCheck.Sanity
-- Copyright   :  (c) 2015-2016 Galois, Inc.
-- License     :  BSD3
-- Maintainer  :  cryptol@galois.com
-- Stability   :  provisional
-- Portability :  portable
{-# Language OverloadedStrings #-}
module Cryptol.TypeCheck.Sanity
  ( tcExpr
  , tcDecls
  , tcModule
  , ProofObligation
  , onlyNonTrivial
  , Error(..)
  , AreSame(..)
  , same
  ) where


import Cryptol.Parser.Position(thing,Range,emptyRange)
import Cryptol.TypeCheck.AST
import Cryptol.TypeCheck.Subst (apSubst, singleTParamSubst)
import Cryptol.TypeCheck.Monad(InferInput(..))
import Cryptol.ModuleSystem.Name(nameLoc)
import Cryptol.Utils.Ident
import Cryptol.Utils.RecordMap
import Cryptol.Utils.PP

import Data.List (sort)
import qualified Data.Set as Set
import MonadLib
import qualified Control.Applicative as A

import           Data.Map ( Map )
import qualified Data.Map as Map


tcExpr :: InferInput -> Expr -> Either (Range, Error) (Schema, [ ProofObligation ])
tcExpr env e = runTcM env (exprSchema e)

tcDecls :: InferInput -> [DeclGroup] -> Either (Range, Error) [ ProofObligation ]
tcDecls env ds0 = case runTcM env (checkDecls ds0) of
                    Left err     -> Left err
                    Right (_,ps) -> Right ps

tcModule :: InferInput -> Module -> Either (Range, Error) [ ProofObligation ]
tcModule env m = case runTcM env check of
                   Left err -> Left err
                   Right (_,ps) -> Right ps
  where check = foldr withTVar k1 (map mtpParam (Map.elems (mParamTypes m)))
        k1    = foldr withAsmp k2 (map thing (mParamConstraints m))
        k2    = withVars (Map.toList (fmap mvpType (mParamFuns m)))
              $ checkDecls (mDecls m)

onlyNonTrivial :: [ProofObligation] -> [ProofObligation]
onlyNonTrivial = filter (not . trivialProofObligation)

-- | Identify proof obligations that are obviously true.
-- We can filter these to avoid clutter
trivialProofObligation :: ProofObligation -> Bool
trivialProofObligation oblig = pIsTrue goal || simpleEq || goal `elem` asmps
  where
  goal  = sType oblig
  asmps = sProps oblig
  simpleEq = case pIsEqual goal of
               Just (t1,t2) -> t1 == t2
               Nothing      -> False


--------------------------------------------------------------------------------

checkDecls :: [DeclGroup] -> TcM ()
checkDecls decls =
  case decls of
    [] -> return ()
    d : ds -> do xs <- checkDeclGroup d
                 withVars xs (checkDecls ds)

-- | Validate a type, returning its kind.
checkType :: Type -> TcM Kind
checkType ty =
  case ty of
    TUser _ _ t -> checkType t    -- Maybe check synonym too?

    TCon tc ts ->
      do ks <- mapM checkType ts
         checkKind (kindOf tc) ks

    TNewtype nt ts ->
      do ks <- mapM checkType ts
         checkKind (kindOf nt) ks

    TVar tv -> lookupTVar tv

    TRec fs ->
      do forM_ fs $ \t ->
           do k <- checkType t
              unless (k == KType) $ reportError $ KindMismatch KType k
         return KType


  where
  checkKind k [] = case k of
                     _ :-> _  -> reportError $ NotEnoughArgumentsInKind k
                     KProp    -> return k
                     KNum     -> return k
                     KType    -> return k

  checkKind (k1 :-> k2) (k : ks)
    | k == k1   = checkKind k2 ks
    | otherwise = reportError $ KindMismatch k1 k

  checkKind k ks = reportError $ BadTypeApplication k ks


-- | Check that the type is valid, and it has the given kind.
checkTypeIs :: Kind -> Type -> TcM ()
checkTypeIs k ty =
  do k1 <- checkType ty
     unless (k == k1) $ reportError $ KindMismatch k k1

-- | Check that this is a valid schema.
checkSchema :: Schema -> TcM ()
checkSchema (Forall as ps t) = foldr withTVar check as
  where check = do mapM_ (checkTypeIs KProp) ps
                   checkTypeIs KType t

data AreSame = SameIf [Prop]
             | NotSame

areSame :: AreSame
areSame = SameIf []

sameAnd :: AreSame -> AreSame -> AreSame
sameAnd x y =
  case (x,y) of
    (SameIf xs, SameIf ys) -> SameIf (xs ++ ys)
    _                      -> NotSame

sameBool :: Bool -> AreSame
sameBool b = if b then areSame else NotSame

sameTypes :: String -> Type -> Type -> TcM ()
sameTypes msg x y = sameSchemas msg (tMono x) (tMono y)

sameSchemas :: String -> Schema -> Schema -> TcM ()
sameSchemas msg x y =
  case same x y of
    NotSame   -> reportError (TypeMismatch msg x y)
    SameIf ps -> mapM_ proofObligation ps




class Same a where
  same :: a -> a -> AreSame

instance Same a => Same [a] where
  same [] [] = areSame
  same (x : xs) (y : ys) = same x y `sameAnd` same xs ys
  same _ _ = NotSame

data Field a b = Field a b

instance (Eq a, Same b) => Same (Field a b) where
  same (Field x a) (Field y b) = sameBool (x == y) `sameAnd` same a b

instance Same Type where
  same t1 t2
    | k1 /= k2    = NotSame
    | k1 == KNum  = if t1 == t2 then SameIf [] else SameIf [ t1 =#= t2 ]
    | otherwise   =
      case (tNoUser t1, tNoUser t2) of
        (TVar x, TVar y)               -> sameBool (x == y)
        (TRec x, TRec y)               -> same (mkRec x) (mkRec y)
        (TNewtype x xs, TNewtype y ys) -> same (Field x xs) (Field y ys)
        (TCon x xs, TCon y ys)         -> same (Field x xs) (Field y ys)
        _                              -> NotSame
      where
      k1 = kindOf t1
      k2 = kindOf t2
      mkRec r = [ Field x y | (x,y) <- canonicalFields r ]

instance Same Schema where
  same (Forall xs ps s) (Forall ys qs t) =
    same xs ys `sameAnd` same ps qs `sameAnd` same s t

instance Same TParam where
  same x y = sameBool (tpName x == tpName y && tpKind x == tpKind y)





--------------------------------------------------------------------------------


-- | Check that the expression is well-formed, and compute its type.
-- Reports an error if the expression is not of a mono type.
exprType :: Expr -> TcM Type
exprType expr =
  do s <- exprSchema expr
     case isMono s of
       Just t  -> return t
       Nothing -> reportError (ExpectedMono s)


-- | Check that the expression is well-formed, and compute its schema.
exprSchema :: Expr -> TcM Schema
exprSchema expr =
  case expr of

    ELocated rng t -> withRange rng (exprSchema t)

    EList es t ->
      do checkTypeIs KType t
         forM_ es $ \e ->
           do t1 <- exprType e
              sameTypes "EList" t1 t

         return $ tMono $ tSeq (tNum (length es)) t

    ETuple es ->
      fmap (tMono . tTuple) (mapM exprType es)

    ERec fs ->
      do fs1 <- traverse exprType fs
         return $ tMono $ TRec fs1

    ESet _ e x v ->
       do ty  <- exprType e
          expe <- checkHas ty x
          has <- exprType v
          sameTypes "ESet" expe has
          return (tMono ty)

    ESel e sel -> do ty <- exprType e
                     ty1 <- checkHas ty sel
                     return (tMono ty1)

    EIf e1 e2 e3 ->
      do ty <- exprType e1
         sameTypes "EIf_condition" tBit ty

         t1 <- exprType e2
         t2 <- exprType e3
         sameTypes "EIf_arms" t1 t2

         return $ tMono t1

    EComp len t e mss ->
      do checkTypeIs KNum len
         checkTypeIs KType t

         (xs,ls) <- unzip `fmap` mapM checkArm mss
         -- XXX: check no duplicates
         elT <- withVars (concat xs) $ exprType e

         case ls of
           [] -> return ()
           _  -> convertible (tSeq len t) (tSeq (foldr1 tMin ls) elT)

         return (tMono (tSeq len t))


    EVar x -> lookupVar x

    ETAbs a e     ->
      do Forall as p t <- withTVar a (exprSchema e)
         when (any (== a) as) $
           reportError $ RepeatedVariableInForall a

         return (Forall (a : as) p t)

    ETApp e t ->
      do k <- checkType t
         s <- exprSchema e
         case s of
           Forall (a : as) ps t1 ->
             do let vs = fvs t

                forM_ (map tpVar as) $ \b ->
                  when (b `Set.member` vs) $ reportError $ Captured b

                let k' = kindOf a
                unless (k == k') $ reportError $ KindMismatch k' k

                let su = singleTParamSubst a t
                return $ Forall as (apSubst su ps) (apSubst su t1)

           Forall [] _ _ -> reportError BadInstantiation

    EApp e1 e2 ->
      do t1 <- exprType e1
         t2 <- exprType e2

         case tNoUser t1 of
           TCon (TC TCFun) [ a, b ]
              | SameIf ps <- same a t2 ->
                do mapM_ proofObligation ps
                   return (tMono b)
           tf -> reportError (BadApplication tf t1)


    EAbs x t e    ->
      do checkTypeIs KType t
         res <- withVar x t (exprType e)
         return $ tMono $ tFun t res


    EProofAbs p e ->
      do checkTypeIs KProp p
         withAsmp p $ do Forall as ps t <- exprSchema e
                         return $ Forall as (p : ps) t

    EProofApp e ->
      do Forall as ps t <- exprSchema e
         case (as,ps) of
           ([], p:qs) -> do proofObligation p
                            return (Forall [] qs t)
           ([], _)    -> reportError BadProofNoAbs
           (_,_)      -> reportError (BadProofTyVars as)


    -- XXX: Check that defined things are distinct?
    EWhere e dgs ->
      let go []       = exprSchema e
          go (d : ds) = do xs <- checkDeclGroup d
                           withVars xs (go ds)
      in go dgs


    EPropGuards _guards typ -> 
      pure Forall {sVars = [], sProps = [], sType = typ}

checkHas :: Type -> Selector -> TcM Type
checkHas t sel =
  case sel of

    TupleSel n mb ->

      case tNoUser t of
        TCon (TC (TCTuple sz)) ts ->
          do case mb of
               Just sz1 ->
                 when (sz /= sz1) (reportError (UnexpectedTupleShape sz1 sz))
               Nothing  -> return ()
             unless (n < sz) $ reportError (TupleSelectorOutOfRange n sz)
             return $ ts !! n

        TCon (TC TCSeq) [s,elT] ->
           do res <- checkHas elT sel
              return (TCon (TC TCSeq) [s,res])

        TCon (TC TCFun) [a,b] ->
            do res <- checkHas b sel
               return (TCon (TC TCFun) [a,res])

        _ -> reportError $ BadSelector sel t


    RecordSel f mb ->
      case tNoUser t of
        TRec fs ->

          do case mb of
               Nothing -> return ()
               Just fs1 ->
                 do let ns  = Set.toList (fieldSet fs)
                        ns1 = sort fs1
                    unless (ns == ns1) $
                      reportError $ UnexpectedRecordShape ns1 ns

             case lookupField f fs of
               Nothing -> reportError $ MissingField f $ displayOrder fs
               Just ft -> return ft

        TCon (TC TCSeq) [s,elT] -> do res <- checkHas elT sel
                                      return (TCon (TC TCSeq) [s,res])

        TCon (TC TCFun) [a,b]   -> do res <- checkHas b sel
                                      return (TCon (TC TCFun) [a,res])


        _ -> reportError $ BadSelector sel t


    -- XXX: Remove this?
    ListSel _ mb ->
      case tNoUser t of
        TCon (TC TCSeq) [ n, elT ] ->

          do case mb of
               Nothing  -> return ()
               Just len ->
                 case tNoUser n of
                   TCon (TC (TCNum m)) []
                     | m == toInteger len -> return ()
                   _ -> reportError $ UnexpectedSequenceShape len n

             return elT

        _ -> reportError $ BadSelector sel t




-- | Check if the one type is convertible to the other.
convertible :: Type -> Type -> TcM ()
convertible t1 t2
  | k1 /= k2    = reportError (KindMismatch k1 k2)
  | k1 == KNum  = proofObligation (t1 =#= t2)
  where
  k1 = kindOf t1
  k2 = kindOf t2

convertible t1 t2 = go t1 t2
  where
  go ty1 ty2 =
    let err   = reportError $ TypeMismatch "convertible" (tMono ty1) (tMono ty2)
        other = tNoUser ty2

        goMany [] []             = return ()
        goMany (x : xs) (y : ys) = convertible x y >> goMany xs ys
        goMany _ _               = err

    in case ty1 of
         TUser _ _ s   -> go s ty2

         TVar x        -> case other of
                            TVar y | x == y  -> return ()
                            _                -> err

         TCon tc1 ts1  -> case other of
                            TCon tc2 ts2
                               | tc1 == tc2 -> goMany ts1 ts2
                            _ -> err

         TNewtype nt1 ts1 ->
            case other of
              TNewtype nt2 ts2
                | nt1 == nt2 -> goMany ts1 ts2
              _ -> err

         TRec fs ->
           case other of
             TRec gs ->
               do unless (fieldSet fs == fieldSet gs) err
                  goMany (recordElements fs) (recordElements gs)
             _ -> err


--------------------------------------------------------------------------------

-- | Check a declaration. The boolean indicates if we should check the siganture
checkDecl :: Bool -> Decl -> TcM (Name, Schema)
checkDecl checkSig d =
  case dDefinition d of

    DPrim ->
      do when checkSig $ checkSchema $ dSignature d
         return (dName d, dSignature d)

    DForeign _ ->
      do when checkSig $ checkSchema $ dSignature d
         return (dName d, dSignature d)

    DExpr e ->
      do let s = dSignature d
         when checkSig $ checkSchema s
         s1 <- exprSchema e
         let nm = dName d
             loc = "definition of " ++ show (pp nm) ++
                            ", at " ++ show (pp (nameLoc nm))
         sameSchemas loc s s1

         return (dName d, s)

checkDeclGroup :: DeclGroup -> TcM [(Name, Schema)]
checkDeclGroup dg =
  case dg of
    NonRecursive d -> do x <- checkDecl True d
                         return [x]
    Recursive ds ->
      do xs <- forM ds $ \d ->
                  do checkSchema (dSignature d)
                     return (dName d, dSignature d)
         withVars xs $ mapM (checkDecl False) ds


checkMatch :: Match -> TcM ((Name, Schema), Type)
checkMatch ma =
  case ma of
    From x len elt e ->
      do checkTypeIs KNum len
         checkTypeIs KType elt
         t1 <- exprType e
         case tNoUser t1 of
           TCon (TC TCSeq) [ l, el ]
             | SameIf ps <- same elt el ->
               do mapM_ proofObligation ps
                  return ((x, tMono elt), l)
             | otherwise -> reportError $ TypeMismatch "From" (tMono elt) (tMono el)


           _ -> reportError $ BadMatch t1

    Let d -> do x <- checkDecl True d
                return (x, tNum (1 :: Int))

checkArm :: [Match] -> TcM ([(Name, Schema)], Type)
checkArm []   = reportError EmptyArm
checkArm [m]  = do (x,l) <- checkMatch m
                   return ([x], l)
checkArm (m : ms) =
  do (x, l)   <- checkMatch m
     (xs, l1) <- withVars [x] $ checkArm ms
     let newLen = tMul l l1
     return $ if fst x `elem` map fst xs
                 then (xs, newLen)
                 else (x : xs, newLen)




--------------------------------------------------------------------------------

data RO = RO
  { roTVars   :: Map Int TParam
  , roAsmps   :: [Prop]
  , roRange   :: Range
  , roVars    :: Map Name Schema
  }

type ProofObligation = Schema -- but the type is of kind Prop

data RW = RW
  { woProofObligations :: [ProofObligation]
  }

newtype TcM a = TcM (ReaderT RO (ExceptionT (Range, Error) (StateT RW Id)) a)

instance Functor TcM where
  fmap = liftM

instance A.Applicative TcM where
  pure a = TcM (pure a)
  (<*>) = ap

instance Monad TcM where
  return      = pure
  TcM m >>= f = TcM (do a <- m
                        let TcM m1 = f a
                        m1)

runTcM :: InferInput -> TcM a -> Either (Range, Error) (a, [ProofObligation])
runTcM env (TcM m) =
  case runM m ro rw of
    (Left err, _) -> Left err
    (Right a, s)  -> Right (a, woProofObligations s)
  where
  allPs = inpParams env

  ro = RO { roTVars = Map.fromList [ (tpUnique x, x)
                                      | tp <- Map.elems (mpnTypes allPs)
                                      , let x = mtpParam tp ]
          , roAsmps = map thing (mpnConstraints allPs)
          , roRange = emptyRange
          , roVars  = Map.union
                        (fmap mvpType (mpnFuns allPs))
                        (inpVars env)
          }
  rw = RW { woProofObligations = [] }


data Error =
    TypeMismatch String Schema Schema    -- ^ expected, actual
  | ExpectedMono Schema           -- ^ expected a mono type, got this
  | TupleSelectorOutOfRange Int Int
  | MissingField Ident [Ident]
  | UnexpectedTupleShape Int Int
  | UnexpectedRecordShape [Ident] [Ident]
  | UnexpectedSequenceShape Int Type
  | BadSelector Selector Type
  | BadInstantiation
  | Captured TVar
  | BadProofNoAbs
  | BadProofTyVars [TParam]
  | KindMismatch Kind Kind
  | NotEnoughArgumentsInKind Kind
  | BadApplication Type Type
  | FreeTypeVariable TVar
  | BadTypeApplication Kind [Kind]
  | RepeatedVariableInForall TParam
  | BadMatch Type
  | EmptyArm
  | UndefinedTypeVaraible TVar
  | UndefinedVariable Name
    deriving Show

reportError :: Error -> TcM a
reportError e = TcM $
  do ro <- ask
     raise (roRange ro, e)

withTVar :: TParam -> TcM a -> TcM a
withTVar a (TcM m) = TcM $
  do ro <- ask
     local ro { roTVars = Map.insert (tpUnique a) a (roTVars ro) } m

withRange :: Range -> TcM a -> TcM a
withRange rng (TcM m) = TcM $
  do ro <- ask
     local ro { roRange = rng } m

withAsmp :: Prop -> TcM a -> TcM a
withAsmp p (TcM m) = TcM $
  do ro <- ask
     local ro { roAsmps = p : roAsmps ro } m

withVar :: Name -> Type -> TcM a -> TcM a
withVar x t = withVars [(x,tMono t)]

withVars :: [(Name, Schema)] -> TcM a -> TcM a
withVars xs (TcM m) = TcM $
  do ro <- ask
     local ro { roVars = Map.union (Map.fromList xs) (roVars ro) } m

proofObligation :: Prop -> TcM ()
proofObligation p = TcM $
  do ro <- ask
     sets_ $ \rw -> rw { woProofObligations =
                             Forall (Map.elems (roTVars ro)) (roAsmps ro) p
                           : woProofObligations rw }

lookupTVar :: TVar -> TcM Kind
lookupTVar x =
  case x of
    TVFree {} -> reportError (FreeTypeVariable x)
    TVBound tpv ->
       do let u = tpUnique tpv
              k = tpKind tpv
          ro <- TcM ask
          case Map.lookup u (roTVars ro) of
            Just tp
              | kindOf tp == k  -> return k
              | otherwise       -> reportError $ KindMismatch (kindOf tp) k
            Nothing  -> reportError $ UndefinedTypeVaraible x

lookupVar :: Name -> TcM Schema
lookupVar x =
  do ro <- TcM ask
     case Map.lookup x (roVars ro) of
       Just s -> return s
       Nothing -> reportError $ UndefinedVariable x


instance PP Error where
  ppPrec _ err =
    case err of

      TypeMismatch what expected actual ->
        ppErr ("Type mismatch in" <+> text what)
          [ "Expected:" <+> pp expected
          , "Actual  :" <+> pp actual
          ]

      ExpectedMono s ->
        ppErr "Not a monomorphic type"
          [ pp s ]

      TupleSelectorOutOfRange sel sz ->
        ppErr "Tuple selector out of range"
          [ "Selector:" <+> int sel
          , "Size    :" <+> int sz
          ]

      MissingField f fs ->
        ppErr "Invalid record selector"
          [ "Field: " <+> pp f
          , "Fields:" <+> commaSep (map pp fs)
          ]

      UnexpectedTupleShape expected actual ->
        ppErr "Unexpected tuple shape"
          [ "Expected:" <+> int expected
          , "Actual  :" <+> int actual
          ]

      UnexpectedRecordShape expected actual ->
        ppErr "Unexpected record shape"
          [ "Expected:" <+> commaSep (map pp expected)
          , "Actual  :" <+> commaSep (map pp actual)
          ]

      UnexpectedSequenceShape n t ->
        ppErr "Unexpected sequence shape"
          [ "Expected:" <+> int n
          , "Actual  :" <+> pp t
          ]

      BadSelector sel t ->
        ppErr "Bad selector"
          [ "Selector:" <+> pp sel
          , "Type    :" <+> pp t
          ]

      BadInstantiation ->
        ppErr "Bad instantiation" []

      Captured x ->
        ppErr "Captured type variable"
          [ "Variable:" <+> pp x ]

      BadProofNoAbs ->
        ppErr "Proof application without a proof abstraction" []

      BadProofTyVars xs ->
        ppErr "Proof application with type abstraction"
          [ "Type parameter:" <+> pp x | x <- xs ]

      KindMismatch expected actual ->
        ppErr "Kind mismatch"
          [ "Expected:" <+> pp expected
          , "Actual  :" <+> pp actual
          ]

      NotEnoughArgumentsInKind k ->
        ppErr "Not enough arguments in kind" [ pp k ]

      BadApplication t1 t2 ->
        ppErr "Bad application"
          [ "Function:" <+> pp t1
          , "Argument:" <+> pp t2
          ]

      FreeTypeVariable x ->
        ppErr "Free type variable"
          [ "Variable:" <+> pp x ]

      BadTypeApplication kf ka ->
        ppErr "Bad type application"
          [ "Function :" <+> pp kf
          , "Arguments:" <+> commaSep (map pp ka)
          ]

      RepeatedVariableInForall x ->
        ppErr "Repeated variable in forall"
          [ "Variable:" <+> pp x ]

      BadMatch t ->
        ppErr "Bad match"
          [ "Type:" <+> pp t ]

      EmptyArm -> ppErr "Empty comprehension arm" []

      UndefinedTypeVaraible x ->
        ppErr "Undefined type variable"
          [ "Variable:" <+> pp x ]

      UndefinedVariable x ->
        ppErr "Undefined variable"
          [ "Variable:" <+> pp x ]

    where
    ppErr x ys = hang x 2 (vcat [ "•" <+> y | y <- ys ])
