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.
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 tobreakpoint
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 arbitraryApplicative
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.
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.
- 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.