packages feed

tilia-0.1.0.0: src/Tilia/Fixity.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Working out the fixity of the operators a module uses.
module Tilia.Fixity
  ( -- * Fixities
    OpName (..),
    Direction (..),
    Fixity (..),
    defaultFixity,
    spellFixity,

    -- * Module declarations
    declaredFixities,
    declaredNames,
    moduleName,

    -- * Module exports
    ExportItem (..),
    moduleExports,
    declaredMembers,
    listedMembers,

    -- * Module imports
    Import (..),
    ImportItem (..),
    moduleImports,
    maySupply,
    certainlyBrings,
    decides,
    Namespace (..),
    Fixities,
    inBothNamespaces,
    Certain (..),
    ModuleChain (..),
    spellModuleChain,
    Scope (..),
    Reach (..),
    reachIn,
    resolveScope,

    -- * Answers
    Provenance (..),
    Resolution (..),
    lookupFixity,

    -- * What could not be answered
    Unknown (..),
    operatorsUsed,
    unknownOperators,
    capturedUses,
    operatorSpelling,
    spellUnreadIn,
    spellDisagreement,

    -- * Module summaries
    ModuleSummary (..),
    summarize,

    -- * What reading a module established
    Established (..),
    unreadable,
    settlesEverything,
    unsettledThrough,
  )
where

import Control.DeepSeq (NFData)
import Data.Bifunctor (first)
import Data.Char (isUpper)
import Data.Choice (Choice, isTrue)
import Data.Foldable (toList)
import Data.Generics.Schemes (listify)
import Data.List (nub, sortOn, tails)
import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe (mapMaybe)
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import GHC.Hs hiding (Fixity, OpName)
import GHC.Types.Fixity qualified as GHC
import GHC.Types.Name.Occurrence (occNameString)
import GHC.Types.Name.Reader (RdrName (..), rdrNameOcc)
import GHC.Types.SrcLoc (GenLocated (..), unLoc)
import Tilia.Gathered (Gathered (..))
import Tilia.Palette (Color (Place), Palette, paint)
import Tilia.Span (Span, covers)
import Tilia.Span.Ghc (spanOf)
import Tilia.Utils (collected, spellList)

----------------------------------------------------------------------------
-- Fixities

-- | An operator, spelled as it appears in an @infix@ declaration: @<+>@, or
-- @div@ for a function used infix in backticks.
newtype OpName = OpName Text
  deriving (Eq, Ord, Show, Generic)

instance NFData OpName

-- | Which way an operator associates.
data Direction = LeftAssoc | RightAssoc | NoAssoc
  deriving (Eq, Show, Generic)

instance NFData Direction

-- | A fixity: how tightly an operator binds, and which way it associates.
data Fixity = Fixity
  { fixityDirection :: Direction,
    fixityPrecedence :: Int
  }
  deriving (Eq, Show, Generic)

instance NFData Fixity

-- | What an operator with no declaration in scope means: @infixl 9@.
defaultFixity :: Fixity
defaultFixity = Fixity LeftAssoc 9

-- | A fixity, written the way it would be declared.
spellFixity :: Fixity -> Text
spellFixity (Fixity direction precedence) =
  which direction <> " " <> T.pack (show precedence)
  where
    which = \case
      LeftAssoc -> "infixl"
      RightAssoc -> "infixr"
      NoAssoc -> "infix"

----------------------------------------------------------------------------
-- Module declarations

-- | The fixities a module declares for its own operators.
declaredFixities :: HsModule GhcPs -> Fixities
declaredFixities hsModule =
  Map.fromList
    [ ((namespace, op), fixity)
    | (specifier, op, fixity) <- concatMap (fromDecl . unLoc) (hsmodDecls hsModule),
      namespace <- namespacesOf specifier op
    ]
  where
    (types, terms) = declaredNamespaces hsModule
    namespacesOf specifier op = case specifier of
      TypeNamespaceSpecifier _ -> [InTypes]
      DataNamespaceSpecifier _ -> [InTerms]
      NoNamespaceSpecifier ->
        case ([InTypes | Set.member op types] <> [InTerms | Set.member op terms]) of
          [] -> [InTypes, InTerms]
          found -> found

    fromDecl = \case
      SigD _ sig -> fromSig sig
      TyClD _ ClassDecl{tcdSigs} -> concatMap (fromSig . unLoc) tcdSigs
      _ -> []
    fromSig = \case
      FixSig _ (FixitySig specifier names fixity) ->
        [(specifier, opName (unLoc n), fromGhcFixity fixity) | n <- names]
      _ -> []

