packages feed

crux-0.9: src/Crux/Log.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

module Crux.Log (
  -- * Output Configuration
  Logs,
  OutputConfig(..)
  , outputHandle
  , quiet
  -- * Standard output API functions
  , CruxLogMessage(..)
  , LogDoc(..)
  , LogProofObligation(..)
  , SayWhat(..)
  , SayLevel(..)
  , SupportsCruxLogMessage
  , cruxLogMessageToSayWhat
  , cruxLogTag
  , logException
  , logGoal
  , logSimResult
  , say
  , sayCrux
  , withCruxLogMessage
  -- * Low-level output API functions
  , output
  , outputLn
  -- * Converting and emitting output
  , logToStd
  ) where

import           Control.Exception ( SomeException, bracket_,  )
import           Control.Lens ( Getter, view )
import qualified Data.Aeson as JSON
import           Data.Aeson.TH ( deriveToJSON )
import qualified Data.List as List
import qualified Data.Text as T
import           Data.Text.IO as TIO ( hPutStr, hPutStrLn )
import           Data.Version ( Version, showVersion )
import           Data.Word ( Word64 )
import           GHC.Generics ( Generic )
import qualified Lumberjack as LJ
import           Prettyprinter ( SimpleDocStream )
import           Prettyprinter.Render.Text ( renderStrict )
import           System.Console.ANSI
    ( Color(..), ColorIntensity(..), ConsoleIntensity(..), ConsoleLayer(..)
    , SGR(..), hSetSGR )
import           System.IO ( Handle, hPutStr, hPutStrLn )

import           Crux.Types
    ( CruxSimulationResult, ProvedGoals, SayLevel(..), SayWhat(..) )
import           Crux.Version
    ( commitBranch, commitDirty, commitHash, version )
import           Lang.Crucible.Backend ( ProofGoal(..), ProofObligation )
import           What4.Expr.Builder ( ExprBuilder )
import           What4.LabeledPred ( labeledPred, labeledPredMsg )

----------------------------------------------------------------------
-- Various functions used by the main code to generate logging output

newtype LogDoc = LogDoc (SimpleDocStream ())

instance JSON.ToJSON LogDoc where
  toJSON (LogDoc doc) = JSON.String (renderStrict doc)

-- | A version of 'ProofObligation' that supports a 'ToJSON' instance.
data LogProofObligation =
  forall t st fs.
  LogProofObligation (ProofObligation (ExprBuilder t st fs))

instance JSON.ToJSON LogProofObligation where
  -- for now, we only need the goal in the IDE
  toJSON (LogProofObligation g) =
    let
      goal = proofGoal g
    in
    JSON.object [ "labeledPred" JSON..= show (view labeledPred goal)
                , "labeledPredMsg" JSON..= show (view labeledPredMsg goal)
                ]

-- | All messages that Crux wants to output should be listed here.
--
-- Messages ought to be serializable to JSON so that they have a somewhat
-- portable machine-readable representation.  Some tools may rely on the
-- serialization format, so be sure to check with consumers before changing the
-- 'ToJSON' instance.
--
-- Other crux-based tools are encouraged to create their own:
--
-- * log message datatype '<Tool>LogMessage', similar to 'CruxLogMessage' below,
--
-- * constraint synonym 'Supports<Tool>LogMessage' similar to
-- 'SupportsCruxLogMessage' below,
--
-- * constraint dispatcher 'with<Tool>LogMessage' similar to
-- 'withCruxLogMessage' below,
--
-- * logger method 'say<Tool>' similar to 'sayCrux' below,
--
-- * default log message converter '<Tool>LogMessageToSayWhat' similar to
-- 'cruxLogMessageToSayWhat'.
--
-- The default log message converter can be used to decide how messages are
-- displayed to the standard output for a human consumer.  Automated tools may
-- prefer to filter and encode log messages in some machine-readable format and
-- exchange it over a separate channel.
data CruxLogMessage
  = AttemptingProvingVCs
  | Checking [FilePath]
  | DisablingBranchCoverageRequiresPathSatisfiability
  | DisablingProfilingIncompatibleWithPathSplitting
  | EndedGoal Integer
  | FoundCounterExample
  | Help LogDoc
  | PathsUnexplored Int
  | ProofObligations [LogProofObligation]
  | SimulationComplete
  | SimulationTimedOut
  | SkippingUnsatCoresBecauseMCSatEnabled
  | StartedGoal Integer
  | TotalPathsExplored Word64
  | UnsupportedTimeoutFor String -- ^ name of the backend
  | Version
      T.Text -- ^ name of the backend
      Version -- ^ backend-specific version
  deriving (Generic)

