Breakpoint
The ability to set breakpoints in a program can provide valuable insights when
debugging. While GHCi has built-in support for setting breakpoints, it is not
actively maintained and suffers from several critical limitations:
- It's prohibitively buggy when used with concurrent programs, such as web servers.
- Breakpoints can only be set in interpreted code.
- Occasionally it simply doesn't work at all.
The breakpoint
library solves these problems by implementing breakpoints as
a GHC plugin.
Usage
Add breakpoint
as a dependency to your project then enable breakpoints in a
module by adding {-# OPTIONS_GHC -fplugin Debug.Breakpoint #-}
to the top of
the file and importing the Debug.Breakpoint
module. You can then use the
breakpoint
, breakpointIO
, or breakpointM
functions as appropriate to set
a breakpoint.
breakpoint :: a -> a
is for use in pure code. Apart from the side-effect of
setting a breakpoint, it is the identity function. The value passed to breakpoint
will appear as a variable called *result
in the output.
breakpointIO :: MonadIO m => m ()
is for monadic code that can perform IO.
breakpointM :: Applicative f => f ()
is for arbitrary Applicative
contexts.
breakpoint
and breakpointM
both use unsafePerformIO
which means they are
at the mercy of the simplifier and all the other pitfalls of lazy IO. For this
reason, it's generally preferable to use breakpointIO
in contexts that
support it.
Here's an example module:
{-# OPTIONS_GHC -fplugin Debug.Breakpoint #-}
import Debug.Breakpoint
main :: IO ()
main = do
x <- getLine
let y = 2 :: Int
z = id :: Bool -> Bool
breakpointIO
pure ()
When the breakpoint expression gets evaluated, you will see terminal output such
as
### Breakpoint Hit ###
(app/Main.hs:24:3-6)
x =
"input"
y =
2
z =
<Bool -> Bool>
Press enter to continue
showing the location of the breakpoint and the free variables that are visible
from the callsite, this includes function arguments, let bindings, where binds,
monadic binds, pattern binds, etc.
If the type of a value has a Show
instance then that will be used to generate
the printed value, otherwise the output will contain the type of the value
within angle brackets.
Execution of the program effectively halts on waiting for user input. In
concurrent programs, all threads will be stopped, not just the one executing
the breakpoint.
Querying variables
In contrast to the standard breakpoint functions which print out the values for
all current variables, the queryVars
, queryVarsM
, and queryVarsIO
functions first print the variables names and then initiate a prompt where you
can enter a specific variable name to have its value printed.
This is useful if you are only interested in certain values or if printing one
or more values would result in a non-terminating process (an infinite data
structure for example).
You can tab-complete variable names at the query prompt. Only the current
thread is blocked while the prompt is active. To resume execution, press enter
with a blank line.
Caveats
- Currently supports GHC version 9.2.x - 9.8.x
- Printing values may cause thunks to be evaluated earlier than they otherwise
would which could be problematic for programs that rely heavily on laziness.
ApplicativeDo
can sometimes cause variables that are in scope to not be traced.
- Implicit params are not currently supported
RecursiveDo
binds aren't visible before they are bound, despite being in scope.
- If there is anything buffered in
stdin
then that will interfere with the
blocking mechanism.
- On Windows or when using the non-threaded runtime, calls to
threadDelay
are not suspended by breakpoints in the sense that time
continues to elapse, however they won't unblock until the breakpoint
finishes.
- Variables being traced cannot have a type that contains type variables with
class constraints, otherwise you get a compiler error. This happens most
commonly with a where clause binding that lacks a type signature. You can
deal with this by using
excludeVars
or giving a type signature to the
binding that doesn't introduce such type variables.