packages feed

tilia-0.0.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
  ( -- * Build plans
    PlanPackage (..),
    PackageSource (..),
    isFetchable,
    sourceHashOf,
    BuildPlan (..),
    readBuildPlan,
    readGivenPlan,
    fetchUninstalled,
    tokenForEnvAndBuildPlan,
    tokenForBuildPlan,
    macrosOf,

    -- * Readiness
    Readiness (..),
    PlanComponent (..),
    spellComponent,
    plannedComponents,
    planPathFor,
    checkReadiness,
    plannedTarballs,
    packageCacheRoot,
    guessedPackageCacheRoot,
    Futility (..),
    undiscoveredFutility,
    prepareWith,
    loadPlan,

    -- * Resolving
    Route (..),
    Resolver (..),
    newResolver,
    newResolverVia,
    withReexports,
    scopeFor,
  )
where

import Codec.Archive.Tar qualified as Tar
import Codec.Compression.GZip qualified as GZip
import Control.Applicative ((<|>))
import Control.Concurrent (getNumCapabilities, newEmptyMVar, putMVar, readMVar)
import Control.Concurrent.QSem (newQSem, signalQSem, waitQSem)
import Control.Exception (bracket_)
import Control.Monad (filterM, foldM, join, void)
import Crypto.Hash.SHA256 qualified as SHA256
import Data.Aeson
  ( FromJSON (..),
    Value,
    decodeStrict,
    eitherDecodeFileStrict,
    withObject,
    (.:),
    (.:?),
  )
import Data.Aeson.Types (parseMaybe)
import Data.ByteString qualified as BS
import Data.ByteString.Base16 qualified as B16
import Data.ByteString.Lazy qualified as BL
import Data.Choice (Choice, fromBool, isTrue, pattern Do)
import Data.Foldable (toList, traverse_)
import Data.IORef
import Data.List (isSuffixOf)
import Data.List qualified
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, listToMaybe, 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 Data.Text.Read qualified as T
import GHC.Hs (HsModule)
import GHC.Hs.Extension (GhcPs)
import GHC.IO.Handle (hDuplicate)
import GHC.LanguageExtensions.Type (Extension (ImplicitPrelude))
import System.Directory
  ( XdgDirectory (XdgCache),
    doesFileExist,
    getAppUserDataDirectory,
    getModificationTime,
    getXdgDirectory,
    listDirectory,
  )
import System.Environment (lookupEnv)
import System.Exit (ExitCode (..))
import System.FilePath (isRelative, takeDirectory, (</>))
import System.IO (hFlush, stderr)
import System.Info qualified
import System.Process
  ( StdStream (Inherit, UseHandle),
    createProcess,
    cwd,
    proc,
    std_err,
    std_out,
    waitForProcess,
  )
import Tilia.Cabal.Package (newPackageReader)
import Tilia.Cpp.Directives (branchLeaves, withoutRuledOut)
import Tilia.Cpp.Macros (Macros (..))
import Tilia.Fixity
import Tilia.Fixity.Builtin (builtinFixities)
import Tilia.Fixity.ByHand (byHandFixities, hscFixities)
import Tilia.Fixity.Cabal
  ( cabalFileAtTop,
    cabalFileInArchive,
    containedModules,
    declaredExtensions,
    entryPosixPath,
    packageModules,
    sourceDirs,
  )
import Tilia.Fixity.Cache
import Tilia.Fixity.Interface
import Tilia.Fixity.PackageDb
import Tilia.Parser
import Tilia.Pragma (effectiveExtensions)
import Tilia.Process (readProgramOutput)
import Tilia.Utils (inParallel, quietly)

----------------------------------------------------------------------------
-- The plan

-- | One package of a build plan.
data PlanPackage = PlanPackage
  { -- | Package name
    ppName :: Text,
    -- | Package version
    ppVersion :: Text,
    -- | Package source
    ppSource :: PackageSource,
    -- | Which components of the package the entry is about.
    --
    -- Usually one, because @cabal@ configures a package one component at a
    -- time and gives each its own entry. A package it cannot take apart—one
    -- with a @Custom@ build type, whose @Setup.hs@ is entitled to do as it
    -- pleases—is planned whole instead, and its entry is about every
    -- component at once.
    ppComponents :: [Text]
  }
  deriving (Eq, Show)

-- | Where a package's source is.
data PackageSource
  = -- | Already installed, so @cabal@ will not build it.
    --
    -- Not the same as "ships with the compiler", though it includes those.
    PreExisting
  | -- | A directory on this machine—the project being formatted, or a
    -- sibling of it in the same repository.
    LocalPackage FilePath
  | -- | Fetched from a repository as a tarball, with the SHA-256 the plan
    -- expects it to have and where it was fetched from.
    RepoPackage (Maybe Text) RepoProvenance
  | -- | A @source-repository-package@ we have not found the sources of.
    SourceRepo
  | -- | The same, found unpacked under the project's own @dist-newstyle@.
    CheckedOut FilePath
  deriving (Eq, Show)

-- | Which repository a package was fetched from, as far as it bears on
-- finding the tarball afterwards.
data RepoProvenance
  = -- | One @cabal@ downloads from, named by its URI. The tarball goes into
    -- the package cache, in a directory named after the repository as the
    -- configuration spells it.
    RepoDownloaded Text
  | -- | A directory of tarballs, named by @file+noindex@. Nothing is
    -- downloaded and nothing is cached: the tarball is already sitting
    -- there, beside the index @cabal@ wrote for it.
    RepoFromDirectory FilePath
  | -- | A plan that does not say. Older @cabal@ wrote nothing here, and
    -- Hackage is the only guess worth making.
    RepoNoProvenance
  deriving (Eq, Show)

-- | Is there a tarball to go and read?
isFetchable :: PlanPackage -> Bool
isFetchable p = case ppSource p of
  RepoPackage _ _ -> True
  _ -> False

-- | Every package the compiler can see.
getInstalledPackages :: Maybe Cache -> IO [InstalledPackage]
getInstalledPackages cache =
  remembered >>= \case
    Just packages -> pure packages
    Nothing -> do
      found <- readInstalledPackages
      traverse_ (`storeInstalled` found) cache
      pure (installedPackages found)
  where
    remembered = maybe (pure Nothing) cachedInstalled cache

-- | Summarize a 'BuildPlan', and the environment it will be read in, by
-- hashing over both.
tokenForBuildPlan :: BuildPlan -> IO PlanToken
tokenForBuildPlan plan = do
  environment <- compilerIdentity
  pure (tokenForEnvAndBuildPlan environment plan)

-- | Similar to 'tokenForBuildPlan', but allows passing a compiler identity
-- as an argument.
tokenForEnvAndBuildPlan :: Text -> BuildPlan -> PlanToken
tokenForEnvAndBuildPlan environment plan =
  PlanToken
    . T.take 16
    . T.decodeUtf8Lenient
    . B16.encode
    . SHA256.hash
    . T.encodeUtf8
    $ T.intercalate
      "\n"
      (environment : bpCompiler plan : Data.List.sort (fmap cacheKey (bpPackages plan)))

-- | The SHA-256 the plan expects this package's tarball to have.
sourceHashOf :: PlanPackage -> Maybe Text
sourceHashOf p = case ppSource p of
  RepoPackage hash _ -> hash
  _ -> Nothing

-- | A resolved build plan.
data BuildPlan = BuildPlan
  { bpCompiler :: Text,
    bpPackages :: [PlanPackage]
  }
  deriving (Eq, Show)

instance FromJSON BuildPlan where
  parseJSON = withObject "BuildPlan" $ \o ->
    BuildPlan
      <$> o .: "compiler-id"
      <*> o .: "install-plan"