$(deriveToJSON JSON.defaultOptions ''CruxLogMessage)


type SupportsCruxLogMessage msgs =
  ( ?injectCruxLogMessage :: CruxLogMessage -> msgs )

withCruxLogMessage ::
  (SupportsCruxLogMessage CruxLogMessage => a) -> a
withCruxLogMessage a =
  let ?injectCruxLogMessage = id in a


cruxLogTag :: T.Text
cruxLogTag = "Crux"


cruxNoisily :: T.Text -> SayWhat
cruxNoisily = SayWhat Noisily cruxLogTag

cruxOK :: T.Text -> SayWhat
cruxOK = SayWhat OK cruxLogTag

cruxSimply :: T.Text -> SayWhat
cruxSimply = SayWhat Simply cruxLogTag

cruxWarn :: T.Text -> SayWhat
cruxWarn = SayWhat Warn cruxLogTag


cruxLogMessageToSayWhat :: CruxLogMessage -> SayWhat

cruxLogMessageToSayWhat AttemptingProvingVCs =
  cruxSimply "Attempting to prove verification conditions."

cruxLogMessageToSayWhat (Checking files) =
  cruxNoisily ("Checking " <> T.intercalate ", " (T.pack <$> files))

cruxLogMessageToSayWhat DisablingBranchCoverageRequiresPathSatisfiability =
  cruxWarn
    "Branch coverage requires enabling path satisfiability checking.  Coverage measurement is disabled!"

cruxLogMessageToSayWhat DisablingProfilingIncompatibleWithPathSplitting =
  cruxWarn
    "Path splitting strategies are incompatible with Crucible profiling. Profiling is disabled!"

-- for now, this message is only for IDE consumers
cruxLogMessageToSayWhat (EndedGoal {}) = SayNothing

cruxLogMessageToSayWhat FoundCounterExample =
  cruxOK "Counterexample found, skipping remaining goals"

cruxLogMessageToSayWhat (Help (LogDoc doc)) =
  cruxOK (renderStrict doc)

cruxLogMessageToSayWhat (PathsUnexplored numberOfPaths) =
  cruxWarn
    ( T.unwords
        [ T.pack $ show numberOfPaths,
          "paths remaining not explored: program might not be fully verified"
        ]
    )

-- for now, this message is only for IDE consumers
cruxLogMessageToSayWhat (ProofObligations {}) = SayNothing

cruxLogMessageToSayWhat SimulationComplete =
  cruxNoisily "Simulation complete."

cruxLogMessageToSayWhat SimulationTimedOut =
  cruxWarn
    "Simulation timed out! Program might not be fully verified!"

cruxLogMessageToSayWhat SkippingUnsatCoresBecauseMCSatEnabled =
  cruxWarn "Warning: skipping unsat cores because MC-SAT is enabled."

-- for now, this message is only for IDE consumers
cruxLogMessageToSayWhat (StartedGoal {}) = SayNothing

cruxLogMessageToSayWhat (TotalPathsExplored i) =
  cruxSimply ("Total paths explored: " <> T.pack (show i))

cruxLogMessageToSayWhat (UnsupportedTimeoutFor backend) =
  cruxWarn
    (T.pack ("Goal timeout requested but not supported by " <> backend))

cruxLogMessageToSayWhat (Version nm ver) =
  cruxOK
    ( T.pack
      ( List.intercalate
        "\n"
        [ unwords
            [ "version: " <> version <> ",",
              T.unpack nm,
              "version: " <> showVersion ver
            ]
        , "Git commit " <> commitHash
        , "    branch " <> commitBranch <> dirtyLab
        ]
      )
    )
  where
    dirtyLab :: String
    dirtyLab
      | commitDirty = " (non-committed files present during build)"
      | otherwise = ""

-- | Main function used to log/output a general text message of some kind
say ::
  Logs msgs =>
  ( ?injectMessage :: msg -> msgs ) =>
  msg -> IO ()
say = LJ.writeLog (view logMsg ?outputConfig) . ?injectMessage

sayCrux ::
  Logs msgs =>
  SupportsCruxLogMessage msgs =>
  CruxLogMessage -> IO ()
sayCrux msg =
  let ?injectMessage = ?injectCruxLogMessage in
  say msg

-- | Function used to log/output exception information
logException :: Logs msgs => SomeException -> IO ()
logException = LJ.writeLog (_logExc ?outputConfig)

