Skip to content

Commit

Permalink
Reframe as debuggable
Browse files Browse the repository at this point in the history
  • Loading branch information
edsko committed Nov 22, 2024
1 parent 605b964 commit 139d99f
Show file tree
Hide file tree
Showing 8 changed files with 452 additions and 395 deletions.
67 changes: 4 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,65 +1,6 @@
# `niio`: functions for debugging that guarantee non-interleaved output
# `provenance`: utilities for tracking provenance

This package provides analogues of various standard output functions; all of
these functions are safe to use in a concurrent setting, and guarantee that the
output of one function call will not be interleaved with the output of another.
Examples include
This package provides utilities that can help track when and where things are
defined, to aid debugging. It also provides some utilities for non-interleaved
output, for debugging concurrent code.

```haskell
niPutStr :: MonadIO m => String -> m ()
niPutStrLn :: MonadIO m => String -> m ()
niPrint :: (MonadIO m, Show a) => a -> m ()
niTrace :: String -> a -> a
niTraceShow :: Show a => a -> b -> b
niTraceShowId :: Show a => a -> a
niTraceM :: Applicative m => String -> m ()
niTraceShowM :: (Applicative m, Show a) => a -> m ()
```

In addition, we provide some support for creating uniques, to be able to
support correlating log messages, and some functionality for working with
these uniques:

```haskell
niGetUnique :: (MonadIO m, HasCallStack) => m NiUnique
niPutStrAt :: MonadIO m => [NiUnique] -> String -> m ()
```

and

```haskell
niBracket ::
(MonadIO m, MonadMask m, HasCallStack)
=> (NiUnique -> m ()) -- ^ Prior to the action
-> (NiUnique -> ExitCase a -> m ()) -- ^ After
-> (NiUnique -> m a)
-> m a
```

For example:

```haskell
-- > niBracket (\i -> niPutStrAt [i] "start") (\i -> niPutStrAt [i] . show) $ \i ->
-- > ..
-- > niBracket (\j -> niPutStrAt [i, j] "start") (\j -> niPutStrAt [i, j] . show) $ \j ->
-- > ..
-- > niPutStrAt [i, j] $ "foo: " ++ E.displayException e
-- > ..
```

might result in

```
-- > ["exampleFun(./Example/File.hs:100:5)/1"] start
-- > ["exampleFun(./Example/File.hs:100:5)/1","exampleFun(./Example/File.hs:120:13)/1"] start
-- > ["exampleFun(./Example/File.hs:100:5)/1","exampleFun(./Example/File.hs:120:13)/1"]
-- > foo: ExampleException
-- > HasCallStack backtrace:
-- > collectBacktraces, called at (..)
-- > toExceptionWithBacktrace, called at (..)
-- > throwIO, called at (..)
-- > ["exampleFun(./Example/File.hs:100:5)/1","exampleFun(./Example/File.hs:120:13)/1"] ExitCaseSuccess ()
-- > ["exampleFun(./Example/File.hs:100:5)/1"] ExitCaseSuccess ()
```

This package is intended for debugging only.
25 changes: 20 additions & 5 deletions niio.cabal → debuggable.cabal
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
cabal-version: 3.0
name: niio
name: debuggable
version: 0.1.0
synopsis: Functions for debugging that guarantee non-interleaved output
synopsis: Utilities for making your applications more debuggable.
description: This package provides various utilities that can be used to
make your application easier to debug. Some of these tools are
intended for use during actual debugging only (similar to
@Debug.Trace@, for example). Other tools can be used as a
regular component in your application, to facilitate debugging
if and when necessary, but always present in your code.
license: BSD-3-Clause
license-file: LICENSE
author: Edsko de Vries
Expand All @@ -28,14 +34,23 @@ common lang
DerivingStrategies
GeneralizedNewtypeDeriving
NamedFieldPuns
ScopedTypeVariables
StandaloneDeriving
TupleSections

library
import: lang
exposed-modules: System.IO.NonInterleaved
hs-source-dirs: src
import: lang
hs-source-dirs: src

exposed-modules:
Debug.NonInterleavedIO
Debug.NonInterleavedIO.Scoped
Debug.NonInterleavedIO.Trace
Debug.Provenance
Debug.Provenance.Scope