-- | The names a module declares among types, and those it declares among
-- terms.
declaredNamespaces :: HsModule GhcPs -> (Set OpName, Set OpName)
declaredNamespaces hsModule =
  ( Set.fromList (concatMap (types . unLoc) decls),
    Set.fromList (concatMap (terms . unLoc) decls)
  )
  where
    decls = hsmodDecls hsModule
    types = \case
      TyClD _ d -> case d of
        FamDecl _ FamilyDecl{fdLName} -> [opName (unLoc fdLName)]
        SynDecl{tcdLName} -> [opName (unLoc tcdLName)]
        DataDecl{tcdLName} -> [opName (unLoc tcdLName)]
        ClassDecl{tcdLName, tcdATs} ->
          opName (unLoc tcdLName)
            : [opName (unLoc (fdLName (unLoc f))) | f <- tcdATs]
      _ -> []
    terms = \case
      ValD _ b -> boundNames b
      SigD _ sig -> signedNames sig
      ForD _ f -> [opName (unLoc (fd_name f))]
      TyClD _ d@DataDecl{} -> fmap snd (membersOf d)
      TyClD _ ClassDecl{tcdSigs} -> concatMap (classMethods . unLoc) tcdSigs
      _ -> []

-- | The methods a class signature declares.
classMethods :: Sig GhcPs -> [OpName]
classMethods = \case
  TypeSig _ ns _ -> fmap (opName . unLoc) ns
  ClassOpSig _ _ ns _ -> fmap (opName . unLoc) ns
  _ -> []

-- | The members of the type or class a declaration declares, by
-- namespace: a data type's constructors and record fields, a class's
-- methods and associated families.
--
-- These are what @T(..)@ stands for, and each of them can carry a fixity of
-- its own—@:|@ is a constructor and @infixr 5@ all the same.
membersOf :: TyClDecl GhcPs -> [(Namespace, OpName)]
membersOf = \case
  DataDecl{tcdDataDefn} ->
    fmap (InTerms,) (concatMap (fromCon . unLoc) (consOf (dd_cons tcdDataDefn)))
  ClassDecl{tcdSigs, tcdATs} ->
    fmap (InTerms,) (concatMap (classMethods . unLoc) tcdSigs)
      <> [(InTypes, opName (unLoc (fdLName (unLoc f)))) | f <- tcdATs]
  _ -> []
  where
    consOf :: DataDefnCons (LConDecl GhcPs) -> [LConDecl GhcPs]
    consOf = toList

    fromCon :: ConDecl GhcPs -> [OpName]
    fromCon = \case
      ConDeclGADT{con_names} -> fmap (opName . unLoc) (toList con_names)
      ConDeclH98{con_name, con_args} ->
        opName (unLoc con_name) : fieldNames con_args

    fieldNames :: HsConDeclH98Details GhcPs -> [OpName]
    fieldNames = \case
      RecCon fields ->
        [ opName (unLoc (foLabel (unLoc n)))
        | f <- unLoc fields,
          n <- cdrf_names (unLoc f)
        ]
      _ -> []

-- | The names a signature is about, leaving fixity declarations aside.
signedNames :: Sig GhcPs -> [OpName]
signedNames = \case
  TypeSig _ ns _ -> fmap (opName . unLoc) ns
  ClassOpSig _ _ ns _ -> fmap (opName . unLoc) ns
  PatSynSig _ ns _ -> fmap (opName . unLoc) ns
  _ -> []

-- | Take a fixity as GHC presents it.
fromGhcFixity :: GHC.Fixity -> Fixity
fromGhcFixity (GHC.Fixity prec dir) = Fixity (fromGhcDirection dir) prec

-- | Take an associativity as GHC presents it.
fromGhcDirection :: GHC.FixityDirection -> Direction
fromGhcDirection = \case
  GHC.InfixL -> LeftAssoc
  GHC.InfixR -> RightAssoc
  GHC.InfixN -> NoAssoc

-- | Render a parsed name as an operator name.
opName :: RdrName -> OpName
opName = OpName . T.pack . occNameString . rdrNameOcc

-- | Every name a module defines itself.
--
-- Not the same question as 'declaredFixities', which is about @infix@
-- declarations. This one is asked of an export list: a name a module
-- exports and also defines needs no chasing, and one it merely reexports
-- does.
declaredNames :: HsModule GhcPs -> Set (Namespace, OpName)
declaredNames hsModule = Set.map (InTypes,) types <> Set.map (InTerms,) terms
  where
    (types, terms) = declaredNamespaces hsModule

-- | The names a binding brings into being.
boundNames :: HsBind GhcPs -> [OpName]
boundNames = \case
  FunBind _ n _ -> [opName (unLoc n)]
  PatBind _ p _ _ -> [opName n | VarPat _ (L _ n) <- listify isVarPat p]
  PatSynBind _ (PSB _ n _ _ _) -> [opName (unLoc n)]
  _ -> []
  where
    isVarPat :: Pat GhcPs -> Bool
    isVarPat = \case
      VarPat{} -> True
      _ -> False

-- | The module's own name, if it declares one.
moduleName :: HsModule GhcPs -> Maybe Text
moduleName = fmap (T.pack . moduleNameString . unLoc) . hsmodName

----------------------------------------------------------------------------
-- Module exports

-- | One entry of a module's export list.
data ExportItem
  = -- | A name, which may or may not be declared in this module, in the
    -- namespace the list writes it in, under the qualifier it was written
    -- with if it was written with one.
    ExportName Namespace (Maybe Text) OpName
  | -- | @T(..)@: the name, and with it whatever the module has to give
    -- under that name. Which names those are cannot be read off the list;
    -- it takes the declaration of @T@, or the module @T@ came from.
    ExportAll (Maybe Text) OpName
  | -- | @T(a, b)@: the name, and the members written out beside it.
    ExportSome (Maybe Text) OpName [OpName]
  | -- | @module M@, re-exporting everything that module brought in.
    ExportModule Text
  deriving (Eq, Show, Generic)

