trace-embrace-1.0.2: src/Debug/TraceEmbrace/Internal/TH.hs
{-# OPTIONS_HADDOCK hide #-}
module Debug.TraceEmbrace.Internal.TH where
import Control.DeepSeq
import Control.Lens hiding (levels)
import Control.Monad.Trans.State.Strict
import Control.Monad.Trans.Class qualified as MT
import Data.Char as C
import Data.IORef
import Data.Generics.Labels ()
import Data.Text qualified as T
import Data.IntMap.Strict qualified as IM
import Data.RadixTree.Word8.Strict qualified as T
import qualified Debug.Trace as T
import Debug.TraceEmbrace.Config
import Debug.TraceEmbrace.FileIndex
import Debug.TraceEmbrace.Internal.Rewrap
import Debug.TraceEmbrace.Show
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import Prelude hiding (Show (..))
import Prelude qualified as P
import Refined
import System.IO
import System.IO.Unsafe (unsafePerformIO)
import Text.Printf
newtype TrMsgAndVars = TrMsgAndVars String deriving (Eq, P.Show)
newtype VarsPart = VarsPart String deriving (Eq, P.Show)
newtype ModTraceFlagVarName = ModTraceFlagVarName Name deriving (Eq, P.Show)
type SVarsFunM a = StateT (Maybe Name) Q a
type SVarsFun = TraceMessageFormat -> VarsPart -> SVarsFunM Exp
showTrace :: Show (ShowTrace a) => a -> String
showTrace = show . ShowTrace
-- | Extract var names from a word.
--
-- @
-- "_" => []
-- "0" => []
-- "(Just" => []
-- "x@[a,_c]"=> ["x", "a"]
-- "l@(h:t)" => ["l", "h", "t"]
-- "{a,b}" => ["a", "b"]
-- @
--
varNamesFromPat :: String -> [String]
varNamesFromPat = filterVars . words . fmap replaceWithSpace . stripStrComment
where
filterVars = filter (\case { h:_ -> C.isLower h; [] -> False; })
replaceWithSpace c
| c `elem` ",!@({[:]})~" = ' '
| otherwise = c
dropTillEndOfString = \case
"" -> ""
'\\' : '"' : t -> dropTillEndOfString t
'"' : t -> t
_ : t -> dropTillEndOfString t
dropTillEndOfLine = \case
"" -> ""
'\n' : t -> t
_ : t -> dropTillEndOfLine t
dropTillEndOfComment = \case
"" -> ""
'-' : '}' : t -> t
'{' : '-' : t -> dropTillEndOfComment $ dropTillEndOfComment t
_ : t -> dropTillEndOfComment t
stripStrComment = \case
"" -> ""
'"' : t -> stripStrComment $ dropTillEndOfString t
'-' : '-' : t -> stripStrComment $ dropTillEndOfLine t
'{' : '-' : t -> stripStrComment $ dropTillEndOfComment t
h : t -> h : stripStrComment t
{- | Interpolate vars in the arugment.
Generated expression has type 'String'.
The argument has literal and interpolated parts.
There parts are separated with right slash (/).
@
foo x y = trace $(svars "get/x y") x
@
The snippet above is expanded into:
@
foo x y = trace ("get; x: " <> show x <> "; y: " <> show y) x
@
'Show' instance of some types (eg lazy ByteString) hide
internal structure which might be important in low level code.
Variables after ";" are wrapped into t'ShowTrace':
@
import Data.ByteString.Lazy
foo x = trace $(svars "get/x;x") x
@
The snippet above is expanded into:
@
foo x = trace ("get; x: " <> show x <> "; x: " <> show (ShowTrace y)) x
@
-}
svars :: SVarsFun
svars tmf (VarsPart vars) = MT.lift $
case span (';' /=) vars of
(showVars, ';' : traceVars) ->
[| $(listE (wordsToVars 'show showVars
<> wordsToVars 'showTrace traceVars)) :: [String] |]
(showVars, "") ->
[| $(listE (wordsToVars 'show showVars)) :: [String] |]
(sv, st) -> do
reportError $ printf "No case for %s %s" sv st
[| [] |]
where
wordsToVars f vss = fmap go (varNamesFromPat vss)
where
go vs =
lookupValueName vs >>= \case
Nothing -> do
reportError $ printf "no variable [%s]" vs
[| $(lift vs) |]
Just vn ->
[| $(lift . unrefine $ tmf ^. #entrySeparator)
<> $(lift vs)
<> $(lift . unrefine $ tmf ^. #keyValueSeparator)
<> $(varE f) $(varE vn)
|]
splitMessageFromVars :: TrMsgAndVars -> (String, VarsPart)
splitMessageFromVars (TrMsgAndVars trMsg) =
case span ('/' /=) trMsg of
(msgPart, '/':varPart) -> (msgPart, VarsPart varPart)
(msgPart, []) -> (msgPart, VarsPart [])
e -> error $ "No case for:" <> show e
traceMessageLevel :: String -> (TraceLevel, TrMsgAndVars)
traceMessageLevel = fmap TrMsgAndVars . charToLevel
-- | Suffix 'svars' with return value.
svarsWith :: SVarsFun
svarsWith tmf vp =
get >>= maybe (calret (put . Just) =<< MT.lift (newName "retVal")) pure >>= \retValVarName -> MT.lift $
[| $(evalStateT (svars tmf vp) Nothing)
<> [ $(lift . unrefine $ tmf ^. #retValPrefix)
, show $(varE retValVarName)
]
{- :: (Rewrap a b, Show a) => a -> [String] -}
|]
concat2 :: Monoid m => [[m]] -> m
concat2 = mconcat . mconcat
{-# INLINE concat2 #-}
-- | Format whole trace message
traceMessage :: TrMsgAndVars -> TraceMessageFormat -> SVarsFun -> Q Exp
traceMessage mavs tmf svarsFun =
runStateT itemExprs Nothing >>= \case
(exprList, Nothing) ->
[| concat2 $(pure $ ListE exprList) |]
(exprList :: [Exp], Just retValVarName) ->
[| \ $(varP retValVarName) -> concat2 $(pure $ ListE exprList) |]
where
itemExprs :: SVarsFunM [Exp]
itemExprs = sequence (genItem <$> (unrefine (tmf ^. #traceLinePattern)))
loc = MT.lift location
strL = ListE . (:[]) . LitE . StringL
pStrL = pure . strL
genItem :: TraceMessageElement -> SVarsFunM Exp
genItem = \case
LiteralMessage -> pStrL . fst $ splitMessageFromVars mavs
Variables -> svarsFun tmf . snd $ splitMessageFromVars mavs
FullyQualifiedModule -> strL . loc_module <$> loc
ModuleName ->
strL . reverse . takeWhile (/= '.') . reverse . loc_module <$> loc
ShortenJavaModule -> do
(eludom, htap) <- span (/= '.') . reverse . loc_module <$> loc
pStrL $ shortenModPath True (reverse htap) <> (reverse eludom)
PackageName -> strL . loc_package <$> loc
FunctionName -> do
lc <- loc
let
m = loc_module lc
line = fst $ loc_start lc
strL <$> MT.lift (fmap snd . IM.lookupLE line <$> getLineFileIndex lc >>= \case
Nothing -> do
reportWarning (printf "No function name for line [%d] in module [%s]" line m)
pure "N/A"
Just (FunName fn) -> pure fn)
LineNumber -> strL . P.show . fst . loc_start <$> loc
Delimiter del -> pStrL del
shortenModPath :: Bool -> String -> String
shortenModPath prevDot
| prevDot = \case
c : r -> c : shortenModPath False r
[] -> []
| otherwise = \case
'.' : r -> '.' : shortenModPath True r
_ : r -> shortenModPath False r
[] -> []
flagVarName :: Q Name
flagVarName =
newName =<< ("_trace_if_flag_on_line_" <>) . show . fst . loc_start <$> location
getModTraceFlagVar :: Q Name
getModTraceFlagVar = do
vn <- flagVarName
putQ (ModTraceFlagVarName vn)
nothingRefT <- [t| IORef (Maybe Bool) |]
nothingRef <- [| unsafePerformIO (newIORef Nothing) |]
addTopDecls
[ SigD vn nothingRefT
, ValD (VarP vn) (NormalB nothingRef) []
, PragmaD (InlineP vn NoInline ConLike AllPhases)
]
pure vn
isLevelOverThreshold :: T.Lookup TraceLevel -> TraceLevel -> Bool
isLevelOverThreshold (T.Lookup _ levelThreshold) tl = levelThreshold <= tl
{-# INLINE isLevelOverThreshold #-}
-- | Eval level and cache
readTraceFlag :: T.Text -> TraceLevel -> DynConfigEnvVar -> IORef (Maybe Bool) -> IO Bool
readTraceFlag modName trLvl evar fv = do
readIORef fv >>= \case
Just r -> pure r
Nothing -> do
T.lookupL T.Open (T.feedText modName) <$> getRuntimeConfig evar >>= \case
Just threshold ->
let !r = isLevelOverThreshold threshold trLvl in
atomicWriteIORef fv (Just r) >> pure r
Nothing ->
atomicWriteIORef fv (Just False) >> pure False
{-# INLINE readTraceFlag #-}
traceG :: TraceEmbraceConfig -> Q Exp -> (TrMsgAndVars -> TraceMessageFormat -> Q Exp) -> String -> Q Exp
traceG c idF genTraceLine s =
case c ^. #mode of
TraceDisabled -> idF
TraceStd -> go
TraceUnsafeIo _ -> go
TraceEvent -> go
where
go =
case traceMessageLevel s of
(TracingDisabled, _) -> idF
(tl, s') -> do
loc <- location
let modName = T.pack $ loc_module loc
case T.lookupL T.Open (T.feedText modName) $ c ^. #levels of
Nothing -> idF
Just threshold
| isLevelOverThreshold threshold tl ->
case envVarName loc (c ^. #runtimeLevelsOverrideEnvVar) of
Just evar -> do
vn <- getModTraceFlagVar
[| case unsafePerformIO (readTraceFlag modName tl evar $(varE vn)) of
True -> $(genTraceLine s' $ c ^. #traceMessage)
False -> $(idF)
|]
Nothing -> genTraceLine s' $ c ^. #traceMessage
| otherwise -> idF
unsafePutStrLn :: IoSink -> String -> a -> a
unsafePutStrLn s msg v =
msg `deepseq` (unsafePerformIO (hPutStrLn (getSinkHandle s) msg)) `seq` v
where
{-# NOINLINE unsafePutStrLn #-}
getSinkHandle :: IoSink -> Handle
getSinkHandle s =
case s of
StdErrSink -> stderr
StdOutSink -> stdout
FileSink fp -> unsafePerformIO $ do
readIORef unsafeIoSink >>= \case
Just h -> pure h
Nothing -> do
nh <- openFile fp AppendMode
(atomicModifyIORef' unsafeIoSink $ \case
Nothing -> (Just nh, (False, nh))
Just oh -> (Just oh, (True, oh))) >>= \case
(True, h) -> hClose nh >> pure h
(False, h) -> pure h
safePutStrLn :: IoSink -> String -> a -> IO a
safePutStrLn s msg v =
hPutStrLn (getSinkHandle s) msg >> pure v
chooseTraceFunOnTh :: Show s => TraceEmbraceConfig -> s -> Q Exp
chooseTraceFunOnTh c s =
case c ^. #mode of
TraceDisabled -> fail $ "Dead code on" <> show s
TraceStd -> pure $ VarE 'T.trace
TraceUnsafeIo snk -> [| unsafePutStrLn snk |]
TraceEvent -> pure $ VarE 'T.traceEvent
tr :: Q Exp -> String -> Q Exp
tr idF rawMsg = do
c <- getConfig
traceG c idF (go c) rawMsg
where
go c s fmt =
[| \x -> unwrap ($(chooseTraceFunOnTh c s) $(traceMessage s fmt svars) (wrap x)) |]
tw :: Q Exp -> String -> Q Exp
tw idF rawMsg = do
c <- getConfig
traceG c idF (go c) rawMsg
where
go c s fmt =
[| \x -> unwrap ($(chooseTraceFunOnTh c s)
($(traceMessage s fmt svarsWith) x)
(wrap x))
|]
tw' :: Q Exp -> String -> Q Exp
tw' idF rawMsg = do
c <- getConfig
traceG c idF (go c) rawMsg
where
go c s fmt =
[| \x -> unwrap ($(chooseTraceFunOnTh c s)
($(traceMessage s fmt svarsWith) (ShowTrace x))
(wrap x))
|]
trIo :: Q Exp -> String -> Q Exp
trIo idF rawMsg = do
c <- getConfig
traceG c idF (go c) rawMsg
where
go c s fmt = do
let trFun = case c ^. #mode of
TraceDisabled -> error $ "Dead code on" <> show s
TraceStd -> 'T.traceIO
TraceUnsafeIo _ -> 'safePutStrLn
TraceEvent -> 'T.traceEventIO
[| $(varE trFun) $(traceMessage s fmt svars) |]
trFunMarker :: Q Exp -> Q Exp
trFunMarker idF = do
c <- getConfig
let finalC = if c ^. #mode == TraceDisabled then c else markerConfig
traceG finalC idF go "/"
where
go s fmt =
[| \x -> unwrap (T.traceMarker $(traceMessage s fmt svars) (wrap x)) |]
trIoFunMarker :: Q Exp -> Q Exp
trIoFunMarker idF = do
c <- getConfig
let finalC = if c ^. #mode == TraceDisabled then c else markerConfig
traceG finalC idF go "/"
where
go s fmt =
[| T.traceMarkerIO $(traceMessage s fmt svars) |]