packages feed

haskell-dap-0.0.4.0: app/Haskell/DAP/GHCi/Utility.hs

{-# LANGUAGE LambdaCase #-}

module Haskell.DAP.GHCi.Utility where

import qualified GHC
import qualified GHCi.UI.Monad as G
import HscTypes
import Outputable
import PprTyThing
import Debugger
import Exception
import DynFlags
import RtClosureInspect

import qualified Data.Char as CH
import qualified Data.ByteString as BS
import qualified Data.Text.Encoding as T
import qualified Data.Text as T
import qualified Data.List as L
import qualified Text.Read as R
import Data.Word

import Control.Monad.IO.Class

import Haskell.DAP.GHCi.Constant
import qualified GHCi.DAP.IFData as D

-- |
--
_SLASH :: Char
_SLASH = '/'


-- |
--
_BACK_SLASH :: Char
_BACK_SLASH = '\\'


-- |
--
_SPACES :: [Char]
_SPACES = [' ', '\n', '\t']


-- |
--
lstrip, rstrip, strip :: String -> String
lstrip = dropWhile (flip elem _SPACES)
rstrip = reverse . lstrip . reverse
strip  = lstrip . rstrip


-- |
--
toLower :: String -> String
toLower = map CH.toLower


-- |
--
toUpper :: String -> String
toUpper = map CH.toUpper


-- |
--
win2unixSlash :: String -> String
win2unixSlash = map (\c -> if c == _BACK_SLASH then _SLASH else c)


-- |
--
unix2winSlash :: String -> String
unix2winSlash = map (\c -> if c == _SLASH then _BACK_SLASH else c)


-- |
--   normalized path
--
nzPath :: FilePath -> FilePath
nzPath = drive2lower . win2unixSlash


-- |
--  to lowercase Windows drive letter 
-- 
drive2lower :: FilePath -> FilePath
drive2lower (x : ':' : xs) = CH.toLower x : ':' : xs
drive2lower xs = xs


------------------------------------------------------------------------------------------------
--  DAP Utility
------------------------------------------------------------------------------------------------

-- |
--
readDAP :: Read a => String -> Either String a
readDAP argsStr = case R.readEither argsStr :: Either String [Word8] of
  Left err -> Left $ "read [Word8] failed. " ++ err ++ " : " ++ argsStr
  Right bs -> case R.readEither (toStr bs) of
    Left err -> Left $ "read response body failed. " ++ err ++ " : " ++  (toStr bs)
    Right a  -> Right a 
  where
    toStr = T.unpack . T.decodeUtf8 . BS.pack


-- |
--
showDAP :: Show a => a -> String
showDAP = show . BS.unpack . T.encodeUtf8 . T.pack . show


-- |
--
printDAP :: Show a => a -> G.GHCi ()
printDAP dat = do
  let outStr = _DAP_HEADER ++ showDAP dat

  liftIO $ putStrLn outStr


-- |
--
showStoppedEventBody :: Maybe GHC.ExecResult -> G.GHCi ()
showStoppedEventBody Nothing = return ()
showStoppedEventBody (Just (GHC.ExecComplete { GHC.execResult = Right _ })) = do
  let body = Right D.defaultStoppedEventBody {
             D.reasonStoppedEventBody = "complete"
           } :: Either String D.StoppedEventBody
      outStr = _DAP_HEADER ++ showDAP body
  liftIO $ putStrLn "[DAP][INFO] debug complete"
  liftIO $ putStrLn outStr
  
showStoppedEventBody (Just (GHC.ExecComplete { GHC.execResult = Left (SomeException e)})) = do
  let body = Right D.defaultStoppedEventBody {
             D.reasonStoppedEventBody = "complete"
           , D.descriptionStoppedEventBody = show e
           , D.textStoppedEventBody = show e
           } :: Either String D.StoppedEventBody
      outStr = _DAP_HEADER ++ showDAP body
  liftIO $ putStrLn $ "[DAP][INFO] debug complete with exception. " ++ show e
  liftIO $ putStrLn outStr

showStoppedEventBody (Just GHC.ExecBreak{}) = getExceptionResume >>= \case
  Nothing  -> stoppedByBreak
  Just _  -> stoopedWithException

  where
    stoopedWithException = do
      evalBody <- getEvalBody "_exception" True
      let body = Right D.defaultStoppedEventBody {
                 D.reasonStoppedEventBody = "exception"
               , D.descriptionStoppedEventBody = D.resultEvaluateBody evalBody
               , D.textStoppedEventBody = D.resultEvaluateBody evalBody
               } :: Either String D.StoppedEventBody
          outStr = _DAP_HEADER ++ showDAP body
      liftIO $ putStrLn "[DAP][INFO] break with exception."
      liftIO $ putStrLn outStr

    stoppedByBreak = do
      let body = Right D.defaultStoppedEventBody {
                 D.reasonStoppedEventBody = "step"
               } :: Either String D.StoppedEventBody
          outStr = _DAP_HEADER ++ showDAP body

      liftIO $ putStrLn "[DAP][INFO] breaks."
      liftIO $ putStrLn outStr


-- |
--
getExceptionResume :: G.GHCi (Maybe GHC.Resume)
getExceptionResume = GHC.getResumeContext >>= \case
    [] -> return Nothing
    (x:_) -> if isExceptionResume x then return (Just x)
               else return Nothing

-- |
--
isExceptionResume :: GHC.Resume -> Bool
isExceptionResume r = L.isInfixOf "exception" (GHC.resumeDecl r)



-- |
--
parseNameErrorHandler :: SomeException -> G.GHCi [GHC.Name]
parseNameErrorHandler e = liftIO $ print e >> return []


-- |
--
showTermErrorHandler :: SomeException -> G.GHCi SDoc
showTermErrorHandler e = return $ text $ show e


-- |
--
--
getEvalBody :: String -> Bool -> G.GHCi D.EvaluateBody
getEvalBody nameStr isForce =
  gcatch (GHC.parseName nameStr) parseNameErrorHandler >>= withNames

  where
    withNames [] = return D.defaultEvaluateBody {
                            D.resultEvaluateBody = "Not in scope: " ++ nameStr
                          , D.typeEvaluateBody   = "force error."
                          , D.variablesReferenceEvaluateBody = 0
                          }
    withNames (n:[]) = GHC.lookupName n >>= \case
      Just ty -> withTyThing ty
      Nothing -> return D.defaultEvaluateBody {
                          D.resultEvaluateBody = "variable not found. " ++ nameStr
                        , D.typeEvaluateBody   = "force error."
                        , D.variablesReferenceEvaluateBody = 0
                        }
    withNames _ = return D.defaultEvaluateBody {
                           D.resultEvaluateBody = "ambiguous name" ++ nameStr
                         , D.typeEvaluateBody   = "force error."
                         , D.variablesReferenceEvaluateBody = 0
                         }

    withTyThing (AnId i) = GHC.obtainTermFromId maxBound isForce i >>= withTerm i

    withTyThing x = do
      dflags <- getDynFlags
      return D.defaultEvaluateBody {
               D.resultEvaluateBody = "unsupported tything. " ++ showSDoc dflags (ppr x)
             , D.typeEvaluateBody   = "force error."
             , D.variablesReferenceEvaluateBody = 0
             }

    -- |
    --  Term https://hackage.haskell.org/package/ghc-8.2.1/docs/RtClosureInspect.html
    --
    withTerm :: GHC.Id -> Term -> G.GHCi D.EvaluateBody
    withTerm _ t@(Term ty _ _ _) = do
      dflags <- getDynFlags
      termSDoc <- gcatch (showTerm t) showTermErrorHandler
      let typeStr = showSDoc dflags (pprTypeForUser ty)
          valStr  = showSDoc dflags termSDoc

      return D.defaultEvaluateBody {
               D.resultEvaluateBody = valStr
             , D.typeEvaluateBody   = typeStr
             }

    withTerm i _ = do
      dflags <- getDynFlags
      idSDoc <- pprTypeAndContents i
      let (_, typeStr, valStr) = getNameTypeValue (showSDoc dflags idSDoc)
      return D.defaultEvaluateBody {
               D.resultEvaluateBody = valStr
             , D.typeEvaluateBody  = typeStr
             }

-- |
--
getNameTypeValue :: String -> (String, String, String)
getNameTypeValue str = (strip nameStr, strip typeStr, strip valueStr)
  where
    nameStr   = takeWhile (/= ' ')  str
    typeStr   = takeWhile (/= '=')  $ drop 4 $ dropWhile (/= ' ') str 
    valueStr_ = tail $ dropWhile (/= '=') str
    valueStr  = if elem "->" (words typeStr) then "function :: " ++ typeStr
                  else valueStr_