instance NFData ExportItem

-- | A module's export list, or 'Nothing' if it has none.
moduleExports :: HsModule GhcPs -> Maybe [ExportItem]
moduleExports =
  fmap (concatMap (fromIE . unLoc) . unLoc) . hsmodExports
  where
    fromIE = \case
      IEVar _ n _ -> [as (ExportName InTerms) n]
      IEThingAbs _ n _ -> [as (ExportName InTypes) n]
      IEThingAll _ n _ -> [as ExportAll n]
      IEThingWith _ n _ ns _ ->
        [as ExportSome n (fmap (opName . ieWrappedName . unLoc) ns)]
      IEModuleContents _ m -> [ExportModule (T.pack (moduleNameString (unLoc m)))]
      _ -> []
    as item n =
      let rdr = ieWrappedName (unLoc n)
       in item (qualifierOf rdr) (opName rdr)

-- | The qualifier a name was written under.
qualifierOf :: RdrName -> Maybe Text
qualifierOf = \case
  Qual m _ -> Just (T.pack (moduleNameString m))
  _ -> Nothing

-- | The members of each type or class a module declares.
declaredMembers :: HsModule GhcPs -> Map OpName (Set (Namespace, OpName))
declaredMembers =
  Map.fromListWith Set.union . concatMap (fromDecl . unLoc) . hsmodDecls
  where
    fromDecl = \case
      TyClD _ d@DataDecl{tcdLName} -> [entry tcdLName d]
      TyClD _ d@ClassDecl{tcdLName} -> [entry tcdLName d]
      _ -> []
    entry name d = (opName (unLoc name), Set.fromList (membersOf d))

-- | The members a module's export list offers with each name.
listedMembers :: HsModule GhcPs -> Map OpName (Set OpName)
listedMembers hsModule = case hsmodExports hsModule of
  Nothing -> declared
  Just items -> Map.fromListWith Set.union (concatMap (fromIE . unLoc) (unLoc items))
  where
    declared = Map.map (Set.map snd) (declaredMembers hsModule)
    fromIE = \case
      IEThingAll _ n _ -> [(nameOf n, kids) | Just kids <- [Map.lookup (nameOf n) declared]]
      IEThingWith _ n _ ns _ -> [(nameOf n, Set.fromList (fmap nameOf ns))]
      _ -> []
    nameOf = opName . ieWrappedName . unLoc

----------------------------------------------------------------------------
-- Module imports

-- | One import declaration, reduced to what bears on fixity.
data Import = Import
  { -- | The module being imported.
    importModule :: Text,
    -- | Whether unqualified names are brought into scope. An import that is
    -- @qualified@ brings none.
    importQualified :: Bool,
    -- | The name qualified uses go through: the alias if there is one,
    -- otherwise the module's own name.
    importAlias :: Text,
    -- | The explicit list, if there is one, and whether it is a @hiding@
    -- list.
    --
    -- Kept as written rather than as a set of names, because @T(..)@ says
    -- what it brings in only once the module it comes from has been asked.
    importNames :: Maybe (Bool, [ImportItem])
  }
  deriving (Eq, Show, Generic)

instance NFData Import

-- | One entry of an import list.
data ImportItem
  = -- | A plain name.
    ImportedName OpName
  | -- | @T(..)@: the name, and every member the module offers with it.
    ImportedAll OpName
  | -- | @T(a, b)@: the name and the members written out beside it.
    ImportedSome OpName [OpName]
  deriving (Eq, Show, Generic)

instance NFData ImportItem

-- | The imports of a module, all its configurations taken together.
moduleImports ::
  -- | Whether @ImplicitPrelude@ is on.
  Choice "implicitPrelude" ->
  -- | The module's configurations, parsed.
  NonEmpty (HsModule GhcPs) ->
  [Import]
moduleImports implicitPrelude configurations = prelude <> written
  where
    written =
      nub (fmap (fromDecl . unLoc) (sortOn spanOf (concatMap hsmodImports configurations)))
    prelude
      | not (isTrue implicitPrelude) = []
      | any ((== "Prelude") . importModule) written = []
      | otherwise =
          [ Import
              { importModule = "Prelude",
                importQualified = False,
                importAlias = "Prelude",
                importNames = Nothing
              }
          ]
    fromDecl d =
      Import
        { importModule = modName (unLoc (ideclName d)),
          importQualified = ideclQualified d /= NotQualified,
          importAlias = maybe (modName (unLoc (ideclName d))) (modName . unLoc) (ideclAs d),
          importNames = fromList <$> ideclImportList d
        }
    fromList (interpretation, names) =
      ( interpretation == EverythingBut,
        mapMaybe (importedItem . unLoc) (unLoc names)
      )
    modName = T.pack . moduleNameString

