diff --git a/README.md b/README.md index f101a25..8fccfb9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,183 @@ # Graph Trace -A ghc plugin that a creates a log file which can be compiled to a graph for -display with `graphviz` or some other means. Contrary to traditional debug -tracing where all output is interleaved into a flat sequence of lines, the -graph structure produced by this plugin takes into account the actual flow of -the program with individual function calls represented as edges in the graph. +A GHC plugin that causes your Haskell program to create a log file which can be +compiled to a graph for display with `Graphviz` or some other means. Contrary +to traditional debug tracing where all output is interleaved into a flat +sequence of lines, the graph structure produced by this plugin takes into +account the actual call graph of the program with individual function calls +represented as edges in the graph. + +__Contents:__ +- [Demonstration](#demonstration) +- [Quickstart](#quickstart) +- [User's guide](#users-guide) + - [`Graph.Trace` plugin](#graphtrace-plugin) + - [`graph-trace-viz` utility](#graph-trace-viz-utility) +- [Caveats](#caveats) + +## Demonstration + +Consider this simplistic program that greets the user by name: +```haskell +import Graph.Trace (TraceDeep, traceM) +import Data.Char (toUpper, toLower) + +main :: TraceDeep => IO () +main = do + firstName <- prompt "Enter your first name" + lastName <- prompt "Enter your last name" + greet firstName lastName + +prompt :: String -> IO String +prompt str = do + putStrLn str + input <- getLine + traceM $ "input: " <> input + pure $ capitalize input + +capitalize :: String -> String +capitalize [] = [] +capitalize (x:xs) = toUpper x : map toLower xs + +greet :: String -> String -> IO () +greet firstName lastName = + putStrLn $ "Hello, " <> firstName <> " " <> lastName <> "!" +``` + +Using the `Graph.Trace` plugin along with the `graph-trace-viz` utility, we can +run this program to generate the following trace of the call graph: + +![demo image](/images/demo.svg) + +## Quickstart + +1. Add the `graph-trace` package as a dependency to your project. +2. Enable the plugin by adding the following GHC options: + `-fplugin=Graph.Trace -fplugin-opt Graph.Trace:debug-all -fno-full-laziness -fno-cse`. + This can be placed in the `ghc-options` field of the cabal or + package.yaml file (depending on whether you use cabal or stack to build). + For example: + + in `package.yaml`: + ``` + executables: + my-exe: + ... + ghc-options: + -fplugin=Graph.Trace + -fplugin-opt Graph.Trace:debug-all + -fno-full-laziness + -fno-cse + ``` + or in `foo.cabal`: + ``` + executable my-exe + ... + ghc-options: + -fplugin=Graph.Trace + -fplugin-opt Graph.Trace:debug-all + -fno-full-laziness + -fno-cse + ``` +3. Build your project (`cabal build all` or `stack build`). +4. Running your program should now generate a file called `.trace`. +5. Install [Graphviz](https://graphviz.org) and the `graph-trace-viz` program. + Invoke `graph-trace-viz` within the same directory as the trace file. +6. There should now be a file such as `.html` which can be + viewed in your browser. + +## User's Guide + +### `Graph.Trace` plugin +To use this plugin simply add `graph-trace` as a package dependency and pass +the `-fplugin=Graph.Trace` option to GHC. +The main functionality of the `Graph.Trace` plugin is to automatically +instrument your code so that it will emit trace logging to a `*.trace` file +when run. There are two types of traces: +- __Function call__ + This trace is emitted when the term returned by a function is evaluated to + WHNF. Each emission of an entry trace generates a unique ID for that particular + function invocation. The trace also includes the ID of the code from which the + function was called (if applicable) so that a graph edge can be constructed + between the two. Entry traces are only emitted for functions that have + signatures. If the function does not have a signature then it will simply + inherit the ID of the calling context and will therefore not generate a new + node in the call graph. + +- __Debug trace__ + Debug traces are textual messages that the user can emit from the body of a + function. When rendered, they appear as plain text in the body of a call graph + node. The API for debug traces matches that of the familiar `Debug.Trace` + module and is available through the `Graph.Trace` module. A debug trace will + only be emitted when the thing it is applied to is evaluated to WHNF. + +The plugin gives you some control over how and when traces are emitted. There +are a variety of constraints you can put on function signatures to control +tracing, all of which are exported by `Graph.Trace`: + +- `Trace` + On its own, this constraint says that traces should be emitted for a given + function using the name of that function as the identifier. Notably, function + calls made from within the body of the function do not inherit this behavior + and so will not emit traces unless otherwise instructed to do so. + +- `TraceDeep` + This is similar to `Trace` except that function calls made within the function + body will also emit traces even if they don't have a trace constraint (unless + they are being muted). For example, putting `TraceDeep` on the `main` function + will result in the full execution the program being traced. + +- `TraceMute` + If a function has this constraint then any invocation of it as well as the + function calls it makes will not emit any traces. This constraint overrides all + the others and is inherited by function calls made from within its body. + +- `TraceKey` + This constraint is the same as `Trace` but it takes a type level string + argument which will be used as the function identifier instead of the + function's actual name. Notably, if the identifier of a called function is + overridden to be the same as that of the function calling it then its output + will be placed in the graph node of the calling function rather than + generating its own node. There is also a `TraceDeepKey` constraint that does + the same thing but for `TraceDeep`. + +If you want every function in your program to emit traces, you can use the +`debug-all` plugin option which effectively adds the `Trace` constraint to all +function definitons. To use this option, pass the `-fplugin-opt Graph.Trace:debug-all` +option to GHC in addition to `-fplugin=Graph.Trace`. + +If a trace file already exists for your executable being then new entries will +be appended to that file. To start a new trace you'll need to rename or delete +the old file. + +### `graph-trace-viz` utility +Once you've generated a `*.trace` file by compiling your program with the +`Graph.Trace` plugin and running it, the `graph-trace-viz` utility can render +the graph as an html document. It is dependent on +[Graphviz](https://graphviz.org), so you must install that on your system +(there should be an executable called `dot` on your PATH). Simply invoke +`graph-trace-viz` in the same directory as the `*.trace` file and it will write +an `*.html` document. By default it will read all trace files in the current +directory but you can specify the files by giving them as command line +arguments instead. + +## Caveats + +There are several known caveats you should be aware of: + +- __Undesirable optimisations__ + If you compile with the common sub-expression or full laziness optimisations, + which are on by default for `O1` and `O2` settings, graph nodes that should + be distinct can sometimes get merged into a single node. To prevent this, it + is recommended that you turn these optimisations off using the `-fno-cse` and + `-fno-full-laziness` flags when compiling with the `graph-trace` plugin. +- __Type class methods__ + For type class instance methods to be traced correctly, the class method + definitions must be in a package compiled with the plugin and you'll also + need to put a type signature on the instance method declarations, which + requires the `InstanceSigs` GHC extension. +- __Impredicative types__ + If you have a function binding that takes a rank-n quantified type as a + parameter, this can cause compilation with the plugin to fail. With GHC 9.2 + and above, giving a type signature to the binding will resolve the issue. +- The plugin does not support GHC versions less than 8.10 diff --git a/exe/app/Class.hs b/exe/app/Class.hs index 6cc942a..a58d189 100644 --- a/exe/app/Class.hs +++ b/exe/app/Class.hs @@ -1,9 +1,10 @@ +{-# OPTIONS_GHC -fplugin=Graph.Trace #-} module Class where import Graph.Trace class Show a => Classy a where - classy :: Trace => a -> String + classy :: a -> String deff :: Trace => a -> String deff = show diff --git a/exe/app/Main.hs b/exe/app/Main.hs index 3869aaa..aa43021 100644 --- a/exe/app/Main.hs +++ b/exe/app/Main.hs @@ -3,6 +3,8 @@ -- {-# OPTIONS_GHC -fno-full-laziness -fno-cse #-} --{-# OPTIONS_GHC -ddump-rn-ast #-} +{-# LANGUAGE ImpredicativeTypes #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE MultiParamTypeClasses #-} @@ -17,123 +19,152 @@ import Data.Functor.Identity (Identity(..)) import Graph.Trace --import Debug.Trace import Class +import Data.Char import qualified System.Random as Rand import System.IO.Unsafe main :: TraceDeep => IO () -main = trace bah print unassuming >> buzzard - where - unassuming :: Either Bool Int - --thisIsABoolean :: Bool - unassuming@(Left thisIsABoolean@True) = - trace bah $! (Left True :: Either Bool Int) +main = do + firstName <- prompt "Enter your first name" + lastName <- prompt "Enter your last name" + greet firstName lastName - buzzard = do - putStrLn $ "please, help" <&> "boo" - traceM bah +prompt :: String -> IO String +prompt str = do + putStrLn str + input <- getLine + traceM $ "input: " <> input + pure $ capitalize input - bah :: String - bah = unsafePerformIO $ do - getLine +capitalize :: String -> String +capitalize [] = [] +capitalize (x:xs) = toUpper x : map toLower xs -(<&>) :: String -> String -> String -a <&> b = a +greet :: String -> String -> IO () +greet first last = + putStrLn $ "Hello, " <> first <> " " <> last <> "!" --- where --- inFlight = putStrLn "need help now" - -- shitty = () - --replicateM_ 2 $ forkIO test --- andAnother --- test' +{-# NOINLINE main #-} +-- main :: TraceDeep => IO () +-- main = trace bah print unassuming >> buzzard +-- where +-- unassuming :: Either Bool Int +-- --thisIsABoolean :: Bool +-- unassuming@(Left thisIsABoolean@True) = +-- trace bah $! (Left True :: Either Bool Int) +-- +-- buzzard = do +-- putStrLn $ "hello" <&> "boo" +-- traceM bah +-- +-- bah :: String +-- bah = unsafePerformIO $ do +-- getLine +-- (<&>) :: String -> String -> String +-- a <&> b = a --- test' :: IO () --- test' = do --- andAnother --- trace "test\ntest" pure () --- traceM "yo" --- putStrLn $ deff (I 3) --- x <- readLn --- case x of --- 3 -> putStrLn $ classy (I x) --- _ -> pure () --- putStrLn $ classier (I 5) --- inWhere --- let inLet :: IO () --- inLet = do --- letWhere --- another --- where letWhere = trace ("hello" \/& "two") pure () --- inLet --- !_ <- another --- let letBound = letBoundThing --- trace letBound pure () --- trace "leaving" pure () --- where --- inWhere :: Debug => IO () --- inWhere = do --- innerWhere --- where --- innerWhere :: Debug => IO () --- innerWhere = trace "innerWhere" pure () --- --- another :: Debug => IO () --- another --- | trace "another" True = do --- pure () --- | otherwise = pure () --- --- andAnother :: Debug => IO () --- andAnother = trace "hello!" pure () --- --- letBoundThing :: Debug => String --- letBoundThing = "bound by let" --- --- (\/&) :: String -> String -> String --- a \/& b = the a <> ('\\' : b) --- --- the :: a -> a --- the = id --- --- newtype I = I Int deriving Show --- --- instance Classy I where --- classy :: Debug => I -> String --- classy = boo --- where --- boo :: Debug => I -> String --- boo i = trace (show i) "..." --- --- instance Classier I where --- classier = show +-- main :: Trace => IO () +-- main = test' + +test' :: Trace => IO () +test' = do + andAnother + trace "test\ntest" pure () + traceM "yo" + putStrLn $ deff (I 3) + x <- readLn + case x of + 3 -> putStrLn $ classy (I x) + _ -> pure () + putStrLn $ classier (I 5) + inWhere + let inLet :: Trace => IO () + inLet = do + letWhere + another + where letWhere = trace ("hello" \/& "two") pure () + inLet + !_ <- another + let letBound = letBoundThing + trace letBound pure () + trace "leaving" pure () + where + inWhere :: Trace => IO () + inWhere = do + innerWhere + where + innerWhere :: Trace => IO () + innerWhere = trace "innerWhere" pure () + +another :: Trace => IO () +another + | trace "another" True = do + pure () + | otherwise = pure () + +andAnother :: (Trace, Monad m) => m () +andAnother = trace "hello!" pure () + +letBoundThing :: Trace => String +letBoundThing = "bound by let" + +(\/&) :: String -> String -> String +a \/& b = the a <> ('\\' : b) + +the :: a -> a +the = id + +newtype I = I Int deriving Show + +instance Classy I where + classy :: Trace => I -> String + classy = boo + where + boo :: Trace => I -> String + boo i = trace (show i) "..." + +instance Classier I where + classier = show -- -- -- test :: (?x :: String) => IO () -- -- test = print ?x -- --- data FieldUpdate a --- = FieldValue a --- | FieldOmitted --- | FieldNull --- --- mkUpdater :: f FieldUpdate --- -> f Maybe --- -> (forall a. f a -> a x) --- -> Maybe x --- mkUpdater update original getField = --- case getField update of --- FieldValue a -> Just a --- FieldOmitted -> getField original --- FieldNull -> Nothing --- --- data T f = --- MkT --- { t1 :: f Bool --- , t2 :: f String --- } --- +data FieldUpdate a + = FieldValue a + | FieldOmitted + | FieldNull + +mkUpdater :: f FieldUpdate + -> f Maybe + -> (forall a. f a -> a x) + -> Maybe x +mkUpdater update original getField = + case getField update of + FieldValue a -> Just a + FieldOmitted -> getField original + FieldNull -> Nothing + +data T f = + MkT + { t1 :: f Bool + , t2 :: f String + } + +type TY = forall x. (forall a. T a -> a x) -> Maybe x + +-- zz :: Int +-- zz = +-- let x :: [forall x. x -> x] +-- x = [id, id] +-- in id head x 4 + +zzz :: Int +zzz = id head [1,2,3] + -- zzzz :: T FieldUpdate -> T Maybe -> T Maybe -- zzzz update orig = --- let updater :: DebugMute => (forall a. T a -> a x) -> Maybe x +-- let updater :: TY --(forall a. T a -> a x) -> Maybe x -- updater | let ?x = 1 -- = mkUpdater update orig -- in MkT @@ -141,34 +172,15 @@ a <&> b = a -- , t2 = updater t2 -- } --- fzzz :: (?_debug_ip :: Maybe DebugIPTy) => T FieldUpdate -> T Maybe -> T Maybe +-- fzzz :: Trace => T FieldUpdate -> T Maybe -> T Maybe -- fzzz update orig = entry $ --- let --updater :: (?_debug_ip :: Maybe DebugIPTy) --- -- => (forall a. T a -> a x) -> Maybe x +-- let updater :: -- (?_debug_ip :: Maybe DebugContext) +-- (forall a. T a -> a x) -> Maybe x -- updater -- | let ?_debug_ip = newIP' -- = --entry $ -- mkUpdater update orig --- where --- newIP' = --- let mPrevTag = fmap snd ?_debug_ip --- in unsafePerformIO $ do --- newId <- Rand.randomIO :: IO Word --- let newTag = DT --- { invocationId = newId --- , debugKey = Right "test" --- } --- pure $ Just (mPrevTag, newTag) +-- addOne = (+1) -- in MkT -- { t1 = updater t1 -- , t2 = updater t2 -- } --- where --- newIP = --- let mPrevTag = fmap snd ?_debug_ip --- in unsafePerformIO $ do --- newId <- Rand.randomIO :: IO Word --- let newTag = DT --- { invocationId = newId --- , debugKey = Right "test" --- } --- pure $ Just (mPrevTag, newTag) diff --git a/exe/exe.cabal b/exe/exe.cabal index ceaa559..87cc325 100644 --- a/exe/exe.cabal +++ b/exe/exe.cabal @@ -34,4 +34,4 @@ executable test-exe , random -- hs-source-dirs: default-language: Haskell2010 --- ghc-options: -fplugin=Graph.Trace +-- ghc-options: -fplugin=Graph.Trace -fplugin-opt Graph.Trace:debug-all -fno-cse -fno-full-laziness diff --git a/images/demo.svg b/images/demo.svg new file mode 100644 index 0000000..8b455a1 --- /dev/null +++ b/images/demo.svg @@ -0,0 +1,106 @@ + + + + + +main138835950170271710 + + + +main + + + + + +prompt + + + + + +prompt + + + + + +greet + + + + + + + +prompt924477568213965436 + + + + + + +prompt + + + + +input: john + + + + + +capitalize + + + + + + + +main138835950170271710:3->prompt924477568213965436 + + + + + + + + +prompt8331185687311588863 + + + + + + +prompt + + + + +input: doe + + + + + +capitalize + + + + + + + +main138835950170271710:2->prompt8331185687311588863 + + + + + + + +