Skip to content

Commit f6ae631

Browse files
committed
Merge branch 'master' into wip/debug-instance
2 parents 7aa3b09 + 6e6c0fb commit f6ae631

File tree

13 files changed

+297
-147
lines changed

13 files changed

+297
-147
lines changed

.github/workflows/debugger.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
on: [push, workflow_call, pull_request]
1+
on: [workflow_call, pull_request]
22

33
name: Debugger CI
44
jobs:

README.md

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,47 +12,40 @@ first major release is ready.
1212

1313
# Installation
1414

15+
Please find up to date installation instructions on the
16+
[project homepage](https://well-typed.github.io/haskell-debugger/)!
17+
1518
> [!WARNING]
16-
> `hdb` is only supported by the latest nightly GHC version.
19+
> `hdb` can currently be compiled with the 9.14 alpha pre-releases or with a nightly version
1720
> The first release it will be compatible with is GHC 9.14.
1821
1922
To install and use the debugger, you need the executable `hdb`
20-
and the VSCode extension `haskell-debugger-extension`.
23+
and the VSCode extension [Haskell Debugger](https://marketplace.visualstudio.com/items?itemName=Well-Typed.haskell-debugger-extension).
2124

2225
Since `hdb` implements the [Debug Adapter Protocol
2326
(DAP)](https://microsoft.github.io/debug-adapter-protocol/), it also supports
2427
debugging with tools such as vim, neovim, or emacs -- as long as a DAP client is
2528
installed and the `launch` arguments for `hdb` configured.
2629

27-
To build, install, and run `hdb` you currently need a nightly
28-
version of GHC in PATH. You can get one using
29-
[GHCup](https://ghcup.readthedocs.io/en/latest/guide/), or [building from source](https://gitlab.haskell.org/ghc/ghc/-/wikis/building/preparation):
30+
To run the debugger, the same version of GHC which compiled it needs to be in
31+
PATH. Make sure the DAP client knows this. For instance, to launch VSCode with a specific GHC use:
3032
```
31-
ghcup config add-release-channel https://ghc.gitlab.haskell.org/ghcup-metadata/ghcup-nightlies-0.0.7.yaml
32-
ghcup install ghc latest-nightly
33-
PATH=$(dirname $(ghcup whereis ghc latest-nightly)):$PATH cabal install haskell-debugger:hdb --enable-executable-dynamic --allow-newer=ghc-bignum,containers,time,ghc
33+
PATH=/path/to/ghc-dir:$PATH code /path/to/proj
3434
```
3535

36-
To run the debugger, the same nightly version of GHC needs to be in PATH. Make
37-
sure the DAP client knows this. For instance, to launch VSCode use:
38-
```
39-
PATH=$(dirname $(ghcup whereis ghc latest-nightly)):$PATH code /path/to/proj
40-
```
41-
Currently, to install the debugger extension, download the `.vsix` file from the
42-
GitHub release artifacts and drag and drop it to the extensions side panel. In
43-
the future we will release it on the marketplace.
44-
4536
# Usage
4637

4738
To use the debugger in VSCode, select the debugger tab, select Haskell Debugger,
4839
and create a `launch.json` file by clicking the debugger settings icon (next to
49-
the green run button).
40+
the green run button). Now, it is also supported to just Run a file which
41+
contains a `main` function.
5042

5143
The `launch.json` file contains some settings about the debugger session here.
5244
Namely:
5345

5446
| Setting | Description |
5547
| --- | --- |
48+
| `projectRoot` | the full path to the project root. this is typically `${workspaceFolder}`, a value which is interpolated by the editor with the actual path |
5649
| `entryFile` | the relative path from the project root to the file with the entry point for execution |
5750
| `entryPoint` | the name of the function that is called to start execution |
5851
| `entryArgs` | the arguments passed to the `entryPoint`. If the `entryPoint` is `main`, these arguments are passed as environment arguments (as in `getArgs`) rather than direct function arguments. |
@@ -63,8 +56,6 @@ Change them accordingly.
6356
To run the debugger, simply hit the green run button.
6457
See the Features section below for what is currently supported.
6558

66-
Note: Listing global variables is only supported in GHC versions newer than May 6, 2025
67-
6859
# Related Work
6960

7061
`hdb` is inspired by the original
@@ -136,7 +127,6 @@ nix-build
136127

137128
```
138129
cd test/integration-tests
139-
nix-shell
140130
make GHC=/path/to/recent/ghc \
141131
DEBUGGER=$(cd ../.. && cabal list-bin -w /path/to/recent/ghc exe:hdb)
142132
```

haskell-debugger/GHC/Debugger/Stopped/Variables.hs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import Control.Monad.Reader
88

99
import GHC
1010
import GHC.Types.FieldLabel
11+
import GHC.Types.Id.Info
12+
import GHC.Types.Var
1113
import GHC.Runtime.Eval
1214
import GHC.Core.DataCon
15+
import GHC.Core.TyCo.Rep
1316
import qualified GHC.Runtime.Debugger as GHCD
1417
import qualified GHC.Runtime.Heap.Inspect as GHCI
1518

@@ -23,10 +26,17 @@ import GHC.Debugger.Utils
2326
-- | 'TyThing' to 'VarInfo'. The 'Bool' argument indicates whether to force the
2427
-- value of the thing (as in @True = :force@, @False = :print@)
2528
tyThingToVarInfo :: TyThing -> Debugger VarInfo
26-
tyThingToVarInfo = \case
27-
t@(AConLike c) -> VarInfo <$> display c <*> display t <*> display t <*> pure False <*> pure NoVariables
28-
t@(ATyCon c) -> VarInfo <$> display c <*> display t <*> display t <*> pure False <*> pure NoVariables
29-
t@(ACoAxiom c) -> VarInfo <$> display c <*> display t <*> display t <*> pure False <*> pure NoVariables
29+
tyThingToVarInfo t = case t of
30+
AConLike c -> VarInfo <$> display c <*> display t <*> display t <*> pure False <*> pure NoVariables
31+
ATyCon c -> VarInfo <$> display c <*> display t <*> display t <*> pure False <*> pure NoVariables
32+
ACoAxiom c -> VarInfo <$> display c <*> display t <*> display t <*> pure False <*> pure NoVariables
33+
AnId i
34+
| DataConWrapId data_con <- idDetails i
35+
-- Newtype cons don't have a runtime representation, so we can't obtain
36+
-- terms! Simply print the newtype cons like we do data cons.
37+
-- See Note [Newtype workers]
38+
, isNewTyCon (dataConTyCon data_con)
39+
-> VarInfo <$> display data_con <*> display t <*> display t <*> pure False <*> pure NoVariables
3040
AnId i -> do
3141
let key = FromId i
3242
term <- obtainTerm key
@@ -73,11 +83,21 @@ termToVarInfo :: TermKey -> Term -> Debugger VarInfo
7383
termToVarInfo key term0 = do
7484
-- Make a VarInfo for a term
7585
let
76-
isThunk
77-
| Suspension{} <- term0 = True
78-
| otherwise = False
7986
ty = GHCI.termType term0
8087

88+
-- Check for function types explicitly since they seem to always match Suspension
89+
-- but should not be shown as thunks in the UI.
90+
checkFn (FunTy _ _ _ _) = True
91+
checkFn (ForAllTy _ t) = checkFn t
92+
checkFn _ = False
93+
isFn = checkFn ty
94+
95+
isThunk = if not isFn then
96+
case term0 of
97+
Suspension{} -> True
98+
_ -> False
99+
else False
100+
81101
term <- if not isThunk && isBoringTy ty
82102
then forceTerm key term0 -- make sure that if it's an evaluated boring term then it is /fully/ evaluated.
83103
else pure term0
@@ -90,13 +110,15 @@ termToVarInfo key term0 = do
90110
| isBoringTy ty = t
91111
| otherwise = case t of
92112
Term{} -> t{subTerms = []}
93-
NewtypeWrap{wrapped_term} -> t{wrapped_term = termHead wrapped_term}
94113
_ -> t
95114
varName <- display key
96115
varType <- display ty
97-
varValue <- do
98-
_ <- onDebugInstance term ty
99-
display =<< GHCD.showTerm (termHead term)
116+
-- Pass type as value for functions since actual value is useless
117+
varValue <- if isFn
118+
then pure $ "<fn> :: " ++ varType
119+
else do
120+
_ <- onDebugInstance term ty
121+
display =<< GHCD.showTerm (termHead term)
100122
-- liftIO $ print (varName, varType, varValue, GHCI.isFullyEvaluatedTerm term)
101123

102124
-- The VarReference allows user to expand variable structure and inspect its value.

hdb/Development/Debug/Adapter/Exit.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ exitCleanupWithMsg
7171
-- killing the output thread with @destroyDebugSession@)
7272
-> String
7373
-- ^ Error message, logged with notification
74-
-> DebugAdaptor ()
74+
-> DebugAdaptor a
7575
exitCleanupWithMsg final_handle msg = do
7676
destroyDebugSession -- kill all session threads (including the output thread)
7777
do -- flush buffer and get all pending output from GHC

hdb/Development/Debug/Adapter/Init.hs

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44
{-# LANGUAGE LambdaCase #-}
55
{-# LANGUAGE OverloadedStrings #-}
66
{-# LANGUAGE RecordWildCards #-}
7+
{-# LANGUAGE ViewPatterns #-}
78

89
-- | TODO: This module should be called Launch.
910
module Development.Debug.Adapter.Init where
1011

1112
import qualified Data.Text as T
1213
import qualified Data.Text.IO as T
1314
import qualified System.Process as P
15+
import Control.Monad.Except
16+
import Control.Monad.Trans
1417
import Data.Function
1518
import Data.Functor
19+
import Data.Maybe
1620
import Data.Version (Version(..), showVersion, makeVersion)
1721
import Control.Monad.IO.Class
1822
import System.IO
@@ -62,16 +66,16 @@ data LaunchArgs
6266
= LaunchArgs
6367
{ __sessionId :: Maybe String
6468
-- ^ SessionID, set by VSCode client
65-
, projectRoot :: FilePath
69+
, projectRoot :: Maybe FilePath
6670
-- ^ Absolute path to the project root
67-
, entryFile :: FilePath
71+
, entryFile :: Maybe FilePath
6872
-- ^ The file with the entry point e.g. @app/Main.hs@
69-
, entryPoint :: String
73+
, entryPoint :: Maybe String
7074
-- ^ Either @main@ or a function name
71-
, entryArgs :: [String]
75+
, entryArgs :: Maybe [String]
7276
-- ^ The arguments to either set as environment arguments when @entryPoint = "main"@
7377
-- or function arguments otherwise.
74-
, extraGhcArgs :: [String]
78+
, extraGhcArgs :: Maybe [String]
7579
-- ^ Additional arguments to pass to the GHC invocation inferred by hie-bios for this project
7680
} deriving stock (Show, Eq, Generic)
7781
deriving anyclass FromJSON
@@ -80,39 +84,53 @@ data LaunchArgs
8084
-- * Launch Debugger
8185
--------------------------------------------------------------------------------
8286

87+
-- | Exception type for when initialization fails
88+
newtype InitFailed = InitFailed String deriving Show
89+
8390
-- | Initialize debugger
8491
--
85-
-- Returns @True@ if successful.
86-
initDebugger :: Recorder (WithSeverity InitLog) -> LaunchArgs -> DebugAdaptor Bool
87-
initDebugger l LaunchArgs{__sessionId, projectRoot, entryFile = entryFile, entryPoint, entryArgs, extraGhcArgs} = do
92+
-- Returns @()@ if successful, throws @InitFailed@ otherwise
93+
initDebugger :: Recorder (WithSeverity InitLog) -> LaunchArgs -> ExceptT InitFailed DebugAdaptor ()
94+
initDebugger l LaunchArgs{ __sessionId
95+
, projectRoot = givenRoot
96+
, entryFile = entryFileMaybe
97+
, entryPoint = fromMaybe "main" -> entryPoint
98+
, entryArgs = fromMaybe [] -> entryArgs
99+
, extraGhcArgs = fromMaybe [] -> extraGhcArgs
100+
} = do
88101
syncRequests <- liftIO newEmptyMVar
89102
syncResponses <- liftIO newEmptyMVar
103+
104+
entryFile <- case entryFileMaybe of
105+
Nothing -> throwError $ InitFailed "Missing \"entryFile\" key in debugger configuration"
106+
Just ef -> pure ef
107+
108+
projectRoot <- maybe (liftIO getCurrentDirectory) pure givenRoot
109+
90110
let hieBiosLogger = cmapWithSev FlagsLog l
91111
cradle <- liftIO (hieBiosCradle hieBiosLogger projectRoot entryFile) >>=
92112
\ case
93-
Left e ->
94-
exitWithMsg e
95-
113+
Left e -> throwError $ InitFailed e
96114
Right c -> pure c
97115

98-
Output.console $ T.pack "Checking GHC version against debugger version..."
116+
lift $ Output.console $ T.pack "Checking GHC version against debugger version..."
99117
-- GHC is found in PATH (by hie-bios as well).
100118
actualVersion <- liftIO (hieBiosRuntimeGhcVersion hieBiosLogger cradle) >>=
101119
\ case
102-
Left e ->
103-
exitWithMsg e
120+
Left e -> throwError $ InitFailed e
104121
Right c -> pure c
105122
-- Compare the GLASGOW_HASKELL version (e.g. 913) with the actualVersion (e.g. 9.13.1):
106123
when (compileTimeGhcWithoutPatchVersion /= forgetPatchVersion actualVersion) $ do
107-
exitWithMsg $ "Aborting...! The GHC version must be the same which " ++
108-
"ghc-debug-adapter was compiled against (" ++
109-
showVersion compileTimeGhcWithoutPatchVersion++
110-
"). Instead, got " ++ (showVersion actualVersion) ++ "."
124+
throwError $ InitFailed $
125+
"Aborting...! The GHC version must be the same which " ++
126+
"ghc-debug-adapter was compiled against (" ++
127+
showVersion compileTimeGhcWithoutPatchVersion++
128+
"). Instead, got " ++ (showVersion actualVersion) ++ "."
111129

112-
Output.console $ T.pack "Discovering session flags with hie-bios..."
130+
lift $ Output.console $ T.pack "Discovering session flags with hie-bios..."
113131
mflags <- liftIO (hieBiosFlags hieBiosLogger cradle projectRoot entryFile)
114132
case mflags of
115-
Left e -> exitWithMsg e
133+
Left e -> throwError $ InitFailed e
116134
Right flags -> do
117135

118136
let nextFreshBreakpointId = 0
@@ -138,7 +156,7 @@ initDebugger l LaunchArgs{__sessionId, projectRoot, entryFile = entryFile, entry
138156
finished_init <- liftIO $ newEmptyMVar
139157

140158
let absEntryFile = normalise $ projectRoot </> entryFile
141-
registerNewDebugSession (maybe "debug-session" T.pack __sessionId) DAS{entryFile=absEntryFile,..}
159+
lift $ registerNewDebugSession (maybe "debug-session" T.pack __sessionId) DAS{entryFile=absEntryFile,..}
142160
[ debuggerThread l finished_init writeDebuggerOutput projectRoot flags extraGhcArgs absEntryFile defaultRunConf syncRequests syncResponses
143161
, handleDebuggerOutput readDebuggerOutput
144162
, stdoutCaptureThread
@@ -147,14 +165,13 @@ initDebugger l LaunchArgs{__sessionId, projectRoot, entryFile = entryFile, entry
147165

148166
-- Do not return until the initialization is finished
149167
liftIO (takeMVar finished_init) >>= \case
150-
Right () -> return True
168+
Right () -> pure ()
151169
Left e -> do
152170
-- The process terminates cleanly with an error code (probably exit failure = 1)
153171
-- This can happen if compilation fails and the compiler exits cleanly.
154172
--
155173
-- Instead of signalInitialized, respond with error and exit.
156-
exitCleanupWithMsg readDebuggerOutput e
157-
return False
174+
throwError $ InitFailed e
158175

159176
-- | This thread captures stdout from the debugger and sends it to the client.
160177
-- NOTE, redirecting the stdout handle is a process-global operation. So this thread

hdb/Main.hs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import System.Environment
55
import Data.Maybe
66
import Data.Version
77
import Text.Read
8+
import Control.Monad.Except
9+
import Control.Monad.IO.Class
810

911
import DAP
1012

@@ -209,14 +211,19 @@ talk l = \ case
209211
sendInitializeResponse
210212
--------------------------------------------------------------------------------
211213
CommandLaunch -> do
212-
success <- initDebugger (cmapWithSev InitLog l) =<< getArguments
213-
if success then do
214-
sendLaunchResponse -- ack
215-
sendInitializedEvent -- our debugger is only ready to be configured after it has launched the session
216-
else
217-
sendError ErrorMessageCancelled Nothing
214+
launch_args <- getArguments
215+
merror <- runExceptT $ initDebugger (cmapWithSev InitLog l) launch_args
216+
case merror of
217+
Right () -> do
218+
sendLaunchResponse -- ack
219+
sendInitializedEvent -- our debugger is only ready to be configured after it has launched the session
220+
Left (InitFailed err) -> do
221+
sendErrorResponse (ErrorMessage (T.pack err)) Nothing
222+
exitCleanly
218223
--------------------------------------------------------------------------------
219-
CommandAttach -> undefined
224+
CommandAttach -> do
225+
sendErrorResponse (ErrorMessage (T.pack "hdb does not support \"attach\" mode yet")) Nothing
226+
exitCleanly
220227
--------------------------------------------------------------------------------
221228
CommandBreakpointLocations -> commandBreakpointLocations
222229
CommandSetBreakpoints -> commandSetBreakpoints
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
module Main where
2+
3+
newtype MyInt = MyInt Int
4+
deriving (Show)
5+
6+
newtype MyIntX = MyIntX X
7+
deriving (Show)
8+
9+
10+
data X = X MyInt
11+
deriving Show
12+
13+
newtype Y = Y MyInt
14+
deriving Show
15+
16+
newtype Y2 = Y2 MyIntX
17+
deriving Show
18+
19+
data R = R {_fieldOfR :: MyInt}
20+
deriving Show
21+
22+
main :: IO ()
23+
main = do
24+
putStrLn $ show $ bpmi $ MyInt 42
25+
putStrLn $ show $ bpx $ X (MyInt 42)
26+
putStrLn $ show $ bpy $ Y (MyInt 42)
27+
putStrLn $ show $ bpy2 $ Y2 (MyIntX (X (MyInt 42)))
28+
putStrLn $ show $ bpr $ R (MyInt 42)
29+
30+
bpmi :: MyInt -> MyInt
31+
bpmi mi = mi
32+
33+
bpx :: X -> X
34+
bpx x = x
35+
36+
bpy :: Y -> Y
37+
bpy y = y
38+
39+
bpy2 :: Y2 -> Y2
40+
bpy2 y2 = y2
41+
42+
bpr :: R -> R
43+
bpr r = r
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
newtype MyIntTy = MyIntCon Int
2+
3+
main :: IO ()
4+
main = pure ()
5+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
main = do
2+
putStrLn "T71 is running!"

0 commit comments

Comments
 (0)