instance FromJSON PlanPackage where
  parseJSON = withObject "PlanPackage" $ \o -> do
    name <- o .: "pkg-name"
    version <- o .: "pkg-version"
    kind <- o .:? "type"
    sourceKind <- o .:? "pkg-src" >>= traverse (.: "type")
    sourcePath <- o .:? "pkg-src" >>= traverse (.:? "path")
    sourceHash <- o .:? "pkg-src-sha256"
    repo <- o .:? "pkg-src" >>= traverse (.:? "repo")
    repoKind <- traverse (traverse (.:? "type")) repo
    repoUri <- traverse (traverse (.:? "uri")) repo
    repoPath <- traverse (traverse (.:? "path")) repo
    named <- o .:? "component-name"
    whole <- o .:? "components"
    pure
      PlanPackage
        { ppName = name,
          ppVersion = version,
          ppComponents = componentsOf named whole,
          ppSource = case (kind :: Maybe Text, sourceKind :: Maybe Text) of
            (Just "pre-existing", _) -> PreExisting
            (_, Just "repo-tar") ->
              RepoPackage
                sourceHash
                ( case (join (join repoKind) :: Maybe Text, join (join repoPath), join (join repoUri)) of
                    (Just "local-repo-no-index", Just dir, _) -> RepoFromDirectory (T.unpack dir)
                    (_, _, Just uri) -> RepoDownloaded uri
                    _ -> RepoNoProvenance
                )
            (_, Just "local") -> LocalPackage (maybe "" T.unpack (join sourcePath))
            (_, Just "source-repo") -> SourceRepo
            -- Anything else is treated as already present.
            _ -> PreExisting
        }

-- | The components one plan entry is about.
--
-- @cabal@ writes this two ways. An entry for a single component names it in
-- @component-name@; an entry for a package planned whole carries a
-- @components@ object instead, keyed by the very same spellings. Reading
-- only the first would leave every @Custom@ package looking like one the
-- plan says nothing about, and a run would keep asking @cabal@ to solve
-- again for components a fresh solve would file exactly where this one did.
--
-- @setup@ is dropped. It is the @Setup.hs@ program @cabal@ builds in order
-- to build the package, not a component of the package, and nothing in the
-- project will ever be formatted as part of it.
componentsOf :: Maybe Text -> Maybe (Map Text Value) -> [Text]
componentsOf named whole = case named of
  Just component -> [component]
  Nothing -> filter (/= "setup") (Map.keys (fromMaybe Map.empty whole))

-- | Read @plan.json@.
readBuildPlan :: FilePath -> IO (Either Text BuildPlan)
readBuildPlan path =
  doesFileExist path >>= \case
    False -> pure (Left ("no build plan at " <> T.pack path))
    True ->
      eitherDecodeFileStrict path >>= \case
        Left why -> pure (Left (T.pack why))
        Right plan -> Right <$> checkedOutIn (takeDirectory (takeDirectory path)) plan

-- | Read a plan that is to be trusted as up to date.
readGivenPlan ::
  -- | The project root.
  FilePath ->
  -- | The plan.
  FilePath ->
  IO (Either Text BuildPlan)
readGivenPlan root path = do
  exists <- doesFileExist path
  if exists
    then
      eitherDecodeFileStrict path >>= \case
        Left why -> pure (Left (T.pack why))
        Right plan -> pure (Right plan{bpPackages = fmap rooted (bpPackages plan)})
    else pure (Left ("no build plan at " <> T.pack path))
  where
    rooted p = case ppSource p of
      LocalPackage dir | isRelative dir -> p{ppSource = LocalPackage (root </> dir)}
      _ -> p

-- | Find where @cabal@ unpacked each @source-repository-package@.
checkedOutIn :: FilePath -> BuildPlan -> IO BuildPlan
checkedOutIn distDir plan = do
  packages <- traverse locate (bpPackages plan)
  pure plan{bpPackages = packages}
  where
    locate p = case ppSource p of
      SourceRepo ->
        clonesOf p >>= \case
          (dir : _) -> pure p{ppSource = CheckedOut dir}
          [] -> pure p
      _ -> pure p
    clonesOf p = quietly [] $ do
      entries <- listDirectory (distDir </> "src")
      filterM
        (isThePackage p)
        [ distDir </> "src" </> e
        | e <- Data.List.sort entries,
          (ppName p <> "-") `T.isPrefixOf` T.pack e
        ]
    isThePackage p dir = quietly False $ do
      contents <- readFileText (dir </> T.unpack (ppName p) <> ".cabal")
      pure (maybe False (describes p) contents)
    describes p text =
      any (names "name:" (ppName p)) (T.lines text)
        && any (names "version:" (ppVersion p)) (T.lines text)
    names field value line = case T.stripPrefix field (T.toLower (T.strip line)) of
      Just rest -> T.strip rest == T.toLower value
      Nothing -> False

-- | The version macros a plan settles.
macrosOf :: BuildPlan -> Macros
macrosOf plan =
  Macros
    { macroVersions =
        Map.fromList
          ( [ ("MIN_VERSION_" <> underscored name, version)
            | (name, [version]) <- Map.toList (Map.map Set.toList versions)
            ]
              <> [("MIN_VERSION_GLASGOW_HASKELL", v) | v <- toList compiler]
          ),
      macroNumbers =
        Map.fromList
          [ entry
          | (major : minor : patches) <- toList compiler,
            entry <-
              [ ("__GLASGOW_HASKELL__", major * 100 + minor),
                ("__GLASGOW_HASKELL_PATCHLEVEL1__", nth 0 patches),
                ("__GLASGOW_HASKELL_PATCHLEVEL2__", nth 1 patches)
              ]
          ]
    }
  where
    versions =
      Map.fromListWith
        Set.union
        [ (ppName p, Set.singleton v)
        | p <- bpPackages plan,
          Just v <- [numberedVersion (ppVersion p)]
        ]
    compiler = do
      version <- T.stripPrefix "ghc-" (bpCompiler plan)
      parts <- numberedVersion version
      case parts of
        _ : _ : _ -> Just (take 4 (parts <> repeat 0))
        _ -> Nothing
    nth i xs = if i < length xs then xs !! i else 0

-- | The modules @cabal@ writes itself for a plan's packages.
generatedModules :: BuildPlan -> Set Text
generatedModules plan =
  Set.fromList
    [ prefix <> underscored (ppName p)
    | p <- bpPackages plan,
      prefix <- ["Paths_", "PackageInfo_"]
    ]

-- | A package's name as a module name spells it, which is with the hyphens
-- turned into underscores. @cabal@ does this for the version macros and for
-- the modules it generates alike.
underscored :: Text -> Text
underscored = T.map (\c -> if c == '-' then '_' else c)

-- | A version as its numbers, or 'Nothing' where any of them is not one.
numberedVersion :: Text -> Maybe [Integer]
numberedVersion = traverse number . T.splitOn "."
  where
    number part = case T.decimal part of
      Right (n, rest) | T.null rest -> Just n
      _ -> Nothing

----------------------------------------------------------------------------
-- Readiness

-- | A component of the project.
data PlanComponent = PlanComponent
  { -- | The package it belongs to.
    pcPackage :: Text,
    -- | @lib@, @exe:name@, @test:name@, @bench:name@.
    pcName :: Text
  }
  deriving (Eq, Ord, Show)

-- | A component as it would be written on the command line.
spellComponent :: PlanComponent -> Text
spellComponent c = pcPackage c <> ":" <> pcName c

-- | The components of the project's own packages that a plan covers.
plannedComponents :: BuildPlan -> [PlanComponent]
plannedComponents plan =
  [ PlanComponent (ppName p) component
  | p <- bpPackages plan,
    LocalPackage _ <- [ppSource p],
    component <- ppComponents p
  ]

-- | Whether everything the resolver needs is on disk.
data Readiness
  = -- | Nothing to do.
    Ready
  | -- | No build plan; @cabal@ has not solved this project yet.
    PlanMissing
  | -- | The plan is older than the files that determine it.
    PlanStale [FilePath]
  | -- | The plan says nothing about components the run is about to format.
    PlanNarrow [Text]
  | -- | The plan is there, and some packages have neither been downloaded
    -- nor built. The names are listed so that a caller can say what it is
    -- waiting for.
    --
    -- Built counts as having them: their interfaces provide everything the
    -- source tarball would.
    SourcesMissing [Text]
  deriving (Eq, Show)

