summaryrefslogtreecommitdiff
path: root/source/Felix/Report/Location.hs
diff options
context:
space:
mode:
authoradelon <22380201+adelon@users.noreply.github.com>2026-08-06 17:54:00 +0200
committeradelon <22380201+adelon@users.noreply.github.com>2026-08-06 17:54:00 +0200
commit82328890108bae64b372b8d58620ebc62699de76 (patch)
tree575404c6b425c19259c0ded296f1c8ffb7ff0e2b /source/Felix/Report/Location.hs
parent1a25421c2a168d420581358c8733fcd8f36f379b (diff)
Migrate to `Felix` namespaceHEADhotg
Diffstat (limited to 'source/Felix/Report/Location.hs')
-rw-r--r--source/Felix/Report/Location.hs284
1 files changed, 284 insertions, 0 deletions
diff --git a/source/Felix/Report/Location.hs b/source/Felix/Report/Location.hs
new file mode 100644
index 0000000..36b8997
--- /dev/null
+++ b/source/Felix/Report/Location.hs
@@ -0,0 +1,284 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+module Felix.Report.Location where
+
+import Base
+import Text.Megaparsec.Pos (SourcePos (sourceColumn, sourceLine), unPos)
+import Data.Bits
+import Control.DeepSeq (NFData)
+import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef)
+import Data.IntMap.Strict qualified as IntMap
+import Data.Map.Strict qualified as Map
+import Data.Text qualified as Text
+import Data.Word (Word16, Word32)
+import System.IO.Unsafe (unsafePerformIO)
+
+-- | File identifier used in packed source locations.
+newtype FileId = FileId
+ { unFileId :: Word16
+ } deriving stock (Show, Eq, Ord, Generic)
+ deriving anyclass (Hashable, NFData)
+
+-- | Packed source location.
+-- Bit layout (high to low):
+-- 16 bits file id | 24 bits line | 24 bits column.
+newtype Location = Location
+ { unLocation :: Word64
+ } deriving stock (Eq, Ord, Generic)
+ deriving anyclass (Hashable, NFData)
+
+data RegisteredFile = RegisteredFile
+ { registeredFileIdentity :: FilePath
+ , registeredFileDisplayPath :: FilePath
+ }
+
+data FileRegistry = FileRegistry
+ { registrationToFileId :: Map (FilePath, FilePath) FileId
+ , fileIdToFile :: IntMap RegisteredFile
+ , fileIdAllocator :: !FileIdAllocator
+ }
+
+data LocationRegistrationError
+ = FileIdSpaceExhausted
+ deriving stock (Show, Eq)
+
+data LocationConstructionError
+ = LocationCoordinateOutOfRange !Int !Int
+ deriving stock (Show, Eq)
+
+newtype FileIdAllocator = FileIdAllocator Word32
+ deriving stock (Show, Eq)
+
+initialFileIdAllocator :: FileIdAllocator
+initialFileIdAllocator = FileIdAllocator 0
+
+allocateFileId
+ :: FileIdAllocator
+ -> Either LocationRegistrationError (FileId, FileIdAllocator)
+allocateFileId (FileIdAllocator next)
+ | next >= fromIntegral (maxBound :: Word16) =
+ Left FileIdSpaceExhausted
+ | otherwise =
+ Right
+ ( FileId (fromIntegral next)
+ , FileIdAllocator (next + 1)
+ )
+
+initialFileRegistry :: FileRegistry
+initialFileRegistry = FileRegistry
+ { registrationToFileId = mempty
+ , fileIdToFile = mempty
+ , fileIdAllocator = initialFileIdAllocator
+ }
+
+fileRegistryRef :: IORef FileRegistry
+fileRegistryRef = unsafePerformIO (newIORef initialFileRegistry)
+{-# NOINLINE fileRegistryRef #-}
+
+registerFilePath
+ :: MonadIO io
+ => FilePath
+ -> io (Either LocationRegistrationError FileId)
+registerFilePath path =
+ registerFilePathWithDisplay path path
+
+-- | Register a file by physical identity while retaining a separate path for
+-- diagnostics. The identity must be stable and unique for the selected
+-- physical source. Distinct display paths receive distinct file identifiers so
+-- one workspace cannot inherit another workspace's presentation. Callers with
+-- only one path should use 'registerFilePath'.
+registerFilePathWithDisplay
+ :: MonadIO io
+ => FilePath
+ -> FilePath
+ -> io (Either LocationRegistrationError FileId)
+registerFilePathWithDisplay identity displayPath = liftIO $
+ atomicModifyIORef' fileRegistryRef \registry ->
+ case Map.lookup registration (registrationToFileId registry) of
+ Just fileId ->
+ (registry, Right fileId)
+ Nothing ->
+ case allocateFileId (fileIdAllocator registry) of
+ Left err ->
+ (registry, Left err)
+ Right (fileId, nextAllocator) ->
+ let fileIdInt = fromIntegral (unFileId fileId)
+ registeredFile =
+ RegisteredFile identity displayPath
+ registry' = FileRegistry
+ { registrationToFileId =
+ Map.insert
+ registration
+ fileId
+ (registrationToFileId registry)
+ , fileIdToFile =
+ IntMap.insert
+ fileIdInt
+ registeredFile
+ (fileIdToFile registry)
+ , fileIdAllocator = nextAllocator
+ }
+ in
+ (registry', Right fileId)
+ where
+ registration = (identity, displayPath)
+
+lookupFilePath :: FileId -> Maybe FilePath
+lookupFilePath fileId = unsafePerformIO do
+ registry <- readIORef fileRegistryRef
+ pure
+ (registeredFileDisplayPath <$>
+ IntMap.lookup
+ (fromIntegral (unFileId fileId))
+ (fileIdToFile registry))
+
+lookupFileIdentityPath :: FileId -> Maybe FilePath
+lookupFileIdentityPath fileId = unsafePerformIO do
+ registry <- readIORef fileRegistryRef
+ pure
+ (registeredFileIdentity <$>
+ IntMap.lookup
+ (fromIntegral (unFileId fileId))
+ (fileIdToFile registry))
+
+fileShift, lineShift :: Int
+fileShift = 48
+lineShift = 24
+
+fileMask, coordMask :: Word64
+fileMask = 0xFFFF
+coordMask = 0xFFFFFF
+
+nowhereWord :: Word64
+nowhereWord = maxBound
+
+mkLocationChecked
+ :: FileId
+ -> Int
+ -> Int
+ -> Either LocationConstructionError Location
+mkLocationChecked fileId line column
+ | line < 0
+ || column < 0
+ || lineWord > coordMask
+ || columnWord > coordMask =
+ Left (LocationCoordinateOutOfRange line column)
+ | otherwise =
+ Right
+ (Location
+ ( (fromIntegral (unFileId fileId) `shiftL` fileShift)
+ .|. (lineWord `shiftL` lineShift)
+ .|. columnWord
+ ))
+ where
+ lineWord = fromIntegral line :: Word64
+ columnWord = fromIntegral column :: Word64
+
+mkLocation :: FileId -> Int -> Int -> Location
+mkLocation fileId line column =
+ either
+ (impossible . ("mkLocation: " <>) . show)
+ id
+ (mkLocationChecked fileId line column)
+
+locFileId :: Location -> Maybe FileId
+locFileId (Location w)
+ | w == nowhereWord = Nothing
+ | otherwise = Just (FileId (fromIntegral ((w `shiftR` fileShift) .&. fileMask)))
+
+locFile :: Location -> FilePath
+locFile loc = case locFileId loc of
+ Nothing -> "<nowhere>"
+ Just fileId ->
+ fromMaybe ("<file#" <> show (unFileId fileId) <> ">") (lookupFilePath fileId)
+
+locLine :: Location -> Int
+locLine (Location w)
+ | w == nowhereWord = -1
+ | otherwise = fromIntegral ((w `shiftR` lineShift) .&. coordMask)
+
+locColumn :: Location -> Int
+locColumn (Location w)
+ | w == nowhereWord = -1
+ | otherwise = fromIntegral (w .&. coordMask)
+
+instance Show Location where
+ showsPrec p loc =
+ showParen (p > appPrec) $
+ showString "Location {locFile = "
+ . shows (locFile loc)
+ . showString ", locLine = "
+ . shows (locLine loc)
+ . showString ", locColumn = "
+ . shows (locColumn loc)
+ . showString "}"
+ where
+ appPrec = 10
+
+fromSourcePosChecked
+ :: FileId
+ -> SourcePos
+ -> Either LocationConstructionError Location
+fromSourcePosChecked fileId pos =
+ mkLocationChecked
+ fileId
+ (unPos (sourceLine pos))
+ (unPos (sourceColumn pos))
+
+prettyLocation :: Location -> String
+prettyLocation loc =
+ locFile loc <> " " <> show (locLine loc) <> ":" <> show (locColumn loc)
+
+-- | Render two locations together, qualifying equal display paths when they
+-- refer to different physical files.
+prettyLocationPair :: Location -> Location -> (String, String)
+prettyLocationPair firstLocation secondLocation =
+ if locFile firstLocation == locFile secondLocation
+ && firstIdentity /= secondIdentity
+ && isJust firstIdentity
+ && isJust secondIdentity
+ then
+ ( qualify firstIdentity firstLocation
+ , qualify secondIdentity secondLocation
+ )
+ else
+ (prettyLocation firstLocation, prettyLocation secondLocation)
+ where
+ firstIdentity =
+ locFileId firstLocation >>= lookupFileIdentityPath
+ secondIdentity =
+ locFileId secondLocation >>= lookupFileIdentityPath
+
+ qualify identity location =
+ locFile location
+ <> " (canonical "
+ <> maybe "<unknown>" show identity
+ <> ") "
+ <> show (locLine location)
+ <> ":"
+ <> show (locColumn location)
+
+locationToText :: Location -> Text
+locationToText loc = Text.pack (prettyLocation loc)
+
+-- | Things that have a location.
+class Locatable a where
+ locate :: a -> Location
+
+pattern Nowhere :: Location
+pattern Nowhere = Location 0xFFFFFFFFFFFFFFFF
+
+instance Locatable Location where
+ locate = id
+
+instance Locatable a => Locatable [a] where
+ locate [] = Nowhere
+ locate (x:_) = locate x
+
+instance Locatable a => Locatable (Maybe a) where
+ locate Nothing = Nowhere
+ locate (Just x) = locate x
+
+instance Locatable a => Locatable (NonEmpty a) where
+ locate (x :| _) = locate x