Skip to content

Commit 833a32e

Browse files
authored
Merge pull request #13 from well-typed/edsko/callback
Support callbacks
2 parents 6f35a2c + 8cc6831 commit 833a32e

File tree

4 files changed

+135
-6
lines changed

4 files changed

+135
-6
lines changed

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
# `provenance`: utilities for tracking provenance
2-
3-
This package provides utilities that can help track when and where things are
4-
defined, to aid debugging. It also provides some utilities for non-interleaved
5-
output, for debugging concurrent code.
1+
# `debuggable`: utilities for making your applications more debuggable.
62

3+
This package provides various utilities that can be used to make your
4+
application easier to debug. Some of these tools are intended for use during
5+
actual debugging only (similar to `Debug.Trace`, for example). Other tools can
6+
be used as a regular component in your application, to facilitate debugging if
7+
and when necessary, but always present in your code.

debuggable.cabal

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,19 @@ common lang
2828
build-depends: base >= 4.14 && < 4.21
2929

3030
default-extensions:
31+
BangPatterns
3132
DeriveAnyClass
3233
DeriveFunctor
3334
DeriveGeneric
3435
DerivingStrategies
3536
GeneralizedNewtypeDeriving
37+
LambdaCase
3638
NamedFieldPuns
39+
RankNTypes
3740
ScopedTypeVariables
3841
StandaloneDeriving
3942
TupleSections
43+
TypeApplications
4044

4145
library
4246
import: lang
@@ -47,6 +51,7 @@ library
4751
Debug.NonInterleavedIO.Scoped
4852
Debug.NonInterleavedIO.Trace
4953
Debug.Provenance
54+
Debug.Provenance.Callback
5055
Debug.Provenance.Scope
5156

5257
build-depends:
@@ -55,3 +60,6 @@ library
5560
, hashable >= 1.4 && < 1.6
5661
, temporary >= 1.2.1 && < 1.4
5762
, unordered-containers >= 0.2 && < 0.3
63+
64+
other-extensions:
65+
ImplicitParams

src/Debug/Provenance/Callback.hs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
{-# LANGUAGE ImplicitParams #-}
2+
{-# OPTIONS_GHC -ddump-simpl -dsuppress-all -ddump-to-file #-}
3+
4+
-- | Provenance for callbacks
5+
module Debug.Provenance.Callback (
6+
Callback -- opaque
7+
, callback
8+
, invokeCallback
9+
) where
10+
11+
import GHC.Stack
12+
13+
import Debug.Provenance
14+
15+
{-------------------------------------------------------------------------------
16+
Callback
17+
-------------------------------------------------------------------------------}
18+
19+
-- | Callback of type @(a -> m b)@
20+
--
21+
-- When we invoke a callback, it is useful to distinguish between two things:
22+
--
23+
-- * The 'CallStack' of the /invocation/ of the callback
24+
-- * The 'CallSite' of the /definition/ of the callback
25+
--
26+
-- The purpose of this module is to be careful about this distinction; a
27+
-- 'HasCallStack' backtrace originating from an invocation of a callback will
28+
-- look something like this:
29+
--
30+
-- > gM, called at ..
31+
-- > ..
32+
-- > g2, called at ..
33+
-- > g1, called at ..
34+
-- > callbackFn, called at ..
35+
-- > invoking callback defined at <callSite>
36+
-- > invokeCallback, called at ..
37+
-- > fN, called at ..
38+
-- > ..
39+
-- > f2, called at ..
40+
-- > f1, called at ..
41+
--
42+
-- where
43+
--
44+
-- * @f1 .. fN@ are the function calls leading up to the callback
45+
-- * @g1 .. gM@ are the function calls made inside of the callback
46+
-- * @\<callSite\>@ tells us where the callback was defined
47+
newtype Callback m a b = Wrap (Callback_ CallStack m a b)
48+
49+
-- | Define 'Callback'
50+
--
51+
-- See 'Callback' for discussion and motivation of the /two/ 'HasCallStack'
52+
-- constraints.
53+
callback :: HasCallStack => (HasCallStack => a -> m b) -> Callback m a b
54+
callback callbackFn = withFrozenCallStack $ Wrap (callback_ callbackFn)
55+
56+
-- | Invoke 'Callback'
57+
invokeCallback :: HasCallStack => Callback m a b -> a -> m b
58+
invokeCallback (Wrap cb) a = invoke_ aux cb a
59+
where
60+
aux :: CallSite -> CallStack -> CallStack
61+
aux defSite = mapCallSites $ \cs ->
62+
case cs of
63+
(_, loc):cs' -> -- this is the call to invoke_
64+
( "invoking callback defined at " ++ prettyCallSite defSite
65+
, loc
66+
)
67+
: cs'
68+
_otherwise ->
69+
error $ "invokeCallback: unexpected CallStack"
70+
71+
{-# NOINLINE callback #-}
72+
{-# NOINLINE invokeCallback #-}
73+
74+
{-------------------------------------------------------------------------------
75+
Internal: generalize over 'CallStack'
76+
77+
By working with a polymorphic @cs@ instead of 'CallStack' here, we avoid
78+
@ghc@ manipulating the 'CallStack' itself. (This of course means that we
79+
depend on the fact that 'HasCallStack' is defined as an implicit parameter.)
80+
-------------------------------------------------------------------------------}
81+
82+
data Callback_ cs m a b = Callback_ {
83+
callbackFunction :: !(cs -> a -> m b)
84+
, callbackDefSite :: !CallSite
85+
}
86+
87+
callback_ :: forall cs m a b.
88+
HasCallStack
89+
=> ((?callStack :: cs) => a -> m b)
90+
-> Callback_ cs m a b
91+
callback_ f = Callback_ (mkExplicit f) callSite
92+
93+
invoke_ ::
94+
(?callStack :: cs)
95+
=> (CallSite -> cs -> cs)
96+
-> Callback_ cs m a b -> a -> m b
97+
invoke_ g Callback_{callbackFunction = fn, callbackDefSite = defSite} a =
98+
mkImplicit (\cs -> fn (g defSite cs) a)
99+
100+
mkExplicit :: ((?callStack :: cs) => a) -> (cs -> a)
101+
mkExplicit f cs = let ?callStack = cs in f
102+
103+
mkImplicit :: (?callStack :: cs) => (cs -> a) -> a
104+
mkImplicit f = f ?callStack
105+
106+
{-# NOINLINE callback_ #-}
107+
{-# NOINLINE invoke_ #-}
108+
{-# NOINLINE mkExplicit #-}
109+
{-# NOINLINE mkImplicit #-}
110+
111+
{-------------------------------------------------------------------------------
112+
Internal: manipulating the callstack
113+
-------------------------------------------------------------------------------}
114+
115+
mapCallSites ::
116+
([([Char], SrcLoc)] -> [([Char], SrcLoc)])
117+
-> CallStack -> CallStack
118+
mapCallSites f = fromCallSiteList . f . getCallStack

src/Debug/Provenance/Scope.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ pushInvocation :: MonadIO m => Invocation -> m ()
9797
pushInvocation i = modifyThreadLocalScope_ (i:)
9898

9999
popInvocation :: MonadIO m => m ()
100-
popInvocation = modifyThreadLocalScope_ tail
100+
popInvocation = modifyThreadLocalScope_ $ \case
101+
[] -> error "popInvocation: empty stack"
102+
_:s -> s
101103

102104
{-------------------------------------------------------------------------------
103105
Internal: globals

0 commit comments

Comments
 (0)