-- | Where @cabal@ writes the plan for a project.
planPathFor :: FilePath -> FilePath
planPathFor projectDir = projectDir </> "dist-newstyle" </> "cache" </> "plan.json"

-- | Check what is missing.
--
-- One read of the plan and one @stat@ per package, so this is fast enough
-- to run before every format.
checkReadiness :: Choice "useCache" -> [PlanComponent] -> FilePath -> IO Readiness
checkReadiness caching wanted projectDir =
  readBuildPlan (planPathFor projectDir) >>= \case
    Left _ -> pure PlanMissing
    Right plan -> do
      newer <- filesNewerThanPlan plan projectDir
      let covered = plannedComponents plan
          missing = [spellComponent c | c <- wanted, c `notElem` covered]
      case (newer, missing) of
        (_ : _, _) -> pure (PlanStale newer)
        ([], _ : _) -> pure (PlanNarrow missing)
        ([], []) ->
          sourcesShortOf caching plan >>= \case
            [] -> pure Ready
            ps -> pure (SourcesMissing (fmap ppName ps))

-- | The packages the plan expects to fetch whose sources are not here.
sourcesShortOf :: Choice "useCache" -> BuildPlan -> IO [PlanPackage]
sourcesShortOf caching plan = do
  tarballs <- filter (isFetchable . fst) <$> plannedTarballs plan
  absent <- fmap fst <$> filterM (fmap not . doesFileExist . snd) tarballs
  short <-
    if null absent
      then pure []
      else do
        cache <- openCache caching =<< tokenForBuildPlan plan
        installed <- getInstalledPackages cache
        pure (filter (not . builtAlready installed) absent)
  pure short

-- | Has the compiler got this package already?
builtAlready :: [InstalledPackage] -> PlanPackage -> Bool
builtAlready installed p = any matches installed
  where
    matches i = ipName i == ppName p && ipVersion i == ppVersion p

-- | The project files that have changed since the plan was written.
filesNewerThanPlan :: BuildPlan -> FilePath -> IO [FilePath]
filesNewerThanPlan plan projectDir = quietly [] $ do
  planTime <- getModificationTime (planPathFor projectDir)
  atRoot <- quietly [] (listDirectory projectDir)
  inPackages <- concat <$> traverse cabalFilesIn (localDirs plan)
  let candidates =
        [projectDir </> f | f <- atRoot, f `elem` projectFiles]
          <> [projectDir </> f | f <- atRoot, ".cabal" `isSuffixOf` f]
          <> inPackages
  newer <- traverse (isNewerThan planTime) candidates
  pure [f | Just f <- newer]
  where
    projectFiles =
      ["cabal.project", "cabal.project.local", "cabal.project.freeze"]
    localDirs p =
      Data.List.nub [dir | LocalPackage dir <- fmap ppSource (bpPackages p)]
    cabalFilesIn dir = quietly [] $ do
      entries <- listDirectory dir
      pure [dir </> f | f <- entries, ".cabal" `isSuffixOf` f]
    isNewerThan planTime path = quietly Nothing $ do
      t <- getModificationTime path
      pure (if t > planTime then Just path else Nothing)

-- | Do whatever is missing, by asking @cabal@.
prepare ::
  -- | Whether to use the cache.
  Choice "useCache" ->
  -- | Whether to download what is missing.
  Choice "download" ->
  -- | The components the run is about to format.
  [PlanComponent] ->
  -- | The project being prepared.
  FilePath ->
  -- | What it was found to be short of.
  Readiness ->
  IO (Either Text ())
prepare caching downloading wanted projectDir readiness =
  prepareWith
    caching
    downloading
    (runCabal projectDir)
    (futilityFor caching projectDir)
    wanted
    projectDir
    readiness

-- | An account of actions we know are not worth attempting.
data Futility = Futility
  { -- | Has solving this plan already been tried and left it as narrow?
    solveWasFutile :: IO Bool,
    -- | Record that a solve has been run and left it narrow, which is what
    -- 'solveWasFutile' answers from afterwards.
    rememberFutileSolve :: IO (),
    -- | The packages an earlier fetch was still short of afterwards.
    fetchWasFutileFor :: IO [Text],
    -- | Remember what a fetch left missing.
    rememberFutileFetch :: [Text] -> IO ()
  }

-- | The state when we know nothing about futile actions yet.
undiscoveredFutility :: Futility
undiscoveredFutility =
  Futility
    { solveWasFutile = pure False,
      rememberFutileSolve = pure (),
      fetchWasFutileFor = pure [],
      rememberFutileFetch = const (pure ())
    }

-- | A memory kept in the cache, under the plan the project has now.
futilityFor :: Choice "useCache" -> FilePath -> Futility
futilityFor caching projectDir =
  Futility
    { solveWasFutile = withCache False cachedFutileSolve,
      rememberFutileSolve = withCache () storeFutileSolve,
      fetchWasFutileFor = withCache [] cachedFutileFetch,
      rememberFutileFetch = \packages -> withCache () (`storeFutileFetch` packages)
    }
  where
    withCache fallback use =
      readBuildPlan (planPathFor projectDir) >>= \case
        Left _ -> pure fallback
        Right plan -> do
          opened <- openCache caching =<< tokenForBuildPlan plan
          maybe (pure fallback) use opened

-- | 'prepare', given a way to run @cabal@ and a memory of what earlier
-- attempts came to.
prepareWith ::
  -- | Whether to use the cache.
  Choice "useCache" ->
  -- | Whether to download what is missing.
  Choice "download" ->
  -- | Run @cabal@ with these arguments.
  ([String] -> IO (Either Text ())) ->
  -- | What earlier attempts came to.
  Futility ->
  -- | The components the run is about to format.
  [PlanComponent] ->
  -- | The project being prepared.
  FilePath ->
  -- | What it was found to be short of.
  Readiness ->
  IO (Either Text ())
prepareWith caching downloading cabal futility wanted projectDir = \case
  Ready -> pure (Right ())
  SourcesMissing _ -> fetch
  PlanMissing -> solveThenFetch
  PlanStale _ -> solveThenFetch
  PlanNarrow _ ->
    solveWasFutile futility >>= \case
      True -> fetchWhatIsShort
      False -> solveThenFetch
  where
    wholeProject = ["--enable-tests", "--enable-benchmarks"]
    tryWholeProject args =
      cabal (args <> wholeProject) >>= \case
        Right () -> pure (Right ())
        Left _ -> cabal args
    fetch
      | isTrue downloading = tryWholeProject ["build", "all", "--only-download"]
      | otherwise = pure (Right ())
    fetchWhatIsShort =
      readBuildPlan (planPathFor projectDir) >>= \case
        Left _ -> pure (Right ())
        Right plan -> do
          short <- fmap ppName <$> sourcesShortOf caching plan
          refused <- fetchWasFutileFor futility
          if null short || all (`elem` refused) short
            then pure (Right ())
            else
              fetch >>= \case
                Left err -> pure (Left err)
                Right () -> do
                  left <- sourcesShortOf caching plan
                  rememberFutileFetch futility (fmap ppName left)
                  pure (Right ())
    solveThenFetch =
      tryWholeProject ["build", "all", "--dry-run"] >>= \case
        Left err -> pure (Left err)
        Right () ->
          checkReadiness caching wanted projectDir >>= \case
            SourcesMissing _ -> fetch
            PlanNarrow _ -> rememberFutileSolve futility >> fetchWhatIsShort
            _ -> pure (Right ())

-- | Run @cabal@ in a project directory, letting it speak for itself.
runCabal :: FilePath -> [String] -> IO (Either Text ())
runCabal projectDir args = quietly (Left "could not run cabal") $ do
  hFlush stderr
  -- A duplicate because 'createProcess' closes the handle it is given once
  -- the child has it, and closing the real standard error would leave
  -- nothing to report the failure on.
  passed <- hDuplicate stderr
  (_, _, _, running) <-
    createProcess
      (proc "cabal" args)
        { cwd = Just projectDir,
          std_out = UseHandle passed,
          std_err = Inherit
        }
  code <- waitForProcess running
  pure $ case code of
    ExitSuccess -> Right ()
    _ -> Left ("cabal " <> T.unwords (fmap T.pack args) <> " failed; see above")

