debug-0.1.1: src/Debug/Hoed.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ViewPatterns #-}
-- | An alternative backend for lazy debugging with call stacks built on top of the "Hoed" package.
--
-- Instrumentation is done via a TH wrapper, which requires the following extensions:
--
-- - 'TemplateHaskell'
-- - 'PartialTypeSignatures'
-- - 'ViewPatterns'
-- - 'ExtendedDefaultRules'
-- - 'FlexibleContexts'
--
-- Moreover, 'Observable' instances are needed for value inspection. The 'debug'' template haskell wrapper can automatically insert these for 'Generic' types.
--
-- > {-# LANGUAGE TemplateHaskell, ViewPatterns, PartialTypeSignatures, ExtendedDefaultRules #-}
-- > {-# OPTIONS_GHC -Wno-partial-type-signatures #-}
-- > module QuickSort(quicksort) where
-- > import Data.List
-- > import Debug.Hoed
-- >
-- > debug [d|
-- > quicksort :: Ord a => [a] -> [a]
-- > quicksort [] = []
-- > quicksort (x:xs) = quicksort lt ++ [x] ++ quicksort gt
-- > where (lt, gt) = partition (<= x) xs
-- > |]
--
-- Now we can debug our expression under 'debugRun':
--
-- > $ ghci examples/QuickSortHoed.hs
-- > GHCi, version 8.2.1: http://www.haskell.org/ghc/ :? for help
-- > [1 of 1] Compiling QuickSortHoed ( QuickSortHoed.hs, interpreted )
-- > Ok, 1 module loaded.
-- > *QuickSort> debugRun $ quicksort "haskell"
-- > "aehklls"
--
-- After our expression is evaluated a web browser is started displaying the recorded
-- information.
--
-- To debug an entire program, wrap the 'main' function below 'debugRun'.
module Debug.Hoed
(
debug
, debug'
, Config(..)
, debugRun
-- * Generate a trace
, getDebugTrace
-- * Trace commands
, debugPrintTrace
, debugJSONTrace
, debugViewTrace
, debugSaveTrace
-- * Reexported from Hoed
, Observable(..)
, observe
, HoedOptions(..)
, defaultHoedOptions
) where
import Control.Monad
import Data.Bifunctor
import Data.Char
import Data.Generics.Uniplate.Data
import Data.Graph.Libgraph
import Data.Hashable
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HMS
import qualified Data.Map.Strict as Map
import qualified Data.HashSet as Set
import Data.List
import Data.Maybe
import Data.Monoid
import Data.Text (Text, pack)
import qualified Data.Text as T
import "Hoed" Debug.Hoed
import Debug.Hoed.Render
import Debug.Util
import Debug.DebugTrace as D (CallData (..),
DebugTrace (..),
Function (..),
debugViewTrace,
debugPrintTrace,
debugJSONTrace,
debugViewTrace,
debugSaveTrace
)
import GHC.Exts (IsList (..))
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import System.Clock
import Prelude
{-# ANN module ("hlint: ignore Redundant bracket" :: String) #-}
-- | Runs the program collecting a debugging trace and then opens a web browser to inspect it.
--
-- @ main = debugRun $ do
-- ...
-- @
debugRun :: IO () -> IO ()
debugRun program = getDebugTrace defaultHoedOptions {prettyWidth = 160, verbose = Verbose} program >>= debugViewTrace
-- | Runs the program collecting a debugging trace
getDebugTrace :: HoedOptions -> IO () -> IO DebugTrace
getDebugTrace hoedOptions program = do
hoedAnalysis <- runO' hoedOptions program
putStrLn "Please wait while the debug trace is constructed..."
let !compTree = hoedCompTree hoedAnalysis
t <- getTime Monotonic
let result = convert compTree
!_ = length(variables result)
t' <- getTime Monotonic
let compTime :: Double = fromIntegral(toNanoSecs(diffTimeSpec t t')) * 1e-9
putStrLn $ "=== Debug Trace (" ++ show compTime ++ " seconds) ==="
return result
data HoedFunctionKey = HoedFunctionKey
{ label :: !Text
, arity :: !Int
, clauses :: ![Text]
}
deriving (Eq)
instance Hashable HoedFunctionKey where
hashWithSalt s HoedFunctionKey{..} =
s `hashWithSalt` label
`hashWithSalt` arity
`hashWithSalt` clauses
type HoedCallKey = Int
data HoedCallDetails = HoedCallDetails
{ argValues
, clauseValues :: ![Hashed Text]
, result :: !(Hashed Text)
, depends, parents :: ![HoedCallKey]
} deriving (Eq, Generic, Hashable)
---------------------------------------------------------------------------
-- Cached pred and succ relationships
data AnnotatedCompTree = AnnotatedCompTree
{ compTree :: CompTree
, predsMap, succsMap:: HMS.HashMap Vertex [Vertex]
}
getPreds :: AnnotatedCompTree -> Vertex -> [Vertex]
getPreds act v = fromMaybe [] $ HMS.lookup v (predsMap act)
getSuccs :: AnnotatedCompTree -> Vertex -> [Vertex]
getSuccs act v = fromMaybe [] $ HMS.lookup v (succsMap act)
annotateCompTree :: CompTree -> AnnotatedCompTree
annotateCompTree compTree = AnnotatedCompTree{..} where
predsMap = HMS.fromListWith (++) [ (t, [s]) | Arc s t _ <- arcs compTree]
succsMap = HMS.fromListWith (++) [ (s, [t]) | Arc s t _ <- arcs compTree]
---------------------------------------------------------------------------
hoedCallValues :: HoedCallDetails -> [Hashed Text]
hoedCallValues HoedCallDetails{..} = result : (argValues ++ clauseValues)
getRelatives :: (Vertex -> [Vertex]) -> Vertex -> [Int]
getRelatives rel v =
[ stmtIdentifier
| v'@Vertex {vertexStmt = CompStmt {stmtIdentifier, stmtDetails = StmtLam {}}} <- rel v
] ++
[ callKey
| v'@Vertex {vertexStmt = CompStmt {stmtDetails = StmtCon {}}} <- rel v
, callKey <- getRelatives rel v'
]
extractHoedCall :: AnnotatedCompTree -> Vertex -> Maybe (Hashed HoedFunctionKey, HoedCallKey, HoedCallDetails)
extractHoedCall hoedCompTree v@Vertex {vertexStmt = c@CompStmt {stmtDetails = StmtLam {..}, ..}} =
Just
( hashed $ HoedFunctionKey (stmtLabel) (length stmtLamArgs) (map fst clauses)
, stmtIdentifier
, HoedCallDetails stmtLamArgs (map snd clauses) stmtLamRes depends parents)
where
clauses =
[ (stmtLabel, stmtCon)
| Vertex {vertexStmt = CompStmt {stmtLabel, stmtDetails = StmtCon {..}}} <-
getSuccs hoedCompTree v
]
depends = snub $ getRelatives (getSuccs hoedCompTree) v
parents = snub $ getRelatives (getPreds hoedCompTree) v
extractHoedCall _ _ = Nothing
-- | Convert a 'Hoed' trace to a 'debug' trace
convert :: CompTree -> DebugTrace
convert hoedCompTree = DebugTrace {..}
where
hoedFunctionCalls :: HashMap (Hashed HoedFunctionKey) [(HoedCallKey, HoedCallDetails)]
hoedFunctionCalls =
HMS.fromListWith (<>)
[ (fnKey, [(callKey, callDetails)])
| Just (fnKey, callKey, callDetails) <-
map (extractHoedCall (annotateCompTree hoedCompTree)) (vertices hoedCompTree)
]
sortedFunctionCalls =
sortOn (\(unhashed -> x, _) -> (label x, arity x)) $ toList hoedFunctionCalls
variablesHashed :: [Hashed Text]
variablesHashed =
Set.toList $
Set.fromList $
foldMap (foldMap (hoedCallValues . snd)) hoedFunctionCalls
variables = map unhashed variablesHashed
lookupFunctionIndex =
fromMaybe (error "bug in convert: lookupFunctionIndex") .
(`HMS.lookup` HMS.fromList (zip (map fst sortedFunctionCalls) [0 ..]))
lookupVariableIndex =
fromMaybe (error "bug in convert: lookupVariableIndex") .
(`HMS.lookup` HMS.fromList (zip variablesHashed [0 ..]))
lookupCallIndex =
fromMaybe (error "bug in convert: lookupCallIndex") .
(`HMS.lookup` HMS.fromList (zip (map fst callsTable) [0 ..]))
(functions, concat -> callsTable) =
unzip
[ (Function{..}
,[( callId, CallData {..})
| (callId, HoedCallDetails {..}) <- toList calls
, let callVals =
map (second lookupVariableIndex) $
("$result", result) :
zipWith (\i v -> ("$arg" <> pack (show i), v)) [(1::Int) ..] argValues ++
zip clauses clauseValues
, let callDepends = map lookupCallIndex depends
, let callParents = map lookupCallIndex parents
])
| (k@(unhashed -> HoedFunctionKey {..}), calls) <- sortedFunctionCalls
, let callFunctionId = lookupFunctionIndex k
, let funResult = "$result"
, let funArguments = map (\i -> "$arg" <> pack(show i)) [1 .. arity] ++ clauses
-- HACK Expects a multiline label with the function name in the first line, and the code afterwards
, let (funName,funSource) = T.break (=='\n') label
]
calls = map snd callsTable
snub :: Ord a => [a] -> [a]
snub = map head . group . sort
----------------------------------------------------------------------------
-- Template Haskell
data Config = Config
{ generateGenericInstances :: Bool -- ^ Insert @deriving stock Generic@ on type declarations that don't already derive 'Generic'. Requires @DeriveGeneric@ and @DerivingStrategies@.
, generateObservableInstances :: Bool -- ^ Insert @deriving anyclass Observable@ on type declarations that don't already derive 'Observable'. Requires @DeriveAnyClass@ and @DerivingStrategies@.
, excludeFromInstanceGeneration :: [String] -- ^ Exclude types from instance generation by name (unqualified).
}
-- | A @TemplateHaskell@ wrapper to convert normal functions into traced functions.
debug :: Q [Dec] -> Q [Dec]
debug = debug' (Config False False [])
-- | A @TemplateHaskell@ wrapper to convert normal functions into traced functions
-- and optionally insert 'Observable' and 'Generic' instances.
debug' :: Config -> Q [Dec] -> Q [Dec]
debug' Config{..} q = do
missing <-
filterM
(fmap not . isExtEnabled)
([ ViewPatterns
, PartialTypeSignatures
, ExtendedDefaultRules
, FlexibleContexts
]
#if __GLASGOW_HASKELL__ >= 802
++
[DeriveAnyClass | generateObservableInstances] ++
[DerivingStrategies | generateObservableInstances] ++
[DeriveGeneric | generateGenericInstances]
#endif
)
when (missing /= []) $
error $
"\ndebug [d| ... |] requires additional extensions:\n" ++
"{-# LANGUAGE " ++ intercalate ", " (map show missing) ++ " #-}\n"
decs <- q
let askSig x =
listToMaybe $
mapMaybe
(\case
SigD y s
| x == y -> Just s
_ -> Nothing)
decs
let checkSig = maybe True (not . hasRankNTypes) . askSig
let sourceNames =
mapMaybe
(\case
FunD n _ -> Just n
ValD (VarP n) _ _ -> Just n
_ -> Nothing)
decs
names <-
sequence [(n, ) <$> newName (mkDebugName (nameBase n)) | n <- sourceNames]
let -- HACK We embed the source code of the function in the label,
-- which is then unpacked by 'convert'
createLabel n dec = nameBase n ++ "\n" ++ prettyPrint dec
#if __GLASGOW_HASKELL__ >= 802
excludedSet = Set.fromList excludeFromInstanceGeneration
updateDerivs derivs
| hasGenericInstance <- not $ null $ filterDerivingClausesByName ''Generic derivs
= [ DerivClause (Just StockStrategy) [ConT ''Generic]
| not hasGenericInstance
, generateGenericInstances
] ++
[ DerivClause (Just AnyclassStrategy) [ConT ''Observable]
| [] == filterDerivingClausesByName ''Observable derivs
, hasGenericInstance || generateGenericInstances
] ++
derivs
filterDerivingClausesByName n' derivs =
[ it | it@(DerivClause _ preds) <- derivs , ConT n <- preds , n == n']
#endif
fmap concat $
forM decs $ \dec ->
case dec of
ValD (VarP n) b clauses
| checkSig n -> do
let Just n' = lookup n names
label = createLabel n dec
newDecl <-
funD n [clause [] (normalB [|observe (pack label) $(varE n')|]) []]
let clauses' = transformBi adjustValD clauses
return [newDecl, ValD (VarP n') b clauses']
FunD n clauses
| checkSig n -> do
let Just n' = lookup n names
label = createLabel n dec
newDecl <-
funD n [clause [] (normalB [|observe (pack label) $(varE n')|]) []]
let clauses' = transformBi (adjustInnerSigD . adjustValD) clauses
return [newDecl, FunD n' clauses']
SigD n ty
| Just n' <- lookup n names
, not (hasRankNTypes ty) -> do
let ty' = adjustTy ty
ty'' <- renameForallTyVars ty'
return [SigD n ty', SigD n' ty'']
#if __GLASGOW_HASKELL__ >= 802
DataD cxt1 name tt k cons derivs
| not $ Set.member (prettyPrint name) excludedSet
-> return [DataD cxt1 name tt k cons $ updateDerivs derivs]
NewtypeD cxt1 name tt k cons derivs
| not $ Set.member (prettyPrint name) excludedSet
-> return [NewtypeD cxt1 name tt k cons $ updateDerivs derivs]
#endif
_ -> return [dec]
mkDebugName :: String -> String
mkDebugName n@(c:_)
| isAlpha c || c == '_' = n ++ "_debug"
| otherwise = n ++ "??"
mkDebugName [] = error "unreachable: impossible"
adjustInnerSigD :: Dec -> Dec
adjustInnerSigD (SigD n ty) = SigD n (adjustTy ty)
adjustInnerSigD other = other
-- Add a wildcard for Observable a
adjustTy :: Type -> Type
adjustTy (ForallT vars ctxt typ) =
ForallT vars (delete WildCardT ctxt ++ [WildCardT]) typ
adjustTy other = adjustTy $ ForallT [] [] other
-- Tyvar renaming is a work around for http://ghc.haskell.org/trac/ghc/ticket/14643
renameForallTyVars :: Type -> Q Type
renameForallTyVars (ForallT vars ctxt typ) = do
let allVarNames = case vars of
[]-> snub $ universeBi ctxt ++ universeBi typ
_ -> map getVarNameFromTyBndr vars
vv <- Map.fromList <$> mapM (\v -> (v,) <$> newName (pprint v)) allVarNames
let Just renamedCtxt = transformBiM (applyRenaming vv) ctxt
Just renamedTyp = transformBiM (applyRenaming vv) typ
Just renamedVars = mapM (applyRenamingToTyBndr vv) vars
return $
ForallT renamedVars renamedCtxt renamedTyp
renameForallTyVars other = return other
applyRenaming :: Map.Map Name Name -> Type -> Maybe Type
applyRenaming nn (VarT n) = VarT <$> Map.lookup n nn
applyRenaming _ other = return other
getVarNameFromTyBndr :: TyVarBndr -> Name
getVarNameFromTyBndr (PlainTV n) = n
getVarNameFromTyBndr (KindedTV n _) = n
applyRenamingToTyBndr :: Map.Map Name Name -> TyVarBndr -> Maybe TyVarBndr
applyRenamingToTyBndr vv (PlainTV n) = PlainTV <$> Map.lookup n vv
applyRenamingToTyBndr vv (KindedTV n k) = (`KindedTV` k) <$> Map.lookup n vv
adjustValD :: Dec -> Dec
adjustValD decl@ValD{} = transformBi adjustPat decl
adjustValD other = other
adjustPat :: Pat -> Pat
adjustPat (VarP x) = ViewP (VarE 'observe `AppE` (VarE 'pack `AppE` toLit x)) (VarP x)
adjustPat x = x
toLit :: Name -> Exp
toLit (Name (OccName x) _) = LitE $ StringL x