1
The Haskell Debugger is ready to use with GHC-9.14!
The installation, configuration, and talks can be found in the official website. The tl;dr first step is installing the debugger:
$ ghc --version # MUST BE GHC 9.14
The Glorious Glasgow Haskell Compilation System, version 9.14.1
$ cabal install haskell-debugger --allow-newer=base,time,containers,ghc,ghc-bignum,template-haskell --enable-executable-dynamic # ON WINDOWS, DO NOT PASS --enable-executable-dynamic
...
$ ~/.local/bin/hdb --version # VERIFY IT'S THE LATEST!
Haskell Debugger, version 0.11.0.0
The second step is configuring your editor to use the debugger via the Debug Adapter Protocol (DAP).
- For VSCode, install the haskell debugger extension.
- For Neovim, install nvim-dap and configure it for haskell-debugger
- For other editors, consult your DAP documentation and let others know how!
Bug reports and discussions are welcome in the haskell-debugger issue tracker.
My MuniHac 2025 talk also walks through the installation, usage, and design of the debugger. Do note much has been improved since the talk was given, and much more will still improve.
A little bit more info
The debugger work is sponsored by Mercury. It’s the project in which I’ve spent most of my full working days (for almost a full year now), with the invaluable help from my team at Well-Typed.
The debugger is meant to work both on trivial files and on large and complex codebases[1]. It is a GHC application so all features are supported. Like HLS, it also uses hie-bios to automatically configure the session based on your cabal or stack project.
Robustness is a main goal of the debugger. If anything doesn’t work, or if you have performance issues, or something crashes, please don’t hesitate to submit a bug. We’ve got a small but respectable testsuite, and have tested performance by debugging GHC itself, but there’s much still to be fixed and improved.
Roadmap: There’s a lot to do. I’m currently working on callstacks and multi-threaded support. Do let me know what features would be most important to you, so I can also factor that into the future planning.
[1] Although, for large codebases, the usability is still rough around the edges because of possibly long bytecode compilation times, and library code not being interpreted. We’ve made considerable progress to improve this with the bytecode artifacts work.
hasufell 2
Will this at some point be shipped with GHC?
romes 3
We don’t intend to ship this with GHC, as coupling yet another thing to its release process seems undesirable. Also because haskell-debugger has many dependencies which are not shipped with GHC.
However, there is a more barebones debugger inside of GHCi (for almost 20 years now), which leverages the same internal debugger capabilities that haskell-debugger does. Things I’ve improved in the GHC internals benefit both the GHCi debugger and the haskell-debugger.
That said, I’m open to discuss the release process for haskell-debugger and learn from your packaging expertise.
I want to mention that there’s an open feature request in the GHCUp repo to, ultimately, be able to offer more binaries installable via GHCUp. That could be nice for hdb!
It’s on my 2026 to-do list to investigate this feature request, but maybe someone else is excited about it and wants to take a look
hasufell 5
GHCup is a simple installer. This feature request is basically asking package manager like functionality, such as tracking of ABI compatibility.
As an example, Gentoo got a similar feature fairly “recently” in 2012: Sub-slots and Slot-Operators - Gentoo wiki
It caused a lot of problems and complexity for end users.
The second problem is that such binaries are also tied to a single GHC version. So how do you install the debugger for multiple GHC versions? For HLS I solved that by just installing all HLS binaries regardless of what GHC versions are installed.
I don’t think micro-managing installed binaries per GHC version is a good user experience.
But that opens up the following problem: if we install HLS for all supported GHC versions, then we have a “dynamic” matrix of ABIs.
ghcupDownloads:
GHC:
9.6.7:
viArch:
A_64:
Linux_CentOS:
( >= 7 && < 8 ): &ghc-967-64-centos7
dlHash: c1a0c27cbb18152a9ee3474c44f6d00399bc6b1633cea2e0169a34b8f57f5f39
dlSubdir: ghc-9.6.7-x86_64-unknown-linux
dlUri: https://downloads.haskell.org/ghcup/unofficial-bindists/ghc/9.6.7/ghc-9.6.7-x86_64-centos7-linux.tar.xz
unknown_versioning: *ghc-967-64-centos7
Linux_Fedora:
'>= 33': &ghc-967-64-fedora33
dlHash: f845a79f15fcbfe8e659c43f4cd0b1f1bc15e2309665726c573de850278296f1
dlSubdir: ghc-9.6.7-x86_64-unknown-linux
dlUri: https://downloads.haskell.org/~ghc/9.6.7/ghc-9.6.7-x86_64-fedora33-linux.tar.xz
unknown_versioning: *ghc-967-64-centos7
Linux_UnknownLinux:
unknown_versioning:
dlHash: e1078062054732eb12ec0267d74a72362a5fc66d45cf0aeebbf4caf425e264e5
dlSubdir: ghc-9.6.7-x86_64-unknown-linux
dlUri: https://downloads.haskell.org/ghcup/unofficial-bindists/ghc/9.6.7/ghc-9.6.7-x86_64-rocky8-linux.tar.xz
9.14.1:
viArch:
A_64:
Linux_UnknownLinux:
unknown_versioning: &ghc9141-x86_64-linux-rocky8
dlHash: f796b25efc1c02f15ab716d69c655b38faab8b398a8d408ba5ff97f41ff2831a
dlSubdir: ghc-9.14.1-x86_64-unknown-linux
dlUri: https://downloads.haskell.org/~ghc/9.14.1/ghc-9.14.1-x86_64-rocky8-linux.tar.xz
Linux_Fedora:
'(>= 33 && < 38)': &ghc9141-x86_64-linux-fedora33
dlHash: b92cd3a35d8b5b83d36ddaf99d7a21907b1e18b32d73bc9457bd2a1029e5c66c
dlSubdir: ghc-9.14.1-x86_64-unknown-linux
dlUri: https://downloads.haskell.org/~ghc/9.14.1/ghc-9.14.1-x86_64-fedora33-linux.tar.xz
'>= 38':
dlHash: cd73823eb6747b16393d802bfb7feacfb0e789a4a6f10b7b5a62c6cdbe806e9e
dlSubdir: ghc-9.14.1-x86_64-unknown-linux
dlUri: https://downloads.haskell.org/ghc/9.14.1/ghc-9.14.1-x86_64-fedora38-linux.tar.xz
unknown_versioning: *ghc9141-x86_64-linux-rocky8
What does this mean? It means that Fedora versions lower than 33 will get a Centos bindist for GHC 9.6.7, but a rocky8 bindist for GHC 9.14.1. It also means that 9.14.1 has more fedora bindists than 9.6.7. So we do not have a simple distroversion ↔ bindist relationship that we could exploit.
Now, say I want to ship HLS, how do I build binaries and ship HLS so that it works on:
- fedora <33 (or: unknown versions)
- fedora >= 33 && < 38
- fedora >= 42
And that across all GHC versions?
Indeed… we need the most exhaustive matrix of distro+distroversion across all supported GHC versions, so we end up with:
ghcupDownloads:
HLS:
2.12.0.0:
viArch:
A_64:
Linux_Fedora:
'(>= 33 && < 38)': &hls-21200-64-fedora33
dlUri: https://downloads.haskell.org/~ghcup/unofficial-bindists/haskell-language-server/2.12.0.0/haskell-language-server-2.12.0.0-x86_64-linux-fedora33.tar.xz
dlSubdir: haskell-language-server-2.12.0.0
dlHash: 236a0786accddd440c277e0812b0a7ad1cab0ff92347a108aff015b6f4a84edc
'(>= 38 && < 42)':
dlUri: https://downloads.haskell.org/~ghcup/unofficial-bindists/haskell-language-server/2.12.0.0/haskell-language-server-2.12.0.0-x86_64-linux-fedora38.tar.xz
dlSubdir: haskell-language-server-2.12.0.0
dlHash: 6fd8d1b5b3876b50e337ee4a426f401a56254ca1544b4634aa753583888fdeaf
'>= 42':
dlUri: https://downloads.haskell.org/~ghcup/unofficial-bindists/haskell-language-server/2.12.0.0/haskell-language-server-2.12.0.0-x86_64-linux-fedora42.tar.xz
dlSubdir: haskell-language-server-2.12.0.0
dlHash: 6e57c3617665774a4b82bb2c7d07583af09421c1ebd4f8590471a1723247d9a5
unknown_versioning: *hls-21200-64-fedora33
This is huge CI complexity and there are cases where it breaks down. If we don’t get the HLS CI matrix correctly, the end user may get a runtime failure saying their HLS+GHC combination is not ABI compatible (this has happened). Uh. After all, GHCup in this case has no knowledge of ABI. We’re just trying to coerce GHCup into doing the right thing.
Now the other option is to make GHCup ABI aware. This is simple if HLS 2.12.0.0 only supports a single GHC version. But it doesn’t. So we have in fact a set of ABIs and they can be in all sorts of different combinations.
The only way this can work is that we record the ABIs per GHC version… but that also means we need separate HLS bindists per GHC version, so that GHCup can discover “oh, you need ABI abc84 for 9.6.7 and ABI dfe93 for 9.14.1”. Then GHCup has to download multiple bindists and install each of them. This is a hard deviation from how GHCup is designed and works today. I am not super keen on exploring this approach.
To me, the right solution is a different one and we are exploring this solution in stable-haskell. That is: instead of this madness of 20 GHC bindists, we only have two different linux bindists (just like rust):
- glibc
- musl
Then we link gmp statically (or use integer-simple), use the new libffi-clib package for libffi and hope that we find a solution for ncurses (which is a bit more delicate to statically link).
This makes the GHC matrix so simple, that it’s feasible to build binaries for HLS/HDB without teaching GHCup about the ABI problem.
Edit: Of course… I’m not even sure what the plan is for HDB and if a single HDB version is meant to support multiple GHC versions. If that is not the case, then there’s even less of a reason to ship it separately from GHC.
Wow, thanks for all of this background info! I assumed that the difficulty came from the DSL design, not the ramifications you describe
On Windows, I could build the executable with Stack and put it on the PATH with:
> stack unpack haskell-debugger
> cd haskell-debugger-0.11.0.0
# Create a stack.yaml file (see below)
> stack install
> hdb --version
Haskell Debugger, version 0.11.0.0
and (a little complicated, because there is not yet a Stackage snapshot for GHC 9.14.1) (EDIT: Updated: I originally picked up 2025-01-10 in error):
# stack.yaml
snapshot: nightly-2026-01-10 # GHC 9.12.3
compiler: ghc-9.14.1
extra-deps:
- dap-0.3.1.0@sha256:2e3f360463bba05cb062e764ae0dd958dba0ad67e83791e0d6d5adb3d259539a,2324
- haskell-debugger-view-0.2.0.0@sha256:0c46825ea9966ecc0c693bb2745aeef0b747781ea2b296009d0a5b96ec5aad80,1840
- implicit-hie-0.1.4.0@sha256:42a8bdc36713d98711c59b62fac238d81f6ce3ef7912752f38d456182260a5a3,3122
allow-newer: true
allow-newer-deps:
- aeson
- dap
- hie-bios
- indexed-traversable
- indexed-traversable-instances
- semialign
- tagged
- text-iso8601
- these
- time-compat
- uuid-types
However, I did not have any joy when I tried it in VS Code with a toy application (stack new testHdb) (also set up for GHC 9.14.1) and .vscode\launch.json:
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "haskell-debugger",
"request": "launch",
"name": "Haskell Debugger",
"projectRoot": "${workspaceFolder}",
"entryFile": "app/Main.hs",
"entryPoint": "main",
"entryArgs": [],
"extraGhcArgs": []
}
]
}
I did not edit entryFile: because the toy executable has app\Main.hs.
However, when I get to the step of ‘Start Debugging (F5)’, it does not stop at the break point that I have set for the line main :: IO (). Rather, I get:
Any tips? I tried a stack build, in case the project needed to be built first, but the behaviour was the same.
EDIT: I should clarify that I am running stack exec -- code . (that is, in the Stack environment) so that GHC 9.14.1 is also on the PATH. See below:
romes 8
Thanks Mike, I can absolutely try to help.
That message usually means that the debugger failed to start. Hopefully with an error.
Can you take a look at the “Output” pane in VSCode? You have to select “Haskell Debugger” from the list of options to see the output. If you could paste it out to me it’d be great.
mpilgrem 9
Here it is:
[Factory] Initialized
[Factory] Launching haskell-debugger on port 61591
[Factory] Waiting for debugger to be ready...
[Factory] haskell-debugger spawned...
[stderr] hdb: Uncaught exception ghc-internal:GHC.Internal.IO.Exception.IOException:
{handle: <file descriptor: 4>}: hDuplicateTo: illegal operation (handles are incompatible)
IPE backtrace:
HasCallStack backtrace:
ioException, called at libraries\gh
[stderr] c-internal\src\GHC\Internal\IO\Handle.hs:725:4 in ghc-internal:GHC.Internal.IO.Handle
[exit] haskell-debugger exited with code 1 and signal null
That hDuplicateTo rings some bells for me on Windows. I see if I can recall where …
EDIT: I was recalling this open GHC issue:
romes 10
Notes:
- Windows testing is still disabled in CI because I hadn’t been able to debug Windows issues without a machine yet ( Enable testing on Windows CI · Issue #149 · well-typed/haskell-debugger · GitHub )
- There are a lot of handles in the Debugger … for redirecting stdout of the debuggee to the console, for proxying it to the terminal pane, etc etc…
It will be hard for me to figure this out without a Windows machine at hand, but perhaps I can interest you in having a quick call with me to see if we can sort this out?
mpilgrem 11
Very happy, and grateful, for the offer of a call. I’ll message you privately.
romes 12
I had a chat with Mike and we figured out a few things. It is likely caused by the bug in the IO manager that Mike linked above (which apparently hints at a fundamental Windows limitation), and the debugger currently relies on duplicating handles to separate the debuggee vs debugger output meant for VSCode.
I have updated the ticket regarding Windows support for the debugger with this information (Windows support and Enable testing on Windows CI · Issue #149 · well-typed/haskell-debugger · GitHub)
I unfortunately can’t change this this weekend, but there is a clear-ish path forward for separating the debuggee output without relying on duplicating handles: by using the External Interpreter. There are other motivations for doing this so I will get to it soon enough.
In the meantime, WSL might come to the rescue.
Thanks Mike!
mpilgrem 13
@romes, thank you for your time and patience! I had joy (with one wrinkle, explained below) with Stack on Ubuntu 24.04.3 LTS (via WSL2).
The stack.yaml above needs an addition for the specified Cabal configuration option, namely:
configure-options:
haskell-debugger:
- --enable-executable-dynamic
The wrinkle is this: my toy executable depends on the main library in the same package ( named testHdb-0.1.0.0). If I use the debugger ‘out of the box’, it fails with output:
[stderr] cannot satisfy -package testHdb-0.1.0.0
(use -v for more information)
"HandlingTerminateServer"
[exit] haskell-debugger exited with code 0 and signal null
That is a limitation that is familar from HLS. It disappears after a successful build of the library component of the package (stack build testHdb:lib or, simply, stack build).
hasufell 14
Well, I was talking about the problem of packaging binaries that require to be ABI compatible with your installed GHC. There aren’t that many. Right now it’s essentially HLS and the new Haskell Debugger that fall in that category.
The DSL design would be an improvement regardless:
- it would probably remove some code duplication in GHCup that I was too lazy to address
- it will allow end users to plug in new binaries without having to hack on GHCup codebase (hlint, cabal-cache, etc.)
There will be some caveats though… e.g. XDG support is already fishy, due to possible filename collisions. But fixing that would be a breaking change, so I also procrastinated that: Better install strategies · Issue #375 · haskell/ghcup-hs · GitHub
Interestingly… that ticket is also related to the lack of progress in Support revisions to avoid in-place updates of bindists · Issue #361 · haskell/ghcup-hs · GitHub
So yeah… I think the DLS design is something that can be worked on regardless.