-- | Fetch the sources of a trusted plan's dependencies that are neither
-- installed nor fetched already, at the versions the plan names.
fetchUninstalled ::
  -- | Whether to use the cache.
  Choice "useCache" ->
  -- | The project the plan is for.
  FilePath ->
  -- | The build plan to use.
  BuildPlan ->
  IO ()
fetchUninstalled caching projectDir plan =
  sourcesShortOf caching plan >>= \case
    [] -> pure ()
    short ->
      void . runCabal projectDir $
        "fetch"
          : "--no-dependencies"
          : [T.unpack (ppName p <> "-" <> ppVersion p) | p <- short]

-- | Get a plan that is safe to use, doing whatever @cabal@ work is needed.
loadPlan ::
  -- | Whether to use the cache.
  Choice "useCache" ->
  -- | Whether to download what is missing.
  Choice "download" ->
  -- | The components the run is about to format, so that a plan which says
  -- nothing about them is solved again rather than trusted.
  [PlanComponent] ->
  -- | The project whose plan it is.
  FilePath ->
  IO (Either Text BuildPlan)
loadPlan caching downloading wanted projectDir = do
  readiness <- checkReadiness caching wanted projectDir
  prepare caching downloading wanted projectDir readiness >>= \case
    Left err -> pure (Left err)
    _ -> readBuildPlan (planPathFor projectDir)

-- | Every planned package whose source could be in the package cache, with
-- where that would be.
plannedTarballs :: BuildPlan -> IO [(PlanPackage, FilePath)]
plannedTarballs plan = do
  cacheRoot <- packageCacheRoot
  repos <- quietly [] (Data.List.sort <$> listDirectory cacheRoot)
  traverse
    (\p -> (,) p <$> tarballFor cacheRoot repos p)
    [p | p <- bpPackages plan, not (isLocal p)]
  where
    isLocal p = case ppSource p of
      LocalPackage _ -> True
      CheckedOut _ -> True
      _ -> False

-- | Where @cabal@ keeps downloaded package sources, one directory per
-- repository it downloads from.
packageCacheRoot :: IO FilePath
packageCacheRoot =
  readProgramOutput
    "cabal"
    ["path", "--remote-repo-cache", "--output-format=json"]
    >>= \case
      Just said | Just dir <- remoteRepoCacheIn said -> pure dir
      _ -> guessedPackageCacheRoot

-- | The package cache directory, out of what @cabal path@ printed.
remoteRepoCacheIn :: Text -> Maybe FilePath
remoteRepoCacheIn said = do
  spoken <-
    listToMaybe $
      reverse (filter (not . T.null) (fmap T.strip (T.lines said)))
  value <- decodeStrict (T.encodeUtf8 spoken)
  parseMaybe (withObject "cabal path" (.: "remote-repo-cache")) value

-- | Where @cabal@ probably keeps its downloaded packages.
guessedPackageCacheRoot :: IO FilePath
guessedPackageCacheRoot =
  lookupEnv "CABAL_DIR" >>= \case
    Just dir -> pure (dir </> "packages")
    Nothing -> do
      places <- cabalDirs
      found <- filterM holdsAnIndex (toList places)
      pure (fromMaybe (NE.head places) (listToMaybe found))
  where
    holdsAnIndex dir =
      quietly False (doesFileExist (dir </> hackage </> "01-index.tar"))
    hackage = "hackage.haskell.org"

-- | Every directory @cabal@ could be keeping a package cache in, the
-- platform's own default first.
cabalDirs :: IO (NonEmpty FilePath)
cabalDirs = do
  appData <- getAppUserDataDirectory "cabal"
  xdg <- quietly Nothing (Just <$> getXdgDirectory XdgCache "cabal")
  pure . fmap (</> "packages") $ case xdg of
    Just dir | not onWindows -> dir :| [appData]
    Just dir -> appData :| [dir]
    Nothing -> appData :| []

-- | Whether this is a Windows build, for the places that differ there.
onWindows :: Bool
onWindows = System.Info.os == "mingw32"

-- | The repository @cabal@ would have kept a package's sources under.
hackageByDefault :: FilePath
hackageByDefault = "hackage.haskell.org"

