packages feed

futhark-0.25.35: src/Futhark/Profile/SourceRange.hs

module Futhark.Profile.SourceRange (SourceRange (..), startLineCol, endLineCol, filter123, overlapsWith, mergeSemigroup, parse, fileName) where

import Control.Arrow ((&&&))
import Control.Monad (void, when)
import Data.Bifunctor (first)
import Data.Loc (Pos (Pos), posCol, posFile, posLine)
import Data.Text qualified as T
import Data.Void (Void)
import Text.Megaparsec qualified as P
import Text.Megaparsec.Char.Lexer qualified as L

-- | I chose this representation over `Loc` from `srcLoc` because it guarantees the presence of a range.
-- Loc is essentially a 'Maybe (Pos, Pos)', because of the 'NoLoc' constructor.
-- I cannot even imagine dealing with cross-file ranges anyway.

-- The end of the range is exclusive
data SourceRange = SourceRange
  { startPos :: !Pos,
    -- | invariant: at least a big as start.line
    endLine :: !Int,
    -- | invariant: at least as big as start.col, unless the range spans multiple lines
    endColumn :: !Int
  }
  deriving (Show, Eq, Ord)

fileName :: SourceRange -> FilePath
fileName = posFile . startPos

-- | Extract start line and column
startLineCol :: SourceRange -> (Int, Int)
startLineCol (SourceRange pos _ _) = (posLine pos, posCol pos)

-- | Extract end line and column
endLineCol :: SourceRange -> (Int, Int)
endLineCol (SourceRange _ line col) = (line, col)

overlapsWith :: SourceRange -> SourceRange -> Bool
overlapsWith a b =
  -- since the end is exclusive, I need to use a different operator
  let rangeOverlaps (sa, ea) (sb, eb) = sa < eb && sb < ea
      startA = startLineCol a
      endA = endLineCol a
      startB = startLineCol b
      endB = endLineCol b
   in rangeOverlaps (startA, endA) (startB, endB)

-- >>> isEmpty $ SourceRange {sourceRangeStartPos = Pos "Futhark.fut" 4 40 (-1), sourceRangeEndLine = 4, sourceRangeEndColumn = 40}
-- True

isEmpty :: SourceRange -> Bool
isEmpty (SourceRange (Pos _ startLine startCol _) endLine endCol) =
  startLine == endLine && startCol == endCol

-- | Parse a source range, respect the invariants noted in the definition
-- and print the MegaParsec errorbundle into a Text.
--
-- >>> parse "example.fut:1:1-5"
-- Right (SourceRange {start = Pos "example.fut" 1 1 (-1), endLine = 1, endColumn = 5})
--
-- >>> parse "directory/example.fut:15:12-17:1"
-- Right (SourceRange {start = Pos "directory/example.fut" 15 12 (-1), endLine = 17, endColumn = 1})
parse :: T.Text -> Either T.Text SourceRange
parse text = first textErrorBundle $ P.parse pSourceRange fname text
  where
    fname = ""
    textErrorBundle = T.pack . P.errorBundlePretty

    lineRangeInvariantMessage =
      "End of Line Range is not bigger than or equal to Start of Line Range."
    columnRangeInvariantMessage =
      "End of Column Range is not bigger than or equal to Start of Column Range"

    pSourceRange :: P.Parsec Void T.Text SourceRange
    pSourceRange = do
      filePath <- L.charLiteral `P.manyTill` P.single ':' -- separator
      startLine <- L.decimal
      void $ P.single ':' -- separator
      startCol <- L.decimal

      void $ P.single '-' -- range begin
      rangeEnd1 <- L.decimal
      -- we can't know yet whether this is going to be a line or column position

      (lineRangeEnd, columnRangeEnd) <-
        P.choice
          [ do
              endCol <- P.single ':' *> L.decimal
              pure (rangeEnd1, endCol),
            pure (startLine, rangeEnd1)
          ]

      let lineRangeInvalid = startLine > lineRangeEnd
      when lineRangeInvalid $ fail lineRangeInvariantMessage

      let columnRangeInvalid =
            startLine == lineRangeEnd && startCol > columnRangeEnd
      when columnRangeInvalid $ fail columnRangeInvariantMessage

      pure $
        SourceRange
          { startPos = Pos filePath startLine startCol (-1),
            endLine = lineRangeEnd,
            endColumn = columnRangeEnd
          }

-- | Assumes that the ranges overlap
mergeSemigroup ::
  (Semigroup s) =>
  (SourceRange, s) ->
  (SourceRange, s) ->
  OneTwoThree (SourceRange, s)
mergeSemigroup a@(rangeA, auxA) b@(rangeB, auxB) =
  let orderedBy f x y = if f x < f y then (x, y) else (y, x)
      (startsEarlier, startsLater) = orderedBy (startLineCol . fst) a b

      startsLaterStart = startLineCol . fst $ startsLater
      fname = posFile . startPos $ rangeA

      (endsEarlier, endsLater) =
        orderedBy
          ((endLine &&& endColumn) . fst)
          a
          b

      firstRange =
        (fst startsEarlier)
          { endLine = fst startsLaterStart,
            endColumn = snd startsLaterStart
          }
      secondRange =
        (fst startsLater)
          { endLine = endLine . fst $ endsEarlier,
            endColumn = endColumn . fst $ endsEarlier
          }

      thirdRange =
        let startLine = endLine secondRange
            startCol = endColumn secondRange
         in (fst endsLater)
              { startPos = Pos fname startLine startCol (-1)
              }

      rawRanges =
        Three
          (firstRange, snd startsEarlier)
          (secondRange, auxA <> auxB)
          (thirdRange, snd endsLater)
   in case filter123 (not . isEmpty . fst) rawRanges of
        Nothing ->
          error . unwords $
            [ "Impossible! `mergeRanges` produced no range at all, input ranges:",
              show rangeA,
              show rangeB
            ]
        Just merged -> merged

data OneTwoThree a = One a | Two a a | Three a a a
  deriving (Show, Functor, Foldable)

filter123 :: (a -> Bool) -> OneTwoThree a -> Maybe (OneTwoThree a)
filter123 p self@(One x) = if p x then Just self else Nothing
filter123 p self@(Two x y) = case (p x, p y) of
  (True, True) -> Just self
  (True, False) -> Just (One x)
  (False, True) -> Just (One y)
  (False, False) -> Nothing
filter123 p self@(Three x y z) = case (p x, p y, p z) of
  (False, False, False) -> Nothing
  (False, False, True) -> Just (One z)
  (False, True, False) -> Just (One y)
  (False, True, True) -> Just (Two y z)
  (True, False, False) -> Just (One x)
  (True, False, True) -> Just (Two x z)
  (True, True, False) -> Just (Two x y)
  (True, True, True) -> Just self