-- | One entry of an import list, as written.
importedItem :: IE GhcPs -> Maybe ImportItem
importedItem = \case
  IEVar _ n _ -> Just (ImportedName (nameOf n))
  IEThingAbs _ n _ -> Just (ImportedName (nameOf n))
  IEThingAll _ n _ -> Just (ImportedAll (nameOf n))
  IEThingWith _ n _ ns _ -> Just (ImportedSome (nameOf n) (fmap nameOf ns))
  _ -> Nothing
  where
    nameOf :: LIEWrappedName GhcPs -> OpName
    nameOf = opName . ieWrappedName . unLoc

-- | Could this import bring the operator in, as far as its list says?
--
-- A @T(..)@ whose members are not known is taken to bring anything in, and
-- to hide nothing but @T@ itself.
mayBring ::
  -- | The members of each name in the list, where they are known.
  Map OpName (Set OpName) ->
  -- | The operator being looked for.
  OpName ->
  Import ->
  Bool
mayBring members op i = case importNames i of
  Nothing -> True
  Just (True, hidden) -> not (any (namedBy members False op) hidden)
  Just (False, shown) -> any (namedBy members True op) shown

-- | Does an item of an import list name the operator, taking a @T(..)@ whose
-- members are not known to name it or not as told?
namedBy ::
  -- | The members of each name in the list, where they are known.
  Map OpName (Set OpName) ->
  -- | What a @T(..)@ whose members are not known is taken to say.
  Bool ->
  -- | The operator being looked for.
  OpName ->
  ImportItem ->
  Bool
namedBy members unknown op = \case
  ImportedName n -> n == op
  ImportedSome parent ns -> parent == op || op `elem` ns
  ImportedAll parent ->
    parent == op || maybe unknown (Set.member op) (Map.lookup parent members)

-- | Could this import supply the operator to a use written under this
-- qualifier?
maySupply ::
  -- | The members of each name in the import's list, where they are
  -- known.
  Map OpName (Set OpName) ->
  -- | The qualifier written at the use site, if any.
  Maybe Text ->
  -- | The operator being looked for.
  OpName ->
  Import ->
  Bool
maySupply members qualifier op i =
  maybe (not (importQualified i)) (== importAlias i) qualifier
    && mayBring members op i

-- | Does this import bring the name in for certain, as far as its list says?
--
-- Where the list could leave the name out, it is taken to: a @T(..)@ whose
-- members are not known hides anything, and of what a list shows, only a
-- variable written on its own, a member written under its type, and a member
-- of a @T(..)@ whose members are known count.
certainlyBrings ::
  -- | The members of each name in the import's list, where they are
  -- known.
  Map OpName (Set OpName) ->
  -- | The name, and the namespace it is in.
  (Namespace, OpName) ->
  Import ->
  Bool
certainlyBrings members (namespace, op) i = case importNames i of
  Nothing -> True
  Just (True, hidden) -> not (any (namedBy members True op) hidden)
  Just (False, shown) -> namespace == InTerms && any listed shown
  where
    listed = \case
      ImportedName n -> n == op && isVariable op
      ImportedSome _ ns -> op `elem` ns
      ImportedAll parent -> maybe False (Set.member op) (Map.lookup parent members)
    isVariable (OpName t) = case T.uncons t of
      Just (c, _) -> not (isUpper c) && c /= ':'
      Nothing -> False

-- | Does this import decide the name's fixity, by certainly bringing it
-- in from a module that settles it?
--
-- If so, no other import can give a use of the name another fixity: it
-- either brings in the same thing or makes the use ambiguous.
decides ::
  -- | What reading the module imported established.
  Established ->
  -- | The name, and the namespace it is in.
  (Namespace, OpName) ->
  Import ->
  Bool
decides established name i =
  Set.member name (certainNames (establishedCertain established))
    && certainlyBrings (establishedMembers established) name i
    && null (unsettledThrough established name)

-- | Which of Haskell's two namespaces an operator is written in.
data Namespace = InTypes | InTerms
  deriving (Eq, Ord, Show, Generic)

instance NFData Namespace

-- | The fixities a module offers.
type Fixities = Map (Namespace, OpName) Fixity

-- | Take fixities that say nothing about namespaces to govern both.
inBothNamespaces :: Map OpName Fixity -> Fixities
inBothNamespaces declared =
  Map.fromList
    [ ((namespace, op), fixity)
    | (op, fixity) <- Map.toList declared,
      namespace <- [InTypes, InTerms]
    ]

-- | What a module certainly brings into scope for a module that imports
-- it whole.
data Certain = Certain
  { -- | Every name it certainly exports.
    certainNames :: Set (Namespace, OpName),
    -- | The members it certainly exports with each type or class.
    certainMembers :: Map OpName (Set (Namespace, OpName))
  }
  deriving (Eq, Show, Generic)

instance NFData Certain

instance Semigroup Certain where
  a <> b =
    Certain
      { certainNames = certainNames a <> certainNames b,
        certainMembers =
          Map.unionWith Set.union (certainMembers a) (certainMembers b)
      }

instance Monoid Certain where
  mempty = Certain Set.empty Map.empty

-- | An import that could not be read, and the way down to the module that
-- actually stopped us. The head is the import as the file being formatted
-- writes it, and the last name is where reading gave up.
newtype ModuleChain = ModuleChain (NonEmpty Text)
  deriving (Eq, Show)