-- | Where a package's source tarball is, or where fetching would put it.
tarballFor :: FilePath -> [FilePath] -> PlanPackage -> IO FilePath
tarballFor cacheRoot repos p = case provenanceOf p of
  RepoFromDirectory dir -> pure (dir </> flat)
  RepoDownloaded uri -> searched (hostOf uri)
  RepoNoProvenance -> searched Nothing
  where
    flat = T.unpack (ppName p <> "-" <> ppVersion p <> ".tar.gz")
    under repo =
      cacheRoot
        </> repo
        </> T.unpack (ppName p)
        </> T.unpack (ppVersion p)
        </> flat
    searched preferred = do
      let first' = fromMaybe hackageByDefault preferred
          rest = filter (/= first') repos
      found <- filterM doesFileExist (fmap under (first' : rest))
      pure (fromMaybe (under first') (listToMaybe found))

-- | Which repository a package came from, where it came from one.
provenanceOf :: PlanPackage -> RepoProvenance
provenanceOf p = case ppSource p of
  RepoPackage _ repo -> repo
  _ -> RepoNoProvenance

-- | The host a URI names, which is what @cabal@ conventionally calls the
-- repository that lives there.
hostOf :: Text -> Maybe FilePath
hostOf uri = case T.breakOn "//" uri of
  (_, rest)
    | not (T.null rest),
      host <- T.takeWhile (/= '/') (T.drop 2 rest),
      not (T.null host) ->
        Just (T.unpack host)
  _ -> Nothing

----------------------------------------------------------------------------
-- 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.
data Resolver = Resolver
  { -- | What a module exports, with 'Nothing' for one that could not be
    -- read, which is not the same as its having no operators; see
    -- 'Tilia.Fixity.resolveScope' for why the difference has to survive.
    askFixities :: Text -> IO (Maybe (Fixities)),
    -- | What a module keeps under each of its names, for the sake of a
    -- @T(..)@ in an import list.
    askChildren :: Text -> IO (Map OpName (Set OpName)),
    -- | The operators an unread module's export list names, asked only of
    -- the modules 'askFixities' gave up on, and what keeps a module that
    -- plainly has no such operator from being blamed for one.
    askExportNames :: Text -> IO (Maybe (Set OpName)),
    -- | The modules reading a module went through before giving up, the one
    -- it gave up on last. Asked only of the modules 'askFixities' gave up
    -- on, and only so that a message can name the exact problematic module
    -- rather than the import that happens to sit above it.
    askChain :: Text -> IO [Text]
  }

-- | 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 = newResolverVia (Do #useCache) [FromInterface, FromSource]

-- | 'newResolver', restricted to the routes given.
newResolverVia ::
  -- | Whether to use the cache.
  Choice "useCache" ->
  -- | Which readings to try, in order.
  [Route] ->
  -- | The build plan to use.
  BuildPlan ->
  IO Resolver
newResolverVia caching routes plan = do
  tarballs <-
    if FromSource `elem` routes
      then plannedTarballs plan
      else pure []
  cache <- openCache caching =<< tokenForBuildPlan plan
  installed <- getInstalledPackages cache
  index <- buildModuleIndex cache installed tarballs
  let interfaces = interfaceIndex installed
  local <- localModules plan
  memo <- newIORef Map.empty
  childrenRead <- newIORef Map.empty
  askPackage <- newPackageReader
  extensionsRead <- newIORef Map.empty
  exportsRead <- newIORef Map.empty
  interfacesRead <- newIORef Map.empty
  reading <- newQSem =<< getNumCapabilities
  let interfaceOf modName = do
        slot <- newEmptyMVar
        claimed <-
          atomicModifyIORef' interfacesRead $ \m -> case Map.lookup modName m of
            Just other -> (m, Left other)
            Nothing -> (Map.insert modName slot m, Right slot)
        case claimed of
          Left other -> readMVar other
          Right mine -> do
            found <- case Map.lookup modName interfaces of
              Nothing -> pure Nothing
              Just (_, path) ->
                bracket_ (waitQSem reading) (signalQSem reading) $
                  readInterface modName path
            -- Being listed is not the same as being readable: @ghc-pkg@
            -- names @GHC.Prim@ among @ghc-prim@'s modules and there is no
            -- file at the path that implies. So the table answers for a
            -- module with nothing to read, however it came to have nothing.
            let interface = case found of
                  Just _ -> found
                  Nothing -> asInterface <$> Map.lookup modName builtinFixities
            putMVar mine interface
            pure interface
  let workings =
        Workings
          { wkRoutes = routes,
            wkCache = cache,
            wkLocal = local,
            wkIndex = index,
            wkInterfaces = interfaces,
            wkInterfaceOf = interfaceOf,
            wkReach = reach,
            wkReachChildren = children,
            wkReachExports = exports,
            wkExtensionsOf = extensionsOf,
            wkMacros = macrosOf plan,
            wkGenerated = generatedModules plan
          }
      resolved visiting modName = do
        known <- readIORef memo
        case Map.lookup modName known of
          Just answer -> pure answer
          Nothing -> do
            answer <- resolveModule workings visiting modName
            atomicModifyIORef' memo (\m -> (Map.insert modName answer m, ()))
            pure answer
      reach visiting modName
        | modName `Set.member` visiting = pure Nothing
        | otherwise = fixitiesEstablished <$> resolved visiting modName
      chain visiting = go Set.empty
        where
          go seen modName
            | modName `Set.member` seen = pure []
            | otherwise =
                resolved visiting modName >>= \case
                  Unreadable (Just below) ->
                    (below :) <$> go (Set.insert modName seen) below
                  _ -> pure []
      exports visiting modName
        | modName `Set.member` visiting = pure Nothing
        | otherwise = do
            seen <- readIORef exportsRead
            case Map.lookup modName seen of
              Just names -> pure names
              Nothing -> do
                names <- exportNamesOfModule workings visiting modName
                atomicModifyIORef' exportsRead (\m -> (Map.insert modName names m, ()))
                pure names
      extensionsOf modName
        | Just path <- Map.lookup modName local =
            either (const []) id <$> askPackage path
        | Just (package, tarball) <- Map.lookup modName index = do
            seen <- readIORef extensionsRead
            case Map.lookup package seen of
              Just extensions -> pure extensions
              Nothing -> do
                extensions <- fromTarball tarball
                atomicModifyIORef' extensionsRead (\m -> (Map.insert package extensions m, ()))
                pure extensions
        | otherwise = pure []
      fromTarball tarball =
        quietly [] $ do
          bytes <- BL.readFile tarball
          pure (foldMap declaredExtensions (cabalFileInArchive (Tar.read (GZip.decompress bytes))))
      children visiting modName
        | modName `Set.member` visiting = pure Map.empty
        | otherwise = do
            seen <- readIORef childrenRead
            case Map.lookup modName seen of
              Just kept -> pure kept
              Nothing -> do
                kept <- childrenOfModule workings visiting modName
                atomicModifyIORef' childrenRead (\m -> (Map.insert modName kept m, ()))
                pure kept
  pure
    Resolver
      { askFixities = reach Set.empty,
        askChildren = children Set.empty,
        askExportNames = exports Set.empty,
        askChain = chain Set.empty
      }

-- | The operators a module's export list names, following what it
-- reexports.
--
-- Asked only about modules whose fixities could not be established, and
-- only to decide which of them an unsettled operator can be blamed on. A
-- module that hands on one nobody could read answers 'Nothing'; one with no
-- export list exports what it declares, which is every fixity it could
-- supply.
exportNamesOfModule :: Workings -> Set Text -> Text -> IO (Maybe (Set OpName))
exportNamesOfModule
  Workings{wkCache, wkLocal, wkIndex, wkReachChildren, wkReachExports, wkMacros}
  visiting
  modName
    | Just path <- Map.lookup modName wkLocal,
      writtenForHsc path =
        pure (Just (hscSupplies modName))
    | Just path <- Map.lookup modName wkLocal = namesIn =<< readFileText path
    | Just (package, tarball) <- Map.lookup modName wkIndex =
        remembered package >>= \case
          Just answer -> pure (exportedNames answer)
          Nothing ->
            readModule tarball modName >>= \case
              Nothing -> pure Nothing
              Just ForHsc -> do
                let names = Just (hscSupplies modName)
                store package (asExported names)
                pure names
              Just (Haskell text) -> do
                names <- namesIn (Just text)
                store package (asExported names)
                pure names
    | otherwise = pure Nothing
    where
      remembered package = case wkCache of
        Nothing -> pure Nothing
        Just c -> cachedExportNames c package modName
      store package answer = case wkCache of
        Nothing -> pure ()
        Just c -> storeExportNames c package modName answer
      namesIn text = case parsedLeaves =<< text of
        Nothing -> pure Nothing
        Just modules ->
          fmap Set.unions . sequence <$> traverse readOne modules
      readOne (implicitPrelude, hsModule) =
        exportNamesWithReexports
          implicitPrelude
          (wkReachExports visiting')
          (wkReachChildren visiting')
          modName
          hsModule
      visiting' = Set.insert modName visiting
      parsedLeaves = configurationsOf wkMacros Nothing modName

-- | What a module keeps under each of its names, so that a @T(..)@ in an
-- import list can be told what it brings in.
childrenOfModule :: Workings -> Set Text -> Text -> IO (Map OpName (Set OpName))
childrenOfModule
  Workings
    { wkRoutes,
      wkCache,
      wkLocal,
      wkIndex,
      wkInterfaces,
      wkInterfaceOf,
      wkReachChildren,
      wkExtensionsOf,
      wkMacros
    }
  visiting
  modName
    | Just path <- Map.lookup modName wkLocal,
      writtenForHsc path =
        pure Map.empty
    | Just path <- Map.lookup modName wkLocal =
        readFileText path >>= \case
          Nothing -> pure Map.empty
          Just text -> inSource text
    | otherwise = firstAnswer (fmap taking wkRoutes)
    where
      taking = \case
        FromInterface -> case Map.lookup modName wkInterfaces of
          Nothing -> pure Nothing
          Just (key, _) -> keptUnder key outOfInterface
        FromSource -> case Map.lookup modName wkIndex of
          Nothing -> pure Nothing
          Just (package, tarball) ->
            keptUnder package $
              readModule tarball modName >>= \case
                Nothing -> pure Nothing
                Just ForHsc -> pure (Just Map.empty)
                Just (Haskell text) -> Just <$> inSource text
      firstAnswer [] = pure Map.empty
      firstAnswer (route : rest) =
        route >>= \case
          Just kept -> pure kept
          Nothing -> firstAnswer rest
      keptUnder package readIt =
        remembered package >>= \case
          Just kept -> pure (Just kept)
          Nothing -> do
            kept <- readIt
            traverse_ (store package) kept
            pure kept
      remembered package = case wkCache of
        Nothing -> pure Nothing
        Just c -> cachedChildren c package modName
      store package kept = case wkCache of
        Nothing -> pure ()
        Just c -> storeChildren c package modName kept
      outOfInterface = fmap interfaceChildren <$> wkInterfaceOf modName
      inSource text = do
        extensions <- wkExtensionsOf modName
        case parsedLeaves extensions text of
          Nothing -> pure Map.empty
          Just modules ->
            Map.unionsWith Set.union
              <$> traverse readOne modules
      readOne (implicitPrelude, hsModule) =
        childrenWithReexports
          implicitPrelude
          (wkReachChildren visiting')
          modName
          hsModule
      visiting' = Set.insert modName visiting
      parsedLeaves extensions = configurationsOf wkMacros (Just extensions) 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 an import the resolver could
-- not read arrives as 'Nothing' and stays 'Nothing', 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 module whose scope is wanted, already parsed.
  HsModule GhcPs ->
  -- | Everything that module can see, and what it could not find out.
  IO Scope
scopeFor resolver implicitPrelude hsModule = do
  let imports = moduleImports implicitPrelude hsModule
  answers <- inParallel (\m -> (m,) <$> askFixities resolver m) (fmap importModule imports)
  let table = Map.fromList answers
      unread = [m | (m, Nothing) <- answers]
  names <- Map.fromList <$> inParallel (\m -> (m,) <$> askExportNames resolver m) unread
  chains <- Map.fromList <$> inParallel (\m -> (m,) <$> askChain resolver m) unread
  kept <-
    Map.fromList
      <$> inParallel
        (\m -> (m,) <$> askChildren resolver m)
        (Set.toList (Set.fromList (fmap importModule (filter expands imports))))
  pure $
    resolveScope
      implicitPrelude
      KnownModules
        { knownFixities = \m -> Map.findWithDefault Nothing m table,
          knownChildren = \m -> Map.findWithDefault Map.empty m kept,
          knownExportNames = \m -> Map.findWithDefault Nothing m names,
          knownChain = \m -> Map.findWithDefault [] m chains
        }
      hsModule
  where
    expands i = case importNames i of
      Nothing -> False
      Just (_, items) -> any isAll items
    isAll = \case
      ImportedAll _ -> True
      _ -> False

-- | Everything a resolver consults, and the way back into it.
--
-- None of it changes from one module to the next, which is why
-- 'newResolverVia' 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 :: Maybe 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
    -- 'newResolverVia', so that the memo it keeps covers the recursive
    -- calls too.
    wkReach :: Set Text -> Text -> IO (Maybe (Fixities)),
    -- | How to reach another module for what its names carry with them,
    -- tied back the same way and against a visiting set of its own.
    wkReachChildren :: Set Text -> Text -> IO (Map OpName (Set OpName)),
    -- | How to reach another module for what its export list names, tied
    -- back the same way again.
    wkReachExports :: Set Text -> Text -> IO (Maybe (Set OpName)),
    -- | What extensions the package a module belongs to puts in force.
    wkExtensionsOf :: Text -> IO [Extension],
    -- | Known macro expansions.
    wkMacros :: Macros,
    -- | The modules @cabal@ writes itself, which are therefore in no
    -- package's sources. See 'generatedModules'.
    wkGenerated :: Set Text
  }

-- | Where a module's fixities come from, in order of cost.
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 ->
  -- | Its operator fixities, or, where they could not be established, the
  -- module below it that stopped us.
  IO Established
resolveModule
  Workings
    { wkRoutes,
      wkCache,
      wkLocal,
      wkIndex,
      wkInterfaces,
      wkInterfaceOf,
      wkReach,
      wkReachChildren,
      wkExtensionsOf,
      wkMacros,
      wkGenerated
    }
  visiting
  modName
    | Just builtin <- Map.lookup modName builtinFixities = pure (Declares builtin)
    | Just path <- Map.lookup modName wkLocal,
      writtenForHsc path =
        pure (hscDeclares modName)
    | Just path <- Map.lookup modName wkLocal =
        readFileText path >>= \case
          Nothing -> pure (Unreadable Nothing)
          Just source -> do
            extensions <- wkExtensionsOf modName
            fromText
              wkMacros
              extensions
              (wkReach visiting')
              (wkReachChildren visiting')
              visiting'
              source
              modName
    | otherwise = answered <$> firstAnswer (fmap taking wkRoutes)
    where
      visiting' = Set.insert modName visiting

      taking = \case
        FromInterface -> viaInterface
        FromSource -> viaArchive

      firstAnswer = go Nothing
        where
          go blamed [] = pure (Unreadable blamed)
          go blamed (route : rest) =
            route >>= \case
              Just (Declares fixities) -> pure (Declares fixities)
              Just (Unreadable below) -> go (blamed <|> below) rest
              Nothing -> go blamed rest

      viaInterface = case Map.lookup modName wkInterfaces of
        Nothing -> pure Nothing
        Just (key, _) ->
          cachedFor key >>= \case
            Just remembered -> pure (Just remembered)
            Nothing -> do
              established <- fromInterface wkInterfaceOf modName
              storeFor key established
              pure (Just established)

      viaArchive = case Map.lookup modName wkIndex of
        Nothing -> pure Nothing
        Just (package, tarball) ->
          cachedFor package >>= \case
            Just remembered -> pure (Just remembered)
            Nothing -> do
              extensions <- wkExtensionsOf modName
              fromSource
                wkMacros
                extensions
                (wkReach visiting')
                (wkReachChildren visiting')
                visiting'
                tarball
                modName
                >>= \case
                  NoArchive -> pure Nothing
                  FromArchive established -> do
                    storeFor package established
                    pure (Just established)

      answered = \case
        Declares fixities -> Declares fixities
        Unreadable below
          | Set.member modName wkGenerated -> Declares Map.empty
          | otherwise -> maybe (Unreadable below) Declares (byHand modName)
      byHand = fmap inBothNamespaces . (`Map.lookup` byHandFixities)
      cachedFor package = case wkCache of
        Nothing -> pure Nothing
        Just c -> cachedFixities c package modName
      storeFor package fixities = case wkCache of
        Nothing -> pure ()
        Just c -> storeFixities c package modName fixities

-- | Which package and tarball holds each module.
buildModuleIndex ::
  -- | Where to remember each package's module list, if anywhere.
  Maybe 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 exposes 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 </> T.unpack (T.replace "." "/" 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-"
        <> T.take 24 (T.decodeUtf8Lenient (B16.encode (SHA256.hash (T.encodeUtf8 (T.pack dir)))))

-- | Present a fixity map as an 'Interface'.
asInterface :: Fixities -> Interface
asInterface fixities =
  Interface
    { interfaceDeclares = fixities,
      interfaceReexports = [],
      interfaceChildren = Map.empty
    }

-- | The fixities a compiled interface reports, and those it passes on.
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 Nothing)
    Just iface -> do
      declarers <- inParallel asked (distinct (fmap fst (interfaceReexports iface)))
      pure $ case [m | (m, Nothing) <- declarers] of
        (m : _) -> Unreadable (Just m)
        [] ->
          Declares . Map.union (interfaceDeclares iface) . Map.unions $
            [ Map.filterWithKey (\(_, o) _ -> o == op) (interfaceDeclares declarer)
            | (m, op) <- interfaceReexports iface,
              Just (Just declarer) <- [lookup m declarers]
            ]
  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, if anywhere.
  Maybe 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 exposes, or 'Nothing' if the tarball is absent, fails
  -- verification, or holds no @.cabal@ file.
  IO (Maybe [Text])
fromCabalFile cache key tarball p = do
  remembered <- case cache of
    Nothing -> pure Nothing
    Just c -> cachedModules c key
  case remembered of
    Just ms -> pure (Just ms)
    Nothing ->
      verified p tarball >>= \case
        False -> pure Nothing
        True ->
          packageModules tarball >>= \case
            Nothing -> pure Nothing
            Just ms -> do
              case cache of
                Nothing -> pure ()
                Just c -> storeModules c key ms
              pure (Just ms)

-- | How a package's cached answers are filed.
cacheKey :: PlanPackage -> Text
cacheKey p =
  ppName p <> "-" <> ppVersion p <> maybe "" (("-" <>) . T.take 16) (sourceHashOf p)

-- | 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 a module's fixities out of a tarball, following re-exports.
fromSource ::
  -- | What the plan settles about its conditionals.
  Macros ->
  -- | What the module's package puts in force, before its own pragmas.
  [Extension] ->
  -- | How to reach another module, for chasing re-exports. This is
  -- 'resolveModule' tied back on itself, with the visiting set already
  -- extended.
  (Text -> IO (Maybe (Fixities))) ->
  -- | How to reach another module for what its names carry with them.
  (Text -> IO (Map OpName (Set OpName))) ->
  -- | Modules currently being resolved, passed through so that a
  -- re-export chain cannot loop.
  Set Text ->
  -- | The tarball holding this module's source.
  FilePath ->
  -- | The module to read.
  Text ->
  -- | What it declares, including what it only reexports, and whether that
  -- is worth remembering.
  IO Reading
fromSource macros extensions reach reachChildren visiting tarball modName =
  doesFileExist tarball >>= \case
    False -> pure NoArchive
    True ->
      readModule tarball modName >>= \case
        Nothing -> pure (FromArchive (Unreadable Nothing))
        Just ForHsc -> pure (FromArchive (hscDeclares modName))
        Just (Haskell source) ->
          FromArchive
            <$> fromText macros extensions reach reachChildren visiting source modName

-- | What came of looking for a module in an archive.
data Reading
  = -- | The archive was there, and this is what reading it established.
    FromArchive Established
  | -- | There was no archive to open.
    NoArchive

-- | The fixities a module's text declares and passes on.
fromText ::
  -- | What the plan settles about its conditionals.
  Macros ->
  -- | What the module's package puts in force, before its own pragmas.
  [Extension] ->
  -- | How to reach another module, for chasing re-exports. This is
  -- 'resolveModule' tied back on itself, with the visiting set already
  -- extended.
  (Text -> IO (Maybe (Fixities))) ->
  -- | How to reach another module for what its names carry with them.
  (Text -> IO (Map OpName (Set OpName))) ->
  -- | Modules currently being resolved, passed through so that a re-export
  -- chain cannot loop.
  Set Text ->
  -- | The module's source.
  Text ->
  -- | Its name.
  Text ->
  IO Established
fromText macros extensions reach reachChildren visiting source modName =
  case configurationsOf macros (Just extensions) modName source of
    Nothing -> pure (Unreadable Nothing)
    Just modules ->
      agreeing <$> traverse readOne modules
  where
    readOne (implicitPrelude, hsModule) =
      withReexports
        implicitPrelude
        reach
        reachChildren
        visiting
        modName
        hsModule

-- | One answer from every configuration that could be read, if they agree.
--
-- 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 is not an answer. Agreement across
-- the ones we could read is one, and a stronger one than the blanked text
-- could give.
--
-- A configuration whose imports could not be resolved is passed over rather
-- than counted against the rest, because almost every one of those is a
-- branch meant for a different platform.
agreeing :: NonEmpty Established -> Established
agreeing answers = case [fixities | Declares fixities <- toList answers] of
  [] -> Unreadable (listToMaybe (catMaybes [below | Unreadable below <- toList answers]))
  readable ->
    maybe (Unreadable Nothing) Declares (foldM together Map.empty readable)
  where
    together settled found
      | and (Map.intersectionWith (==) settled found) = Just (Map.union settled found)
      | otherwise = Nothing

-- | 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
      entries <- listDirectory dir
      case filter (".cabal" `isSuffixOf`) entries of
        [] -> pure Map.empty
        (cabalFile : _) -> do
          contents <- readFileText (dir </> cabalFile)
          case contents of
            Nothing -> pure Map.empty
            Just text ->
              Map.fromList . concat
                <$> traverse (locate dir (sourceDirs text)) (containedModules text)
    locate dir dirs m = do
      found <-
        filterM
          doesFileExist
          [ dir </> T.unpack d </> modulePath m ending
          | d <- dirs,
            ending <- moduleEndings
          ]
      pure [(m, path) | path <- take 1 found]

    modulePath m ending = T.unpack (T.replace "." "/" m) <> ending

-- | Read a file, if it is there and is text.
readFileText :: FilePath -> IO (Maybe Text)
readFileText path = quietly Nothing $ do
  there <- doesFileExist path
  if there
    then Just . T.decodeUtf8Lenient <$> BS.readFile path
    else pure Nothing

-- | What a module reexports, as well as what it declares.
withReexports ::
  -- | Whether @ImplicitPrelude@ is on in the module being read.
  Choice "implicitPrelude" ->
  -- | How to reach another module, for names this one only passes on.
  (Text -> IO (Maybe (Fixities))) ->
  -- | How to reach another module for what its names carry with them,
  -- which is what a @T(..)@ this module hands on amounts to.
  (Text -> IO (Map OpName (Set OpName))) ->
  -- | Modules currently being resolved. A candidate already in here is
  -- skipped rather than followed.
  Set Text ->
  -- | The name this module was looked up under, used to recognise a
  -- @module M@ export that refers to the module itself.
  Text ->
  -- | The module, already parsed.
  HsModule GhcPs ->
  -- | What it declares together with what it re-exports, or the module it
  -- passes names on from that could not be read.
  IO Established
withReexports implicitPrelude reach reachChildren visiting modName hsModule =
  case moduleExports hsModule of
    Nothing -> pure (Declares own)
    Just items -> do
      carried <- carriedNames implicitPrelude reachChildren hsModule items
      let wanted = wantedNames items <> fromCarried carried
      visible <-
        if null wanted
          then pure []
          else
            traverse
              (\i -> (,) i <$> fromModule (importModule i))
              (moduleImports implicitPrelude hsModule)
      let handedOnWhole =
            wantedModules implicitPrelude modName hsModule items
      wholeModules <- traverse (\m -> (,) m <$> fromModule m) handedOnWhole
      pure $ case stoppedAt visible wholeModules of
        Just below -> Unreadable (Just below)
        Nothing ->
          let seen = [(i, exported) | (i, Just exported) <- visible]
              whole = [exported | (_, Just exported) <- wholeModules]
              passedOn =
                Map.unions
                  [ found
                  | (qualifier, op) <- wanted,
                    found <- take 1 (from qualifier op seen)
                  ]
           in Declares (Map.unions (own : passedOn : whole))
  where
    stoppedAt visible wholeModules =
      listToMaybe $
        [importModule i | (i, Nothing) <- visible]
          <> [m | (m, Nothing) <- wholeModules]
    own = declaredFixities hsModule
    defined = declaredNames hsModule
    wantedNames items =
      [(qualifier, op) | ExportName qualifier op <- items, not (Set.member op defined)]
        <> [(qualifier, op) | ExportAll qualifier op <- items, not (Set.member op defined)]
    fromCarried carried =
      [ (qualifier, op)
      | ((qualifier, _), Just ops) <- carried,
        op <- Set.toList ops,
        not (Set.member op defined)
      ]
    from qualifier op seen =
      [ found
      | (i, exported) <- seen,
        canSupply qualifier op i,
        let found = Map.filterWithKey (\(_, o) _ -> o == op) exported,
        not (Map.null found)
      ]
    fromModule m
      | m `Set.member` visiting = pure (Just Map.empty)
      | otherwise = reach m

-- | What each name a module's export list hands on carries with it.
childrenWithReexports ::
  -- | Whether @ImplicitPrelude@ is on in the module being read.
  Choice "implicitPrelude" ->
  -- | How to reach another module for what its names carry.
  (Text -> IO (Map OpName (Set OpName))) ->
  -- | The name this module was looked up under.
  Text ->
  -- | The module, already parsed.
  HsModule GhcPs ->
  IO (Map OpName (Set OpName))
childrenWithReexports implicitPrelude reachChildren modName hsModule =
  case moduleExports hsModule of
    Nothing -> pure (moduleChildren hsModule)
    Just items -> do
      carried <- carriedNames implicitPrelude reachChildren hsModule items
      let handedOnWhole =
            wantedModules implicitPrelude modName hsModule items
      wholes <- traverse reachChildren handedOnWhole
      pure . Map.unionsWith Set.union $
        moduleChildren hsModule
          : Map.fromListWith Set.union [(parent, ops) | ((_, parent), Just ops) <- carried]
          : wholes

-- | The operators a module's export list names, following what it
-- reexports.
exportNamesWithReexports ::
  -- | Whether @ImplicitPrelude@ is on in the module being read.
  Choice "implicitPrelude" ->
  -- | How to reach another module for what its export list names.
  (Text -> IO (Maybe (Set OpName))) ->
  -- | How to reach another module for what its names carry.
  (Text -> IO (Map OpName (Set OpName))) ->
  -- | The name this module was looked up under.
  Text ->
  -- | The module, already parsed.
  HsModule GhcPs ->
  IO (Maybe (Set OpName))
exportNamesWithReexports
  implicitPrelude
  reachNames
  reachChildren
  modName
  hsModule =
    case moduleExports hsModule of
      Nothing -> pure (Just (Set.fromList [op | (_, op) <- Map.keys (declaredFixities hsModule)]))
      Just items -> do
        carried <- carriedNames implicitPrelude reachChildren hsModule items
        let handedOnWhole =
              wantedModules implicitPrelude modName hsModule items
        wholes <- traverse reachNames handedOnWhole
        pure $ do
          fromWholes <- sequence wholes
          fromCarried <-
            traverse (\((_, parent), kids) -> Set.insert parent <$> kids) carried
          pure (Set.unions (named items : declaredHere items : fromCarried <> fromWholes))
    where
      declared = declaredChildren hsModule
      named items = Set.fromList [op | ExportName _ op <- items]
      declaredHere items =
        Set.unions
          [ Set.insert parent kids
          | ExportAll _ parent <- items,
            Just kids <- [Map.lookup parent declared]
          ]

-- | What the types a module reexports carry with them, asked of the modules
-- they could have come from.
carriedNames ::
  -- | Whether @ImplicitPrelude@ is on in the module being read.
  Choice "implicitPrelude" ->
  -- | How to reach another module for what its export list names.
  (Text -> IO (Map OpName (Set OpName))) ->
  -- | The module, already parsed.
  HsModule GhcPs ->
  -- | Export items.
  [ExportItem] ->
  -- | For each reexported name, what it carries, or 'Nothing' where no
  -- module that could have supplied it had anything to say about it.
  IO [((Maybe Text, OpName), Maybe (Set OpName))]
carriedNames implicitPrelude reachChildren hsModule items =
  traverse (\(qualifier, parent) -> ((qualifier, parent),) <$> carriedBy qualifier parent) handedOn
  where
    declared = declaredChildren hsModule
    imports = moduleImports implicitPrelude hsModule
    handedOn =
      [ (qualifier, parent)
      | ExportAll qualifier parent <- items,
        not (Map.member parent declared)
      ]
    carriedBy qualifier parent = do
      answers <- traverse (reachChildren . importModule) (filter (canSupply qualifier parent) imports)
      pure $ case mapMaybe (Map.lookup parent) answers of
        [] -> Nothing
        kids -> Just (Set.unions kids)

-- | Could this import have supplied a name an export list reexports?
canSupply :: Maybe Text -> OpName -> Import -> Bool
canSupply qualifier op i =
  reaches && case importNames i of
    Nothing -> True
    Just (True, hidden) -> not (surelyNames Map.empty op hidden)
    Just (False, shown) -> mightBring Map.empty op shown
  where
    reaches = case qualifier of
      Nothing -> not (importQualified i)
      Just q -> importAlias i == q

-- | The modules a @module M@ export reexports whole, by their own names.
wantedModules ::
  -- | Whether @ImplicitPrelude@ is on in the module being read.
  Choice "implicitPrelude" ->
  -- | The name this module was looked up under, so that a module handing
  -- itself on under it is not chased.
  Text ->
  -- | The module, already parsed.
  HsModule GhcPs ->
  -- | Export items.
  [ExportItem] ->
  -- | The modules named, with an alias resolved to what it was imported
  -- as, and this module itself left out.
  [Text]
wantedModules implicitPrelude modName hsModule items =
  Set.toList . Set.fromList $
    concat [under m | ExportModule m <- items, not (isSelf m)]
  where
    under m = case [importModule i | i <- imports, importAlias i == m] of
      [] -> [m]
      aliased -> aliased
    imports = moduleImports implicitPrelude hsModule
    isSelf m = Just m == moduleName hsModule || m == modName

-- | Every configuration the preprocessor allows of a module's text that is
-- Haskell, parsed, each with whether it has the Prelude without importing
-- it.
configurationsOf ::
  -- | What the plan settles about the questions its conditionals ask.
  Macros ->
  -- | What extensions the module's package puts in force.
  Maybe [Extension] ->
  -- | The module's name, for the parser to put in its errors.
  Text ->
  -- | Its text.
  Text ->
  Maybe (NonEmpty (Choice "implicitPrelude", HsModule GhcPs))
configurationsOf macros extensions modName text =
  NE.nonEmpty . mapMaybe parsed
    =<< whatParsed (branchLeaves (withoutRuledOut macros text))
  where
    parsed leaf =
      (,) (hasImplicitPrelude (fromMaybe [] extensions) leaf) . pmModule
        <$> whatParsed (parseModule (configOf leaf) named leaf)
    whatParsed = either (const Nothing) Just
    configOf leaf = maybe defaultParserConfig (configFor leaf) extensions
    configFor leaf exts = parserConfigFor (effectiveExtensions exts leaf)
    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)

-- | Find a module inside a tarball and say what was found.
readModule :: FilePath -> Text -> IO (Maybe InArchive)
readModule tarball modName = quietly Nothing $ do
  bytes <- BL.readFile tarball
  let (cabal, candidates) = sweep Nothing [] (Tar.read (GZip.decompress bytes))
      dirs = maybe [] sourceDirs cabal
  pure (listToMaybe (mapMaybe (pick dirs candidates) moduleEndings))
  where
    suffix ending = "/" <> T.unpack (T.replace "." "/" modName) <> ending
    suffixes = fmap suffix moduleEndings
    sweep cabal found = \case
      Tar.Next entry rest
        | Tar.NormalFile content _ <- Tar.entryContent entry,
          cabalFileAtTop (entryPosixPath entry),
          Nothing <- cabal ->
            sweep (Just (decode content)) found rest
        | Tar.NormalFile content _ <- Tar.entryContent entry,
          any (`isSuffixOf` entryPosixPath entry) suffixes ->
            sweep cabal ((entryPosixPath entry, decode content) : found) rest
        | otherwise -> sweep cabal found rest
      _ -> (cabal, reverse found)
    pick dirs candidates ending =
      inArchive ending . snd
        <$> listToMaybe (under sfx dirs matching <> matching)
      where
        sfx = suffix ending
        matching = [c | c <- candidates, sfx `isSuffixOf` fst c]
    under sfx dirs matching = [e | d <- dirs, e <- matching, inDir sfx d (fst e)]
    inDir sfx d path
      | d == "." = takeWhile (/= '/') path <> sfx == path
      | otherwise = ("/" <> T.unpack d <> sfx) `isSuffixOf` path
    decode = T.decodeUtf8Lenient . BL.toStrict

-- | The endings a package may write a module under, in the order they are
-- tried.
moduleEndings :: [String]
moduleEndings = [".hs", ".hsc"]

-- | What an archive holds for a module.
data InArchive
  = -- | Haskell, as the package wrote it.
    Haskell Text
  | -- | A module written for @hsc2hs@. Its text is not kept: there is
    -- nothing to be done with it, and 'hscFixities' answers for it.
    ForHsc

-- | What was found under one ending amounts to.
inArchive :: String -> Text -> InArchive
inArchive ending text
  | writtenForHsc ending = ForHsc
  | otherwise = Haskell text

-- | Is this a module @hsc2hs@ writes rather than one anybody compiles?
writtenForHsc :: FilePath -> Bool
writtenForHsc = isSuffixOf ".hsc"

-- | What an @.hsc@ module declares, which is nothing unless it is named.
hscDeclares :: Text -> Established
hscDeclares modName =
  Declares (maybe Map.empty inBothNamespaces (Map.lookup modName hscFixities))

-- | The fixities an 'Established' holds, where it holds any.
fixitiesEstablished :: Established -> Maybe (Fixities)
fixitiesEstablished = \case
  Declares fixities -> Just fixities
  Unreadable _ -> Nothing

-- | The operators an @.hsc@ module can supply, on the same reasoning.
hscSupplies :: Text -> Set OpName
hscSupplies modName =
  maybe Set.empty Map.keysSet (Map.lookup modName hscFixities)