-- | Function used to output the summary result for a completed
-- simulation.  If the flag is true, show the details of each failed
-- goal before showing the summary.
logSimResult :: Logs msgs => Bool -> CruxSimulationResult -> IO ()
logSimResult showFailedGoals = LJ.writeLog (_logSimResult ?outputConfig showFailedGoals)

-- | Function used to output any individual goal proof failures from a simulation
logGoal :: Logs msgs => ProvedGoals -> IO ()
logGoal = LJ.writeLog (_logGoal ?outputConfig)


----------------------------------------------------------------------
-- Logging types and constraints

-- | The Logs constraint should be applied for any function that will
-- use the main logging functions: says, logException, logSimResult,
-- or logGoal.
type Logs msgs = ( ?outputConfig :: OutputConfig msgs )

-- | Global options for Crux's main. These are not CruxOptions because
-- they are expected to be set directly by main, rather than by a
-- user's command-line options. They exist primarily to improve the
-- testability of the code.
--
-- The Crux mkOutputConfig is recommended as a helper function for
-- creating this object, although it can be initialized directly if
-- needed.
data OutputConfig msgs =
  OutputConfig { _outputHandle :: Handle
               , _quiet :: Bool
               , _logMsg :: LJ.LogAction IO msgs
               , _logExc :: LJ.LogAction IO SomeException
               , _logSimResult :: Bool -> LJ.LogAction IO CruxSimulationResult
                 -- ^ True to show individual goals, false for summary only
               , _logGoal :: LJ.LogAction IO ProvedGoals
               }

-- | Some client code will want to output to the main output stream
-- directly instead of using the logging/output functions above.  It
-- can either get the _outputHandle directly or it can use the
-- output/outputLn functions below.
outputHandle :: Getter (OutputConfig msgs) Handle
outputHandle f o = o <$ f (_outputHandle o)

-- | Lens to allow client code to determine if running in quiet mode.
quiet :: Getter (OutputConfig msgs) Bool
quiet f o = o <$ f (_quiet o)

logMsg :: Getter (OutputConfig msgs) (LJ.LogAction IO msgs)
logMsg f o = o <$ f (_logMsg o)


----------------------------------------------------------------------
-- Logging default implementations (can be over-ridden by different
-- front end configurations).

-- | This is the default logging output function for a SayWhat
-- message.  It will emit the information with possible coloring (if
-- enabled via the first argument) to the proper handle (normal output
-- goes to the first handle, error output goes to the second handle,
-- and the same handle may be used for both).
--
-- Front-ends that don't have specific output needs can simply use
-- this function.  Alternative output functions can be used where
-- useful (e.g. JSON output).
logToStd :: (Handle, Bool) -> (Handle, Bool) -> SayWhat -> IO ()
logToStd outSpec errSpec = \case
  SayWhat lvl frm msg ->
    let sayer = case lvl of
                  OK -> colOut Green outSpec
                  Warn -> colOut Yellow errSpec
                  Fail -> colOut Red errSpec
                  Simply -> straightOut outSpec
                  Noisily -> straightOut outSpec
        colOut clr (hndl, True) l =
           do bracket_
                (hSetSGR hndl [ SetConsoleIntensity BoldIntensity
                              , SetColor Foreground Vivid clr])
                (hSetSGR hndl [Reset])
                (TIO.hPutStr hndl $ "[" <> frm <> "] ")
              TIO.hPutStrLn hndl l
        colOut _ hSpec@(_, False) l = straightOut hSpec l
        straightOut (hndl, _) l = do TIO.hPutStr hndl $ "[" <> frm <> "] "
                                     TIO.hPutStrLn hndl l
    in mapM_ sayer $ T.lines msg
  SayMore s1 s2 -> do logToStd outSpec errSpec s1
                      logToStd outSpec errSpec s2
  SayNothing -> return ()


----------------------------------------------------------------------
-- Direct Output

-- | This function emits output to the normal output handle (specified
-- at initialization time).  This function is not recommended for
-- general use (the 'says', 'logException', 'logSimResult', 'logGoal',
-- etc. are preferred), but can be used by code needing more control
-- over the output formatting.
--
-- This function does _not_ emit a newline at the end of the output.

output :: Logs msgs => String -> IO ()
output str = System.IO.hPutStr (view outputHandle ?outputConfig) str

-- | This function emits output to the normal output handle (specified
-- at initialization time).  This function is not recommended for
-- general use (the 'says', 'logException', 'logSimResult', 'logGoal',
-- etc. are preferred), but can be used by code needing more control
-- over the output formatting.
--
-- This function emits a newline at the end of the output.

outputLn :: Logs msgs => String -> IO ()
outputLn str = System.IO.hPutStrLn (view outputHandle ?outputConfig) str