-- | A chain as it is shown, with the modules painted and arrows between
-- them.
spellModuleChain :: Palette -> ModuleChain -> Text
spellModuleChain palette (ModuleChain modules) =
  T.intercalate " → " (fmap (paint palette Place) (toList modules))

-- | Every fixity a module can see, and how.
data Scope = Scope
  { -- | What is in scope for an operator written among types.
    scopeInTypes :: Reach,
    -- | What is in scope for one written among terms.
    scopeInTerms :: Reach,
    -- | The imports whose modules leave the fixities of some names
    -- unsettled, each with what reading its module established.
    scopeUnsettled :: [(Import, Established)]
  }
  deriving (Eq, Show)

-- | What one namespace of a scope holds.
data Reach = Reach
  { -- | Reachable without qualification, with where it came from.
    reachUnqualified :: Map OpName (Fixity, Provenance),
    -- | Reachable as @M.op@, keyed by the alias actually written—or by the
    -- module's own name, under which its own declarations are reachable.
    reachQualified :: Map (Text, OpName) (Fixity, Provenance),
    -- | Operators the imports bring in with different fixities, as they
    -- would have to be written to run into it—without a qualifier, or under
    -- the alias the disagreeing imports share—with the module each import
    -- names and the fixity it brings.
    reachAmbiguous :: Map (Maybe Text, OpName) (NonEmpty (Text, Fixity)),
    -- | The uses a module that was read decides in this namespace, as they
    -- would be written: of a name the module defines itself, or one an import
    -- that was read certainly brings in, which no unread import can give
    -- another fixity.
    reachDecided :: Set (Maybe Text, OpName)
  }
  deriving (Eq, Show)

-- | The half of a scope an operator written in this namespace is settled
-- against.
reachIn :: Namespace -> Scope -> Reach
reachIn = \case
  InTypes -> scopeInTypes
  InTerms -> scopeInTerms

-- | Work out what a module can see.
resolveScope ::
  -- | Whether @ImplicitPrelude@ is on.
  Choice "implicitPrelude" ->
  -- | What reading each module this one imports established.
  (Text -> Established) ->
  -- | The module's configurations, parsed.
  NonEmpty (HsModule GhcPs) ->
  Scope
resolveScope implicitPrelude known configurations =
  Scope
    { scopeInTypes = reachAmong InTypes,
      scopeInTerms = reachAmong InTerms,
      scopeUnsettled =
        [ (i, established)
        | i <- imports,
          let established = known (importModule i),
          not (settlesEverything established)
        ]
    }
  where
    imports = moduleImports implicitPrelude configurations
    declared = Map.unions (fmap declaredFixities configurations)

    reachAmong namespace =
      Reach
        { reachUnqualified = Map.union own (Map.map settled unqualified),
          reachQualified = qualified,
          reachAmbiguous =
            Map.union
              (Map.mapKeys (Nothing,) (Map.mapMaybe disagreeing unqualified))
              (Map.mapKeys (first Just) (Map.mapMaybe disagreeing qualifiedFrom)),
          reachDecided =
            Set.fromList $
              [ (qualifier, op)
              | c <- toList configurations,
                op <- Set.toList (inNamespace (declaredNamespaces c)),
                qualifier <- Nothing : fmap Just ownNames
              ]
                <> [ (qualifier, op)
                   | i <- imports,
                     let established = known (importModule i),
                     name@(n, op) <- Set.toList (certainNames (establishedCertain established)),
                     n == namespace,
                     decides established name i,
                     qualifier <- [Nothing | not (importQualified i)] <> [Just (importAlias i)]
                   ]
        }
      where
        inNamespace = case namespace of
          InTypes -> fst
          InTerms -> snd
        ownNames = concatMap (toList . moduleName) configurations
        own = Map.map (,DeclaredHere) (fixitiesIn namespace declared)
        offered m = fixitiesIn namespace (establishedFixities (known m))
        unqualified =
          Map.unionsWith
            (<>)
            [ visible offered i
            | i <- imports,
              not (importQualified i)
            ]
        qualified = Map.union ownQualified (Map.map settled qualifiedFrom)
        ownQualified =
          Map.fromList
            [ ((m, op), entry)
            | m <- ownNames,
              (op, entry) <- Map.toList own
            ]
        qualifiedFrom =
          Map.unionsWith
            (<>)
            [ Map.mapKeys (importAlias i,) (visible offered i)
            | i <- imports
            ]

    settled ((m, fixity) :| _) = (fixity, DeclaredIn m)

    disagreeing offers@((_, fixity) :| _)
      | all ((== fixity) . snd) offers = Nothing
      | otherwise = Just (NE.nub offers)

    visible offered i =
      Map.filterWithKey
        (\op _ -> mayBring (establishedMembers (known (importModule i))) op i)
        (Map.map (\fixity -> (importModule i, fixity) :| []) (offered (importModule i)))

-- | The fixities in one namespace, by the operator alone.
fixitiesIn :: Namespace -> Fixities -> Map OpName Fixity
fixitiesIn namespace declared =
  Map.fromList
    [ (op, fixity)
    | ((n, op), fixity) <- Map.toList declared,
      n == namespace
    ]

