[Git][ghc/ghc][wip/romes/step-out] debugger: Implement step-out feature

Rodrigo Mesquita pushed to branch wip/romes/step-out at Glasgow Haskell Compiler / GHC Commits: 1cb33002 by Rodrigo Mesquita at 2025-05-16T14:05:17+01:00 debugger: Implement step-out feature Implements support for stepping-out of a function (aka breaking right after returning from a function) in the interactive debugger. It also introduces a GHCi command :stepout to step-out of a function being debugged in the interpreter. The feature is described as: Stop at the first breakpoint immediately after returning from the current function scope. Known limitations: because a function tail-call does not push a stack frame, if step-out is used inside of a function that was tail-called, execution will not be returned to its caller, but rather its caller's first non-tail caller. On the other hand, it means the debugger follows the more realistic execution of the program. In the following example: .. code-block:: none f = do a b <--- (1) set breakpoint then step in here c b = do ... d <--- (2) step-into this tail call d = do ... something <--- (3) step-out here ... Stepping-out will stop execution at the `c` invokation in `f`, rather than stopping at `b`. The key implementation bit is simple: When step-out is set and the interpreter hits a RETURN instruction, enable "stop at the immediate next breakpoint" (aka single-step). See also `Note [Debugger Step-out]` in `rts/Interpreter.c` Note [Debugger Step-out] ~~~~~~~~~~~~~~~~~~~~~~~~ When the global debugger step-out flag is set (`rts_stop_after_return`), the interpreter must yield execution right after the first RETURN. When stepping-out, we simply enable `rts_stop_next_breakpoint` when we hit a return instruction (in `do_return_pointer` and `do_return_nonpointer`). The step-out flag is cleared and must be re-enabled explicitly to step-out again. A limitation of this approach is that stepping-out of a function that was tail-called will skip its caller since no stack frame is pushed for a tail call (i.e. a tail call returns directly to its caller's first non-tail caller). Fixes #26042 - - - - - 20 changed files: - compiler/GHC/Driver/Config.hs - compiler/GHC/Runtime/Eval.hs - compiler/GHC/Runtime/Eval/Types.hs - docs/users_guide/ghci.rst - ghc/GHCi/UI.hs - libraries/ghci/GHCi/Message.hs - libraries/ghci/GHCi/Run.hs - rts/Interpreter.c - rts/RtsSymbols.c - rts/include/stg/MiscClosures.h - + testsuite/tests/ghci.debugger/scripts/T26042a.hs - + testsuite/tests/ghci.debugger/scripts/T26042a.script - + testsuite/tests/ghci.debugger/scripts/T26042a.stdout - + testsuite/tests/ghci.debugger/scripts/T26042b.hs - + testsuite/tests/ghci.debugger/scripts/T26042b.script - + testsuite/tests/ghci.debugger/scripts/T26042b.stdout - + testsuite/tests/ghci.debugger/scripts/T26042c.hs - + testsuite/tests/ghci.debugger/scripts/T26042c.script - + testsuite/tests/ghci.debugger/scripts/T26042c.stdout - testsuite/tests/ghci.debugger/scripts/all.T Changes: ===================================== compiler/GHC/Driver/Config.hs ===================================== @@ -3,6 +3,7 @@ module GHC.Driver.Config ( initOptCoercionOpts , initSimpleOpts , initEvalOpts + , EvalStep(..) ) where @@ -28,13 +29,28 @@ initSimpleOpts dflags = SimpleOpts , so_inline = True } +-- | Instruct the interpreter evaluation to break... +data EvalStep + -- | ... at every breakpoint tick + = EvalStepSingle + -- | ... after every return stmt + | EvalStepOut + -- | ... only on explicit breakpoints + | EvalStepNone + -- | Extract GHCi options from DynFlags and step -initEvalOpts :: DynFlags -> Bool -> EvalOpts +initEvalOpts :: DynFlags -> EvalStep -> EvalOpts initEvalOpts dflags step = EvalOpts { useSandboxThread = gopt Opt_GhciSandbox dflags - , singleStep = step + , singleStep = singleStep + , stepOut = stepOut , breakOnException = gopt Opt_BreakOnException dflags , breakOnError = gopt Opt_BreakOnError dflags } + where + (singleStep, stepOut) = case step of + EvalStepSingle -> (True, False) + EvalStepOut -> (False, True) + EvalStepNone -> (False, False) ===================================== compiler/GHC/Runtime/Eval.hs ===================================== @@ -343,7 +343,12 @@ handleRunStatus step expr bindings final_ids status history0 = do setSession hsc_env2 return (ExecBreak names Nothing) - -- Just case: we stopped at a breakpoint + -- EvalBreak (Just ...) case: the interpreter stopped at a breakpoint + -- + -- The interpreter yields on a breakpoint if: + -- - the breakpoint was explicitly enabled (in @BreakArray@) + -- - or @singleStep = True@ in EvalOpts + -- - or @stepOut = True@ in EvalOpts EvalBreak apStack_ref (Just eval_break) resume_ctxt ccs -> do let ibi = evalBreakpointToId eval_break tick_brks <- liftIO $ readModBreaks hsc_env (ibi_tick_mod ibi) @@ -351,13 +356,14 @@ handleRunStatus step expr bindings final_ids status history0 = do span = modBreaks_locs tick_brks ! ibi_tick_index ibi decl = intercalate "." $ modBreaks_decls tick_brks ! ibi_tick_index ibi + -- Was the breakpoint explicitly enabled? b <- liftIO $ breakpointStatus interp (modBreaks_flags tick_brks) (ibi_tick_index ibi) apStack_fhv <- liftIO $ mkFinalizedHValue interp apStack_ref resume_ctxt_fhv <- liftIO $ mkFinalizedHValue interp resume_ctxt - -- This breakpoint is explicitly enabled; we want to stop - -- instead of just logging it. + -- This breakpoint is enabled or we mean to break here; + -- we want to stop instead of just logging it. if b || breakHere step span then do -- This function only returns control to ghci with 'ExecBreak' when it is really meant to break. -- Specifically, for :steplocal or :stepmodule, don't return control @@ -1247,7 +1253,7 @@ compileParsedExprRemote expr@(L loc _) = withSession $ \hsc_env -> do _ -> panic "compileParsedExprRemote" updateFixityEnv fix_env - let eval_opts = initEvalOpts dflags False + let eval_opts = initEvalOpts dflags EvalStepNone status <- liftIO $ evalStmt interp eval_opts (EvalThis hvals_io) case status of EvalComplete _ (EvalSuccess [hval]) -> return hval ===================================== compiler/GHC/Runtime/Eval/Types.hs ===================================== @@ -17,6 +17,7 @@ import GHC.Prelude import GHCi.RemoteTypes import GHCi.Message (EvalExpr, ResumeContext) +import GHC.Driver.Config (EvalStep(..)) import GHC.Types.Id import GHC.Types.Name import GHC.Types.TyThing @@ -46,6 +47,9 @@ data SingleStep -- | :step [expr] | SingleStep + -- | :stepout [expr] + | StepOut + -- | :steplocal [expr] | LocalStep { breakAt :: SrcSpan } @@ -55,10 +59,12 @@ data SingleStep { breakAt :: SrcSpan } -- | Whether this 'SingleStep' mode requires instructing the interpreter to --- step at every breakpoint. -enableGhcStepMode :: SingleStep -> Bool -enableGhcStepMode RunToCompletion = False -enableGhcStepMode _ = True +-- step at every breakpoint or after every return (see @'EvalStep'@). +enableGhcStepMode :: SingleStep -> EvalStep +enableGhcStepMode RunToCompletion = EvalStepNone +enableGhcStepMode StepOut = EvalStepOut +-- for the remaining step modes we need to stop at every single breakpoint. +enableGhcStepMode _ = EvalStepSingle -- | Given a 'SingleStep' mode and the SrcSpan of a breakpoint we hit, return -- @True@ if based on the step-mode alone we should stop at this breakpoint. @@ -70,6 +76,7 @@ breakHere :: SingleStep -> SrcSpan -> Bool breakHere step break_span = case step of RunToCompletion -> False RunAndLogSteps -> False + StepOut -> True SingleStep -> True LocalStep span -> break_span `isSubspanOf` span ModuleStep span -> srcSpanFileName_maybe span == srcSpanFileName_maybe break_span ===================================== docs/users_guide/ghci.rst ===================================== @@ -2980,6 +2980,35 @@ commonly used commands. hit by an error (:ghc-flag:`-fbreak-on-error`) or an exception (:ghc-flag:`-fbreak-on-exception`). +.. ghci-cmd:: :stepout + + Stop at the first breakpoint immediately after returning from the current + function scope. + + Known limitations: because a function tail-call does not push a stack + frame, if step-out is used inside of a function that was tail-called, + execution will not be returned to its caller, but rather its caller's + first non-tail caller. On the other hand, it means the debugger + follows the more realistic execution of the program. + In the following example: + + .. code-block:: none + + f = do + a + b <--- (1) set breakpoint then step in here + c + b = do + ... + d <--- (2) step-into this tail call + d = do + ... + something <--- (3) step-out here + ... + + Stepping-out will stop execution at the `c` invokation in `f`, rather than + stopping at `b`. + .. ghci-cmd:: :stepmodule Enable only breakpoints in the current module and resume evaluation ===================================== ghc/GHCi/UI.hs ===================================== @@ -247,6 +247,7 @@ ghciCommands = map mkCmd [ ("sprint", keepGoing sprintCmd, completeExpression), ("step", keepGoing stepCmd, completeIdentifier), ("steplocal", keepGoing stepLocalCmd, completeIdentifier), + ("stepout", keepGoing stepOutCmd, completeIdentifier), ("stepmodule",keepGoing stepModuleCmd, completeIdentifier), ("type", keepGoingMulti' typeOfExpr, completeExpression), ("trace", keepGoing traceCmd, completeExpression), @@ -407,6 +408,7 @@ defFullHelpText = " :step single-step after stopping at a breakpoint\n"++ " :step <expr> single-step into <expr>\n"++ " :steplocal single-step within the current top-level binding\n"++ + " :stepout stop at the first breakpoint after returning from the current scope\n"++ " :stepmodule single-step restricted to the current module\n"++ " :trace trace after stopping at a breakpoint\n"++ " :trace <expr> evaluate <expr> with tracing on (see :history)\n"++ @@ -3793,6 +3795,12 @@ stepCmd arg = withSandboxOnly ":step" $ step arg step [] = doContinue GHC.SingleStep step expression = runStmt expression GHC.SingleStep >> return () +stepOutCmd :: GhciMonad m => String -> m () +stepOutCmd arg = withSandboxOnly ":stepout" $ step arg + where + step [] = doContinue GHC.StepOut + step expression = stepCmd expression + stepLocalCmd :: GhciMonad m => String -> m () stepLocalCmd arg = withSandboxOnly ":steplocal" $ step arg where ===================================== libraries/ghci/GHCi/Message.hs ===================================== @@ -374,6 +374,7 @@ putTHMessage m = case m of data EvalOpts = EvalOpts { useSandboxThread :: Bool , singleStep :: Bool + , stepOut :: Bool , breakOnException :: Bool , breakOnError :: Bool } ===================================== libraries/ghci/GHCi/Run.hs ===================================== @@ -210,6 +210,7 @@ evalOptsSeq :: EvalOpts evalOptsSeq = EvalOpts { useSandboxThread = True , singleStep = False + , stepOut = False , breakOnException = False , breakOnError = False } @@ -333,6 +334,7 @@ withBreakAction opts breakMVar statusMVar act poke breakPointIOAction stablePtr when (breakOnException opts) $ poke exceptionFlag 1 when (singleStep opts) $ setStepFlag + when (stepOut opts) $ poke stepOutFlag 1 return stablePtr -- Breaking on exceptions is not enabled by default, since it -- might be a bit surprising. The exception flag is turned off @@ -363,6 +365,7 @@ withBreakAction opts breakMVar statusMVar act resetBreakAction stablePtr = do poke breakPointIOAction noBreakStablePtr poke exceptionFlag 0 + poke stepOutFlag 0 resetStepFlag freeStablePtr stablePtr @@ -398,6 +401,7 @@ abandonStmt hvref = do foreign import ccall "&rts_stop_next_breakpoint" stepFlag :: Ptr CInt foreign import ccall "&rts_stop_on_exception" exceptionFlag :: Ptr CInt +foreign import ccall "&rts_stop_after_return" stepOutFlag :: Ptr CInt setStepFlag :: IO () setStepFlag = poke stepFlag 1 ===================================== rts/Interpreter.c ===================================== @@ -194,6 +194,24 @@ See also Note [Width of parameters] for some more motivation. #define WITHIN_CHUNK_BOUNDS(n, s) \ (RTS_LIKELY((StgWord*)(Sp_plusW(n)) < ((s)->stack + (s)->stack_size - sizeofW(StgUnderflowFrame)))) +// Note [Debugger Step-out] +// ~~~~~~~~~~~~~~~~~~~~~~~~ +// When the global debugger step-out flag is set (`rts_stop_after_return`), +// the interpreter must yield execution right after the first RETURN. +// +// When stepping-out, we simply enable `rts_stop_next_breakpoint` when we hit a +// return instruction (in `do_return_pointer` and `do_return_nonpointer`). +// The step-out flag is cleared and must be re-enabled explicitly to step-out again. +// +// A limitation of this approach is that stepping-out of a function that was +// tail-called will skip its caller since no stack frame is pushed for a tail +// call (i.e. a tail call returns directly to its caller's first non-tail caller). +#define CHECK_BRK_AFTER_RET() \ + if (rts_stop_after_return) \ + { \ + rts_stop_next_breakpoint = true; \ + rts_stop_after_return = false; \ + } \ /* Note [PUSH_L underflow] ~~~~~~~~~~~~~~~~~~~~~~~ @@ -245,6 +263,7 @@ allocate_NONUPD (Capability *cap, int n_words) int rts_stop_next_breakpoint = 0; int rts_stop_on_exception = 0; +int rts_stop_after_return = 0; #if defined(INTERP_STATS) @@ -734,6 +753,8 @@ do_return_pointer: IF_DEBUG(sanity,checkStackChunk(Sp, cap->r.rCurrentTSO->stackobj->stack+cap->r.rCurrentTSO->stackobj->stack_size)); + CHECK_BRK_AFTER_RET(); + switch (get_itbl((StgClosure *)Sp)->type) { case RET_SMALL: { @@ -883,6 +904,8 @@ do_return_nonpointer: // get the offset of the header of the next stack frame offset = stack_frame_sizeW((StgClosure *)Sp); + CHECK_BRK_AFTER_RET(); + switch (get_itbl((StgClosure*)(Sp_plusW(offset)))->type) { case RET_BCO: ===================================== rts/RtsSymbols.c ===================================== @@ -908,6 +908,7 @@ extern char **environ; SymI_NeedsDataProto(rts_breakpoint_io_action) \ SymI_NeedsDataProto(rts_stop_next_breakpoint) \ SymI_NeedsDataProto(rts_stop_on_exception) \ + SymI_NeedsDataProto(rts_stop_after_return) \ SymI_HasProto(stopTimer) \ SymI_HasProto(n_capabilities) \ SymI_HasProto(max_n_capabilities) \ ===================================== rts/include/stg/MiscClosures.h ===================================== @@ -619,6 +619,7 @@ RTS_FUN_DECL(stg_castFloatToWord32zh); // Interpreter.c extern StgWord rts_stop_next_breakpoint[]; extern StgWord rts_stop_on_exception[]; +extern StgWord rts_stop_after_return[]; extern StgWord rts_breakpoint_io_action[]; // Schedule.c ===================================== testsuite/tests/ghci.debugger/scripts/T26042a.hs ===================================== @@ -0,0 +1,17 @@ +module Main where + +main :: IO () +main = do + a <- foo + print a + +foo :: IO Int +foo = do + let x = 3 + y = 4 + b <- bar (x + y) + return b + +bar :: Int -> IO Int +bar z = return (z * 2) + ===================================== testsuite/tests/ghci.debugger/scripts/T26042a.script ===================================== @@ -0,0 +1,20 @@ +:load T26042a.hs +-- simple use of stepout +:break bar +main +:list +:stepout +:list +:stepout +:list +-- from here on we're going to evaluate the thunks for `a` in `print a` +:stepout +:list +:stepout +:list +:stepout +:list +:stepout +:list +-- finish execution +:stepout ===================================== testsuite/tests/ghci.debugger/scripts/T26042a.stdout ===================================== @@ -0,0 +1,50 @@ +Breakpoint 0 activated at T26042a.hs:16:9-22 +Stopped in Main.bar, T26042a.hs:16:9-22 +_result :: IO Int = _ +z :: Int = _ +15 bar :: Int -> IO Int +16 bar z = return (z * 2) + ^^^^^^^^^^^^^^ +17 +Stopped in Main.foo, T26042a.hs:13:3-10 +_result :: IO Int = _ +b :: Int = _ +12 b <- bar (x + y) +13 return b + ^^^^^^^^ +14 +Stopped in Main.main, T26042a.hs:6:3-9 +_result :: IO () = _ +a :: Int = _ +5 a <- foo +6 print a + ^^^^^^^ +7 +Stopped in Main.bar, T26042a.hs:16:17-21 +_result :: Int = _ +z :: Int = _ +15 bar :: Int -> IO Int +16 bar z = return (z * 2) + ^^^^^ +17 +Stopped in Main.foo, T26042a.hs:12:13-17 +_result :: Int = _ +x :: Int = _ +y :: Int = _ +11 y = 4 +12 b <- bar (x + y) + ^^^^^ +13 return b +Stopped in Main.foo.x, T26042a.hs:10:11 +_result :: Int = _ +9 foo = do +10 let x = 3 + ^ +11 y = 4 +Stopped in Main.foo.y, T26042a.hs:11:11 +_result :: Int = _ +10 let x = 3 +11 y = 4 + ^ +12 b <- bar (x + y) +14 ===================================== testsuite/tests/ghci.debugger/scripts/T26042b.hs ===================================== @@ -0,0 +1,22 @@ +module Main where + +main :: IO () +main = do + a <- foo False undefined + print a + +foo :: Bool -> Int -> IO Int +foo True i = return i +foo False _ = do + let x = 3 + y = 4 + n <- bar (x + y) + return n + +bar :: Int -> IO Int +bar z = do + let t = z * 2 + y <- foo True t + return y + + ===================================== testsuite/tests/ghci.debugger/scripts/T26042b.script ===================================== @@ -0,0 +1,16 @@ +:load T26042b.hs +-- break on the True branch of foo +:break 9 +main +:list +-- stepout of foo True to caller (ie bar) +:stepout +:list +-- stepout of bar (to branch of foo False, where bar was called) +:stepout +:list +-- stepout to right after the call to foo False in main +:stepout +:list +-- done +:continue ===================================== testsuite/tests/ghci.debugger/scripts/T26042b.stdout ===================================== @@ -0,0 +1,30 @@ +Breakpoint 0 activated at T26042b.hs:9:15-22 +Stopped in Main.foo, T26042b.hs:9:15-22 +_result :: IO Int = _ +i :: Int = _ +8 foo :: Bool -> Int -> IO Int +9 foo True i = return i + ^^^^^^^^ +10 foo False _ = do +Stopped in Main.bar, T26042b.hs:20:3-10 +_result :: IO Int = _ +y :: Int = _ +19 y <- foo True t +20 return y + ^^^^^^^^ +21 +Stopped in Main.foo, T26042b.hs:14:3-10 +_result :: IO Int = _ +n :: Int = _ +13 n <- bar (x + y) +14 return n + ^^^^^^^^ +15 +Stopped in Main.main, T26042b.hs:6:3-9 +_result :: IO () = _ +a :: Int = _ +5 a <- foo False undefined +6 print a + ^^^^^^^ +7 +14 ===================================== testsuite/tests/ghci.debugger/scripts/T26042c.hs ===================================== @@ -0,0 +1,19 @@ +module Main where + +main :: IO () +main = do + a <- foo False undefined + print a + +foo :: Bool -> Int -> IO Int +foo True i = return i +foo False _ = do + let x = 3 + y = 4 + bar (x + y) + +bar :: Int -> IO Int +bar z = do + let t = z * 2 + foo True t + ===================================== testsuite/tests/ghci.debugger/scripts/T26042c.script ===================================== @@ -0,0 +1,24 @@ +:load T26042c.hs +-- similar to T26042b, but uses tail calls +-- recall: for step-out, we skip the caller of tail calls +-- (because we don't push a stack frame for tail calls, so +-- there's no RET instruction to stop after) + +-- break on foo True branch +:break 9 +main +:list +-- step out of foo True and observe that we have skipped its call in bar, +-- and the call of bar in foo False. +-- we go straight to `main`. +:stepout +:list +-- stepping out from here will jump into the thunk because it's where we'll +-- go after returning. +:stepout +:list +-- and so on +:stepout +:list +-- finish +:continue ===================================== testsuite/tests/ghci.debugger/scripts/T26042c.stdout ===================================== @@ -0,0 +1,31 @@ +Breakpoint 0 activated at T26042c.hs:9:15-22 +Stopped in Main.foo, T26042c.hs:9:15-22 +_result :: IO Int = _ +i :: Int = _ +8 foo :: Bool -> Int -> IO Int +9 foo True i = return i + ^^^^^^^^ +10 foo False _ = do +Stopped in Main.main, T26042c.hs:6:3-9 +_result :: IO () = _ +a :: Int = _ +5 a <- foo False undefined +6 print a + ^^^^^^^ +7 +Stopped in Main.bar.t, T26042c.hs:17:11-15 +_result :: Int = _ +z :: Int = _ +16 bar z = do +17 let t = z * 2 + ^^^^^ +18 foo True t +Stopped in Main.foo, T26042c.hs:13:8-12 +_result :: Int = _ +x :: Int = _ +y :: Int = _ +12 y = 4 +13 bar (x + y) + ^^^^^ +14 +14 ===================================== testsuite/tests/ghci.debugger/scripts/all.T ===================================== @@ -144,3 +144,6 @@ test('T24306', normal, ghci_script, ['T24306.script']) test('T24712', normal, ghci_script, ['T24712.script']) test('T25109', normal, ghci_script, ['T25109.script']) test('T25932', extra_files(['T25932.hs']), ghci_script, ['T25932.script']) +test('T26042a', extra_files(['T26042a.hs']), ghci_script, ['T26042a.script']) +test('T26042b', extra_files(['T26042b.hs']), ghci_script, ['T26042b.script']) +test('T26042c', extra_files(['T26042c.hs']), ghci_script, ['T26042c.script']) View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/1cb330020a43ac0b7098744f56a6d4a5... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/1cb330020a43ac0b7098744f56a6d4a5... You're receiving this email because of your account on gitlab.haskell.org.
participants (1)
-
Rodrigo Mesquita (@alt-romes)