debug-0.1.1: src/Debug/Variables.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS -fno-warn-name-shadowing #-}
{-# OPTIONS -fno-warn-unused-matches #-}
{-# OPTIONS -fno-warn-unused-imports #-}
-- Any moved from GHC.Prim to GHC.Types
-- so import both and use unused imports to get compatibility
-- | Module for debugging Haskell programs. To use, take the functions that
-- you are interested in debugging, e.g.:
--
-- > module QuickSort(quicksort) where
-- > import Data.List
-- >
-- > quicksort :: Ord a => [a] -> [a]
-- > quicksort [] = []
-- > quicksort (x:xs) = quicksort lt ++ [x] ++ quicksort gt
-- > where (lt, gt) = partition (<= x) xs
--
-- Turn on the @TemplateHaskell@ and @ViewPatterns@ extensions, import "Debug",
-- indent your code and place it under a call to 'debug', e.g.:
--
-- > {-# LANGUAGE TemplateHaskell, ViewPatterns, PartialTypeSignatures #-}
-- > {-# OPTIONS_GHC -Wno-partial-type-signatures #-}
-- > module QuickSort(quicksort) where
-- > import Data.List
-- > import Debug
-- >
-- > debug [d|
-- > quicksort :: Ord a => [a] -> [a]
-- > quicksort [] = []
-- > quicksort (x:xs) = quicksort lt ++ [x] ++ quicksort gt
-- > where (lt, gt) = partition (<= x) xs
-- > |]
--
-- We can now run our debugger with:
--
-- > $ ghci QuickSort.hs
-- > GHCi, version 8.2.1: http://www.haskell.org/ghc/ :? for help
-- > [1 of 1] Compiling QuickSort ( QuickSort.hs, interpreted )
-- > Ok, 1 module loaded.
-- > *QuickSort> quicksort "haskell"
-- > "aehklls"
-- > *QuickSort> debugView
--
-- The final call to 'debugView' starts a web browser to view the recorded information.
-- Alternatively call 'debugSave' to write the web page to a known location.
--
-- For more ways to view the result (e.g. producing JSON) or record traces (without using
-- @TemplateHaskell@) see "Debug.DebugTrace".
module Debug.Variables(
debug,
debugClear,
debugRun,
debugPrint,
debugJSON,
debugView,
debugSave,
DebugTrace(..),
getDebugTrace,
-- * Recording
funInfo, fun, var,
) where
import Control.DeepSeq
import Control.Exception
import Control.Monad
import Control.Monad.Extra
import Data.Aeson
import Data.Aeson.Text
import Data.Aeson.Types
import qualified Data.ByteString.Lazy.Char8 as B
import Data.Char
import Data.Generics.Uniplate.Data
import Data.Hashable
import qualified Data.HashMap.Strict as HM
import Data.IORef
import Data.List.Extra
import Data.Maybe
import Data.Monoid ()
import Data.Text (Text, pack)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.IO as TL
import Data.Text.Read as T
import Data.Tuple.Extra
import qualified Data.Vector as V
import Debug.DebugTrace
import Debug.Util
import GHC.Generics
import GHC.Prim
import GHC.Types
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import System.Directory
import System.IO
import System.IO.Unsafe
import Text.Show.Functions ()
import Unsafe.Coerce
-- | Run a computation and open a browser window showing observed function calls.
--
-- @ main = debugRun $ do
-- ...
-- @
debugRun :: IO a -> IO a
debugRun = bracket_ debugClear debugView
-- | Print information about the observed function calls to 'stdout',
-- in a human-readable format.
debugPrint :: IO ()
debugPrint = getDebugTrace >>= debugPrintTrace
-- | Save information about observed functions to the specified file, in HTML format.
debugSave :: FilePath -> IO ()
debugSave fp = debugSaveTrace fp =<< getDebugTrace
-- | Open a web browser showing information about observed functions.
debugView :: IO ()
debugView = getDebugTrace >>= debugViewTrace
-- | Obtain information about observed functions in JSON format.
-- The JSON format is not considered a stable part of the interface,
-- more presented as a back door to allow exploration of alternative
-- views.
debugJSON :: IO String
debugJSON = B.unpack . debugJSONTrace <$> getDebugTrace
------------------------------------------------------------------
-- DebugTrace Backend
-- | A single function call, used to attach additional information
data Call = Call Function (IORef [(Text, Var)])
{-# NOINLINE refVariables #-}
refVariables :: IORef Variables
refVariables = unsafePerformIO $ newIORef newVariables
{-# NOINLINE refCalls #-}
refCalls :: IORef [Call]
refCalls = unsafePerformIO $ newIORef []
-- | Clear all debug information. Useful when working in @ghci@ to reset
-- any previous debugging work and reduce the amount of output.
debugClear :: IO ()
debugClear = do
writeIORef refVariables newVariables
writeIORef refCalls []
-- | Returns all the information about the observed function accumulated so far
-- in the variables.
getDebugTrace :: IO DebugTrace
getDebugTrace = do
vars <- readIORef refVariables
vars <- return $ map varShow $ listVariables vars
calls <- readIORef refCalls
let infos = nubOrd [x | Call x _ <- calls]
infoId = HM.fromList $ zip infos [0::Int ..]
callEntries <-
forM (reverse calls) $ \(Call info vars) -> do
vars <- readIORef vars
let callFunctionId = infoId HM.! info
callVals = map (second varId) vars
callDepends = [] -- available in the Hoed backend but not in this one
callParents = [] -- available in the Hoed backend but not in this one
return CallData{..}
return $ DebugTrace infos (map T.pack vars) callEntries
------------------------------------------------------------------
-- Instrumentation
{-# NOINLINE fun #-}
-- | Called under a lambda with a function name to provide a unique context for
-- a particular call, e.g.:
--
-- > tracedAdd x y = fun "add" $ \t -> var t "x" x + var t "y" y
--
-- This function involves giving identity to function calls, so is unsafe,
-- and will only work under a lambda.
fun :: Show a => String -> (Call -> a) -> a
fun name = funInfo $ Function (T.pack name) "" [] ""
-- | A version of 'fun' allowing you to pass further information about the
-- 'Function' which is used when showing debug views.
funInfo :: Show a => Function -> (Call -> a) -> a
{-# NOINLINE funInfo #-}
funInfo info f = unsafePerformIO $ do
ref <- newIORef []
let x = Call info ref
atomicModifyIORef refCalls $ \v -> (x:v, ())
return $ f x
{-# NOINLINE var #-}
-- | Used in conjunction with 'fun' to annotate variables. See 'fun' for an example.
var :: Show a => Call -> String -> a -> a
var (Call _ ref) name val = unsafePerformIO $ do
when (show val /= "<function>") $ do -- these make the variable list long without providing useful info
var <- atomicModifyIORef refVariables $ addVariable val
name' <- unShadowName ref $ pack name
whenJust name' (\n -> atomicModifyIORef ref $ \v -> ((n, var) :v, ()))
return val
-- | If a name is already used, find the next available name by adding ' (prime) chars until
-- the resulting name is unique
unShadowName :: IORef [(Text, Var)] -> Text -> IO (Maybe Text)
unShadowName ioRef t = do
pairs <- readIORef ioRef
let matches = filter (isPrefixPrime t) $ map fst pairs
let shadowedLimit = 3
if not (null matches)
then do
let lengths = map T.length matches
let zipped = zip matches lengths
let maxLen = maximum lengths
let maxName = fst $ fromJust $ find (\p -> snd p == maxLen) zipped
case length matches of -- --i.e., case (the number of (')s)
n | n > shadowedLimit -> return Nothing
_ -> return $ Just $ maxName `T.append` "'"
else return $ Just t
-- | Is the second string equal to the first plus some number of ' (prime) characters?
-- | e.g. x `isPrefixPrime` x' == true, x isPrefixPrime x'' == True, but x isPrefixPrime xs == False
isPrefixPrime :: Text -> Text -> Bool
isPrefixPrime s t = s == T.dropWhileEnd (== '\'') t
-- | A @TemplateHaskell@ wrapper to convert a normal function into a traced function.
-- For an example see "Debug". Inserts 'funInfo' and 'var' calls.
debug :: Q [Dec] -> Q [Dec]
debug q = do
missing <- filterM (notM . isExtEnabled) [ViewPatterns, PartialTypeSignatures]
when (missing /= []) $
error $ "\ndebug [d| ... |] requires additional extensions:\n" ++
"{-# LANGUAGE " ++ intercalate ", " (map show missing) ++ " #-}\n"
decs <- q
let askSig x = find (\case SigD y _ -> x == y; _ -> False) decs
mapM (adjustDec askSig) decs
adjustDec :: (Name -> Maybe Dec) -> Dec -> Q Dec
-- try and shove in a "_ =>" if we can, to capture necessary Show instances
adjustDec askSig x@(SigD name ty@(ForallT vars ctxt typ))
| hasRankNTypes ty = return x
| otherwise = return $
SigD name $ ForallT vars (delete WildCardT ctxt ++ [WildCardT]) typ
adjustDec askSig (SigD name typ) = adjustDec askSig $ SigD name $ ForallT [] [] typ
adjustDec askSig o@(FunD name clauses@(Clause arity _ _:_))
| Just (SigD _ ty) <- askSig name
, hasRankNTypes ty = return o
| otherwise = do
inner <- newName "inner"
tag <- newName "tag"
args <- sequence [newName $ "arg" ++ show i | i <- [1 .. length arity]]
let addTag (Clause ps bod inner) = Clause (VarP tag:ps) bod inner
let clauses2 = map addTag $ transformBi (adjustPat tag) clauses
let args2 = [VarE 'var `AppE` VarE tag `AppE` toLitPre "$" a `AppE` VarE a | a <- args]
let info = ConE 'Function `AppE`
packLit (toLit name) `AppE`
packLit (LitE (StringL $ prettyPrint $ maybeToList (askSig name) ++ [o])) `AppE`
ListE (map (packLit . toLitPre "$") args) `AppE`
packLit (LitE (StringL "$result"))
let body2 = VarE 'var `AppE` VarE tag `AppE` LitE (StringL "$result") `AppE` foldl AppE (VarE inner) (VarE tag : args2)
let body = VarE 'funInfo `AppE` info `AppE` LamE [VarP tag] body2
afterApps <- transformApps tag clauses2
return $ FunD name [Clause (map VarP args) (NormalB body) [FunD inner afterApps]]
adjustDec askSig x = return x
transformApps :: Name -> [Clause] -> Q [Clause]
transformApps tag = mapM (appsFromClause tag)
appsFromClause :: Name -> Clause -> Q Clause
appsFromClause tag cl@(Clause pats body decs) = do
newBody <- appsFromBody tag body
newDecs <- mapM (appsFromDec tag) decs
return $ Clause pats newBody newDecs
appsFromBody :: Name -> Body -> Q Body
appsFromBody _ b@(GuardedB _) = return b -- TODO: implement guards
appsFromBody tag (NormalB e) = NormalB <$> appsFromExp tag e
appsFromExp :: Name -> Exp -> Q Exp
appsFromExp tag e@(AppE e1 e2) = do
newE1 <- appsFromExp tag e1
newE2 <- appsFromExp tag e2
adjustApp tag (AppE newE1 newE2)
appsFromExp tag e@(LetE decs exp) = do
newDecs <- traverse (appsFromDec tag) decs
LetE newDecs <$> appsFromExp tag exp
appsFromExp tag e@(InfixE e1May e2 e3May) = do
newE1 <- appsFromExpMay tag e1May
newE2 <- appsFromExp tag e2
newE3 <- appsFromExpMay tag e3May
adjustApp tag (InfixE newE1 newE2 newE3)
appsFromExp tag e@(CaseE exp matches) = do
newExp <- appsFromExp tag exp
newMatches <- traverse (appsFromMatch tag) matches
return $ CaseE newExp newMatches
appsFromExp tag e = return e
appsFromExpMay :: Name -> Maybe Exp -> Q (Maybe Exp)
appsFromExpMay tag Nothing = return Nothing
appsFromExpMay tag (Just e) = sequence $ Just $ appsFromExp tag e
appsFromDec :: Name -> Dec -> Q Dec
appsFromDec tag d@(ValD pat body dec) = do
newBody <- appsFromBody tag body
return $ ValD pat newBody dec
appsFromDec tag d@(FunD name subClauses) =
FunD name <$> traverse (appsFromClause tag) subClauses
appsFromDec _ d = return d
appsFromMatch :: Name -> Match -> Q Match
appsFromMatch tag (Match pat body decs) = do
newBody <- appsFromBody tag body
newDecs <- traverse (appsFromDec tag) decs
return $ Match pat newBody newDecs
adjustApp :: Name -> Exp -> Q Exp
adjustApp tag (AppE e1 e2) = do
let displayName = expDisplayName e1
e1n <- newName displayName
let viewP = ViewP (VarE 'var `AppE` VarE tag `AppE` LitE (StringL displayName)) (VarP e1n)
let result = LetE [ValD viewP (NormalB (AppE e1 e2)) []] (VarE e1n)
return result
adjustApp tag e@(InfixE e1May e2 e3May) = do
let displayName = infixExpDisplayName e2 -- infix symbol, e.g "++"
if displayName == "$" --don't record $ as a function application
then return e
else do
let legalInfixVar = mkLegalInfixVar displayName -- infix name as valid variable, e.g. "plus_plus"
e2Var <- newName legalInfixVar
let viewP = ViewP (VarE 'var `AppE` VarE tag `AppE` LitE (StringL displayName)) (VarP e2Var)
return $ LetE [ValD viewP (NormalB (InfixE e1May e2 e3May)) []] (VarE e2Var)
adjustApp _ e@(UInfixE e1 e2 e3) = return e --TODO: These might need to be processed
adjustApp _ e = return e
-- Find the (unqualified) function name to use as the UI display name
expDisplayName :: Exp -> String
expDisplayName e =
let name = removeLet $ (show . ppr) e
in removeExtraDigits (takeWhileEnd (/= '.') ((head . words) name))
-- Same as expDisplayName but for infix functions
infixExpDisplayName :: Exp -> String
infixExpDisplayName e =
let name = removeLet $ (show . ppr) e
name' = removeExtraDigits (takeWhileEnd (/= '.') ((head . words) name))
in fromMaybe name' $ stripSuffix ")" name'
adjustPat :: Name -> Pat -> Pat
adjustPat tag (VarP x) = ViewP (VarE 'var `AppE` VarE tag `AppE` toLit x) (VarP x)
adjustPat tag x = x
toLit :: Name -> Exp
toLit = toLitPre ""
toLitPre :: String -> Name -> Exp
toLitPre pre (Name (OccName x) _) = LitE $ StringL $ pre ++ x
packLit :: Exp -> Exp
packLit = AppE (VarE 'pack)
------------------------------------------------------------------
-- Data structures
data Variables = Variables
Int -- Number in the array
[(Any, String)] -- Entries, (key a, show a), indexed from [n..0]
data Var = Var Int String -- index into Variables, show a
deriving (Eq,Ord)
instance Show Var where
show (Var i s) = s ++ " @" ++ show i
varId :: Var -> Int
varId (Var x _) = x
varShow :: Var -> String
varShow (Var _ x) = x
newVariables :: Variables
newVariables = Variables 0 []
listVariables :: Variables -> [Var]
listVariables (Variables n xs) = [Var i s | (i,(_,s)) <- zipFrom 0 $ reverse xs]
addVariable :: Show a => a -> Variables -> (Variables, Var)
addVariable a vs@(Variables n xs) =
case findIndex (\(key,_) -> ptrEqual key keyA) xs of
Nothing -> (Variables (n+1) ((keyA,showA):xs), Var n showA)
Just i -> (vs, Var (n-i-1) $ snd $ xs !! i)
where
keyA = unsafeCoerce a
showA = show a
ptrEqual :: Any -> Any -> Bool
ptrEqual a b = unsafePerformIO $ do
a <- evaluate a
b <- evaluate b
return $ isTrue# (reallyUnsafePtrEquality# a b)