----------------------------------------------------------------------------
-- Answers

-- | Where a fixity came from.
--
-- Kept so that an answer can be explained, and so that
-- 'ReportDefault'—which is a real answer, not a guess—cannot be confused
-- with not having one.
data Provenance
  = -- | An @infix@ declaration in the module being formatted.
    DeclaredHere
  | -- | An @infix@ declaration in the named imported module.
    DeclaredIn Text
  | -- | The language itself, as for @:@, whatever is in scope.
    BuiltIn
  | -- | No declaration exists anywhere in scope, and every module in scope
    -- was successfully consulted, so the Report's @infixl 9@ applies.
    ReportDefault
  deriving (Eq, Show)

-- | What is known about an operator at a use site.
data Resolution
  = -- | Established, and here is where from.
    Resolved Fixity Provenance
  | -- | Not established.
    Unresolved (NonEmpty ModuleChain)
  deriving (Eq, Show)

-- | The fixity of an operator as this module sees it.
lookupFixity ::
  -- | The scope.
  Scope ->
  -- | The namespace the operator is written in.
  Namespace ->
  -- | The qualifier written at the use site, if any.
  Maybe Text ->
  -- | Operator to resolve.
  OpName ->
  -- | The resolution.
  Resolution
lookupFixity scope namespace qualifier op =
  case fixityInScope scope namespace qualifier op of
    Just (_, (fixity, provenance)) -> Resolved fixity provenance
    Nothing -> case nonEmpty (unreadThatMightDeclare scope namespace qualifier op) of
      Nothing -> Resolved defaultFixity ReportDefault
      Just missing -> Unresolved missing

-- | What the scope itself has for a use, and the namespace it came from.
fixityInScope ::
  -- | The scope.
  Scope ->
  -- | The namespace the operator is written in.
  Namespace ->
  -- | The qualifier written at the use site, if any.
  Maybe Text ->
  -- | Operator to resolve.
  OpName ->
  -- | The fixity and where it came from, under the namespace that supplied
  -- it. 'Nothing' where the scope has no answer.
  Maybe (Namespace, (Fixity, Provenance))
fixityInScope scope namespace qualifier op
  | Just fixity <- Map.lookup op builtInSyntax =
      Just (namespace, (fixity, BuiltIn))
  | otherwise =
      case mapMaybe found (namespace : promotedFrom namespace) of
        (answer : _) -> Just answer
        [] -> Nothing
  where
    found n =
      (n,) <$> case qualifier of
        Nothing -> Map.lookup op (reachUnqualified (reachIn n scope))
        Just q -> Map.lookup (q, op) (reachQualified (reachIn n scope))

-- | The operators the language gives a fixity, rather than a declaration
-- anywhere: @:@ alone.
builtInSyntax :: Map OpName Fixity
builtInSyntax = Map.singleton (OpName ":") (Fixity RightAssoc 5)

-- | The namespaces a use written in this one may also refer to.
promotedFrom :: Namespace -> [Namespace]
promotedFrom = \case
  InTypes -> [InTerms]
  InTerms -> []

-- | The unread imports that could have declared this operator.
unreadThatMightDeclare ::
  -- | The scope.
  Scope ->
  -- | The namespace the operator is written in.
  Namespace ->
  -- | The qualifier written at the use site, if any.
  Maybe Text ->
  -- | Operator being resolved.
  OpName ->
  -- | The imports that could hold the answer, each down to the module that
  -- actually stopped us, and each module once.
  [ModuleChain]
unreadThatMightDeclare scope namespace qualifier op
  | null blamed = []
  | Set.member (qualifier, op) (reachDecided (reachIn namespace scope)) = []
  | otherwise = nub blamed
  where
    blamed =
      [ ModuleChain (importModule i :| chain)
      | (i, established) <- scopeUnsettled scope,
        maySupply (establishedMembers established) qualifier op i,
        n <- namespace : promotedFrom namespace,
        chain <- unsettledThrough established (n, op)
      ]

----------------------------------------------------------------------------
-- What could not be answered

-- | Why an operator's fixity could not be determined.
data Unknown
  = -- | These imports could not be read, each given down to the module that
    -- actually stopped us, and the declaration the answer depends on may be
    -- in any of them.
    NotRead (NonEmpty ModuleChain)
  | -- | The imports in scope bring it in with different fixities, given as
    -- the module each import names and the fixity it brings, so which one
    -- applies cannot be read off the imports alone.
    Ambiguous (NonEmpty (Text, Fixity))
  deriving (Eq, Show)

