packages feed

haskell-debugger-0.12.0.0: hdb/Development/Debug/Interactive.hs

{-# LANGUAGE LambdaCase, ViewPatterns, RecordWildCards, OverloadedRecordDot #-}
module Development.Debug.Interactive where

import Data.Functor.Contravariant
import System.IO
import System.Exit
import System.Directory
import System.Console.Haskeline
-- import System.Console.Haskeline.Completion
import System.FilePath
import Control.Monad.Except
import Control.Monad.State
import Control.Monad.Reader
import Control.Monad.RWS
import Options.Applicative
-- import Options.Applicative.BashCompletion

import Development.Debug.Session.Setup

import Colog.Core
import GHC.Debugger.Interface.Messages
import GHC.Debugger.Monad
import GHC.Debugger
import Control.Monad
import Data.List (intercalate)
import qualified Data.Maybe as Maybe

data RunOptions = RunOptions
  { runEntryFile :: FilePath
  , runEntryPoint :: String
  , runEntryArgs :: [String]
  }

data RunContext = RunContext
  { runCurrentThread :: Maybe RemoteThreadId
  , runLastCommand :: Maybe Command
  }

-- | Interactive debugging monad
type InteractiveDM a = InputT (RWST RunOptions () RunContext Debugger) a

data InteractiveLog
  = IDebuggerLog DebuggerLog
  | ISessionSetupLog (WithSeverity SessionSetupLog)

-- | Run it
runIDM :: LogAction IO InteractiveLog
       -> String   -- ^ entryPoint
       -> FilePath -- ^ entryFile
       -> [String] -- ^ entryArgs
       -> [String] -- ^ extraGhcArgs
       -> RunDebuggerSettings
       -> InteractiveDM a
       -> IO a
runIDM logger entryPoint entryFile entryArgs extraGhcArgs runConf act = do
  projectRoot <- getCurrentDirectory

  let hieBiosLogger = contramap ISessionSetupLog logger
  runExceptT (hieBiosSetup hieBiosLogger projectRoot entryFile) >>= \case
    Left e               -> exitWithMsg e
    Right (Left e)       -> exitWithMsg e
    Right (Right flags)
      | HieBiosFlags{..} <- flags
                         -> do

      let absEntryFile = normalise $ projectRoot </> entryFile
      let debugRec = contramap IDebuggerLog logger

      runDebugger debugRec rootDir componentDir libdir units ghcInvocation extraGhcArgs absEntryFile runConf $
        fmap fst $
          evalRWST (runInputT (setComplete noCompletion defaultSettings) act)
                   (RunOptions { runEntryFile = entryFile, runEntryPoint = entryPoint, runEntryArgs = entryArgs })
                   (RunContext { runLastCommand = Nothing, runCurrentThread = Nothing } )
  where
    exitWithMsg txt = do
      hPutStrLn stderr txt
      exitWith (ExitFailure 33)

  --   completeF = completeWordWithPrev Nothing filenameWordBreakChars $
  --     \(reverse -> previous) word -> do
  --       let comp_words = words previous ++ [word]
  --           comp_cword = length comp_words
  --       case execParserPure parserPrefs cmdParserInfo
  --           ("--bash-completion-index":show comp_cword:
  --             concat (zipWith (\fl a -> [fl, show a]) (repeat "--bash-completion-word") comp_words)) of
  --         CompletionInvoked CompletionResult{execCompletion} ->
  --           map simpleCompletion . words <$> liftIO (execCompletion "")
  --         _ -> return []

-- | Run the interactive command-line debugger
debugInteractive :: InteractiveDM ()
debugInteractive = withInterrupt loop
  where
    loop = handleInterrupt loop $ do
      minput <- getInputLine "(hdb) "
      case minput of
        Nothing -> outputStrLn "Exiting..." >> liftIO (exitWith ExitSuccess)
        Just "" -> do
          lift (gets runLastCommand) >>= \case
            Nothing -> return ()
            Just cmd -> do
              out <- lift . lift $ execute cmd -- repeat last command
              printResponse out
        Just input -> do
          mcmd <- parseCmd input
          lift $ modify (\ o -> o { runLastCommand = mcmd })
          case mcmd of
            Nothing -> return ()
            Just cmd -> do
              out <- lift . lift $ execute cmd
              printResponse out
      loop

showExceptionDetails :: RemoteThreadId -> InteractiveDM ()
showExceptionDetails tid = do
  infoResp <- lift . lift $ execute (GetExceptionInfo tid)
  case infoResp of
    GotExceptionInfo exc_info -> outputStrLn $ renderExceptionInfo exc_info
    _ -> pure ()
  stackResp <- lift . lift $ execute (GetStacktrace tid)
  case stackResp of
    GotStacktrace (frame:_) ->
      outputStrLn $
        "Exception location: " ++ renderSourceSpan (frame.sourceSpan)
    _ -> outputStrLn "Exception location: <unknown>"

--------------------------------------------------------------------------------
-- Printing
--------------------------------------------------------------------------------

printResponse :: Response -> InteractiveDM ()
printResponse = \case
  DidEval er -> outputEvalResult er
  DidSetBreakpoint bf       -> outputStrLn $ show bf
  DidRemoveBreakpoint bf    -> outputStrLn $ show bf
  DidGetBreakpoints mb_span -> outputStrLn $ show mb_span
  DidClearBreakpoints -> outputStrLn "Cleared all breakpoints."
  DidContinue er -> outputEvalResult er
  DidStep er -> printEvalResult er
  DidExec er -> outputEvalResult er
  GotThreads threads -> outputStrLn $ show threads
  GotStacktrace stackframes -> outputStrLn $ show stackframes
  GotScopes scopeinfos -> outputStrLn $ show scopeinfos
  GotVariables vis -> outputVariables vis
  GotExceptionInfo exc_info -> outputStrLn $ renderExceptionInfo exc_info
  Aborted err_str -> outputStrLn ("Aborted: " ++ err_str)
  Initialised -> pure ()
  where
    outputEvalResult er = do
      outputStrLn (showEvalResult er)
      maybeShowException er
      rememberThreadContext er

    maybeShowException EvalStopped{breakId = Nothing, breakThread=tid} =
      showExceptionDetails tid
    maybeShowException _ = pure ()

    rememberThreadContext er =
      case er of
        EvalCompleted{} -> lift $ modify' (\ ctx -> ctx { runCurrentThread = Nothing } )
        EvalException{} -> pure () -- TODO: why does this not have a thread associated?
        EvalStopped{breakThread} -> lift $ modify' (\ ctx -> ctx { runCurrentThread = Just breakThread } )
        EvalAbortedWith{} -> lift $ modify' (\ ctx -> ctx { runCurrentThread = Nothing } )

    outputVariables (ForcedVariable var) = outputVariables (VariableFields [var])
    outputVariables (VariableFields vars) = do
       ctx <- lift get
       case runCurrentThread ctx of
         Just threadId ->
          mapM_ (outputVarWithFields  threadId 0) vars
         Nothing -> error "no thread id"

    outputVarWithFields threadId frameIx var = do
      outputStrLn (showVarInfo var)
      fields <- fetchFields threadId frameIx var
      mapM_ (outputStrLn . ("  " ++) . showVarInfo) fields

    fetchFields _ _ VarInfo{varRef = NoVariables} = pure []
    fetchFields threadId frameIx VarInfo{varRef = ref@(SpecificVariable _), ..} = do
      resp <- lift . lift $ execute (GetVariables threadId frameIx ref)
      case resp of
        GotVariables res -> pure (variableResultToList res)
        Aborted err -> outputStrLn ("Failed to fetch fields for " ++ varName ++ ": " ++ err) >> pure []
        _ -> outputStrLn ("Unexpected response when fetching fields for " ++ varName) >> pure []
    fetchFields _ _ _ = pure []

printEvalResult :: EvalResult -> InteractiveDM ()
printEvalResult EvalStopped{..} = do
  out <- lift . lift $ execute (GetScopes breakThread 0)
  printResponse out
  when (breakId == Nothing) $
    showExceptionDetails breakThread
printEvalResult er = outputStrLn $ showEvalResult er

showEvalResult :: EvalResult -> String
showEvalResult (EvalCompleted{..}) = resultVal
showEvalResult (EvalException{..}) = resultVal
showEvalResult (EvalStopped{}) = "Stopped at breakpoint"
showEvalResult (EvalAbortedWith err) = "Aborted: " ++ err

showVarInfoResult :: VariableResult -> String
showVarInfoResult (ForcedVariable vi) = showVarInfo vi
showVarInfoResult (VariableFields vis) = unlines $ map showVarInfo vis

showVarInfo :: VarInfo -> String
showVarInfo VarInfo{..} = unwords [varName, ":", varType, "=", varValue]

renderSourceSpan :: SourceSpan -> String
renderSourceSpan SourceSpan{..} =
  file ++ ":" ++ show startLine ++ ":" ++ show startCol

renderExceptionInfo :: ExceptionInfo -> String
renderExceptionInfo = unlines . go 0
  where
    go depth exInfo =
      let indent = replicate (depth * 2) ' '
          typeLine = indent ++ "Exception: " ++ exceptionInfoTypeName exInfo
          messageLine = indent ++ "Message: " ++ exceptionInfoMessage exInfo
          ctxLine = case exceptionInfoContext exInfo of
            Nothing -> indent ++ "Call stack: <unavailable>"
            Just ctx -> indent ++ "Call stack:\n" ++ indentMultiline (depth + 1) ctx
          innerLines = case exceptionInfoInner exInfo of
            [] -> []
            xs -> (indent ++ "Inner exceptions:") : concatMap (go (depth + 1)) xs
      in typeLine : messageLine : ctxLine : innerLines

    indentMultiline depth txt =
      let pref = replicate (depth * 2) ' '
      in intercalate "\n" (map (pref ++) (lines txt))

--------------------------------------------------------------------------------
-- Command parser
--------------------------------------------------------------------------------

breakpointParser :: Parser Breakpoint
breakpointParser =
  ( ModuleBreak
  <$> argument str
      ( metavar "PATH" -- todo: accept module breaks using module name
     <> help "Path to module to break at" )
  <*> argument auto
      ( metavar "LINE_NUM"
     <> help "The line number to break at" )
  <*> optional (argument auto
      ( metavar "COLUMN_NUM"
     <> help "The column number to break at" ))
  )
  <|>
  ( FunctionBreak
    <$> option str
      ( long "name"
     <> short 'n'
     <> metavar "FUNCTION_NAME"
     <> help "Set a breakpoint using the function name" )
  )
  <|>
  ( flag' OnExceptionsBreak ( long "exceptions" )
  )
  <|>
  ( flag' OnUncaughtExceptionsBreak ( long "error" )
  )

conditionalBreakParser :: Parser (Maybe String)
conditionalBreakParser =
  optional (option str
    ( long "condition"
   <> metavar "CONDITION"
   <> help "Only stop when CONDITION evaluates to True" ))

hitCountBreakParser :: Parser (Maybe Int)
hitCountBreakParser =
  optional (option auto
    ( long "hit"
   <> metavar "N:INT"
   <> help "Ignore first N:INT times this breakpoint is hit" ))

runParser :: RunOptions -> Parser Command
runParser opts =
  -- --entry <name> with some args
  -- (DebugExecution <$> parseEntry <*> parseSomeArgs)
  -- --entry <name> without any args
  -- <|> (DebugExecution <$> parseEntry <*> pure [])
  -- just some args
  (DebugExecution (mkEntry (runEntryPoint opts)) (runEntryFile opts) <$> parseSomeArgs)
  -- just "run"
  <|> (pure $ DebugExecution (mkEntry (runEntryPoint opts)) (runEntryFile opts) (runEntryArgs opts))
  where
    _parseEntry =
      fmap mkEntry $
      option str
        ( long "entry"
        <> short 'e'
        <> metavar "FUNCTION_NAME"
        <> help "Run with this entry point"
        )
    parseSomeArgs =
      some ( argument str
        ( metavar "ARGS" <> help "Arguments to pass to the entry point. If empty, the arguments given at the debugger invocation are used." ) )
    mkEntry entry
      | entry == "main" = MainEntry Nothing
      | otherwise = FunctionEntry (runEntryPoint opts)

-- | Combined parser for 'Command'
cmdParser :: RunOptions -> RunContext -> Parser Command
cmdParser opts ctx = hsubparser
   (
    Options.Applicative.command "delete"
    ( info (DelBreakpoint <$> breakpointParser)
      ( progDesc "Delete a breakpoint" ) )
  <>
    Options.Applicative.command "run"
    ( info (runParser opts)
      ( progDesc "Run the debuggee" ) )
  <>
    Options.Applicative.command "next"
    ( info (pure DoStepLocal)
      ( progDesc "Step over to the next line" ) )
  <>
    Options.Applicative.command "step"
    ( info (pure DoSingleStep)
      ( progDesc "Step-in to the next immediate location" ) )
  <>
    Options.Applicative.command "finish"
    ( info (pure DoStepOut)
      ( progDesc "Step-out of the current function into the caller/its continuation" ) )
  <>
    Options.Applicative.command "continue"
    ( info (pure DoContinue)
      ( progDesc "Continue executing from the current breakpoint" ) )
  <>
    Options.Applicative.command "print"
    ( info (DoEval . unwords <$> many (argument str ( metavar "EXPRESSION"
     <> help "Expression to evaluate in the current context" )))
      ( progDesc "Evaluate an expression in the current context" ) )
  <>
    Options.Applicative.command "exit"
    ( info (pure TerminateProcess)
      ( progDesc "Terminate and exit the debugger session" ) )
  <>
    Options.Applicative.command "threads"
    ( info (pure GetThreads)
      ( progDesc "Print all user threads" ) )
  <>
    Options.Applicative.command "backtrace"
    ( info (stackTraceParser ctx <**> helper)
      ( progDesc "Print stack trace" ) )
  <>
    Options.Applicative.command "variables"
    ( info (variablesParser ctx <**> helper)
      ( progDesc "Print local variables" ) )
  <> Options.Applicative.command "break"
    ( info (SetBreakpoint <$> breakpointParser <*> hitCountBreakParser <*> conditionalBreakParser)
      ( progDesc "Set a breakpoint" ) )
  )

stackTraceParser :: RunContext -> Parser Command
stackTraceParser ctx =
  GetStacktrace <$> threadIdParser ctx "Print backtrace of THREAD_ID. Defaults to current thread at breakpoint."

variablesParser :: RunContext -> Parser Command
variablesParser ctx =
  GetVariables
    <$> threadIdParser ctx "Show variables of THREAD_ID. Defaults to current thread at breakpoint."
    <*> pure 0
    <*> pure LocalVariables

threadIdParser :: RunContext -> String -> Parser RemoteThreadId
threadIdParser ctx helpMsg =
     RemoteThreadId <$> argument auto (metavar "THREAD_ID" <> help helpMsg)
 <|> Maybe.maybe (empty <**> abortOption (ErrorMsg "Not stopped at a Breakpoint") mempty) pure (runCurrentThread ctx)

-- | Main parser info
cmdParserInfo :: RunOptions -> RunContext -> ParserInfo Command
cmdParserInfo opts ctx = info (cmdParser opts ctx)
  ( fullDesc )

-- | Parse command line arguments
parseCmd :: String -> InteractiveDM (Maybe Command)
parseCmd input = do
  opts <- lift ask
  ctx <- lift get
  let
    res = execParserPure
     parserPrefs
     (cmdParserInfo opts ctx)
     (words input)
   in case res of
    Success x ->
      return (Just x)
    Failure bad ->
      let (msg, _exit) = renderFailure bad "(hdb)"
       in outputStrLn msg >> pure Nothing
    _ -> outputStrLn "Unsupported command parsing mode" >> pure Nothing

parserPrefs :: ParserPrefs
parserPrefs = prefs (disambiguate <> showHelpOnError <> showHelpOnEmpty)