tilia-0.1.0.0: src/Tilia/Fixity/Debug.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
-- | An account of how a module's fixities were determined.
module Tilia.Fixity.Debug
( FixityNotes (..),
ImportNote (..),
OperatorNote (..),
fixityNotes,
renderFixityNotes,
)
where
import Data.Choice (Choice)
import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Tilia.Fixity
( Certain (..),
Established (..),
Fixity (..),
Import (..),
OpName (..),
Provenance (..),
Resolution (..),
Scope (..),
lookupFixity,
moduleImports,
operatorSpelling,
operatorsUsed,
reachAmbiguous,
reachIn,
reachUnqualified,
spellDisagreement,
spellFixity,
spellUnreadIn,
)
import Tilia.Palette (Color (Operator, Place), Palette, paint)
import Tilia.Parser (ParsedModule (..))
import Tilia.Utils (indent, lineWidth, wrapTo)
-- | Everything that decided one module's fixities.
data FixityNotes = FixityNotes
{ -- | What each import brought, in the order the module writes them.
notedImports :: [ImportNote],
-- | Resolutions per operator.
notedOperators :: [OperatorNote],
-- | What operators the module declares.
notedDeclarations :: [(Text, Fixity)]
}
deriving (Eq, Show)
-- | One import in the module.
data ImportNote = ImportNote
{ -- | The module imported.
noteModule :: Text,
-- | The name it goes under here, when that differs from its own.
noteAlias :: Maybe Text,
-- | Whether it was imported qualified.
noteQualified :: Bool,
-- | How many operators it was read for, or 'Nothing' when it could not
-- be read at all.
noteOperatorCount :: Maybe Int,
-- | Where reading it went before giving up, for each way that left some
-- names unsettled, ending at the module that actually stopped it. Empty
-- for an import that settles every name; a way is empty for one unread
-- on its own account.
noteUnsettled :: [[Text]]
}
deriving (Eq, Show)
-- | One operator the module uses.
data OperatorNote = OperatorNote
{ -- | The operator as the module writes it, qualifier and all.
noteSpelling :: Text,
-- | What the scope answered for it.
noteResolution :: Resolution,
-- | What each import in scope brings, where they disagree about it.
noteDisagreement :: Maybe (NonEmpty (Text, Fixity))
}
deriving (Eq, Show)
-- | Record everything that decided one module's fixities.
fixityNotes ::
-- | Whether @ImplicitPrelude@ is on, so that the Prelude is listed among
-- the imports exactly when the module actually has it.
Choice "implicitPrelude" ->
-- | What reading each module in scope established, as the resolver
-- answers it.
(Text -> IO Established) ->
-- | The scope the module was formatted under.
Scope ->
-- | The module's configurations.
NonEmpty ParsedModule ->
IO FixityNotes
fixityNotes implicitPrelude resolve scope configurations = do
importNotes <- traverse alongside (moduleImports implicitPrelude (fmap pmModule configurations))
pure
FixityNotes
{ notedImports = importNotes,
notedOperators = fmap aboutOperator used,
notedDeclarations = here
}
where
alongside i = do
answer <- resolve (importModule i)
let fixities = establishedFixities answer
readNothing =
Map.null fixities
&& Set.null (certainNames (establishedCertain answer))
&& not (Set.null (establishedUntold answer))
pure
ImportNote
{ noteModule = importModule i,
noteAlias =
if importAlias i == importModule i
then Nothing
else Just (importAlias i),
noteQualified = importQualified i,
noteOperatorCount =
if readNothing
then Nothing
else Just (Set.size (Set.map snd (Map.keysSet fixities))),
noteUnsettled =
Set.toList (Map.keysSet (establishedUnsettled answer) <> establishedUntold answer)
}
used =
Map.elems
( Map.fromList
[ ((namespace, uncurry operatorSpelling u), (namespace, u))
| (namespace, u) <- concatMap (operatorsUsed . pmGathered) configurations
]
)
here =
[ (op, fixity)
| (OpName op, (fixity, DeclaredHere)) <- Map.toList declaredHere
]
declaredHere =
Map.union
(reachUnqualified (scopeInTerms scope))
(reachUnqualified (scopeInTypes scope))
aboutOperator (namespace, (qualifier, op)) =
OperatorNote
{ noteSpelling = operatorSpelling qualifier op,
noteResolution = lookupFixity scope namespace qualifier op,
noteDisagreement =
Map.lookup (qualifier, op) (reachAmbiguous (reachIn namespace scope))
}
-- | Set out all the 'FixityNotes' per file.
renderFixityNotes :: Palette -> Map FilePath FixityNotes -> [Text]
renderFixityNotes palette notes =
concat
[ (indent 1 <> "fixities for " <> paint palette Place (T.pack path))
: aboutFile palette told
| (path, told) <- Map.toList notes
]
-- | One file's account, in reading order.
aboutFile :: Palette -> FixityNotes -> [Text]
aboutFile palette notes =
concat
[ section "imports" fromImport (notedImports notes),
section "operators" fromOperator (notedOperators notes),
section "declared here" fromOwn (notedDeclarations notes)
]
where
section what render items
| null items = []
| otherwise = heading what : concatMap (entry . render) items
heading what = indent 2 <> "· " <> what
entry line = case wrapTo (lineWidth - 8) line of
[] -> []
(opening : rest) -> (indent 3 <> "· " <> opening) : fmap (indent 4 <>) rest
fromImport i =
named (noteModule i)
<> qualification i
<> ": "
<> case noteOperatorCount i of
Nothing -> "could not be read" <> through (noteUnsettled i)
Just n
| null (noteUnsettled i) -> operators n
| otherwise -> operators n <> ", not settling every name" <> through (noteUnsettled i)
through ways = case filter (not . null) ways of
[] -> ""
below -> ", through " <> T.intercalate " or " (fmap (T.intercalate " → " . fmap named) below)
qualification i = case (noteQualified i, noteAlias i) of
(True, Just alias) -> " qualified as " <> named alias
(True, Nothing) -> " qualified"
(False, Just alias) -> " as " <> named alias
(False, Nothing) -> ""
fromOwn (op, fixity) = operator op <> " " <> spellFixity fixity
fromOperator o =
operator (noteSpelling o)
<> " "
<> case noteResolution o of
Resolved fixity provenance ->
spellFixity fixity <> ", " <> from provenance <> ambiguously o
Unresolved missing ->
"unknown: may be declared in " <> spellUnreadIn palette missing
from = \case
DeclaredHere -> "declared in this module"
DeclaredIn m -> "declared in " <> named m
BuiltIn -> "built into the language"
ReportDefault -> "the Report's default, nothing in scope declaring it"
ambiguously o = case noteDisagreement o of
Nothing -> ""
Just offers ->
", and the imports disagree about it: " <> spellDisagreement palette offers
named = paint palette Place
operator = paint palette Operator
operators = \case
1 -> "1 operator"
n -> T.pack (show n) <> " operators"