packages feed

camfort-1.0: src/Camfort/Analysis/Logger.hs

{-# LANGUAGE DefaultSignatures          #-}
{-# LANGUAGE DeriveFunctor              #-}
{-# LANGUAGE DeriveTraversable          #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE FunctionalDependencies     #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE NamedFieldPuns             #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE RankNTypes                 #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE UndecidableInstances       #-}

{-# OPTIONS_GHC -Wall            #-}

{-|

Provides logging for analyses. 'MonadLogger' is a type class for monads which
support logging. 'LoggerT' is a concrete monad transformer instantiating the
class.

As a logger runs, it may print out log messages on the fly (depending on the
provided 'LogOutput' function). It also collects logs to be inspected at the end
of the computation.

A log message must usually include an 'Origin', which describes where in a
Fortran source file the message originated. This is made more convenient via
functions such as 'logWarn\'', which produces an 'Origin' based on a piece of
Fortran syntax, along with a default source file stored in the environment.

Log messages each come with an associated 'LogLevel':

- 'LogError' is for hard errors which will often cause the computation to fail.
- 'LogWarn' is for messages about things that are likely to cause problems.
- 'LogInfo' is for general information about what the computation is doing.
- 'LogDebug' is for extra-verbose output that helps with debugging, but which
  will be uninteresting to most users.

-}
module Camfort.Analysis.Logger
  (
  -- * Conversion to text description
    Describe(..)
  , tellDescribe
  , describeShow
  , builderToStrict
  , Builder
  , Text
  , (<>)
  -- * Messages
  , Origin(..)
  , ParsedOrigin(..)
  , parseOrigin
  , oFile
  , oSpan
  , LogLevel(..)
  , LogMessage(..)
  , lmOrigin
  , lmMsg
  , SomeMessage(..)
  , _MsgError
  , _MsgWarn
  , _MsgInfo
  , _MsgDebug
  -- * Logging monad
  , MonadLogger(..)
  , atSpanned
  , atSpannedInFile
  , LoggerT
  , mapLoggerT
  -- * Running a logger
  , LogOutput
  , logOutputStd
  , logOutputNone
  , runLoggerT
  ) where

import qualified Data.Semigroup                 as SG
import           Data.Void                      (Void)

import           Control.DeepSeq
import           Control.Lens

import           Control.Monad.Except
import           Control.Monad.Morph
import           Control.Monad.Reader
import           Control.Monad.RWS
import qualified Control.Monad.State            as Lazy
import           Control.Monad.State.Strict
import           Control.Monad.Writer
import           Control.Monad.Fail

import           Data.Text                      (Text)
import qualified Data.Text.IO                   as Text
import qualified Data.Text.Lazy                 as Lazy
import           Data.Text.Lazy.Builder         (Builder)
import qualified Data.Text.Lazy.Builder         as Builder

import           GHC.Generics

import           Text.Read                      (readMaybe)
import qualified Language.Fortran.Util.Position as F

--------------------------------------------------------------------------------
--  'Describe' class
--------------------------------------------------------------------------------

-- TODO: More 'Describe' instances for built-in types.

-- | A type class for efficiently converting values to human-readable output.
-- Can be automatically instantiated for 'Show' types, but this will not be very
-- human-readable for a lot of types.
class Describe a where
  -- | Convert the value to a human-readable output as a strict 'Text' value.
  describe :: a -> Text

  -- | Convert the value to human-readable output in a text 'Builder' which can
  -- be efficiently concatenated with other 'Builder's.
  describeBuilder :: a -> Builder

  default describeBuilder :: Show a => a -> Builder
  describe = builderToStrict . describeBuilder
  describeBuilder = Builder.fromString . show

instance Describe F.SrcSpan
instance Describe Text where
  describeBuilder = Builder.fromText
instance Describe [Char] where
  describeBuilder = Builder.fromString
instance Describe () where
  describeBuilder = const mempty
instance Describe Int
instance Describe Integer
instance Describe Float
instance Describe Double
instance Describe Void

-- | A convenience combinator to directly convert a lazy text 'Builder' to a
-- strict 'Text' value.
builderToStrict :: Builder -> Text
builderToStrict = Lazy.toStrict . Builder.toLazyText

-- | Write a 'Describe'-able value directly into a writer monad.
tellDescribe :: (MonadWriter Builder m, Describe a) => a -> m ()
tellDescribe = tell . describeBuilder

-- | Convert a 'Show'-able value directly to strict 'Text'. Useful when you have
-- a 'Show' instance but not a 'Describe' instance.
describeShow :: (Show a) => a -> Text
describeShow = describe . show

--------------------------------------------------------------------------------
--  Messages
--------------------------------------------------------------------------------

-- | A message origin, containing a file and a source span.
data Origin =
  Origin
  { _oFile :: FilePath
  , _oSpan :: F.SrcSpan
  }
  deriving (Show, Eq, Ord, Generic)

makeLenses ''Origin

instance NFData Origin

instance Describe Origin where
  describeBuilder origin =
    "at [" <> Builder.fromString (origin ^. oFile) <>
    ", " <> describeBuilder (origin ^. oSpan) <> "]"

data ParsedOrigin = ParsedOrigin FilePath (Int, Int) (Int, Int)
  deriving (Show, Eq, Ord)

-- | Extract information about filename and source span from a string.
parseOrigin :: String -> Maybe ParsedOrigin
parseOrigin str
  | not (null filename)
  , Just (pos1, rest) <- parsePos comma
  , Just (pos2, _)    <- parsePos rest = Just (ParsedOrigin filename pos1 pos2)
  | otherwise                          = Nothing
  where
    lbrack            = dropWhile (/= '[') str
    (filename, comma) = break (==',') (drop 1 lbrack)

parsePos :: String -> Maybe ((Int, Int), String)
parsePos str
  | Just l <- readMaybe line
  , Just c <- readMaybe col  = Just ((l, c), rest)
  | otherwise                = Nothing
  where
    lparen        = dropWhile (/= '(') str
    (line, colon) = break (== ':') (drop 1 lparen)
    (col, rest)   = span (/= ')') (drop 1 colon)

-- | A logging level. At each logging level, only produce output at that level or lower.
data LogLevel
  = LogError
  -- ^ At level 'LogError', only error messages are shown.
  | LogWarn
  -- ^ At level 'LogWarn', error and warning messages are shown.
  | LogInfo
  -- ^ At level 'LogInfo', error, warning and information messages are shown.
  | LogDebug
  -- ^ At level 'LogDebug', error, warning, information and debug output is
  -- shown.
  deriving (Show, Eq, Ord)

instance Describe LogLevel where
  describeBuilder LogError = "ERROR"
  describeBuilder LogWarn  = "WARN"
  describeBuilder LogInfo  = "INFO"
  describeBuilder LogDebug = "DEBUG"

-- | A logged message with an origin and a message value.
data LogMessage a =
  LogMessage
  { _lmOrigin :: Maybe Origin
  , _lmMsg    :: a
  }
  deriving (Show, Eq, Functor, Foldable, Traversable, Generic)

makeLenses ''LogMessage

instance NFData a => NFData (LogMessage a)

instance Describe a => Describe (LogMessage a) where
  describeBuilder msg =
    maybe "" describeBuilder (msg ^. lmOrigin) <>
    ": " <> describeBuilder (msg ^. lmMsg)

-- | A message at one of the four 'LogLevel's.
data SomeMessage e w
  = MsgError (LogMessage e)
  | MsgWarn (LogMessage w)
  | MsgInfo (LogMessage Text)
  | MsgDebug (LogMessage Text)
  deriving (Show, Eq, Generic)

makePrisms ''SomeMessage

instance (NFData e, NFData w) => NFData (SomeMessage e w)

someMessageOrigin :: Lens' (SomeMessage e w) (Maybe Origin)
someMessageOrigin =
  lens
  (preview $
    (_MsgError . lmOrigin `failing`
     _MsgWarn  . lmOrigin `failing`
     _MsgInfo  . lmOrigin `failing`
     _MsgDebug . lmOrigin
    ) . _Just)
  (flip $ \o ->
   set (_MsgError . lmOrigin) o .
   set (_MsgWarn  . lmOrigin) o .
   set (_MsgInfo  . lmOrigin) o .
   set (_MsgDebug . lmOrigin) o)


instance (Describe e, Describe w) => Describe (SomeMessage e w) where
  describeBuilder msg = case msg of
    MsgError m -> "ERROR: " <> describeBuilder m
    MsgWarn  m -> "WARN: "  <> describeBuilder m
    MsgInfo  m -> "INFO: "  <> describeBuilder m
    MsgDebug m -> "DEBUG: " <> describeBuilder m

--------------------------------------------------------------------------------
--  'MonadLogger' class
--------------------------------------------------------------------------------

-- | Make an origin at the source span of a piece of Fortran syntax, in the
-- current file.
atSpanned :: (MonadLogger e w m, F.Spanned a) => a -> m Origin
atSpanned astElem = do
  sf <- getDefaultSourceFile
  let sp = F.getSpan astElem
  return $ Origin sf sp

-- | Make an origin at the source span of a piece of Fortran syntax, in the given
-- file.
atSpannedInFile :: (F.Spanned a) => FilePath -> a -> Origin
atSpannedInFile sf = Origin sf . F.getSpan

-- | MTL-style type class for monads that support logging.
class Monad m => MonadLogger e w m | m -> e w where
  -- | Set the default source file, i.e. the file in which messages originate by
  -- default.
  setDefaultSourceFile :: FilePath -> m ()

  -- | Get the current default source file, i.e. the file in which messages
  -- originate by default.
  getDefaultSourceFile :: m FilePath

  -- | Record a log message. Output it based on the 'LogOutput' function used
  -- and store it in the collected logs.
  recordLogMessage :: SomeMessage e w -> m ()

  -- | Log an error message at the given 'Origin'.
  logError :: Origin -> e -> m ()
  logError = logGeneral MsgError

  -- | Log an error message. The origin is the current default source file, with
  -- the source span of the given piece of Fortran syntax.
  logError' :: (F.Spanned a) => a -> e -> m ()
  logError' = withSpannedOrigin logError


  -- | Log a warning message at the given 'Origin'.
  logWarn :: Origin -> w -> m ()
  logWarn = logGeneral MsgWarn

  -- | Log a warning message. The origin is the current default source file, with
  -- the source span of the given piece of Fortran syntax.
  logWarn' :: (F.Spanned a) => a -> w -> m ()
  logWarn' = withSpannedOrigin logWarn


  -- | Log an information message at the given 'Origin'.
  logInfo :: Origin -> Text -> m ()
  logInfo = logGeneral MsgInfo

  -- | Log an information message. The origin is the current default source
  -- file, with the source span of the given piece of Fortran syntax.
  logInfo' :: (F.Spanned a) => a -> Text -> m ()
  logInfo' = withSpannedOrigin logInfo

  -- | Log an information message with no origin. For example, use this when
  -- printing output about the progress of an analysis which cannot be
  -- associated with a particular bit of source code.
  logInfoNoOrigin :: Text -> m ()
  logInfoNoOrigin msg = recordLogMessage (MsgInfo (LogMessage Nothing msg))


  -- | Log a debugging message at the given 'Origin'.
  logDebug :: Origin -> Text -> m ()
  logDebug = logGeneral MsgDebug

  -- | Log a debugging message. The origin is the current default source
  -- file, with the source span of the given piece of Fortran syntax.
  logDebug' :: (F.Spanned a) => a -> Text -> m ()
  logDebug' = withSpannedOrigin logDebug

  default recordLogMessage
    :: (MonadTrans t, MonadLogger e w m', m ~ t m') => SomeMessage e w -> m ()
  default setDefaultSourceFile
    :: (MonadTrans t, MonadLogger e w m', m ~ t m') => FilePath -> m ()
  default getDefaultSourceFile
    :: (MonadTrans t, MonadLogger e w m', m ~ t m') => m FilePath

  recordLogMessage = lift . recordLogMessage
  setDefaultSourceFile = lift . setDefaultSourceFile
  getDefaultSourceFile = lift getDefaultSourceFile

logGeneral :: (MonadLogger e w m) => (LogMessage a -> SomeMessage e w) -> Origin -> a -> m ()
logGeneral mkMsg origin msg =
  recordLogMessage (mkMsg (LogMessage (Just origin) msg))

withSpannedOrigin
  :: (MonadLogger e w m, F.Spanned a)
  => (Origin -> b -> m c) -> a -> b -> m c
withSpannedOrigin f x m = do
  origin <- atSpanned x
  f origin m

instance MonadLogger e w m => MonadLogger e w (ReaderT r m)
instance MonadLogger e w m => MonadLogger e w (ExceptT e' m)
instance MonadLogger e w m => MonadLogger e w (StateT s m)
instance (MonadLogger e w m, Monoid w') => MonadLogger e w (WriterT w' m)
instance MonadLogger e w m => MonadLogger e w (Lazy.StateT s m)
-- instance (MonadLogger e w m, Monoid w') => MonadLogger e w (Lazy.WriterT w' m)
instance (MonadLogger e w m, Monoid w') => MonadLogger e w (RWST r w' s m)
-- instance (MonadLogger e w m, Monoid w') => MonadLogger e w (Lazy.RWST r w' s m)

--------------------------------------------------------------------------------
--  'LoggerT' monad
--------------------------------------------------------------------------------

data LoggerState =
  LoggerState
  { _lsLogLevel          :: !LogLevel
  , _lsDefaultSourceFile :: !FilePath
  , _lsPreviousOrigin    :: !(Maybe Origin)
  }

data OpMonoid a = OpMonoid { getOpMonoid :: a }

makeWrapped ''OpMonoid

instance SG.Semigroup a => SG.Semigroup (OpMonoid a) where
  OpMonoid x <> OpMonoid y = OpMonoid (y SG.<> x)

instance (SG.Semigroup a, Monoid a) => Monoid (OpMonoid a) where
  mempty = OpMonoid mempty
  mappend = (SG.<>)

data LoggerEnv m =
  LoggerEnv
  { _leLogFunc :: !(Bool -> LogLevel -> LogLevel -> Text -> Text -> m ())
  }

makeLenses ''LoggerState
makeLenses ''LoggerEnv

hoistEnv :: (m () -> n ()) -> LoggerEnv m -> LoggerEnv n
hoistEnv f = leLogFunc %~ \logFunc b l1 l2 m1 m2 -> f $ logFunc b l1 l2 m1 m2

-- | The logging monad transformer, containing errors of type @e@ and warnings
-- of type @w@.
newtype LoggerT e w m a =
  LoggerT (RWST (LoggerEnv m) (OpMonoid [SomeMessage e w]) LoggerState m a)
  deriving
    ( Functor
    , Applicative
    , Monad
    , MonadIO
    , MonadError e'
    , MonadFail
    )

instance MonadTrans (LoggerT e w) where
  lift = LoggerT . lift

instance (MonadState s m) => MonadState s (LoggerT e w m) where
  get = lift get
  put = lift . put
  state = lift . state

instance (MonadReader r m) => MonadReader r (LoggerT e w m) where
  ask = lift ask
  local f (LoggerT (RWST k)) = LoggerT $ RWST $ \e -> local f . k e

instance (MonadWriter w' m) => MonadWriter w' (LoggerT e w m) where
  tell = lift . tell

  listen (LoggerT (RWST k)) = LoggerT $ RWST $ \e s -> do
    ((x, w, s'), w') <- listen (k e s)
    return ((x, w'), w, s')

  pass (LoggerT (RWST k)) = LoggerT $ RWST $ \e s ->
    pass $ (\((x, f), w, s') -> ((x, w, s'), f)) <$> k e s

instance (Monad m, Describe e, Describe w) =>
         MonadLogger e w (LoggerT e w m) where
  setDefaultSourceFile = LoggerT . (lsDefaultSourceFile .=)
  getDefaultSourceFile = LoggerT (use lsDefaultSourceFile)

  recordLogMessage msg = do
    LoggerT $ tell (OpMonoid [msg])
    logSomeMessage msg


-- | This doesn't behave quite as you may think. When a 'LoggerT' is hoisted,
-- the resulting 'LoggerT' cannot output as it goes. It still collects logs to
-- be inspected when it finishes.
instance MFunctor (LoggerT e w) where
  hoist f (LoggerT (RWST k)) = LoggerT $ RWST $ \e s ->
    let e' = hoistEnv (const (return ())) e
    in f (k e' s)


-- | A function to output logs in a particular monad @m@.
data LogOutput m = LogOutput
  { _loConciseOutput :: Bool
  , _loPrintFunc     :: Text -> m ()
  }


-- | Output logs to standard output (i.e. the console).
logOutputStd
  :: MonadIO m
  => Bool
  -- ^ If 'True', print more concise output when message origin is repeated.
  -> LogOutput m
logOutputStd b = LogOutput
  { _loConciseOutput = b
  , _loPrintFunc = liftIO . Text.putStrLn
  }


-- | Output no logs.
logOutputNone
  :: Monad m
  => Bool
  -- ^ If 'True', print more concise output when message origin is repeated.
  -> LogOutput m
logOutputNone b = LogOutput
  { _loConciseOutput = b
  , _loPrintFunc = const (return ())
  }


-- | Run the logging monad transformer. Returns the action's result value and a
-- list of logs which were collected as it ran.
runLoggerT
  :: (Monad m, Describe e, Describe w)
  => FilePath
  -- ^ The initial default source file. This is only used for displaying message
  -- origins.
  -> LogOutput m
  -- ^ The logging output function. E.g. 'logOutputStd' or 'logOutputNone'.
  -> LogLevel
  -- ^ The log level for on-the-fly logging. Doesn't affect which logs are
  -- collected at the end.
  -> LoggerT e w m a
  -- ^ The logging action to run.
  -> m (a, [SomeMessage e w])
runLoggerT sourceFile output logLevel (LoggerT action) = do
  let st = LoggerState
        { _lsLogLevel = logLevel
        , _lsDefaultSourceFile = sourceFile
        , _lsPreviousOrigin = Nothing
        }

      env = LoggerEnv
        { _leLogFunc = logFuncFrom output
        }

  (x, _, logs) <- runRWST action env st

  return (x, reverse (getOpMonoid logs))


-- | Change the error and warning types in a logger computation. To change the
-- underlying monad use 'hoist'.
mapLoggerT
  :: (Functor m)
  => (e -> e') -> (w -> w')
  -> LoggerT e w m a -> LoggerT e' w' m a
mapLoggerT mapErr mapWarn (LoggerT x) = LoggerT (mapRWST mapInner x)
  where
    mapInner =
      let messages ty = _3 . _Wrapped . traverse . ty . lmMsg
      in fmap (over (messages _MsgWarn) mapWarn . over (messages _MsgError) mapErr)

--------------------------------------------------------------------------------
--  Internal
--------------------------------------------------------------------------------

logFuncFrom
  :: (Monad m)
  => LogOutput m
  -> (Bool -> LogLevel -> LogLevel -> Text -> Text -> m ())
logFuncFrom LogOutput{ _loConciseOutput, _loPrintFunc } = lf
  where
    lf repeatedOrigin maxLevel level originMsg actualMsg
      | level <= maxLevel =
        let outputMsg =
              describeBuilder level <>
              (if not _loConciseOutput || not repeatedOrigin
               then " " <> describeBuilder originMsg else "") <> ": " <>
              describeBuilder actualMsg
        in _loPrintFunc (builderToStrict outputMsg)
      | otherwise = return ()


someLogLevel :: SomeMessage e w -> LogLevel
someLogLevel (MsgError _) = LogError
someLogLevel (MsgWarn _)  = LogWarn
someLogLevel (MsgInfo _)  = LogInfo
someLogLevel (MsgDebug _) = LogDebug

someMsgText :: (Describe e, Describe w) => SomeMessage e w -> Text
someMsgText (MsgError msg) = describe (msg ^. lmMsg)
someMsgText (MsgWarn msg)  = describe (msg ^. lmMsg)
someMsgText (MsgInfo msg)  = msg ^. lmMsg
someMsgText (MsgDebug msg) = msg ^. lmMsg


logSomeMessage
  :: (Monad m, Describe e, Describe w)
  => SomeMessage e w -> LoggerT e w m ()
logSomeMessage msg = do
  let msgText = someMsgText msg
      msgLevel = someLogLevel msg
      msgOrigin = msg ^. someMessageOrigin
      originText = maybe "" describe msgOrigin

  prevOrigin <- LoggerT $ use lsPreviousOrigin
  LoggerT $ lsPreviousOrigin .= msg ^. someMessageOrigin

  let repeatedOrigin = msgOrigin == prevOrigin

  logFunc <- LoggerT $ view leLogFunc
  logLevel <- LoggerT $ use lsLogLevel

  lift $ logFunc repeatedOrigin logLevel msgLevel originText msgText