tilia-0.1.2.0: src/Tilia/Fixity/Plan.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
-- | "Tilia.Fixity" resolves a module's operators exactly, given a function
-- that says what each imported module exports. This is that function, built
-- from what the project itself is compiled against.
module Tilia.Fixity.Plan
( Route (..),
Resolver (..),
newResolver,
newResolverVia,
newResolverWith,
newResolverFrom,
withReexports,
scopeFor,
)
where
import Control.Applicative ((<|>))
import Control.Concurrent (getNumCapabilities)
import Control.Concurrent.QSem (newQSem, signalQSem, waitQSem)
import Control.DeepSeq (force)
import Control.Exception (bracket_, evaluate)
import Control.Monad (filterM, join)
import Crypto.Hash.SHA256 qualified as SHA256
import Data.ByteString.Base16 qualified as B16
import Data.ByteString.Lazy qualified as BL
import Data.Choice (Choice, fromBool, pattern Do)
import Data.Foldable (toList)
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe (catMaybes, fromMaybe, isNothing, mapMaybe)
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import GHC.Hs (HsModule)
import GHC.Hs.Extension (GhcPs)
import GHC.LanguageExtensions.Type (Extension (ImplicitPrelude))
import System.Directory (doesFileExist)
import System.FilePath ((</>))
import Tilia.Cabal.Package (PackageReader, declaredModules, newPackageReader, readPackageDescription)
import Tilia.Cabal.Plan
import Tilia.Cabal.Project (cabalFilesIn)
import Tilia.Cpp.Directives (branchLeaves, withoutRuledOut)
import Tilia.Cpp.Macros (Macros (..))
import Tilia.Fixity
import Tilia.Fixity.Archive (Archive (..), InArchive (..), moduleEndings, moduleIn, pathOfModule, readArchive, writtenForHsc)
import Tilia.Fixity.Cabal (declaredExtensions, packageModules)
import Tilia.Fixity.Cache
import Tilia.Fixity.HiFile (HiFile, primopFixities)
import Tilia.Fixity.Interface
import Tilia.Fixity.PackageDb
import Tilia.Memo (memoized, newFlights, newMemo)
import Tilia.Parser
import Tilia.Pragma (effectiveExtensions)
import Tilia.Utils (byteOrderMark, inParallel, quietly, readFileText, sha256Of)
----------------------------------------------------------------------------
-- Resolving
-- | Where a module's fixities can be read from.
data Route
= -- | The compiled interface the package database points at. Cheap, and
-- authoritative where it exists, since it is the compiler's own account
-- of what it settled on.
FromInterface
| -- | The module's source, out of the package's tarball in Cabal's
-- package cache. Slower, and the only route for a package that is
-- planned but not built.
FromSource
deriving (Eq, Show)
-- | What can be asked about a module, once a plan says where to look.
newtype Resolver = Resolver
{ -- | What reading a module established about the names it exports.
askModule :: Text -> IO Established
}
-- | Build a new 'Resolver'.
--
-- Answers are remembered on disk between runs by "Tilia.Fixity.Cache", so a
-- package is decompressed and parsed once per machine rather than once per
-- file.
newResolver ::
-- | The build plan to use.
BuildPlan ->
IO Resolver
newResolver plan = do
packageReader <- newPackageReader (plannedCompiler plan)
newResolverVia (Do #useCache) [FromInterface, FromSource] packageReader plan
-- | 'newResolver', restricted to the routes given.
newResolverVia ::
-- | Whether to use the cache.
Choice "useCache" ->
-- | Which readings to try, in order.
[Route] ->
-- | What the package of each of the project's own modules puts in force.
PackageReader ->
-- | The build plan to use.
BuildPlan ->
IO Resolver
newResolverVia caching routes packageReader plan = do
cache <- openCache caching =<< tokenForBuildPlan plan
tarballs <-
if FromSource `elem` routes
then plannedTarballs cache plan
else pure []
installed <- getInstalledPackages cache
newResolverWith routes cache installed tarballs packageReader plan
-- | 'newResolverVia', given what it would find out from the machine.
newResolverWith ::
-- | Which readings to try, in order.
[Route] ->
-- | Where to remember answers between runs.
Cache ->
-- | Every package the compiler can see.
[InstalledPackage] ->
-- | Every planned package that might have a tarball, and where it would
-- be.
[(PlanPackage, FilePath)] ->
-- | What the package of each of the project's own modules puts in force.
PackageReader ->
-- | The build plan to use.
BuildPlan ->
IO Resolver
newResolverWith routes cache installed tarballs packageReader plan = do
index <- buildModuleIndex cache installed tarballs
let interfaces = interfaceIndex installed
local <- localModules plan
flights <- newFlights
answersRead <- newMemo
extensionsRead <- newMemo
archivesRead <- newMemo
interfacesRead <- newMemo
reading <- newQSem =<< getNumCapabilities
let interfaceOf modName =
memoized flights interfacesRead Nothing modName $
case Map.lookup modName primopFixities of
Just declared -> pure (Just (asInterface declared))
Nothing -> case Map.lookup modName interfaces of
Nothing -> pure Nothing
Just (_, path) ->
bracket_ (waitQSem reading) (signalQSem reading) $
readInterface modName path
let workings =
Workings
{ wkRoutes = routes,
wkCache = cache,
wkLocal = local,
wkIndex = index,
wkInterfaces = interfaces,
wkInterfaceOf = interfaceOf,
wkReach = reach,
wkSummariesOf = summariesOf,
wkModuleInArchive = moduleInArchive,
wkGenerated = generatedModules plan,
wkReexported =
Map.fromListWith
(\_ first' -> first')
[pair | i <- installed, pair <- ipReexports i]
}
-- The stand-in is what 'withReexports' makes of a module it is in the
-- middle of reading.
reach visiting modName
| modName `Set.member` visiting = pure mempty
| otherwise =
memoized flights answersRead mempty modName $
resolveModule workings visiting modName
summariesOf modName text =
extensionsOf modName >>= \case
Nothing -> pure Nothing
Just extensions -> do
let summarized =
evaluate . force $
configurationsOf (macrosOf plan) extensions modName text
stamp =
digestOf . T.intercalate "\n" $
[T.pack (show extensions), macrosRead, text]
case digestOf . T.pack <$> Map.lookup modName local of
Nothing -> summarized
Just key ->
join
<$> recalled
(cachedSummaries cache key stamp)
(storeSummaries cache key stamp)
(Just <$> summarized)
macrosRead = T.pack (show (macrosOf plan))
extensionsOf modName
| Just path <- Map.lookup modName local =
either (const Nothing) Just <$> packageReader path
| Just (package, tarball) <- Map.lookup modName index =
memoized flights extensionsRead Nothing package $
fmap declaredExtensions . (archiveCabal =<<) <$> archiveOf tarball
| otherwise = pure Nothing
archiveOf tarball =
memoized flights archivesRead Nothing (T.pack tarball) (readArchive tarball)
moduleInArchive tarball modName = (moduleIn modName =<<) <$> archiveOf tarball
pure Resolver{askModule = reach Set.empty}
-- | A 'Resolver' that reads every module out of the interface given for it,
-- for a program that has compiled the project and holds its interfaces.
newResolverFrom ::
-- | The interface of a module, by name.
(Text -> IO (Maybe HiFile)) ->
IO Resolver
newResolverFrom given = do
flights <- newFlights
interfacesRead <- newMemo
answersRead <- newMemo
let interfaceOf modName =
memoized flights interfacesRead Nothing modName $
case Map.lookup modName primopFixities of
Just declared -> pure (Just (asInterface declared))
Nothing ->
(either (const Nothing) Just . fromHiFile modName =<<)
<$> given modName
pure
Resolver
{ askModule = \modName ->
memoized flights answersRead unreadable modName $
fromInterface interfaceOf modName
}
-- | Work out what a module can see, using a resolver to reach its imports.
--
-- This is the join between the pure half of "Tilia.Fixity" and the half
-- that touches the disk: the imports are resolved first, and the scope is
-- then computed from the answers. Note that a name an import leaves
-- unsettled stays unsettled, which is what lets 'Tilia.Fixity.lookupFixity'
-- distinguish a conclusion from a guess.
scopeFor ::
-- | What can be asked about the modules it imports.
Resolver ->
-- | Whether @ImplicitPrelude@ is on, which the module's own pragmas
-- and its package's @default-extensions@ decide between them.
Choice "implicitPrelude" ->
-- | The configurations of the module whose scope is wanted, already
-- parsed.
NonEmpty (HsModule GhcPs) ->
-- | Everything that module can see, and what it could not find out.
IO Scope
scopeFor resolver implicitPrelude configurations = do
let imports = moduleImports implicitPrelude configurations
answers <-
inParallel
(\m -> (m,) <$> askModule resolver m)
(fmap importModule imports)
let table = Map.fromList answers
pure $
resolveScope
implicitPrelude
(\m -> Map.findWithDefault unreadable m table)
configurations
-- | Everything a resolver consults, and the way back into it.
--
-- None of it changes from one module to the next, which is why
-- 'newResolverWith' builds it once and hands it over whole. What comes back
-- out of that is the 'Resolver'; this is what is behind it.
data Workings = Workings
{ -- | Which readings to try, in the order given.
wkRoutes :: [Route],
-- | Where to remember answers between runs.
wkCache :: Cache,
-- | The modules of the project's own packages, which are read straight
-- from disk rather than out of an archive.
wkLocal :: Map Text FilePath,
-- | Which package holds each module, and the tarball to find it in; the
-- package is the cache key, which carries the hash the tarball was
-- verified against.
wkIndex :: Map Text (Text, FilePath),
-- | What to file an answer read out of each module's interface under.
wkInterfaces :: Map Text (Text, FilePath),
-- | A module's interface, read at most once a run.
wkInterfaceOf :: Text -> IO (Maybe Interface),
-- | How to reach another module. Tied back on itself by
-- 'newResolverWith', so that the memo it keeps covers the recursive
-- calls too.
wkReach :: Set Text -> Text -> IO Established,
-- | What each configuration of a module says, given its text, worked
-- out once a run.
wkSummariesOf :: Text -> Text -> IO (Maybe (NonEmpty ModuleSummary)),
-- | What a tarball holds for a module, each tarball read once a run.
wkModuleInArchive :: FilePath -> Text -> IO (Maybe InArchive),
-- | The modules @cabal@ writes itself, which are therefore in no
-- package's sources. See 'generatedModules'.
wkGenerated :: Set Text,
-- | The modules an installed package exposes that another one holds,
-- each with its name there.
wkReexported :: Map Text Text
}
-- | What reading a module establishes, by the cheapest route that settles
-- it.
resolveModule ::
-- | Where to look, and how to get back to the resolver.
Workings ->
-- | Modules currently being resolved further up the call chain.
Set Text ->
-- | The module to resolve.
Text ->
IO Established
resolveModule
Workings
{ wkRoutes,
wkCache,
wkLocal,
wkIndex,
wkInterfaces,
wkInterfaceOf,
wkReach,
wkSummariesOf,
wkModuleInArchive,
wkGenerated,
wkReexported
}
visiting
modName
| Just path <- Map.lookup modName wkLocal,
writtenForHsc path =
pure mempty
| Just path <- Map.lookup modName wkLocal =
readFileText path >>= \case
Nothing -> pure unreadable
Just source ->
fromSummaries (wkReach visiting') modName
=<< wkSummariesOf modName (snd (byteOrderMark source))
| Just original <- Map.lookup modName wkReexported,
Map.notMember modName wkInterfaces =
wkReach visiting' original
| otherwise = answered <$> firstAnswer (fmap taking wkRoutes)
where
visiting' = Set.insert modName visiting
taking = \case
FromInterface -> viaInterface
FromSource -> viaArchive
-- A route that read some of the module is kept in case no later one
-- reads all of it.
firstAnswer = go Nothing
where
go partial [] = pure (fromMaybe unreadable partial)
go partial (route : rest) =
route >>= \case
Just established
| settlesEverything established -> pure established
| established /= unreadable -> go (partial <|> Just established) rest
_ -> go partial rest
viaInterface = case Map.lookup modName wkInterfaces of
Nothing -> pure Nothing
Just (key, _) ->
throughCache key (Just <$> fromInterface wkInterfaceOf modName)
viaArchive = case Map.lookup modName wkIndex of
Nothing -> pure Nothing
Just (package, tarball) ->
throughCache package $
fromSource
wkModuleInArchive
wkSummariesOf
(wkReach visiting')
tarball
modName
answered established
| settlesEverything established = established
| Set.member modName wkGenerated =
established
{ establishedFixities = Map.empty,
establishedUnsettled = Map.empty,
establishedUntold = Set.empty
}
| otherwise = established
throughCache key =
recalled
(cachedEstablished wkCache key modName)
(storeEstablished wkCache key modName)
-- | Which package and tarball holds each module.
buildModuleIndex ::
-- | Where to remember each package's module list.
Cache ->
-- | What the compiler says is installed. Empty if @ghc-pkg@ could not be
-- run, in which case every package falls back to its @.cabal@ file.
[InstalledPackage] ->
-- | Every package that might have a tarball, and where it would be.
[(PlanPackage, FilePath)] ->
-- | For each module, the package that holds it (as a cache key) and
-- the tarball holding its source.
IO (Map Text (Text, FilePath))
buildModuleIndex cache installed tarballs =
Map.fromListWith (\_ first' -> first') . concat <$> traverse one tarballs
where
byNameVersion =
Map.fromList [((ipName i, ipVersion i), ipModules i) | i <- installed]
one (p, tarball) = do
let key = cacheKey p
let exposed = Map.lookup (ppName p, ppVersion p) byNameVersion
held <- fromCabalFile cache key tarball p
let modules = case (exposed, held) of
(Nothing, Nothing) -> []
(a, b) -> concat (catMaybes [a, b])
pure [(m, (key, tarball)) | m <- modules]
-- | Where each installed module's compiled interface is.
interfaceIndex :: [InstalledPackage] -> Map Text (Text, FilePath)
interfaceIndex installed =
Map.fromListWith
(\_ first' -> first')
[ (m, (key, dir </> pathOfModule m <> ".hi"))
| i <- installed,
dir <- ipImportDirs i,
-- Bound out here so that the directory is hashed once rather than
-- once for each of the modules found in it.
let key = keyFor dir,
m <- ipModules i
]
where
keyFor dir = "interface-" <> digestOf (T.pack dir)
-- | A name for some text, short enough to file something under.
digestOf :: Text -> Text
digestOf = T.take 24 . sha256Of
-- | Present a fixity map as an 'Interface'.
asInterface :: Fixities -> Interface
asInterface fixities =
Interface
{ interfaceDeclares = fixities,
interfaceReexports = [],
interfaceMembers = Map.empty,
interfaceExports = Nothing
}
-- | What a compiled interface says a module exports, with the fixities of
-- what it passes on read from where each is declared.
fromInterface ::
-- | A module's interface, if it has one.
(Text -> IO (Maybe Interface)) ->
-- | The module to read.
Text ->
IO Established
fromInterface interfaceOf modName =
interfaceOf modName >>= \case
Nothing -> pure unreadable
Just iface -> do
declarers <-
inParallel
asked
(distinct (fmap fst (interfaceReexports iface)))
let declarer m = join (lookup m declarers)
pure
Established
{ establishedFixities =
Map.union (interfaceDeclares iface) . Map.unions $
[ Map.filterWithKey
(\(_, o) _ -> o == op)
(interfaceDeclares declared)
| (m, op) <- interfaceReexports iface,
Just declared <- [declarer m]
],
establishedUnsettled =
Map.fromListWith
Set.union
[ ([m], Set.fromList [(InTypes, op), (InTerms, op)])
| (m, op) <- interfaceReexports iface,
Nothing <- [declarer m]
],
establishedUntold = Set.empty,
establishedCertain = fromMaybe mempty (interfaceExports iface),
establishedMembers = interfaceMembers iface
}
where
asked m = do
interface <- interfaceOf m
pure (m, interface)
distinct = Map.keys . Map.fromList . fmap (,())
-- | A package's module list from the @.cabal@ file in its tarball.
fromCabalFile ::
-- | Where to remember the answer.
Cache ->
-- | What to file it under. Carries the hash the tarball was verified
-- against, so a changed tarball misses rather than matching stale data.
Text ->
-- | The tarball to read the @.cabal@ file out of.
FilePath ->
-- | The package it belongs to, consulted for the hash to verify against.
PlanPackage ->
-- | The modules it holds, or 'Nothing' if the tarball is absent, fails
-- verification, or holds no @.cabal@ file.
IO (Maybe [Text])
fromCabalFile cache key tarball p =
recalled (cachedModules cache key) (storeModules cache key) $
verified p tarball >>= \case
False -> pure Nothing
True -> packageModules tarball
-- | Does the tarball hash to what the plan says it should?
verified :: PlanPackage -> FilePath -> IO Bool
verified p tarball = case sourceHashOf p of
Nothing -> pure True
Just expected ->
quietly False $ do
actual <- sha256OfFile tarball
pure (actual == T.toLower expected)
-- | The SHA-256 of a file, as lower-case hex.
sha256OfFile :: FilePath -> IO Text
sha256OfFile path = do
bytes <- BL.readFile path
pure (T.decodeUtf8Lenient (B16.encode (SHA256.hashlazy bytes)))
-- | Read what a module exports out of a tarball, following re-exports.
fromSource ::
-- | What a tarball holds for a module.
(FilePath -> Text -> IO (Maybe InArchive)) ->
-- | What each configuration of a module says, given its text.
(Text -> Text -> IO (Maybe (NonEmpty ModuleSummary))) ->
-- | How to reach another module, for chasing re-exports. This is
-- 'resolveModule' tied back on itself, with the visiting set already
-- extended.
(Text -> IO Established) ->
-- | The tarball holding this module's source.
FilePath ->
-- | The module to read.
Text ->
-- | What reading it established, or 'Nothing' if there is no archive to
-- read it from.
IO (Maybe Established)
fromSource moduleInArchive summariesOf reach tarball modName =
doesFileExist tarball >>= \case
False -> pure Nothing
True ->
fmap Just $
moduleInArchive tarball modName >>= \case
Nothing -> pure unreadable
Just ForHsc -> pure mempty
Just (Haskell source) ->
fromSummaries reach modName =<< summariesOf modName source
-- | What a module exports, out of what each of its configurations says.
fromSummaries ::
-- | How to reach another module, for chasing re-exports. This is
-- 'resolveModule' tied back on itself, with the visiting set already
-- extended.
(Text -> IO Established) ->
-- | The name the module was looked up under.
Text ->
-- | What each configuration of it says, or 'Nothing' where none of them
-- parses.
Maybe (NonEmpty ModuleSummary) ->
IO Established
fromSummaries reach modName = \case
Nothing -> pure unreadable
Just summaries ->
agreeing <$> traverse (withReexports reach modName) summaries
-- | One answer from every configuration of a module that counts.
--
-- A module may declare a fixity in one configuration and a different one in
-- another. Which of them holds depends on how the module is compiled, which
-- is not ours to decide, so disagreement leaves the name unsettled.
-- Agreement across the ones that count is an answer, and a stronger one
-- than the blanked text could give.
--
-- A configuration that leaves names unsettled is passed over where another
-- settles every name, because almost every one of those is a branch meant
-- for a different platform. Where none does, every configuration counts,
-- and a name any of them leaves unsettled stays unsettled.
agreeing :: NonEmpty Established -> Established
agreeing answers =
settledOnly
Established
{ establishedFixities = Map.mapMaybe sole declared,
establishedUnsettled =
Map.unionsWith
Set.union
( maybe
Map.empty
(Map.singleton [])
disagreed
: fmap establishedUnsettled (toList counted)
),
establishedUntold = foldMap establishedUntold counted,
establishedCertain = foldr1 inEvery (fmap establishedCertain counted),
establishedMembers =
Map.unionsWith Set.union (fmap establishedMembers counted)
}
where
counted =
fromMaybe answers $
NE.nonEmpty (NE.filter settlesEverything answers)
declared =
Map.unionsWith
(<>)
[Map.map pure (establishedFixities a) | a <- toList counted]
sole (fixity :| rest) =
if all (== fixity) rest then Just fixity else Nothing
disagreed = inhabited (Map.keysSet (Map.filter (isNothing . sole) declared))
inEvery a b =
Certain
{ certainNames = Set.intersection (certainNames a) (certainNames b),
certainMembers =
Map.intersectionWith
Set.intersection
(certainMembers a)
(certainMembers b)
}
-- | Where each module that lives in a directory rather than an archive is.
localModules :: BuildPlan -> IO (Map Text FilePath)
localModules plan =
Map.unions <$> traverse forPackage (concatMap directoryOf (bpPackages plan))
where
directoryOf p = case ppSource p of
LocalPackage dir -> [dir]
CheckedOut dir -> [dir]
_ -> []
forPackage dir = quietly Map.empty $ do
cabalFilesIn dir >>= \case
[] -> pure Map.empty
(cabalFile : _) ->
readPackageDescription cabalFile >>= \case
Left _ -> pure Map.empty
Right described ->
Map.fromListWith (\_ first' -> first') . concat
<$> traverse (locate dir) (declaredModules described)
locate dir (m, unwritten) = do
found <- filterM doesFileExist [dir </> unwritten <> e | e <- moduleEndings]
pure [(m, path) | path <- take 1 found]
-- | What a module exports, out of what it declares and what its imports
-- bring in.
withReexports ::
-- | How to reach another module, for names this one only passes on.
(Text -> IO Established) ->
-- | The name this module was looked up under.
Text ->
-- | The module.
ModuleSummary ->
IO Established
withReexports reach modName summary =
case summaryExports summary of
Nothing -> pure own
Just items -> do
listed <-
reached $ \i ->
any (suppliedBy i) (concatMap named items)
|| ( not (importQualified i)
&& importAlias i
`elem` [m | ExportModule m <- items]
)
let reexported =
Map.fromList
[ ((qualifier, parent), membersFrom listed qualifier parent)
| ExportAll qualifier parent <- items,
Map.notMember parent declared
]
members =
[ (qualifier, kid)
| ((qualifier, _), Just kids) <- Map.toList reexported,
kid <- Set.toList kids,
Set.notMember kid defined
]
more <- reached $ \i ->
Map.notMember (importModule i) listed && any (suppliedBy i) members
let answers = Map.union listed more
parts = fmap (exported answers reexported) items
pure . together $
mempty
{ establishedFixities =
Map.restrictKeys
(summaryFixities summary)
(foldMap (certainNames . establishedCertain) parts),
establishedMembers = summaryListedMembers summary
}
: parts
where
imports = summaryImports summary
declared = summaryDeclaredMembers summary
defined =
Set.map snd (summaryNames summary)
<> Set.map snd (Map.keysSet (summaryFixities summary))
own =
mempty
{ establishedFixities = summaryFixities summary,
establishedCertain = Certain (summaryNames summary) declared,
establishedMembers = Map.map (Set.map snd) declared
}
reached wanted =
Map.fromList
<$> traverse
(\m -> (m,) <$> reach m)
(Set.toList (Set.fromList [importModule i | i <- imports, wanted i]))
suppliedBy i (qualifier, op) = maySupply Map.empty qualifier op i
named = \case
ExportName _ qualifier op ->
[(qualifier, op) | Set.notMember op defined]
ExportAll qualifier parent ->
[(qualifier, parent) | Set.notMember parent defined]
ExportSome qualifier parent kids ->
[(qualifier, op) | op <- parent : kids, Set.notMember op defined]
ExportModule _ -> []
exported answers reexported = \case
ExportName namespace qualifier op ->
settled answers True qualifier (namespace, op)
<> certainly (Set.singleton (namespace, op)) Map.empty
ExportAll qualifier parent
| Just kids <- Map.lookup parent declared -> withMembers parent kids
| otherwise ->
let found = settled answers True qualifier (InTypes, parent)
kids =
Map.findWithDefault Nothing (qualifier, parent) reexported
in found
<> mempty{establishedUntold = Map.keysSet (establishedUnsettled found)}
<> foldMap (member answers qualifier parent) (foldMap Set.toList kids)
<> case kids of
Nothing -> certainly (Set.singleton (InTypes, parent)) Map.empty
Just known ->
withMembers parent (certainMembersOf answers qualifier parent)
<> mempty{establishedMembers = Map.singleton parent known}
ExportSome qualifier parent kids ->
settled answers True qualifier (InTypes, parent)
<> foldMap (member answers qualifier parent) kids
<> withMembers
parent
( Set.filter
((`elem` kids) . snd)
( fromMaybe
(certainMembersOf answers qualifier parent)
(Map.lookup parent declared)
)
)
ExportModule m ->
(if Just m == summaryName summary || m == modName then own else mempty)
<> foldMap
(whole answers)
[i | i <- imports, not (importQualified i), importAlias i == m]
certainly names members = mempty{establishedCertain = Certain names members}
withMembers parent kids =
certainly (Set.insert (InTypes, parent) kids) (Map.singleton parent kids)
member answers qualifier parent kid
| Set.member kid defined = mempty
| otherwise =
case [ (i, established, certainKids)
| (i, established) <- candidates answers qualifier parent,
decides established (InTypes, parent) i,
let certainKids =
Set.filter
((== kid) . snd)
( Map.findWithDefault
Set.empty
parent
(certainMembers (establishedCertain established))
),
not (Set.null certainKids)
] of
(i, established, certainKids) : _ ->
foldMap (from [(i, established)]) certainKids
[] ->
foldMap
( \namespace ->
settled answers False qualifier (namespace, kid)
)
[InTypes, InTerms]
candidates answers qualifier op =
[ (i, established)
| i <- imports,
Just established <- [Map.lookup (importModule i) answers],
maySupply (establishedMembers established) qualifier op i
]
settled answers certain qualifier name@(_, op)
| Set.member op defined = mempty
| otherwise = case [ (i, established)
| certain,
(i, established) <- found,
decides established name i
] of
decider : _ -> from [decider] name
[] -> from found name
where
found = candidates answers qualifier op
from imported name =
case [ importModule i : chain
| (i, established) <- imported,
chain <- unsettledThrough established name
] of
[] ->
mempty
{ establishedFixities =
Map.fromList
( take
1
[ (name, fixity)
| (_, a) <- imported,
Just fixity <- [Map.lookup name (establishedFixities a)]
]
)
}
chains ->
mempty
{ establishedUnsettled =
Map.fromList [(chain, Set.singleton name) | chain <- chains]
}
membersFrom answers qualifier parent =
case mapMaybe
(Map.lookup parent . establishedMembers . snd)
(candidates answers qualifier parent) of
[] -> Nothing
kids -> Just (Set.unions kids)
certainMembersOf answers qualifier parent =
Set.unions
[ Set.filter (\kid -> certainlyBrings (establishedMembers established) kid i) kids
| (i, established) <- candidates answers qualifier parent,
let certain = establishedCertain established,
Set.member (InTypes, parent) (certainNames certain),
Just kids <- [Map.lookup parent (certainMembers certain)]
]
whole answers i =
Established
{ establishedFixities =
Map.filterWithKey
(\(_, op) _ -> admitted op)
(establishedFixities established),
establishedUnsettled =
Map.mapKeys
(importModule i :)
( Map.mapMaybe
(inhabited . Set.filter (admitted . snd))
(establishedUnsettled established)
),
establishedUntold =
Set.map
(importModule i :)
(establishedUntold established),
establishedCertain =
Certain
{ certainNames = Set.filter included (certainNames certain),
certainMembers =
Map.map
(Set.filter included)
( Map.filterWithKey
(\parent _ -> included (InTypes, parent))
(certainMembers certain)
)
},
establishedMembers = establishedMembers established
}
where
established = Map.findWithDefault mempty (importModule i) answers
certain = establishedCertain established
admitted op = maySupply (establishedMembers established) Nothing op i
included name = certainlyBrings (establishedMembers established) name i
-- | The parts of what a module exports, together.
--
-- A name one part certainly exports with a settled fixity is the one every
-- other part exports under that name too, or the module would not compile,
-- so no part leaves it unsettled.
together :: [Established] -> Established
together parts =
settledOnly
combined
{ establishedUnsettled =
Map.mapMaybe
(inhabited . (`Set.difference` certain))
(establishedUnsettled combined)
}
where
combined = mconcat parts
certain =
Set.unions
[ Set.filter
(null . unsettledThrough part)
(certainNames (establishedCertain part))
| part <- parts
]
-- | An answer without the fixities of the names it leaves unsettled.
settledOnly :: Established -> Established
settledOnly established
| settlesEverything established = established
| otherwise =
established
{ establishedFixities =
Map.filterWithKey
(\name _ -> null (unsettledThrough established name))
(establishedFixities established)
}
-- | A set, unless it is empty.
inhabited :: Set a -> Maybe (Set a)
inhabited names
| Set.null names = Nothing
| otherwise = Just names
-- | Every configuration the preprocessor allows of a module's text that is
-- Haskell, parsed and summarized.
configurationsOf ::
-- | What the plan settles about the questions its conditionals ask.
Macros ->
-- | What extensions the module's package puts in force.
[Extension] ->
-- | The module's name, for the parser to put in its errors.
Text ->
-- | Its text.
Text ->
Maybe (NonEmpty ModuleSummary)
configurationsOf macros extensions modName text =
NE.nonEmpty . mapMaybe parsed
=<< whatParsed (branchLeaves (withoutRuledOut macros text))
where
parsed leaf =
summarize (hasImplicitPrelude extensions leaf) . pmModule
<$> whatParsed (parseModule (ParserConfig extensions) named leaf)
whatParsed = either (const Nothing) Just
named = T.unpack modName
-- | Does this module see the Prelude without importing it?
hasImplicitPrelude :: [Extension] -> Text -> Choice "implicitPrelude"
hasImplicitPrelude extensions source =
fromBool (ImplicitPrelude `elem` effectiveExtensions extensions source)