build-depends:
, containers >= 0.6 && < 0.8
, exceptions >= 0.9 && < 0.11
, hashable >= 1.4 && < 1.6
, temporary >= 1.2.1 && < 1.4
Expand Down
100 changes: 100 additions & 0 deletions src/Debug/NonInterleavedIO.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
-- | Functions for non-interleaved output
--
-- Intended for qualifed import.
--
-- > import qualified Debug.NonInterleavedIO as NIIO
--
-- Alternatively, you can import "Debug.NonInterleavedIO.Trace" as a drop-in
-- replacement for "Debug.Trace".
--
-- The functions in this module can all be called concurrently, without
-- resulting in interleaved output: each function call is atomic.
--
-- The first time any of these functions is called, we lookup the @NIIO_OUTPUT@
-- environment variable. If set, we will write to the file specified (if the
-- file already exists, it will be overwritten). If @NIIO_OUTPUT@ is not set, a
-- temporary file will be created in the system temporary directory; typically
-- such a file will be called @/tmp/niio<number>@. The name of this file is
-- written to @stderr@ (this is the /only/ output origiating from functions in
-- this module that is not written to the file).
module Debug.NonInterleavedIO (
-- * Output functions
putStr
, putStrLn
, print
-- * Tracing functions
, trace
, traceShow
, traceShowId
, traceM
, traceShowM
) where

import Prelude hiding (putStr, putStrLn, print)

import Control.Concurrent
import Control.Monad.IO.Class
import System.Environment
import System.IO.Temp (getCanonicalTemporaryDirectory)
import System.IO.Unsafe

import qualified System.IO as IO

{-------------------------------------------------------------------------------
Output functions
-------------------------------------------------------------------------------}

-- | Non-interleaved version of 'Prelude.putStr'
putStr :: MonadIO m => String -> m ()
putStr str = liftIO $ withMVar globalHandle $ \h -> IO.hPutStr h str

-- | Non-interleaved version of 'Prelude.putStrLn'
putStrLn :: MonadIO m => String -> m ()
putStrLn = putStr . (++ "\n")

-- | Non-interleaved version of 'Prelude.print'
print :: MonadIO m => Show a => a -> m ()
print = putStrLn . show

{-------------------------------------------------------------------------------
Tracing
-------------------------------------------------------------------------------}

-- | Non-interleaved version of 'Debug.Trace.trace'
trace :: String -> a -> a
trace str a = unsafePerformIO $ putStrLn str >> return a

-- | Non-interleaved version of 'Debug.Trace.traceShow'
traceShow :: Show a => a -> b -> b
traceShow = trace . show

-- | Non-interleaved version of 'Debug.Trace.traceShowId'
traceShowId :: Show a => a -> a
traceShowId a = traceShow (show a) a

-- | Non-interleaved version of 'Debug.Trace.traceM'
traceM :: Applicative m => String -> m ()
traceM str = trace str $ pure ()

-- | Non-interleaved version of 'Debug.Trace.traceShowM'
traceShowM :: (Applicative m, Show a) => a -> m ()
traceShowM = traceM . show

{-------------------------------------------------------------------------------
Internal: globals
-------------------------------------------------------------------------------}

globalHandle :: MVar IO.Handle
{-# NOINLINE globalHandle #-}
globalHandle = unsafePerformIO $ do
mOutput <- lookupEnv "NIIO_OUTPUT"
(fp, h) <- case mOutput of
Nothing -> do
tmpDir <- getCanonicalTemporaryDirectory
IO.openTempFile tmpDir "niio"
Just fp -> do
(fp,) <$> IO.openFile fp IO.WriteMode
IO.hPutStrLn IO.stderr $ "niio output to " ++ fp
IO.hFlush IO.stderr
IO.hSetBuffering h IO.NoBuffering
newMVar h
39 changes: 39 additions & 0 deletions src/Debug/NonInterleavedIO/Scoped.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{-# OPTIONS_GHC -Wno-orphans #-}

-- | Utilities for writing debugging messages that include provenance
--
-- Intended for qualified import.
--
-- > import qualified Debug.NonInterleavedIO.Scoped as Scoped
module Debug.NonInterleavedIO.Scoped (
putStrLn
) where

import Prelude hiding (putStrLn)

import Control.Monad.IO.Class
import Data.List (intercalate)
import GHC.Stack

import Debug.Provenance
import Debug.Provenance.Scope

import qualified Debug.NonInterleavedIO as NIIO

{-------------------------------------------------------------------------------
Uniques
-------------------------------------------------------------------------------}

-- | Print debug message, showing current scope
putStrLn :: MonadIO m => String -> m ()
putStrLn str = withFrozenCallStack $ do
scope <- getScope
here <- newInvocation callSite

let prettyScope :: String
prettyScope = intercalate "," $ map prettyInvocation (here : scope)

NIIO.putStrLn $
case lines str of
[one] -> prettyScope ++ " " ++ one
many -> intercalate "\n" $ prettyScope : map (" " ++) many
14 changes: 14 additions & 0 deletions src/Debug/NonInterleavedIO/Trace.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- | Drop-in replacement for "Debug.Trace"
--
-- This module just re-exports some functions from "Debug.NonInterleavedIO";
-- since it does not export anything that clashes with "Prelude" it can be
-- imported unqualified.
module Debug.NonInterleavedIO.Trace (
trace
, traceShow
, traceShowId
, traceM
, traceShowM
) where

import Debug.NonInterleavedIO
Loading

0 comments on commit 139d99f

Please sign in to comment.