moonlight-pale-0.1.0.0: src-ghc-surface/Moonlight/Pale/Ghc/Hie/SourceKey.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-| Source-path keys and indexed lookup for HIE oracle artifacts. -}
module Moonlight.Pale.Ghc.Hie.SourceKey
( HieSourceKeyKind (..),
TriedKey (..),
HieOracleArtifact (..),
OracleLookup (..),
OracleAttachFailure (..),
HieOracleIndex,
OracleQuery (..),
buildHieOracleIndex,
lookupModuleOracle,
oracleLookupOracle,
oracleAttachFailure,
)
where
import Data.Char (isAlpha, toUpper)
import Data.Either (partitionEithers)
import Data.IntSet (IntSet)
import Data.IntSet qualified as IntSet
import Data.Kind (Type)
import Data.List (intercalate, stripPrefix)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe (mapMaybe)
import Data.Vector (Vector)
import Data.Vector qualified as Vector
import Moonlight.Pale.Ghc.Hie.Oracle (ModuleNameOracle (..))
type HieSourceKeyKind :: Type
data HieSourceKeyKind
= GivenPathKey
| AbsolutePathKey
| RootRelativeKey
| ModuleSuffixKey
deriving stock (Eq, Ord, Show, Read, Enum, Bounded)
type TriedKey :: Type
data TriedKey = TriedKey !HieSourceKeyKind !FilePath
deriving stock (Eq, Ord, Show, Read)
type HieOracleArtifact :: Type
data HieOracleArtifact = HieOracleArtifact
{ hieArtifactPath :: !FilePath,
hieArtifactOracle :: !ModuleNameOracle
}
deriving stock (Eq, Show)
type OracleLookup :: Type
data OracleLookup
= OracleFound !HieSourceKeyKind !HieOracleArtifact
| OracleMissing ![TriedKey]
| OracleAmbiguous !HieSourceKeyKind !FilePath ![FilePath]
| OracleIndexObstruction ![Int]
deriving stock (Eq, Show)
type OracleAttachFailure :: Type
data OracleAttachFailure
= OracleLookupMissing ![TriedKey]
| OracleLookupAmbiguous !HieSourceKeyKind !FilePath ![FilePath]
| OracleLookupIndexObstruction ![Int]
deriving stock (Eq, Ord, Show, Read)
data PathAnchor
= RelativeAnchor
| PosixRootAnchor
| DriveRootAnchor !Char
| UncRootAnchor !String !String
deriving stock (Eq, Ord, Show)
data CanonicalPath = CanonicalPath
{ cpAnchor :: !PathAnchor,
cpComponents :: ![FilePath]
}
deriving stock (Eq, Ord, Show)
data PathPart
= ComponentPart !FilePath
| AnchorPart !PathAnchor
deriving stock (Eq, Ord, Show)
newtype OracleId = OracleId Int
deriving stock (Eq, Ord, Show)
data CandidateSummary
= NoCandidate
| OneCandidate !OracleId
| ManyCandidates !IntSet
deriving stock (Eq, Show)
data PathTrie = PathTrie
{ ptTerminal :: !CandidateSummary,
ptDescendants :: !CandidateSummary,
ptChildren :: !(Map PathPart PathTrie)
}
deriving stock (Eq, Show)
data HieOracleIndex = HieOracleIndex
{ hoiArtifacts :: !(Vector HieOracleArtifact),
hoiPaths :: !PathTrie
}
deriving stock (Eq, Show)
type OracleQuery :: Type
data OracleQuery = OracleQuery
{ oqGivenPath :: !FilePath,
oqAbsolutePath :: !(Maybe FilePath),
oqSourceRoots :: ![FilePath]
}
deriving stock (Eq, Show)
buildHieOracleIndex :: [HieOracleArtifact] -> HieOracleIndex
buildHieOracleIndex artifacts =
HieOracleIndex
{ hoiArtifacts = Vector.fromList artifacts,
hoiPaths =
foldl'
( \pathTrie (oracleIndex, artifact) ->
insertPath
(OracleId oracleIndex)
(canonicalPath (mnoSourcePath (hieArtifactOracle artifact)))
pathTrie
)
emptyPathTrie
(zip [0 ..] artifacts)
}
lookupModuleOracle :: HieOracleIndex -> OracleQuery -> OracleLookup
lookupModuleOracle oracleIndex query =
case firstExactLookup oracleIndex (exactQueryKeys query) of
Just exactResult ->
exactResult
Nothing ->
maybe
(OracleMissing (exactTriedKeys query <> suffixTriedKeys query))
( \(matchedPath, candidates) ->
lookupOutcome
oracleIndex
ModuleSuffixKey
(renderCanonicalPath matchedPath)
candidates
)
(deepestSuffixCandidates (canonicalPath (oqGivenPath query)) (hoiPaths oracleIndex))
firstExactLookup :: HieOracleIndex -> [(HieSourceKeyKind, CanonicalPath)] -> Maybe OracleLookup
firstExactLookup oracleIndex =
foldr
( \(keyKind, pathValue) next ->
case exactCandidates pathValue (hoiPaths oracleIndex) of
NoCandidate ->
next
candidates ->
Just
( lookupOutcome
oracleIndex
keyKind
(renderCanonicalPath pathValue)
candidates
)
)
Nothing
lookupOutcome :: HieOracleIndex -> HieSourceKeyKind -> FilePath -> CandidateSummary -> OracleLookup
lookupOutcome oracleIndex keyKind matchedKey candidates =
case candidateArtifacts candidates (hoiArtifacts oracleIndex) of
Left missingOracleIds ->
OracleIndexObstruction missingOracleIds
Right [] ->
OracleMissing [TriedKey keyKind matchedKey]
Right [artifact] ->
OracleFound keyKind artifact
Right ambiguous ->
OracleAmbiguous keyKind matchedKey (fmap hieArtifactPath ambiguous)
candidateArtifacts :: CandidateSummary -> Vector HieOracleArtifact -> Either [Int] [HieOracleArtifact]
candidateArtifacts summary artifacts =
case
partitionEithers
( fmap
( \artifactIndex ->
maybe
(Left artifactIndex)
Right
(artifacts Vector.!? artifactIndex)
)
(IntSet.toAscList (candidateIds summary))
)
of
([], foundArtifacts) ->
Right foundArtifacts
(missingArtifactIds, _) ->
Left missingArtifactIds
candidateIds :: CandidateSummary -> IntSet
candidateIds = \case
NoCandidate ->
IntSet.empty
OneCandidate (OracleId oracleId) ->
IntSet.singleton oracleId
ManyCandidates oracleIds ->
oracleIds
oracleLookupOracle :: OracleLookup -> Maybe ModuleNameOracle
oracleLookupOracle = \case
OracleFound _ artifact ->
Just (hieArtifactOracle artifact)
OracleMissing _ ->
Nothing
OracleAmbiguous _ _ _ ->
Nothing
OracleIndexObstruction _ ->
Nothing
oracleAttachFailure :: OracleLookup -> Maybe OracleAttachFailure
oracleAttachFailure = \case
OracleFound {} ->
Nothing
OracleMissing triedKeys ->
Just (OracleLookupMissing triedKeys)
OracleAmbiguous keyKind keyValue candidates ->
Just (OracleLookupAmbiguous keyKind keyValue candidates)
OracleIndexObstruction missingOracleIds ->
Just (OracleLookupIndexObstruction missingOracleIds)
emptyPathTrie :: PathTrie
emptyPathTrie =
PathTrie
{ ptTerminal = NoCandidate,
ptDescendants = NoCandidate,
ptChildren = Map.empty
}
insertPath :: OracleId -> CanonicalPath -> PathTrie -> PathTrie
insertPath oracleId pathValue =
insertParts
(fmap ComponentPart (reverse (cpComponents pathValue)) <> [AnchorPart (cpAnchor pathValue)])
where
insertParts parts pathTrie =
case parts of
[] ->
pathTrie
{ ptTerminal = insertCandidate oracleId (ptTerminal pathTrie),
ptDescendants = insertCandidate oracleId (ptDescendants pathTrie)
}
pathPart : remaining ->
pathTrie
{ ptDescendants = insertCandidate oracleId (ptDescendants pathTrie),
ptChildren =
Map.alter
( Just
. insertParts remaining
. maybe emptyPathTrie id
)
pathPart
(ptChildren pathTrie)
}
insertCandidate :: OracleId -> CandidateSummary -> CandidateSummary
insertCandidate oracleId = \case
NoCandidate ->
OneCandidate oracleId
OneCandidate existing
| existing == oracleId ->
OneCandidate existing
| otherwise ->
ManyCandidates
(IntSet.fromList [oracleIdInt existing, oracleIdInt oracleId])
ManyCandidates existing ->
ManyCandidates (IntSet.insert (oracleIdInt oracleId) existing)
oracleIdInt :: OracleId -> Int
oracleIdInt (OracleId oracleId) =
oracleId
exactCandidates :: CanonicalPath -> PathTrie -> CandidateSummary
exactCandidates pathValue =
descend
(fmap ComponentPart (reverse (cpComponents pathValue)) <> [AnchorPart (cpAnchor pathValue)])
where
descend parts pathTrie =
case parts of
[] ->
ptTerminal pathTrie
pathPart : remaining ->
maybe
NoCandidate
(descend remaining)
(Map.lookup pathPart (ptChildren pathTrie))
deepestSuffixCandidates :: CanonicalPath -> PathTrie -> Maybe (CanonicalPath, CandidateSummary)
deepestSuffixCandidates queryPath =
descend Nothing [] (reverse (cpComponents queryPath))
where
descend best matchedComponents remaining pathTrie =
case remaining of
[] ->
best
component : nextComponents ->
case Map.lookup (ComponentPart component) (ptChildren pathTrie) of
Nothing ->
best
Just childTrie ->
let nextMatchedComponents = component : matchedComponents
nextBest =
case ptDescendants childTrie of
NoCandidate ->
best
candidates ->
Just
( CanonicalPath RelativeAnchor nextMatchedComponents,
candidates
)
in descend nextBest nextMatchedComponents nextComponents childTrie
exactQueryKeys :: OracleQuery -> [(HieSourceKeyKind, CanonicalPath)]
exactQueryKeys query =
[(GivenPathKey, canonicalPath (oqGivenPath query))]
<> maybe [] (\absolutePath -> [(AbsolutePathKey, canonicalPath absolutePath)]) (oqAbsolutePath query)
<> fmap (\relativePath -> (RootRelativeKey, relativePath)) (rootRelativePaths query)
exactTriedKeys :: OracleQuery -> [TriedKey]
exactTriedKeys =
mapMaybe
( \(keyKind, pathValue) ->
case renderCanonicalPath pathValue of
"" ->
Nothing
renderedPath ->
Just (TriedKey keyKind renderedPath)
)
. exactQueryKeys
rootRelativePaths :: OracleQuery -> [CanonicalPath]
rootRelativePaths query =
[ relativePath
| root <- fmap canonicalPath (oqSourceRoots query),
pathValue <-
canonicalPath (oqGivenPath query)
: maybe [] (pure . canonicalPath) (oqAbsolutePath query),
Just relativePath <- [stripCanonicalRoot root pathValue]
]
stripCanonicalRoot :: CanonicalPath -> CanonicalPath -> Maybe CanonicalPath
stripCanonicalRoot root pathValue
| cpAnchor root /= cpAnchor pathValue =
Nothing
| otherwise =
CanonicalPath RelativeAnchor
<$> stripPrefix (cpComponents root) (cpComponents pathValue)
suffixTriedKeys :: OracleQuery -> [TriedKey]
suffixTriedKeys query =
fmap
(TriedKey ModuleSuffixKey . renderCanonicalPath . CanonicalPath RelativeAnchor)
(componentSuffixes (cpComponents (canonicalPath (oqGivenPath query))))
componentSuffixes :: [FilePath] -> [[FilePath]]
componentSuffixes components =
case components of
[] ->
[]
_ : remaining ->
components : componentSuffixes remaining
canonicalPath :: FilePath -> CanonicalPath
canonicalPath rawPath =
case rawPath of
firstSeparator : secondSeparator : remaining
| pathSeparator firstSeparator,
pathSeparator secondSeparator ->
case splitPathComponents remaining of
server : share : components ->
CanonicalPath
(UncRootAnchor server share)
(normaliseComponents True components)
components ->
CanonicalPath PosixRootAnchor (normaliseComponents True components)
driveLetter : ':' : remaining
| isAlpha driveLetter ->
CanonicalPath
(DriveRootAnchor (toUpper driveLetter))
(normaliseComponents True (splitPathComponents remaining))
firstSeparator : remaining
| pathSeparator firstSeparator ->
CanonicalPath
PosixRootAnchor
(normaliseComponents True (splitPathComponents remaining))
_ ->
CanonicalPath
RelativeAnchor
(normaliseComponents False (splitPathComponents rawPath))
splitPathComponents :: FilePath -> [FilePath]
splitPathComponents pathValue =
case dropWhile pathSeparator pathValue of
[] ->
[]
remaining ->
let (component, next) = break pathSeparator remaining
in component : splitPathComponents next
normaliseComponents :: Bool -> [FilePath] -> [FilePath]
normaliseComponents rooted =
reverse . foldl' normaliseComponent []
where
normaliseComponent reversedComponents component
| component == "." || null component =
reversedComponents
| component == ".." =
case reversedComponents of
previous : remaining
| previous /= ".." ->
remaining
_
| rooted ->
reversedComponents
| otherwise ->
".." : reversedComponents
| otherwise =
component : reversedComponents
renderCanonicalPath :: CanonicalPath -> FilePath
renderCanonicalPath pathValue =
let componentText = intercalate "/" (cpComponents pathValue)
in case cpAnchor pathValue of
RelativeAnchor ->
componentText
PosixRootAnchor ->
"/" <> componentText
DriveRootAnchor driveLetter ->
driveLetter : ':' : '/' : componentText
UncRootAnchor server share ->
"//" <> server <> "/" <> share
<> if null componentText
then ""
else "/" <> componentText
pathSeparator :: Char -> Bool
pathSeparator character =
character == '/' || character == '\\'