Skip to content

Commit ffe4d17

Browse files
committed
Allow inspecting arbitrarily deep structures and displaying forced strings in full
This is done by refactoring how we were preserving terms in a cache and when how we were returning the fields of existing terms. Namely, any nested-field we were getting by looking at the subterms of the parent -- and the subterms of the parent were only traversed up to depth 5. In a structure such as (p = Depth "d=1" (Depth "d=2" (Depth "d=3" (Depth "d=4" (Depth "d=5" (Depth "d=6" OK)))))) only the top-most identifier "p" would get a Term cached and all nested fields were gotten from it. Now, we've refactored and simplified the code s.t. getting this right is much easier: Every referenceable term (either directly by name or via a name + path of fields) can be obtained by traversing the heap using `GHC.Debugger.Runtime.obtainTerm`. They are indexed by the new `TermKey` from `GHC.Debugger.Runtime.Term.Key`. Moreover, `obtainTerm` uses a cache (`GHC.Debugger.Runtime.Term.Cache`) to avoid redundant computation -- but if it misses the cache it always knows how to reconstruct a term. The reconstruction for Ids is trivially @obtainTermFromId@, while for Id+Paths it is done by recursively `seq`ing the wanted subterms until the leaf is found and returned. Fixes #8 and #9
1 parent bcc314f commit ffe4d17

File tree

8 files changed

+290
-129
lines changed

8 files changed

+290
-129
lines changed

ghc-debugger.cabal

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ library
4747
GHC.Debugger.Stopped,
4848
GHC.Debugger.Stopped.Variables,
4949
GHC.Debugger.Utils,
50+
GHC.Debugger.Runtime,
51+
52+
GHC.Debugger.Runtime.Term.Key,
53+
GHC.Debugger.Runtime.Term.Cache,
54+
5055
GHC.Debugger.Monad,
5156
GHC.Debugger.Interface.Messages
5257
-- other-modules:

ghc-debugger/GHC/Debugger/Evaluation.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,5 +153,5 @@ inspectName n = do
153153
Nothing -> do
154154
liftIO . putStrLn =<< display (text "Failed to lookup name: " <+> ppr n)
155155
pure Nothing
156-
Just tt -> Just <$> tyThingToVarInfo 2 tt
156+
Just tt -> Just <$> tyThingToVarInfo tt
157157

ghc-debugger/GHC/Debugger/Monad.hs

Lines changed: 34 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import qualified Data.IntMap as IM
4242
import Control.Monad.Reader
4343

4444
import GHC.Debugger.Interface.Messages
45+
import GHC.Debugger.Runtime.Term.Key
46+
import GHC.Debugger.Runtime.Term.Cache
4547
import System.Posix.Signals
4648

