Skip to content

Commit 1b4d225

Browse files
committed
Expand Terms as they the user expands them
The defaultDepth to which we expand Ids is low because otherwise the performance cost is too great and perceivably slow. However, the user has to iteractively expand the branches of the representation tree of variables as they want to view them. We can leverage this interactivity to overcome the limited depth to which we expand. Simply: when the user expands a variable, always expand its corresponding Term a little bit more. Fixes #97
1 parent eddc66a commit 1b4d225

File tree

6 files changed

+78
-24
lines changed

6 files changed

+78
-24
lines changed

haskell-debugger/GHC/Debugger/Runtime.hs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{-# LANGUAGE GADTs, LambdaCase, NamedFieldPuns #-}
1+
{-# LANGUAGE OrPatterns, GADTs, LambdaCase, NamedFieldPuns #-}
22
module GHC.Debugger.Runtime where
33

44
import Data.IORef
@@ -9,6 +9,7 @@ import GHC
99
import GHC.Types.FieldLabel
1010
import GHC.Tc.Utils.TcType
1111
import GHC.Runtime.Eval
12+
import GHC.Runtime.Heap.Inspect
1213

1314
import GHC.Debugger.Runtime.Term.Key
1415
import GHC.Debugger.Runtime.Term.Cache
@@ -21,8 +22,9 @@ import GHC.Debugger.Monad
2122
-- scratch and stored in the cache.
2223
obtainTerm :: TermKey -> Debugger Term
2324
obtainTerm key = do
24-
tc_ref <- asks termCache
25-
tc <- liftIO $ readIORef tc_ref
25+
hsc_env <- getSession
26+
tc_ref <- asks termCache
27+
tc <- liftIO $ readIORef tc_ref
2628
case lookupTermCache key tc of
2729
-- cache miss: reconstruct, then store.
2830
Nothing ->
@@ -37,17 +39,8 @@ obtainTerm key = do
3739
getTerm = \case
3840
FromId i -> GHC.obtainTermFromId (depth i) False{-don't force-} i
3941
FromPath k pf -> do
40-
term <- getTerm k >>= \case
41-
-- When the key points to a Suspension, the real thing should
42-
-- already be forced. It's just that the shallow depth meant we
43-
-- returned a Suspension nonetheless while recursing in `getTerm`.
44-
t@Suspension{} -> do
45-
t' <- seqTerm t
46-
-- update term cache with intermediate values?
47-
-- insertTermCache k t'
48-
return t'
49-
t -> return t
50-
return $ case term of
42+
term <- getTerm k
43+
liftIO $ expandTerm hsc_env $ case term of
5144
Term{dc=Right dc, subTerms} -> case pf of
5245
PositionalIndex ix -> subTerms !! (ix-1)
5346
LabeledField fl ->
@@ -59,12 +52,25 @@ obtainTerm key = do
5952
_ -> error "Unexpected term for the given TermKey"
6053
in do
6154
term <- getTerm key
62-
liftIO $ writeIORef tc_ref (insertTermCache key term tc)
55+
liftIO $ modifyIORef tc_ref (insertTermCache key term)
6356
return term
6457

6558
-- cache hit
6659
Just hit -> return hit
6760

61+
-- | Before returning a 'Term' we want to expand its heap representation up to the 'defaultDepth'
62+
--
63+
-- For 'Id's, this is done by 'GHC.obtainTermFromId'. For other 'TermKey's this
64+
-- function should be used
65+
expandTerm :: HscEnv -> Term -> IO Term
66+
expandTerm hsc_env term = case term of
67+
Term{val, ty} -> cvObtainTerm hsc_env defaultDepth False ty val
68+
(NewtypeWrap{}; RefWrap{}) -> do
69+
-- TODO: we don't do anything clever here yet
70+
return term
71+
-- For other terms there's no point in trying to expand
72+
(Suspension{}; Prim{}) -> return term
73+
6874
-- | A boring type is one for which we don't care about the structure and would
6975
-- rather see "whole" when being inspected. Strings and literals are a good
7076
-- example, because it's more useful to see the string value than it is to see

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ tyThingToVarInfo t = case t of
4343
termToVarInfo key term
4444

4545
-- | Construct the VarInfos of the fields ('VarFields') of the given 'TermKey'/'Term'
46+
--
47+
-- This is used to come up with terms for the fields of an already `seq`ed
48+
-- variable which was expanded.
4649
termVarFields :: TermKey -> Term -> Debugger VarFields
4750
termVarFields top_key top_term =
4851

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module Main where
2+
3+
data T97 a = T97 a
4+
deriving Show
5+
6+
7+
main = do
8+
let y :: T97 (T97 (T97 (T97 (T97 (T97 String)))))
9+
y = T97 (T97 (T97 (T97 (T97 (T97 "hello")))))
10+
11+
print y
12+
print y
13+

test/integration-tests/flake.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/integration-tests/flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
description = "Integration tests for hdb";
33

44
inputs = {
5-
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; # or unstable
5+
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; # or unstable
66
flake-utils.url = "github:numtide/flake-utils";
77
};
88

test/integration-tests/test/adapter.test.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -435,8 +435,7 @@ describe("Debug Adapter Tests", function () {
435435
}
436436

437437
// Finally, we should be at the OK constructor
438-
const focusF = await forceLazy(focus);
439-
assert.strictEqual(focusF.value, 'OK');
438+
assert.strictEqual(focus.value, 'OK');
440439
})
441440

442441
it('strings that are fields of the expanded vars and are not thunks are fully evaluated (issue #11)', async () => {
@@ -453,15 +452,14 @@ describe("Debug Adapter Tests", function () {
453452
await dc.hitBreakpoint(config, { path: config.entryFile, line: 5 }, expected, expected)
454453

455454
// Force only the 2nd "hello" and check the third is already there.
456-
// It relies on repeat seemingly only re-using every other thunk?!!?
457455
// (Mimics reproducer in #11)
458456
let locals = await fetchLocalVars();
459457
const xVar = await forceLazy(locals.get('x'));
460458
const xChild = await expandVar(xVar);
461459
const _2Var = await xChild.get('_2'); // NOTE: Doesn't need to be forced because of this seemingly weird `repeat` behavior where it looks like every other binding is shared but the others are not
462460
const _2Child = await expandVar(_2Var);
463461
const _2_1Var = await forceLazy(_2Child.get('_1'));
464-
const _2_2Var = await forceLazy(_2Child.get('_2'));
462+
const _2_2Var = await _2Child.get('_2');
465463
const _2_2Child = await expandVar(_2_2Var);
466464
const _2_2_1Var = await _2_2Child.get('_1') // NOTE: doesn't need to be forced as above
467465
assertIsString(_2_2_1Var, '"hello"');
@@ -533,6 +531,40 @@ describe("Debug Adapter Tests", function () {
533531
const myintCon = await moduleVars.get("MyIntCon")
534532
assert.strictEqual(myintCon.value, "Data constructor ‘MyIntCon’")
535533
})
534+
535+
it('expand iteratively s.t. forced structure appears as forced (issue #97)', async () => {
536+
let config = mkConfig({
537+
projectRoot: "/data/T97",
538+
entryFile: "Main.hs",
539+
entryPoint: "main",
540+
entryArgs: [],
541+
extraGhcArgs: []
542+
})
543+
544+
const expected = { path: config.projectRoot + "/" + config.entryFile, line: 12 }
545+
546+
await dc.hitBreakpoint(config, { path: config.entryFile, line: 12 }, expected, expected)
547+
548+
let locals = await fetchLocalVars();
549+
const xVar = await locals.get('y');
550+
// NOTE: This tests that we don't have to force a single variable
551+
// as we expand because the data should be fully forced at this
552+
// point.
553+
const xChild = await expandVar(xVar);
554+
const _1Var = await xChild.get('_1');
555+
const _1Child = await expandVar(_1Var);
556+
const _1_1Var = await _1Child.get('_1');
557+
assert.strictEqual(_1_1Var.value, 'T97'); // just check
558+
const _1_1Child = await expandVar(_1_1Var);
559+
const _1_1_1Var = await _1_1Child.get('_1');
560+
const _1_1_1Child = await expandVar(_1_1_1Var);
561+
const _1_1_1_1Var = await _1_1_1Child.get('_1');
562+
const _1_1_1_1Child = await expandVar(_1_1_1_1Var);
563+
const _1_1_1_1_1Var = await _1_1_1_1Child.get('_1');
564+
const _1_1_1_1_1Child = await expandVar(_1_1_1_1_1Var);
565+
const _1_1_1_1_1_1Var = await _1_1_1_1_1Child.get('_1');
566+
assertIsString(_1_1_1_1_1_1Var, '"hello"');
567+
})
536568
})
537569
describe("Stepping out (step-out)", function () {
538570

0 commit comments

Comments
 (0)