-- | Every operator the module uses where its fixity decides the layout and
-- the scope has to settle it, which is all of them but the uses
-- 'capturedUses' settles.
--
-- Only these positions. An operator chain in an expression and one in a type
-- are regrouped by precedence, so getting the precedence wrong changes what
-- the code means. Everywhere else—a section, the left-hand side of a
-- definition, an @infix@ declaration—the operator stands on its own and
-- nothing is regrouped around it.
operatorsUsed :: Gathered -> [(Namespace, (Maybe Text, OpName))]
operatorsUsed found =
  fmap (named InTerms) inExpressions <> fmap (named InTypes) inTypes
  where
    captured = capturedUses found
    inExpressions =
      [ n
      | OpApp _ _ op _ <- gatheredExpressions found,
        HsVar _ (L _ n) <- [unLoc op],
        maybe True (`Map.notMember` captured) (spanOf op)
      ]
    inTypes =
      [n | HsOpTy _ _ _ (L _ n) _ <- gatheredTypes found]
    named namespace n =
      (namespace, (qualifierOf n, OpName (T.pack (occNameString (rdrNameOcc n)))))

-- | The uses of an operator in an expression that a local binding captures,
-- by where the operator is written, with the fixity the binding's group
-- declares for it, or @infixl 9@.
--
-- A use is captured where it falls within what a binder is in scope over,
-- which is read off spans: the right-hand sides and the @where@ of a match,
-- a @let@, the statements after a bind. Bindings that are in scope more
-- widely than that, as in @mdo@, are passed over, and so is a use that
-- bindings with different fixities could each capture.
capturedUses :: Gathered -> Map Span Fixity
capturedUses found =
  Map.fromList
    [ (s, fixity)
    | (s, name) <- uses,
      [fixity] <- [nub [f | (f, region) <- Map.findWithDefault [] name binders, covers region s]]
    ]
  where
    uses =
      [ (s, opName n)
      | OpApp _ _ op _ <- gatheredExpressions found,
        HsVar _ (L _ n@(Unqual _)) <- [unLoc op],
        Just s <- [spanOf op]
      ]
    binders =
      Map.fromListWith
        (<>)
        [ (name, [(f, region)])
        | (name, f, regions) <- concatMap fromMatch (gatheredMatches found) <> concatMap fromExpression (gatheredExpressions found),
          Set.member name used,
          Just region <- regions
        ]
    used = Set.fromList (fmap snd uses)

    fromMatch (Match _ _ (L _ pats) (GRHSs _ rhss binds)) =
      [ (name, f, fmap spanOf (toList rhss) <> bindingSpans binds)
      | (name, f) <- patternBinders pats <> localBinders binds
      ]
        <> concatMap guarded rhss
    fromExpression = \case
      HsLet _ binds body ->
        [(name, f, bindingSpans binds <> [spanOf body]) | (name, f) <- localBinders binds]
      HsDo _ _ (L _ stmts) -> inSequence stmts []
      HsMultiIf _ rhss -> concatMap guarded rhss
      _ -> []
    guarded (L _ (GRHS _ guards body)) = inSequence guards [spanOf body]

    inSequence stmts after =
      concat
        [ case unLoc stmt of
            BindStmt _ pat _ ->
              [(name, f, fmap spanOf later <> after) | (name, f) <- patternBinders [pat]]
            LetStmt _ binds ->
              [(name, f, fmap spanOf (stmt : later) <> after) | (name, f) <- localBinders binds]
            _ -> []
        | stmt : later <- tails stmts
        ]

    patternBinders pats =
      [(opName n, defaultFixity) | n <- collectPatsBinders CollNoDictBinders pats]
    localBinders binds =
      [ (opName n, Map.findWithDefault defaultFixity (opName n) declared)
      | let declared = localFixities binds,
        n <- collectLocalBinders CollNoDictBinders binds
      ]
    localFixities = \case
      HsValBinds _ (ValBinds _ _ sigs) ->
        Map.fromList
          [ (opName (unLoc n), fromGhcFixity fixity)
          | L _ (FixSig _ (FixitySig _ names fixity)) <- sigs,
            n <- names
          ]
      _ -> Map.empty
    bindingSpans = \case
      HsValBinds _ (ValBinds _ bs _) -> fmap spanOf bs
      _ -> []

-- | The operators this module uses that the scope cannot settle, as the
-- module writes them.
--
-- Empty is the only acceptable answer: an operator whose fixity is not
-- known cannot be laid out, only guessed at.
unknownOperators :: Scope -> NonEmpty Gathered -> [((Maybe Text, OpName), Unknown)]
unknownOperators scope found =
  Map.toList (Map.fromList (mapMaybe unsettled (concatMap operatorsUsed found)))
  where
    unsettled (namespace, (qualifier, op)) =
      case fixityInScope scope namespace qualifier op of
        Just (answering, _) ->
          ((qualifier, op),) . Ambiguous
            <$> Map.lookup (qualifier, op) (reachAmbiguous (reachIn answering scope))
        Nothing -> case nonEmpty (unreadThatMightDeclare scope namespace qualifier op) of
          Just missing -> Just ((qualifier, op), NotRead missing)
          Nothing -> Nothing

-- | An operator as a use site writes it, qualifier and all.
operatorSpelling :: Maybe Text -> OpName -> Text
operatorSpelling qualifier (OpName op) = maybe "" (<> ".") qualifier <> op

-- | Spell out where an unsettled operator may have come from, and the fact
-- that this run could not read any of it.
spellUnreadIn ::
  -- | Whether there is anybody there to see color.
  Palette ->
  -- | The chains, as 'Unresolved' gives them.
  NonEmpty ModuleChain ->
  Text
