packages feed

hs-bindgen-1.0.0.0: src-internal/HsBindgen/Frontend/Pass/Parse/Monad/SourceRangeMap.hs

-- | Mapping of source location ranges in C code to @a@ values
--
-- Intended for unqualified import (unless context is unambiguous).
--
-- > import HsBindgen.Frontend.Pass.Parse.Monad.SourceRangeMap
module HsBindgen.Frontend.Pass.Parse.Monad.SourceRangeMap (
    SourceRangeMap
  , initSourceRangeMap
  , recordAt
  , LookupResult (..)
  , lookupRange
  , lookupAt
  ) where

import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.List.NonEmpty qualified as NonEmpty
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Semigroup (Semigroup (sconcat))

import Clang.HighLevel.Types (Range (rangeEnd, rangeStart), RealPath,
                              SingleLoc (singleLocColumn, singleLocLine, singleLocPath))

-- | Position storing @(line, column)@ so that lookups are precise to the
-- column: two values recorded on the same line (e.g. a struct tag and a field
-- type, both macro expansions) are attributed to the correct extent.
--
-- See https://github.com/well-typed/hs-bindgen/issues/2049.
data Pos = Pos {
    line :: Int
  , column :: Int
  }
  deriving stock (Eq, Show)

-- | We provide a custom 'Ord' instance because it is important to compare lines
--   before columns!
instance Ord Pos where
  left `compare` right =
       left.line   `compare` right.line
    <> left.column `compare` right.column

toPos :: SingleLoc RealPath -> Pos
toPos loc = Pos loc.singleLocLine loc.singleLocColumn

-- | Mapping of source location ranges in C code to @a@ values
newtype SourceRangeMap a = SRM {
    -- | We use a stacked map so we can lookup values in source location ranges
    -- reasonably fast.
    unwrap :: Map RealPath (Map Pos (NonEmpty a))
  }

-- | An empty 'SourceRangeMap'
initSourceRangeMap :: SourceRangeMap a
initSourceRangeMap = SRM Map.empty

-- | Map the given location to a value of type @a@
recordAt :: forall a. SingleLoc RealPath -> a -> SourceRangeMap a -> SourceRangeMap a
recordAt loc new srm = SRM (addMacro srm.unwrap)
  where
    pos :: Pos
    pos = toPos loc

    addMacro ::
         Map RealPath (Map Pos (NonEmpty a))
      -> Map RealPath (Map Pos (NonEmpty a))
    addMacro = Map.alter addMacroAtFile loc.singleLocPath

    addMacroAtFile ::
         Maybe (Map Pos (NonEmpty a))
      -> Maybe (Map Pos (NonEmpty a))
    addMacroAtFile = Just . \case
      Nothing ->
        Map.singleton pos $ NonEmpty.singleton new
      Just posMap ->
        Map.alter addMacroAtPos pos posMap

    addMacroAtPos :: Maybe (NonEmpty a) -> Maybe (NonEmpty a)
    addMacroAtPos = Just . \case
      Nothing     -> NonEmpty.singleton new
      Just macros -> NonEmpty.cons      new macros

data LookupResult a =
    -- | Ranges that span multiple files are not supported in lookups
    LookupErrorMultipleFiles
  | LookupNotFound
  | LookupFound a

-- | Lookup values in the given half-open source location range
--
-- The end of a range is exclusive: Clang reports the end of an extent as the
-- position just after its last token, where the next token may start.
lookupRange :: forall a. Range (SingleLoc RealPath) -> SourceRangeMap a -> LookupResult (NonEmpty a)
lookupRange range srm
  | range.rangeStart.singleLocPath /= range.rangeEnd.singleLocPath
  = LookupErrorMultipleFiles
  | otherwise
  = maybe LookupNotFound LookupFound aux
  where
    aux :: Maybe (NonEmpty a)
    aux = do
        posMap <- Map.lookup range.rangeStart.singleLocPath srm.unwrap
        fmap sconcat $ NE.nonEmpty $ Map.elems $
          Map.takeWhileAntitone (toPos range.rangeEnd >) $
            Map.dropWhileAntitone (toPos range.rangeStart >) posMap

-- | Lookup values recorded at the given source location
lookupAt :: SingleLoc RealPath -> SourceRangeMap a -> Maybe (NonEmpty a)
lookupAt loc srm =
    Map.lookup (toPos loc) =<< Map.lookup loc.singleLocPath srm.unwrap