ipedb-0.2.0.0: src/IpeDB/Types/SrcLoc.hs
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : IpeDB.Types.SrcLoc
Description : Representation for GHC source locations.
Stability : experimental
Portability : portable
-}
module IpeDB.Types.SrcLoc (
SrcLoc (SrcLoc, UnhelpfulSrcLoc, srcFilePath, srcRange),
ppSrcLoc,
parseSrcLoc,
Range (Range, start, end, ..),
ppRange,
parseRange,
Point (..),
) where
import Codec.LEB128.Generic (decodeLEB128, encodeLEB128)
import Data.Binary (Binary (..), Get, Put, getWord8, putWord8)
import Data.Binary.Text (getStringUTF8LEB128, putStringUTF8LEB128)
import Data.Char (isDigit)
import Data.List qualified as L
import Data.Word (Word32)
import GHC.Generics (Generic)
import GHC.Records (HasField (..))
import Text.ParserCombinators.ReadP (ReadP)
import Text.ParserCombinators.ReadP qualified as P
--------------------------------------------------------------------------------
-- Source Locations
--------------------------------------------------------------------------------
{- |
The type of source location information.
-}
data SrcLoc = SrcLoc
{ srcFilePath :: !FilePath
, srcRange :: !(Maybe Range)
}
deriving stock (Generic, Eq, Ord, Show, Read)
{- |
Simple constructor for unhelpful source locations.
-}
pattern UnhelpfulSrcLoc :: SrcLoc
pattern UnhelpfulSrcLoc = SrcLoc "" Nothing
{- |
Pretty-printer for GHC source location information
-}
ppSrcLoc :: SrcLoc -> ShowS
ppSrcLoc SrcLoc{..} =
showString srcFilePath . showString ":" . maybe mempty ppRange srcRange
{- |
Parser for GHC source location information.
-}
parseSrcLoc :: String -> Either String SrcLoc
parseSrcLoc str0 = parseSrcLocRev (reverse str0)
where
parseSrcLocRev :: String -> Either String SrcLoc
parseSrcLocRev strRev0 = do
let (maybeSrcRangeRevStr0, strRev1) = span (/= ':') strRev0
let stripColon = \case (':' : str) -> str; str -> str
let strRev2 = stripColon strRev1
case maybeSrcRangeRevStr0 of
-- If this SrcLoc uses the empty range...
[] | ":" `L.isPrefixOf` strRev1 -> do
let srcFilePath = reverse strRev2
let srcRange = Nothing
pure SrcLoc{..}
-- If this SrcLoc uses a multiline range...
')' : _ -> do
let srcFilePath = reverse strRev2
srcRange <- Just <$> parseRange (reverse maybeSrcRangeRevStr0)
pure SrcLoc{..}
-- If this SrcLoc uses a oneline or point range...
(c : _) | isDigit c -> do
let (maybeSrcRangeRevStr1, strRev3) = span (/= ':') strRev2
let srcFilePath = reverse (stripColon strRev3)
srcRange <- Just <$> parseRange (reverse maybeSrcRangeRevStr1 <> ":" <> reverse maybeSrcRangeRevStr0)
pure SrcLoc{..}
-- Otherwise, this SrcLoc has an invalid range...
_otherwise -> do
Left "parseSrcLoc: no parse"
--------------------------------------------------------------------------------
-- Source Ranges
--------------------------------------------------------------------------------
{- |
A position in a source file.
-}
data Point
= Point
{ line :: !Word32
, column :: !Word32
}
deriving stock (Generic, Eq, Ord, Show, Read)
{- |
A range in a source file.
-}
data Range
= Range'Point
{ line :: !Word32
, column :: !Word32
}
| Range'OneLine
{ line :: !Word32
, column :: !Word32
, endColumn :: !Word32
}
| Range'MultiLine
{ line :: !Word32
, column :: !Word32
, endLine :: !Word32
, endColumn :: !Word32
}
deriving stock (Generic, Eq, Ord, Show, Read)
{- |
Smart constructor for ranges.
-}
toRange :: (Point, Point) -> Range
toRange (start@Point{..}, end)
| start == end = Range'Point{..}
| line == end.line = Range'OneLine{endColumn = end.column, ..}
| otherwise = Range'MultiLine{endLine = end.line, endColumn = end.column, ..}
{- |
Smart destructor for ranges.
-}
fromRange :: Range -> (Point, Point)
fromRange r = (r.start, r.end)
{- |
Smart constructor for ranges.
-}
pattern Range :: Point -> Point -> Range
pattern Range{start, end} <- (fromRange -> (start, end))
where
Range start end = toRange (start, end)
{-# COMPLETE Range #-}
{- |
Get the start position of a range.
-}
instance HasField "start" Range Point where
getField :: Range -> Point
getField = \case
Range'Point{..} -> Point{..}
Range'OneLine{..} -> Point{..}
Range'MultiLine{..} -> Point{..}
{- |
Get the end position of a range.
-}
instance HasField "end" Range Point where
getField :: Range -> Point
getField = \case
Range'Point{..} -> Point{..}
Range'OneLine{..} -> Point{column = endColumn, ..}
Range'MultiLine{..} -> Point{line = endLine, column = endColumn}
instance Semigroup Range where
(<>) :: Range -> Range -> Range
r1 <> r2 = Range (r1.start `min` r2.start) (r1.end `max` r2.end)
{- Note [Parsing and Pretty-Printing]
The parser and pretty-printer are based on the following pretty-printer for @RealSrcSpan@ in the GHC source:
https://github.com/ghc/ghc/blob/674858e3fb1b7fe644d7ec5d4b0c6d9f18d7bc51/compiler/GHC/Types/SrcLoc.hs#L752-L774
-}
{- |
Pretty-printer for ranges.
-}
ppRange :: Range -> ShowS
ppRange = \case
Range'Point{..} ->
shows line . showString ":" . shows column
Range'OneLine{..} ->
shows line . showString ":" . shows column . showString "-" . shows endColumn
Range{..} ->
ppPoint start . showString "-" . ppPoint end
where
ppPoint :: Point -> ShowS
ppPoint p =
showParen True (shows p.line . showString "," . shows p.column)
{- |
Internal helper.
Parser for ranges.
-}
pRange :: ReadP Range
pRange = pRange'MultiLine P.<++ (pRange'OneLine P.+++ pPointRange)
where
pRange'MultiLine :: ReadP Range
pRange'MultiLine = Range <$> pPoint <* P.char '-' <*> pPoint
pRange'OneLine :: ReadP Range
pRange'OneLine = Range'OneLine <$> pIndex <* P.char ':' <*> pIndex <* P.char '-' <*> pIndex
pPointRange :: ReadP Range
pPointRange = Range'Point <$> pIndex <* P.char ':' <*> pIndex
pIndex :: ReadP Word32
pIndex = read <$> P.munch1 isDigit
pPoint :: ReadP Point
pPoint = Point <$ P.char '(' <*> pIndex <* P.char ',' <*> pIndex <* P.char ')'
{- |
Parse a source range.
-}
parseRange :: String -> Either String Range
parseRange s =
case [x | (x, "") <- P.readP_to_S pRange s] of
[x] -> Right x
[] -> Left "parseSrcLoc: no parse"
_ -> Left "parseSrcLoc: ambiguous parse"
--------------------------------------------------------------------------------
-- Instances for binary serialisation of SrcLoc
--------------------------------------------------------------------------------
{- |
Internal helper.
Serialise source location information.
-}
putSrcLoc :: SrcLoc -> Put
putSrcLoc SrcLoc{..} = do
putStringUTF8LEB128 srcFilePath
putMaybeRange srcRange
{- |
Internal helper.
Deserialise source location information.
-}
getSrcLoc :: Get SrcLoc
getSrcLoc = do
srcFilePath <- getStringUTF8LEB128
srcRange <- getMaybeRange
pure SrcLoc{..}
instance Binary SrcLoc where
put :: SrcLoc -> Put
put = putSrcLoc
get :: Get SrcLoc
get = getSrcLoc
--------------------------------------------------------------------------------
-- Instances for binary serialisation of optional ranges
--------------------------------------------------------------------------------
{- |
Internal helper.
Constructor tags for binary serialisation of ranges.
-}
data MaybeRangeTag
= TagNothing
| TagJustRange'Point
| TagJustRange'OneLine
| TagJustRange'MultiLine
{- |
Internal helper.
Serialise a constructor tag for an optional range.
-}
putMaybeRangeTag :: MaybeRangeTag -> Put
putMaybeRangeTag = \case
TagNothing -> putWord8 0x01
TagJustRange'Point -> putWord8 0x02
TagJustRange'OneLine -> putWord8 0x03
TagJustRange'MultiLine -> putWord8 0x04
{- |
Internal helper.
Deserialise a constructor tag for an optional range.
-}
getMaybeRangeTag :: Get MaybeRangeTag
getMaybeRangeTag = do
maybeRangeTag <- getWord8
case maybeRangeTag of
0x01 -> pure TagNothing
0x02 -> pure TagJustRange'Point
0x03 -> pure TagJustRange'OneLine
0x04 -> pure TagJustRange'MultiLine
_otherwise -> fail $ "Unexpected tag " <> show maybeRangeTag
{- |
Internal helper.
Serialise an optional range to binary.
-}
putMaybeRange :: Maybe Range -> Put
putMaybeRange = \case
Nothing ->
putMaybeRangeTag TagNothing
Just Range'Point{..} -> do
putMaybeRangeTag TagJustRange'Point
encodeLEB128 putWord8 line
encodeLEB128 putWord8 column
Just Range'OneLine{..} -> do
putMaybeRangeTag TagJustRange'OneLine
encodeLEB128 putWord8 line
encodeLEB128 putWord8 column
encodeLEB128 putWord8 endColumn
Just Range'MultiLine{..} -> do
putMaybeRangeTag TagJustRange'MultiLine
encodeLEB128 putWord8 line
encodeLEB128 putWord8 column
encodeLEB128 putWord8 endLine
encodeLEB128 putWord8 endColumn
{- |
Internal helper.
Deserialise an optional range from binary.
-}
getMaybeRange :: Get (Maybe Range)
getMaybeRange =
getMaybeRangeTag >>= \case
TagNothing ->
pure Nothing
TagJustRange'Point -> do
line <- decodeLEB128 getWord8
column <- decodeLEB128 getWord8
pure . Just $! Range'Point{..}
TagJustRange'OneLine -> do
line <- decodeLEB128 getWord8
column <- decodeLEB128 getWord8
endColumn <- decodeLEB128 getWord8
pure . Just $! Range'OneLine{..}
TagJustRange'MultiLine -> do
line <- decodeLEB128 getWord8
column <- decodeLEB128 getWord8
endLine <- decodeLEB128 getWord8
endColumn <- decodeLEB128 getWord8
pure . Just $! Range'MultiLine{..}