spellUnreadIn palette missing =
  T.intercalate " or " (fmap (spellModuleChain palette) (toList missing))
    <> ", "
    <> ofThose
  where
    ofThose = case toList missing of
      [_] -> "which this run could not read"
      [_, _] -> "neither of which this run could read"
      _ -> "none of which this run could read"

-- | Spell out the fixities an ambiguous operator is brought in with, and the
-- modules that bring each.
spellDisagreement ::
  -- | Whether there is anybody there to see color.
  Palette ->
  -- | What each import brings, as 'Ambiguous' gives it.
  NonEmpty (Text, Fixity) ->
  Text
spellDisagreement palette offers =
  case fmap bringing (collected [(fixity, m) | (m, fixity) <- toList offers]) of
    [one, other] -> one <> " but " <> other
    each -> spellList each
  where
    bringing (fixity, ms) =
      spellFixity fixity <> " in " <> spellList (fmap (paint palette Place) ms)

----------------------------------------------------------------------------
-- Module summaries

-- | Summary of a module: raw facts (per CPP configuration) about a module
-- we read from source. This is built for both local modules and dependency
-- modules when they come from tarballs. This is an optimization
-- mechanism—we create a summary once and then share it during a run. For
-- local modules it is also persisted in the cache on disk.
data ModuleSummary = ModuleSummary
  { -- | The name it gives itself, if it gives one.
    summaryName :: Maybe Text,
    -- | Its export list, if it has one.
    summaryExports :: Maybe [ExportItem],
    -- | Its imports, an implicit Prelude among them.
    summaryImports :: [Import],
    -- | The fixities it declares.
    summaryFixities :: Fixities,
    -- | Every name it defines.
    summaryNames :: Set (Namespace, OpName),
    -- | The members of each type or class it declares.
    summaryDeclaredMembers :: Map OpName (Set (Namespace, OpName)),
    -- | The members its export list offers with each name.
    summaryListedMembers :: Map OpName (Set OpName)
  }
  deriving (Eq, Show, Generic)

instance NFData ModuleSummary

-- | Summarize a module.
summarize ::
  -- | Whether @ImplicitPrelude@ is on.
  Choice "implicitPrelude" ->
  -- | Parsed module.
  HsModule GhcPs ->
  -- | Module summary.
  ModuleSummary
summarize implicitPrelude hsModule =
  ModuleSummary
    { summaryName = moduleName hsModule,
      summaryExports = moduleExports hsModule,
      summaryImports = moduleImports implicitPrelude (pure hsModule),
      summaryFixities = declaredFixities hsModule,
      summaryNames = declaredNames hsModule,
      summaryDeclaredMembers = declaredMembers hsModule,
      summaryListedMembers = listedMembers hsModule
    }

----------------------------------------------------------------------------
-- What reading a module established

-- | What reading a module established about the names it exports.
data Established = Established
  { -- | The fixities declared for the names it exports, leaving out the ones
    -- 'establishedUnsettled' leaves unsettled.
    establishedFixities :: Fixities,
    -- | The names whose fixities could not be established, under the way
    -- down to the module where reading gave up, which is empty where that
    -- is this module.
    establishedUnsettled :: Map [Text] (Set (Namespace, OpName)),
    -- | The ways down through which it may export names that cannot be
    -- told, which leaves every name it does not certainly bring in
    -- unsettled.
    establishedUntold :: Set [Text],
    -- | What it certainly brings in for a module that imports it whole.
    establishedCertain :: Certain,
    -- | The members of each of its names, so that a @T(..)@ in an import
    -- list can be told what it brings in. Every type 'establishedCertain'
    -- gives members of is among them.
    establishedMembers :: Map OpName (Set OpName)
  }
  deriving (Eq, Show, Generic)

instance NFData Established

instance Semigroup Established where
  a <> b =
    Established
      { establishedFixities = Map.union (establishedFixities a) (establishedFixities b),
        establishedUnsettled =
          Map.unionWith Set.union (establishedUnsettled a) (establishedUnsettled b),
        establishedUntold = Set.union (establishedUntold a) (establishedUntold b),
        establishedCertain = establishedCertain a <> establishedCertain b,
        establishedMembers =
          Map.unionWith Set.union (establishedMembers a) (establishedMembers b)
      }

instance Monoid Established where
  mempty = Established Map.empty Map.empty Set.empty mempty Map.empty

-- | What is established about a module that could not be read: nothing.
unreadable :: Established
unreadable = mempty{establishedUntold = Set.singleton []}

-- | Does reading a module settle the fixity of every name it exports?
settlesEverything :: Established -> Bool
settlesEverything established =
  Map.null (establishedUnsettled established) && Set.null (establishedUntold established)

-- | The ways down to a module that could not be read that leave the fixity
-- of a name unsettled.
unsettledThrough :: Established -> (Namespace, OpName) -> [[Text]]
unsettledThrough established name =
  [chain | (chain, names) <- Map.toList (establishedUnsettled established), Set.member name names]
    <> [ chain
       | Set.notMember name (certainNames (establishedCertain established)),
         chain <- Set.toList (establishedUntold established)
       ]