4749
-- | A debugger action.
@@ -57,20 +59,21 @@ data DebuggerState = DebuggerState
5759
{ activeBreakpoints :: IORef (ModuleEnv (IM.IntMap (BreakpointStatus, BreakpointKind)))
5860
-- ^ Maps a 'BreakpointId' in Trie representation to the
5961
-- 'BreakpointStatus' it was activated with.
60-
, varReferences :: IORef (IM.IntMap (Name, Term), NameEnv Int)
62+
63+
, varReferences :: IORef (IM.IntMap TermKey, TermKeyMap Int)
6164
-- ^ When we're stopped at a breakpoint, this maps variable reference to
6265
-- Terms to allow further inspection and forcing by reference.
6366
--
6467
-- This map is only valid while stopped in this context. After stepping
6568
-- or resuming evaluation in any available way, this map becomes invalid
6669
-- and should therefore be cleaned.
6770
--
68-
-- The NameEnv map is a reverse lookup map to find which references already exist for given names
69-
, varFieldsMap :: IORef (NameEnv (IM.IntMap Name))
70-
-- ^ A mapping from Name to an IntMap mapping indices to their unique Names
71-
-- e.g. `x :-> { 1 :-> _1, 2 :-> _2 }
72-
-- This map allows us to re-use Names for sub-fields rather than creating
73-
-- them new every time. See 'mkIndexVarName'
71+
-- The TermKeyMap map is a reverse lookup map to find which references
72+
-- already exist for given names
73+
74+
, termCache :: IORef TermCache
75+
-- ^ TermCache
76+
7477
, genUniq :: IORef Int
7578
-- ^ Generates unique ints
7679
}
@@ -309,54 +312,29 @@ getModuleByPath path = do
309312
--------------------------------------------------------------------------------
310313

311314
-- | Find a variable's associated Term and Name by reference ('Int')
312-
lookupVarByReference :: Int -> Debugger (Maybe (Name, Term))
315+
lookupVarByReference :: Int -> Debugger (Maybe TermKey)
313316
lookupVarByReference i = do
314317
ioref <- asks varReferences
315318
(rm, _) <- readIORef ioref & liftIO
316319
return $ IM.lookup i rm
317320

318-
-- | Inserts a mapping from the given variable reference to the variable's
319-
-- associated Term and the Name it is bound to for display
320-
--
321-
-- Returns: the variable reference (either a fresh one, or the existing one for this name)
322-
insertVarReference :: Name -> Term -> Debugger Int
323-
insertVarReference name term = do
324-
ioref <- asks varReferences
325-
(rm, nm) <- readIORef ioref & liftIO
326-
(i, nm') <- case lookupNameEnv nm name of
327-
Nothing -> do
321+
-- | Finds or creates an integer var reference for the given 'TermKey'.
322+
-- TODO: Arguably, this mapping should be part of the debug-adapter, and
323+
-- ghc-debugger should deal in 'TermKey' terms only.
324+
getVarReference :: TermKey -> Debugger Int
325+
getVarReference key = do
326+
ioref <- asks varReferences
327+
(rm, tkm) <- readIORef ioref & liftIO
328+
(i, tkm') <- case lookupTermKeyMap key tkm of
329+
Nothing -> do
328330
new_i <- freshInt
329-
return (new_i, extendNameEnv nm name new_i)
330-
Just existing ->
331-
return (existing, nm)
332-
let
333-
rm' = IM.insert i (name, term) rm
334-
writeIORef ioref (rm', nm') & liftIO
331+
return (new_i, insertTermKeyMap key new_i tkm)
332+
Just existing_i ->
333+
return (existing_i, tkm)
334+
let rm' = IM.insert i key rm
335+
writeIORef ioref (rm', tkm') & liftIO
335336
return i
336337

337-
-- | Create or find the 'Name' for a parent Name by positional index
338-
--
339-
-- The name is cached the first time it is created and re-used for subsequent
340-
-- accesses in the same context.
341-
mkPositionalVarFieldName :: Name {-^ Parent Name -} -> Int {-^ Index -} -> Debugger Name
342-
mkPositionalVarFieldName parent ix = do
343-
vfm_ref <- asks varFieldsMap
344-
vfm <- liftIO $ readIORef vfm_ref
345-
let ixmap = case lookupNameEnv vfm parent of
346-
Nothing -> mempty
347-
Just ixm -> ixm
348-
case IM.lookup ix ixmap of
349-
Just fieldName -> return fieldName
350-
Nothing -> do
351-
u <- liftIO $ uniqFromTag 'F'
352-
let
353-
fieldName
354-
= mkDerivedInternalName (\nocc -> mkVarOcc ("_" ++ show @Int ix))
355-
u parent
356-
liftIO $ writeIORef vfm_ref $
357-
extendNameEnv vfm parent (IM.insert ix fieldName ixmap)
358-
return fieldName
359-
360338
-- | Whenever we run a request that continues execution from the current
361339
-- suspended state, such as Next,Step,Continue, this function should be called
362340
-- to delete the variable references that become invalid as we leave the
@@ -367,17 +345,18 @@ mkPositionalVarFieldName parent ix = do
367345
-- See also section "Lifetime of Objects References" in the DAP specification.
368346
leaveSuspendedState :: Debugger ()
369347
leaveSuspendedState = do
370-
-- TODO:
371-
-- [ ] Preserve bindings introduced by evaluate requests?
372348
ioref <- asks varReferences
373-
vfRef <- asks varFieldsMap
374349
liftIO $ writeIORef ioref mempty
375-
liftIO $ writeIORef vfRef mempty
376350

377351
--------------------------------------------------------------------------------
378352
-- Utilities
379353
--------------------------------------------------------------------------------
380354

355+
defaultDepth :: Int
356+
defaultDepth = 2 -- the depth determines how much of the runtime structure is traversed.
357+
-- @obtainTerm@ and friends handle fetching arbitrarily nested data structures
358+
-- so we only depth enough to get to the next level of subterms.
359+
381360
-- | Evaluate a suspended Term to WHNF.
382361
--
383362
-- Used in @'getVariables'@ to reply to a variable introspection request.
@@ -393,9 +372,11 @@ seqTerm term = do
393372
() <- fromEvalResult r
394373
let
395374
forceThunks = False {- whether to force the thunk subterms -}
396-
forceDepth = 5
375+
forceDepth = defaultDepth
397376
cvObtainTerm hsc_env forceDepth forceThunks ty val
398-
NewtypeWrap{wrapped_term} -> seqTerm wrapped_term
377+
NewtypeWrap{wrapped_term} -> do
378+
wrapped_term' <- seqTerm wrapped_term
379+
return term{wrapped_term=wrapped_term'}
399380
_ -> return term
400381

401382
-- | Evaluate a Term to NF

ghc-debugger/GHC/Debugger/Runtime.hs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{-# LANGUAGE GADTs, LambdaCase, NamedFieldPuns #-}
2+
module GHC.Debugger.Runtime where
3+
4+
import Data.IORef
5+
import Control.Monad.Reader
6+
import Control.Monad.IO.Class
7+
import qualified Data.List as L
8+
9+
import GHC
10+
import GHC.Types.Id
11+
import GHC.Types.FieldLabel
12+
import GHC.Tc.Utils.TcType
13+
import GHC.Runtime.Eval
14+
import GHC.Types.Unique.Supply (uniqFromTag)
15+
import GHC.Types.Name.Env
16+
17+
import GHC.Debugger.Runtime.Term.Key
18+
import GHC.Debugger.Runtime.Term.Cache
19+
import GHC.Debugger.Monad
20+
21+
-- | Obtain the runtime 'Term' from a 'TermKey'.
22+
--
23+
-- The 'TermKey' will be looked up in the 'TermCache' to avoid recomputing the
24+
-- 'Term' if possible. On a cache miss the Term will be reconstructed from
25+
-- scratch and stored in the cache.
26+
obtainTerm :: TermKey -> Debugger Term
27+
obtainTerm key = do
28+
tc_ref <- asks termCache
29+
tc <- liftIO $ readIORef tc_ref
30+
case lookupTermCache key tc of
31+
-- cache miss: reconstruct, then store.
32+
Nothing ->
33+
let
34+
-- For boring types we want to get the value as it is (by traversing it to
35+
-- the end), rather than stopping short and returning a suspension (e.g.
36+
-- for the string tail), because boring types are printed whole rather than
37+
-- being represented by an expandable structure.
38+
depth i = if isBoringTy (GHC.idType i) then maxBound else defaultDepth
39+
40+
-- Recursively get terms until we hit the desired key.
41+
getTerm = \case
42+
FromId i -> GHC.obtainTermFromId (depth i) False{-don't force-} i
43+
FromPath k pf -> do
44+
term <- getTerm k >>= \case
45+
-- When the key points to a Suspension, the real thing should
46+
-- already be forced. It's just that the shallow depth meant we
47+
-- returned a Suspension nonetheless while recursing in `getTerm`.
48+
t@Suspension{} -> do
49+
t' <- seqTerm t
50+
-- update term cache with intermediate values?
51+
-- insertTermCache k t'
52+
return t'
53+
t -> return t
54+
return $ case term of
55+
Term{dc=Right dc, subTerms} -> case pf of
56+
PositionalIndex ix -> subTerms !! (ix-1)
57+
LabeledField fl ->
58+
case L.findIndex (== fl) (map flSelector $ dataConFieldLabels dc) of
59+
Just ix -> subTerms !! (ix-1)
60+
Nothing -> error "Couldn't find labeled field in dataConFieldLabels"
61+
NewtypeWrap{wrapped_term} ->
62+
wrapped_term -- regardless of PathFragment
63+
_ -> error "Unexpected term for the given TermKey"
64+
in do
65+
term <- getTerm key
66+
liftIO $ writeIORef tc_ref (insertTermCache key term tc)
67+
return term
68+
69+
-- cache hit
70+
Just hit -> return hit
71+
72+
-- | A boring type is one for which we don't care about the structure and would
73+
-- rather see "whole" when being inspected. Strings and literals are a good
74+
-- example, because it's more useful to see the string value than it is to see
75+
-- a linked list of characters where each has to be forced individually.
76+
isBoringTy :: Type -> Bool
77+
isBoringTy t = isDoubleTy t || isFloatTy t || isIntTy t || isWordTy t || isStringTy t
78+
|| isIntegerTy t || isNaturalTy t || isCharTy t
79+
80+
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{-# LANGUAGE GADTs #-}
2+
module GHC.Debugger.Runtime.Term.Cache where
3+
4+
import GHC
5+
import GHC.Types.Id
6+
import GHC.Tc.Utils.TcType
7+
import GHC.Runtime.Eval
8+
import GHC.Types.Unique.Supply (uniqFromTag)
9+
import GHC.Types.Var.Env
10+
11+
import GHC.Debugger.Runtime.Term.Key
12+
13+
import Data.Map (Map)
14+
import qualified Data.Map as M
15+
16+
--------------------------------------------------------------------------------
17+
-- * Term Cache
18+
--------------------------------------------------------------------------------
19+
20+
-- | A term cache maps Names to Terms.
21+
--
22+
-- We use the term cache to avoid redundant computation forcing (unique) names
23+
-- we've already forced before.
24+
--
25+
-- A kind of trie map from 'TermKey's. The Map entry for no-path-fragments is
26+
-- the 'Term' of the original 'Id'.
27+
type TermCache = TermKeyMap Term
28+
29+
-- | Lookup a 'TermKey' in a 'TermCache'.
30+
-- Returns @Nothing@ for a cache miss and @Just@ otherwise.
31+
lookupTermCache :: TermKey -> TermCache -> Maybe Term
32+
lookupTermCache = lookupTermKeyMap
33+
34+
-- | Inserts a 'Term' for the given 'TermKey' in the 'TermCache'.
35+
--
36+
-- Overwrites existing values.
37+
insertTermCache :: TermKey -> Term -> TermCache -> TermCache
38+
insertTermCache = insertTermKeyMap
39+
40+
--------------------------------------------------------------------------------
41+
-- * TermKeyMap
42+
--------------------------------------------------------------------------------
43+
44+
-- | Mapping from 'TermKey' to @a@. Backs 'TermCache', but is more general.
45+
type TermKeyMap a = IdEnv (Map [PathFragment] a)
46+
47+
-- | Lookup a 'TermKey' in a 'TermKeyMap'.
48+
lookupTermKeyMap :: TermKey -> TermKeyMap a -> Maybe a
49+
lookupTermKeyMap key tc = do
50+
let (i, path) = unconsTermKey key
51+
path_map <- lookupVarEnv tc i
52+
M.lookup path path_map
53+
54+
-- | Inserts a 'Term' for the given 'TermKey' in the 'TermKeyMap'.
55+
--
56+
-- Overwrites existing values.
57+
insertTermKeyMap :: TermKey -> a -> TermKeyMap a -> TermKeyMap a
58+
insertTermKeyMap key term tc =
59+
let
60+
(i, path) = unconsTermKey key
61+
new_map = case lookupVarEnv tc i of
62+
Nothing -> M.singleton path term
63+
Just existing_map -> M.insert path term existing_map
64+
in extendVarEnv tc i new_map
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{-# LANGUAGE GADTs, ViewPatterns #-}
2+
module GHC.Debugger.Runtime.Term.Key where
3+
4+
import Prelude hiding ((<>))
5+
6+
import GHC
7+
import GHC.Utils.Outputable
8+
import GHC.Types.Id
9+
import GHC.Tc.Utils.TcType
10+
import GHC.Runtime.Eval
11+
import GHC.Types.Unique.Supply (uniqFromTag)
12+
import GHC.Types.Name.Env
13+
14+
-- | A 'TermKey' serves to fetch a Term in a Debugger session.
15+
-- Note: A 'TermKey' is only valid in the stopped context it was created in.
16+
data TermKey where
17+
-- | Obtain a term from an Id.
18+
FromId :: Id -> TermKey
19+
20+
-- | Append a PathFragment to the current Term Key. Used to construct keys
21+
-- for indexed and labeled fields.
22+
FromPath :: TermKey -> PathFragment -> TermKey
23+
24+
-- | A term may be identified by an 'Id' (such as a local variable) plus a list
25+
-- of 'PathFragment's to an arbitrarily nested field.
26+
data PathFragment
27+
-- | A positional index is an index from 1 to inf
28+
= PositionalIndex Int
29+
-- | A labeled field indexes a datacon fields by name
30+
| LabeledField Name
31+
deriving (Eq, Ord)
32+
33+
instance Outputable TermKey where
34+
ppr (FromId i) = ppr i
35+
ppr (FromPath _ last_p) = ppr last_p
36+
37+
instance Outputable PathFragment where
38+
ppr (PositionalIndex i) = text "_" <> ppr i
39+
ppr (LabeledField n) = ppr n
40+
41+
-- | >>> unconsTermKey (FromPath (FromPath (FromId hi) (Pos 1)) (Pos 2))
42+
-- (hi, [1, 2])
43+
unconsTermKey :: TermKey -> (Id, [PathFragment])
44+
unconsTermKey = go [] where
45+
go acc (FromId i) = (i, reverse acc)
46+
go acc (FromPath k p) = go (p:acc) k

0 commit comments

Comments
 (0)