tilia 0.1.1.0 → 0.1.2.0
raw patch · 152 files changed
+4573/−3866 lines, 152 filesdep ~basePVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependency ranges changed: base
API changes (from Hackage documentation)
- Tilia.Cabal.Target: Unparseable :: FilePath -> [Text] -> TargetProblem
- Tilia.Cpp: answeredLeaves :: Text -> Either CppError [(Assignment, Text)]
- Tilia.Cpp: answeredLinearLeaves :: Text -> Either CppError [(Assignment, Text)]
- Tilia.Cpp: configurations :: Text -> Maybe Configurations
- Tilia.Cpp: countLeaves :: Text -> Either CppError Integer
- Tilia.Cpp: leaves :: Text -> Either CppError [Text]
- Tilia.Cpp: linearLeaves :: Text -> Either CppError [Text]
- Tilia.Cpp: regions :: Doc -> Map Span Doc
- Tilia.Cpp.Directives: answeredLeaves :: Text -> Either CppError [(Assignment, Text)]
- Tilia.Cpp.Directives: answeredLinearLeaves :: Text -> Either CppError [(Assignment, Text)]
- Tilia.Cpp.Directives: configurations :: Text -> Maybe Configurations
- Tilia.Cpp.Directives: countLeaves :: Text -> Either CppError Integer
- Tilia.Cpp.Directives: leaves :: Text -> Either CppError [Text]
- Tilia.Cpp.Directives: linearLeaves :: Text -> Either CppError [Text]
- Tilia.Cpp.Merge: instance GHC.Classes.Eq Tilia.Cpp.Merge.Place
- Tilia.Doc: RenderOptions :: Int -> RenderOptions
- Tilia.Doc: [roIndentStep] :: RenderOptions -> Int
- Tilia.Doc: defaultRenderOptions :: RenderOptions
- Tilia.Doc: newtype RenderOptions
- Tilia.Doc.Internal: RenderOptions :: Int -> RenderOptions
- Tilia.Doc.Internal: [roIndentStep] :: RenderOptions -> Int
- Tilia.Doc.Internal: defaultRenderOptions :: RenderOptions
- Tilia.Doc.Internal: instance GHC.Classes.Eq Tilia.Doc.Internal.RenderOptions
- Tilia.Doc.Internal: instance GHC.Internal.Show.Show Tilia.Doc.Internal.RenderOptions
- Tilia.Doc.Internal: newtype RenderOptions
- Tilia.Fixity.ByHand: hscFixities :: Map Text (Map OpName Fixity)
- Tilia.Fixity.Plan: BuildPlan :: Text -> [PlanPackage] -> BuildPlan
- Tilia.Fixity.Plan: CheckedOut :: FilePath -> PackageSource
- Tilia.Fixity.Plan: Futility :: IO Bool -> IO () -> IO [Text] -> ([Text] -> IO ()) -> (UTCTime -> IO ()) -> Futility
- Tilia.Fixity.Plan: LocalPackage :: FilePath -> PackageSource
- Tilia.Fixity.Plan: PlanComponent :: Text -> Text -> PlanComponent
- Tilia.Fixity.Plan: PlanMissing :: Readiness
- Tilia.Fixity.Plan: PlanNarrow :: [Text] -> Readiness
- Tilia.Fixity.Plan: PlanPackage :: Text -> Text -> PackageSource -> [Text] -> PlanPackage
- Tilia.Fixity.Plan: PlanStale :: [FilePath] -> Readiness
- Tilia.Fixity.Plan: PreExisting :: PackageSource
- Tilia.Fixity.Plan: Ready :: Readiness
- Tilia.Fixity.Plan: RepoPackage :: Maybe Text -> RepoProvenance -> PackageSource
- Tilia.Fixity.Plan: SourceRepo :: PackageSource
- Tilia.Fixity.Plan: SourcesMissing :: [Text] -> Readiness
- Tilia.Fixity.Plan: [bpCompiler] :: BuildPlan -> Text
- Tilia.Fixity.Plan: [bpPackages] :: BuildPlan -> [PlanPackage]
- Tilia.Fixity.Plan: [fetchWasFutileFor] :: Futility -> IO [Text]
- Tilia.Fixity.Plan: [pcName] :: PlanComponent -> Text
- Tilia.Fixity.Plan: [pcPackage] :: PlanComponent -> Text
- Tilia.Fixity.Plan: [ppComponents] :: PlanPackage -> [Text]
- Tilia.Fixity.Plan: [ppName] :: PlanPackage -> Text
- Tilia.Fixity.Plan: [ppSource] :: PlanPackage -> PackageSource
- Tilia.Fixity.Plan: [ppVersion] :: PlanPackage -> Text
- Tilia.Fixity.Plan: [rememberFutileFetch] :: Futility -> [Text] -> IO ()
- Tilia.Fixity.Plan: [rememberFutileSolve] :: Futility -> IO ()
- Tilia.Fixity.Plan: [rememberSolveTime] :: Futility -> UTCTime -> IO ()
- Tilia.Fixity.Plan: [solveWasFutile] :: Futility -> IO Bool
- Tilia.Fixity.Plan: checkReadiness :: Choice "useCache" -> [PlanComponent] -> FilePath -> IO Readiness
- Tilia.Fixity.Plan: data BuildPlan
- Tilia.Fixity.Plan: data Futility
- Tilia.Fixity.Plan: data PackageSource
- Tilia.Fixity.Plan: data PlanComponent
- Tilia.Fixity.Plan: data PlanPackage
- Tilia.Fixity.Plan: data Readiness
- Tilia.Fixity.Plan: fetchUninstalled :: Choice "useCache" -> FilePath -> BuildPlan -> IO ()
- Tilia.Fixity.Plan: futilityFor :: Choice "useCache" -> FilePath -> Futility
- Tilia.Fixity.Plan: guessedPackageCacheRoot :: IO FilePath
- Tilia.Fixity.Plan: instance Control.DeepSeq.NFData Tilia.Fixity.Plan.Archive
- Tilia.Fixity.Plan: instance Data.Aeson.Types.FromJSON.FromJSON Tilia.Fixity.Plan.BuildPlan
- Tilia.Fixity.Plan: instance Data.Aeson.Types.FromJSON.FromJSON Tilia.Fixity.Plan.PlanPackage
- Tilia.Fixity.Plan: instance GHC.Classes.Eq Tilia.Fixity.Plan.BuildPlan
- Tilia.Fixity.Plan: instance GHC.Classes.Eq Tilia.Fixity.Plan.PackageSource
- Tilia.Fixity.Plan: instance GHC.Classes.Eq Tilia.Fixity.Plan.PlanComponent
- Tilia.Fixity.Plan: instance GHC.Classes.Eq Tilia.Fixity.Plan.PlanPackage
- Tilia.Fixity.Plan: instance GHC.Classes.Eq Tilia.Fixity.Plan.Readiness
- Tilia.Fixity.Plan: instance GHC.Classes.Eq Tilia.Fixity.Plan.RepoProvenance
- Tilia.Fixity.Plan: instance GHC.Classes.Ord Tilia.Fixity.Plan.PlanComponent
- Tilia.Fixity.Plan: instance GHC.Internal.Generics.Generic Tilia.Fixity.Plan.Archive
- Tilia.Fixity.Plan: instance GHC.Internal.Show.Show Tilia.Fixity.Plan.BuildPlan
- Tilia.Fixity.Plan: instance GHC.Internal.Show.Show Tilia.Fixity.Plan.PackageSource
- Tilia.Fixity.Plan: instance GHC.Internal.Show.Show Tilia.Fixity.Plan.PlanComponent
- Tilia.Fixity.Plan: instance GHC.Internal.Show.Show Tilia.Fixity.Plan.PlanPackage
- Tilia.Fixity.Plan: instance GHC.Internal.Show.Show Tilia.Fixity.Plan.Readiness
- Tilia.Fixity.Plan: instance GHC.Internal.Show.Show Tilia.Fixity.Plan.RepoProvenance
- Tilia.Fixity.Plan: isFetchable :: PlanPackage -> Bool
- Tilia.Fixity.Plan: loadPlan :: Choice "useCache" -> Choice "download" -> [PlanComponent] -> FilePath -> IO (Either Text BuildPlan)
- Tilia.Fixity.Plan: macrosOf :: BuildPlan -> Macros
- Tilia.Fixity.Plan: packageCacheRoot :: IO FilePath
- Tilia.Fixity.Plan: planPathFor :: FilePath -> FilePath
- Tilia.Fixity.Plan: plannedCompiler :: BuildPlan -> Maybe Version
- Tilia.Fixity.Plan: plannedComponents :: BuildPlan -> [PlanComponent]
- Tilia.Fixity.Plan: plannedTarballs :: Cache -> BuildPlan -> IO [(PlanPackage, FilePath)]
- Tilia.Fixity.Plan: prepareWith :: Choice "useCache" -> Choice "download" -> ([String] -> IO (Either Text ())) -> Futility -> [PlanComponent] -> FilePath -> Readiness -> IO (Either Text ())
- Tilia.Fixity.Plan: readBuildPlan :: FilePath -> IO (Either Text BuildPlan)
- Tilia.Fixity.Plan: readGivenPlan :: FilePath -> FilePath -> IO (Either Text BuildPlan)
- Tilia.Fixity.Plan: sourceHashOf :: PlanPackage -> Maybe Text
- Tilia.Fixity.Plan: spellComponent :: PlanComponent -> Text
- Tilia.Fixity.Plan: tokenForBuildPlan :: BuildPlan -> IO PlanToken
- Tilia.Fixity.Plan: tokenForEnvAndBuildPlan :: Text -> BuildPlan -> PlanToken
- Tilia.Fixity.Plan: undiscoveredFutility :: Futility
- Tilia.Palette: Unchanged :: Color
- Tilia.Parser: defaultParserConfig :: ParserConfig
- Tilia.Parser: parserConfigFor :: [Extension] -> ParserConfig
- Tilia.Render.Body: CmdBody :: Ctx -> Site -> LHsCmd GhcPs -> CmdBody
- Tilia.Render.Body: CmdTopBody :: Ctx -> Site -> LHsCmdTop GhcPs -> CmdTopBody
- Tilia.Render.Body: ExprBody :: Ctx -> Site -> LHsExpr GhcPs -> ExprBody
- Tilia.Render.Body: cmdTopHangs :: HsCmdTop GhcPs -> Placement
- Tilia.Render.Body: data CmdBody
- Tilia.Render.Body: data CmdTopBody
- Tilia.Render.Body: data ExprBody
- Tilia.Render.Body: exprHangs :: HsExpr GhcPs -> Placement
- Tilia.Render.Body: instance Tilia.Doc.Body.Body Tilia.Render.Body.CmdBody
- Tilia.Render.Body: instance Tilia.Doc.Body.Body Tilia.Render.Body.CmdTopBody
- Tilia.Render.Body: instance Tilia.Doc.Body.Body Tilia.Render.Body.ExprBody
- Tilia.Render.Body: operatorName :: LHsExpr GhcPs -> Maybe RdrName
- Tilia.Render.Context: [knotCmd] :: Knot -> Ctx -> Site -> LHsCmd GhcPs -> Doc
- Tilia.Render.Expression: hsCmd :: Ctx -> Site -> LHsCmd GhcPs -> Doc
- Tilia.Render.Header: [hpOrder] :: HeaderPragma -> PragmaOrder
- Tilia.Render.Layout: data Place
- Tilia.Render.Layout: instance GHC.Classes.Eq Tilia.Render.Layout.Place
- Tilia.Render.Layout: instance GHC.Internal.Show.Show Tilia.Render.Layout.Place
- Tilia.Render.Layout: places :: [a] -> [(Place, a)]
- Tilia.Run: declined :: Outcome -> Bool
- Tilia.Run: failed :: Outcome -> Bool
- Tilia.Run: inParallel :: (a -> IO b) -> [a] -> IO [b]
- Tilia.Span.Ghc: annSpan :: HasLoc l => l -> Maybe Span
- Tilia.Span.Ghc: tokenSpan :: forall (sym :: Symbol). EpToken sym -> Maybe Span
- Tilia.Span.Ghc: uniTokenSpan :: forall (tok :: Symbol) (utok :: Symbol). EpUniToken tok utok -> Maybe Span
+ Tilia.Cabal.Package: declaredModules :: GenericPackageDescription -> [(Text, FilePath)]
+ Tilia.Cabal.Package: isDeclaredModule :: Set FilePath -> Set FilePath -> FilePath -> Bool
+ Tilia.Cabal.Package: readPackageDescription :: FilePath -> IO (Either PackageProblem GenericPackageDescription)
+ Tilia.Cabal.Plan: BuildPlan :: Text -> [PlanPackage] -> BuildPlan
+ Tilia.Cabal.Plan: CheckedOut :: FilePath -> PackageSource
+ Tilia.Cabal.Plan: Futility :: IO Bool -> IO () -> IO [Text] -> ([Text] -> IO ()) -> (UTCTime -> IO ()) -> Futility
+ Tilia.Cabal.Plan: LocalPackage :: FilePath -> PackageSource
+ Tilia.Cabal.Plan: PlanComponent :: Text -> Text -> PlanComponent
+ Tilia.Cabal.Plan: PlanMissing :: Readiness
+ Tilia.Cabal.Plan: PlanNarrow :: [Text] -> Readiness
+ Tilia.Cabal.Plan: PlanPackage :: Text -> Text -> PackageSource -> [Text] -> PlanPackage
+ Tilia.Cabal.Plan: PlanStale :: [FilePath] -> Readiness
+ Tilia.Cabal.Plan: PreExisting :: PackageSource
+ Tilia.Cabal.Plan: Ready :: Readiness
+ Tilia.Cabal.Plan: RepoPackage :: Maybe Text -> RepoProvenance -> PackageSource
+ Tilia.Cabal.Plan: SourceRepo :: PackageSource
+ Tilia.Cabal.Plan: SourcesMissing :: [Text] -> Readiness
+ Tilia.Cabal.Plan: [bpCompiler] :: BuildPlan -> Text
+ Tilia.Cabal.Plan: [bpPackages] :: BuildPlan -> [PlanPackage]
+ Tilia.Cabal.Plan: [fetchWasFutileFor] :: Futility -> IO [Text]
+ Tilia.Cabal.Plan: [pcName] :: PlanComponent -> Text
+ Tilia.Cabal.Plan: [pcPackage] :: PlanComponent -> Text
+ Tilia.Cabal.Plan: [ppComponents] :: PlanPackage -> [Text]
+ Tilia.Cabal.Plan: [ppName] :: PlanPackage -> Text
+ Tilia.Cabal.Plan: [ppSource] :: PlanPackage -> PackageSource
+ Tilia.Cabal.Plan: [ppVersion] :: PlanPackage -> Text
+ Tilia.Cabal.Plan: [rememberFutileFetch] :: Futility -> [Text] -> IO ()
+ Tilia.Cabal.Plan: [rememberFutileSolve] :: Futility -> IO ()
+ Tilia.Cabal.Plan: [rememberSolveTime] :: Futility -> UTCTime -> IO ()
+ Tilia.Cabal.Plan: [solveWasFutile] :: Futility -> IO Bool
+ Tilia.Cabal.Plan: cacheKey :: PlanPackage -> Text
+ Tilia.Cabal.Plan: checkReadiness :: Choice "useCache" -> [PlanComponent] -> FilePath -> IO Readiness
+ Tilia.Cabal.Plan: data BuildPlan
+ Tilia.Cabal.Plan: data Futility
+ Tilia.Cabal.Plan: data PackageSource
+ Tilia.Cabal.Plan: data PlanComponent
+ Tilia.Cabal.Plan: data PlanPackage
+ Tilia.Cabal.Plan: data Readiness
+ Tilia.Cabal.Plan: fetchUninstalled :: Choice "useCache" -> FilePath -> BuildPlan -> IO ()
+ Tilia.Cabal.Plan: futilityFor :: Choice "useCache" -> FilePath -> Futility
+ Tilia.Cabal.Plan: generatedModules :: BuildPlan -> Set Text
+ Tilia.Cabal.Plan: getInstalledPackages :: Cache -> IO [InstalledPackage]
+ Tilia.Cabal.Plan: guessedPackageCacheRoot :: IO FilePath
+ Tilia.Cabal.Plan: instance Data.Aeson.Types.FromJSON.FromJSON Tilia.Cabal.Plan.BuildPlan
+ Tilia.Cabal.Plan: instance Data.Aeson.Types.FromJSON.FromJSON Tilia.Cabal.Plan.PlanPackage
+ Tilia.Cabal.Plan: instance GHC.Classes.Eq Tilia.Cabal.Plan.BuildPlan
+ Tilia.Cabal.Plan: instance GHC.Classes.Eq Tilia.Cabal.Plan.PackageSource
+ Tilia.Cabal.Plan: instance GHC.Classes.Eq Tilia.Cabal.Plan.PlanComponent
+ Tilia.Cabal.Plan: instance GHC.Classes.Eq Tilia.Cabal.Plan.PlanPackage
+ Tilia.Cabal.Plan: instance GHC.Classes.Eq Tilia.Cabal.Plan.Readiness
+ Tilia.Cabal.Plan: instance GHC.Classes.Eq Tilia.Cabal.Plan.RepoProvenance
+ Tilia.Cabal.Plan: instance GHC.Classes.Ord Tilia.Cabal.Plan.PlanComponent
+ Tilia.Cabal.Plan: instance GHC.Internal.Show.Show Tilia.Cabal.Plan.BuildPlan
+ Tilia.Cabal.Plan: instance GHC.Internal.Show.Show Tilia.Cabal.Plan.PackageSource
+ Tilia.Cabal.Plan: instance GHC.Internal.Show.Show Tilia.Cabal.Plan.PlanComponent
+ Tilia.Cabal.Plan: instance GHC.Internal.Show.Show Tilia.Cabal.Plan.PlanPackage
+ Tilia.Cabal.Plan: instance GHC.Internal.Show.Show Tilia.Cabal.Plan.Readiness
+ Tilia.Cabal.Plan: instance GHC.Internal.Show.Show Tilia.Cabal.Plan.RepoProvenance
+ Tilia.Cabal.Plan: isFetchable :: PlanPackage -> Bool
+ Tilia.Cabal.Plan: loadPlan :: Choice "useCache" -> Choice "download" -> [PlanComponent] -> FilePath -> IO (Either Text BuildPlan)
+ Tilia.Cabal.Plan: macrosOf :: BuildPlan -> Macros
+ Tilia.Cabal.Plan: packageCacheRoot :: IO FilePath
+ Tilia.Cabal.Plan: planPathFor :: FilePath -> FilePath
+ Tilia.Cabal.Plan: plannedCompiler :: BuildPlan -> Maybe Version
+ Tilia.Cabal.Plan: plannedComponents :: BuildPlan -> [PlanComponent]
+ Tilia.Cabal.Plan: plannedTarballs :: Cache -> BuildPlan -> IO [(PlanPackage, FilePath)]
+ Tilia.Cabal.Plan: prepareWith :: Choice "useCache" -> Choice "download" -> ([String] -> IO (Either Text ())) -> Futility -> [PlanComponent] -> FilePath -> Readiness -> IO (Either Text ())
+ Tilia.Cabal.Plan: readBuildPlan :: FilePath -> IO (Either Text BuildPlan)
+ Tilia.Cabal.Plan: readGivenPlan :: FilePath -> FilePath -> IO (Either Text BuildPlan)
+ Tilia.Cabal.Plan: sourceHashOf :: PlanPackage -> Maybe Text
+ Tilia.Cabal.Plan: spellPlanComponent :: PlanComponent -> Text
+ Tilia.Cabal.Plan: tokenForBuildPlan :: BuildPlan -> IO PlanToken
+ Tilia.Cabal.Plan: tokenForEnvAndBuildPlan :: Text -> BuildPlan -> PlanToken
+ Tilia.Cabal.Plan: undiscoveredFutility :: Futility
+ Tilia.Cabal.Plan: versionMacros :: Maybe [Integer] -> Map Text [Integer] -> Macros
+ Tilia.Cabal.Project: cabalFilesIn :: FilePath -> IO [FilePath]
+ Tilia.Cabal.Target: UnreadablePackage :: PackageProblem -> TargetProblem
+ Tilia.Comments: afterOpener :: Text -> Maybe Text
+ Tilia.Comments: carriesOn :: Int -> Span -> Maybe Span -> Bool
+ Tilia.Comments: triggerOn :: Text -> Maybe Text
+ Tilia.Cpp.Directives: type Assignment = Map [Guard] Int
+ Tilia.Cpp.Merge: instance GHC.Classes.Eq Tilia.Cpp.Merge.PartStart
+ Tilia.Cpp.Place: foldRegions :: Monoid m => (Span -> Doc -> m -> m) -> Doc -> m
+ Tilia.Cpp.Place: regionsIn :: Doc -> [(Span, Doc)]
+ Tilia.Editor: Compiled :: [Extension] -> Version -> Map Text Version -> (Text -> IO (Maybe HiFile)) -> Compiled
+ Tilia.Editor: [compiledExtensions] :: Compiled -> [Extension]
+ Tilia.Editor: [compiledInterface] :: Compiled -> Text -> IO (Maybe HiFile)
+ Tilia.Editor: [compiledPackages] :: Compiled -> Map Text Version
+ Tilia.Editor: [compiledWith] :: Compiled -> Version
+ Tilia.Editor: compiledSession :: FilePath -> Compiled -> Choice "checkAst" -> Choice "checkIdempotence" -> IO (Either FormatError Session)
+ Tilia.Editor: data Compiled
+ Tilia.Fixity: readPrecedence :: Text -> Maybe Int
+ Tilia.Fixity.Archive: Archive :: Maybe Text -> [(FilePath, ByteString)] -> Archive
+ Tilia.Fixity.Archive: ForHsc :: InArchive
+ Tilia.Fixity.Archive: Haskell :: Text -> InArchive
+ Tilia.Fixity.Archive: [archiveCabal] :: Archive -> Maybe Text
+ Tilia.Fixity.Archive: [archiveFiles] :: Archive -> [(FilePath, ByteString)]
+ Tilia.Fixity.Archive: data Archive
+ Tilia.Fixity.Archive: data InArchive
+ Tilia.Fixity.Archive: instance Control.DeepSeq.NFData Tilia.Fixity.Archive.Archive
+ Tilia.Fixity.Archive: instance GHC.Internal.Generics.Generic Tilia.Fixity.Archive.Archive
+ Tilia.Fixity.Archive: moduleEndings :: [String]
+ Tilia.Fixity.Archive: moduleIn :: Text -> Archive -> Maybe InArchive
+ Tilia.Fixity.Archive: pathOfModule :: Text -> FilePath
+ Tilia.Fixity.Archive: readArchive :: FilePath -> IO (Maybe Archive)
+ Tilia.Fixity.Archive: writtenForHsc :: FilePath -> Bool
+ Tilia.Fixity.Plan: newResolverFrom :: (Text -> IO (Maybe HiFile)) -> IO Resolver
+ Tilia.Format: Compiled :: [Extension] -> Version -> Map Text Version -> (Text -> IO (Maybe HiFile)) -> Compiled
+ Tilia.Format: [compiledExtensions] :: Compiled -> [Extension]
+ Tilia.Format: [compiledInterface] :: Compiled -> Text -> IO (Maybe HiFile)
+ Tilia.Format: [compiledPackages] :: Compiled -> Map Text Version
+ Tilia.Format: [compiledWith] :: Compiled -> Version
+ Tilia.Format: compiledSession :: FilePath -> Compiled -> Choice "checkAst" -> Choice "checkIdempotence" -> IO (Either FormatError Session)
+ Tilia.Format: data Compiled
+ Tilia.Memo: data Flights
+ Tilia.Memo: data Memo v
+ Tilia.Memo: memoized :: MVar Flights -> Memo v -> v -> Text -> IO v -> IO v
+ Tilia.Memo: newFlights :: IO (MVar Flights)
+ Tilia.Memo: newMemo :: IO (Memo v)
+ Tilia.Palette: Kept :: Color
+ Tilia.Pragma: applyExtensionName :: [Extension] -> Text -> [Extension]
+ Tilia.Pragma: languageNames :: Text -> [Text]
+ Tilia.Pragma: lookupEdition :: Text -> Maybe Language
+ Tilia.Render.Context: negativeLiteralGap :: Ctx -> Doc
+ Tilia.Render.Expression: instance Tilia.Doc.Body.Body Tilia.Render.Expression.CmdBody
+ Tilia.Render.Expression: instance Tilia.Doc.Body.Body Tilia.Render.Expression.CmdTopBody
+ Tilia.Render.Expression: instance Tilia.Doc.Body.Body Tilia.Render.Expression.ExprBody
+ Tilia.Render.Layout: data Position
+ Tilia.Render.Layout: instance GHC.Classes.Eq Tilia.Render.Layout.Position
+ Tilia.Render.Layout: instance GHC.Internal.Show.Show Tilia.Render.Layout.Position
+ Tilia.Render.Layout: positioned :: [a] -> [(Position, a)]
+ Tilia.Render.Operator: links :: NonEmpty (OpChain a op) -> [op] -> [(OpChain a op, op, OpChain a op, Bool)]
+ Tilia.Render.Pattern: boxityBrackets :: Boxity -> ClosingIndent -> Doc -> Doc
+ Tilia.Render.Type: leftContext :: Ctx -> LHsContext GhcPs -> Doc
+ Tilia.Source.Lines: conditionalKeywords :: [Text]
+ Tilia.Source.Lines: continuesGroup :: [Text]
+ Tilia.Source.Lines: opaqueKeywords :: [Text]
+ Tilia.Source.Lines: opensGroup :: [Text]
+ Tilia.Span: zeroWidth :: Span -> Bool
+ Tilia.Utils: byteOrderMark :: Text -> (Text, Text)
+ Tilia.Utils: fieldWords :: Text -> [Text]
+ Tilia.Utils: readFileText :: FilePath -> IO (Maybe Text)
+ Tilia.Utils: sha256Of :: Text -> Text
- Tilia.Comments: commentsOf :: Lines -> [LEpaComment] -> [Comment]
+ Tilia.Comments: commentsOf :: Lines -> [(RealSrcSpan, Token)] -> [Comment]
- Tilia.Doc: printDoc :: RenderOptions -> Doc -> Text
+ Tilia.Doc: printDoc :: Doc -> Text
- Tilia.Doc.Internal: render :: RenderOptions -> Doc -> Text
+ Tilia.Doc.Internal: render :: Doc -> Text
- Tilia.Fixity.Plan: newResolverVia :: Choice "useCache" -> [Route] -> BuildPlan -> IO Resolver
+ Tilia.Fixity.Plan: newResolverVia :: Choice "useCache" -> [Route] -> PackageReader -> BuildPlan -> IO Resolver
- Tilia.Fixity.Plan: newResolverWith :: [Route] -> Cache -> [InstalledPackage] -> [(PlanPackage, FilePath)] -> BuildPlan -> IO Resolver
+ Tilia.Fixity.Plan: newResolverWith :: [Route] -> Cache -> [InstalledPackage] -> [(PlanPackage, FilePath)] -> PackageReader -> BuildPlan -> IO Resolver
- Tilia.Format: newSession :: FilePath -> PlanSource -> Choice "useCache" -> Choice "download" -> Choice "checkAst" -> Choice "checkIdempotence" -> Choice "debugFixity" -> IO (Either FormatError Session)
+ Tilia.Format: newSession :: ProjectRoot -> PlanSource -> Choice "useCache" -> Choice "download" -> Choice "checkAst" -> Choice "checkIdempotence" -> Choice "debugFixity" -> IO (Either FormatError Session)
- Tilia.Render.Context: Ctx :: Set Extension -> SourceType -> Maybe Scope -> Map Span Fixity -> Map (Int, Int) Comment -> Set (Int, Int) -> Source -> Map (Int, Int) Comment -> Knot -> Ctx
+ Tilia.Render.Context: Ctx :: Set Extension -> SourceType -> Maybe Scope -> Map Span Fixity -> Map (Int, Int) Comment -> Map (Int, Int) Int -> Source -> Map (Int, Int) Comment -> Knot -> Ctx
- Tilia.Render.Context: Knot :: (Ctx -> Site -> LHsExpr GhcPs -> Doc) -> (Ctx -> Site -> LHsCmd GhcPs -> Doc) -> (Ctx -> SpliceDecoration -> HsUntypedSplice GhcPs -> Doc) -> (Ctx -> Sig GhcPs -> Doc) -> (Ctx -> FamilyStyle -> [LHsDecl GhcPs] -> Doc) -> (Ctx -> FamilyStyle -> [LHsDecl GhcPs] -> Doc) -> Knot
+ Tilia.Render.Context: Knot :: (Ctx -> Site -> LHsExpr GhcPs -> Doc) -> (Ctx -> SpliceDecoration -> HsUntypedSplice GhcPs -> Doc) -> (Ctx -> Sig GhcPs -> Doc) -> (Ctx -> FamilyStyle -> [LHsDecl GhcPs] -> Doc) -> (Ctx -> FamilyStyle -> [LHsDecl GhcPs] -> Doc) -> Knot
- Tilia.Render.Context: [ctxCarryingOn] :: Ctx -> Set (Int, Int)
+ Tilia.Render.Context: [ctxCarryingOn] :: Ctx -> Map (Int, Int) Int
- Tilia.Render.Data: dataDecl :: Ctx -> FamilyStyle -> LocatedN RdrName -> [tyVar] -> (tyVar -> Maybe Span) -> (tyVar -> Doc) -> LexicalFixity -> Doc -> HsDataDefn GhcPs -> Doc
+ Tilia.Render.Data: dataDecl :: Ctx -> Choice "instanceWord" -> LocatedN RdrName -> [tyVar] -> (tyVar -> Maybe Span) -> (tyVar -> Doc) -> LexicalFixity -> Doc -> HsDataDefn GhcPs -> Doc
- Tilia.Render.Header: HeaderPragma :: Span -> Int -> PragmaOrder -> Text -> Text -> HeaderPragma
+ Tilia.Render.Header: HeaderPragma :: Span -> Int -> Text -> Text -> HeaderPragma
- Tilia.Render.Layout: First :: Place
+ Tilia.Render.Layout: First :: Position
- Tilia.Render.Layout: Last :: Place
+ Tilia.Render.Layout: Last :: Position
- Tilia.Render.Layout: Middle :: Place
+ Tilia.Render.Layout: Middle :: Position
- Tilia.Render.Layout: Only :: Place
+ Tilia.Render.Layout: Only :: Position
- Tilia.Render.Type: outerBndrs :: Ctx -> HsOuterTyVarBndrs Specificity GhcPs -> Doc
+ Tilia.Render.Type: outerBndrs :: TyVarBndrFlag flag => Ctx -> Doc -> HsOuterTyVarBndrs flag GhcPs -> Doc
- Tilia.Source: sourceOf :: Lines -> [LEpaComment] -> IntMap Int -> Source
+ Tilia.Source: sourceOf :: Lines -> [(RealSrcSpan, Token)] -> IntMap Int -> Source
- Tilia.Span.Ghc: spanOf :: HasLoc l => GenLocated l a -> Maybe Span
+ Tilia.Span.Ghc: spanOf :: HasLoc l => l -> Maybe Span
Files
- CHANGELOG.md +38/−0
- README.md +1/−0
- app/Main.hs +4/−5
- bench/Tilia/Bench/Cases.hs +18/−30
- bench/Tilia/Bench/Fixity.hs +9/−8
- bench/bench.record +56/−56
- corpora/hackage/hackage.manifest +40/−40
- corpora/vendored/declaration/value/fixity-local-to-a-binding-out.hs +29/−0
- corpora/vendored/declaration/value/fixity-local-to-a-binding.hs +29/−0
- corpora/vendored/declaration/value/fixity-without-a-precedence-out.hs +24/−0
- corpora/vendored/declaration/value/fixity-without-a-precedence.hs +21/−0
- corpora/vendored/declaration/value/layout-as-written-out.hs +8/−0
- corpora/vendored/declaration/value/layout-as-written.hs +8/−0
- corpora/vendored/declaration/value/operator-beginning-with-a-hash-out.hs +23/−0
- corpora/vendored/declaration/value/operator-beginning-with-a-hash.hs +23/−0
- corpora/vendored/declaration/value/operator-chains-out.hs +18/−0
- corpora/vendored/declaration/value/operator-chains.hs +16/−0
- corpora/vendored/import/qualified-after-the-module-name-out.hs +6/−0
- corpora/vendored/import/qualified-after-the-module-name.hs +4/−0
- corpora/vendored/import/qualified-before-the-module-name-out.hs +4/−0
- corpora/vendored/import/qualified-before-the-module-name.hs +3/−0
- corpora/vendored/other/comment-beside-a-wildcard-out.hs +19/−0
- corpora/vendored/other/comment-beside-a-wildcard.hs +18/−0
- corpora/vendored/other/comment-carried-on-under-a-remark-and-more-out.hs +20/−0
- corpora/vendored/other/comment-carried-on-under-a-remark-and-more.hs +20/−0
- corpora/vendored/other/comment-under-a-quasi-quotation-out.hs +19/−0
- corpora/vendored/other/comment-under-a-quasi-quotation.hs +19/−0
- corpora/vendored/other/comments-beside-an-alternative-and-a-bracket-out.hs +16/−0
- corpora/vendored/other/comments-beside-an-alternative-and-a-bracket.hs +16/−0
- corpora/vendored/other/comments-beside-the-where-of-the-header-out.hs +10/−0
- corpora/vendored/other/comments-beside-the-where-of-the-header.hs +10/−0
- corpora/vendored/other/comments-kept-where-written-out.hs +17/−0
- corpora/vendored/other/comments-kept-where-written.hs +17/−0
- corpora/vendored/other/comments-where-they-are-hard-to-put-back-out.hs +25/−0
- corpora/vendored/other/comments-where-they-are-hard-to-put-back.hs +25/−0
- corpora/vendored/other/cpp/code-after-an-optional-prefix-out.hs +32/−0
- corpora/vendored/other/cpp/code-after-an-optional-prefix.hs +32/−0
- corpora/vendored/other/cpp/where-after-an-export-list-in-a-conditional-out.hs +16/−0
- corpora/vendored/other/cpp/where-after-an-export-list-in-a-conditional.hs +15/−0
- corpora/vendored/other/extension-pack-before-what-it-enables-out.hs +4/−0
- corpora/vendored/other/extension-pack-before-what-it-enables.hs +3/−0
- corpora/vendored/other/extension-taken-back-from-the-edition-out.hs +6/−0
- corpora/vendored/other/extension-taken-back-from-the-edition.hs +4/−0
- corpora/vendored/other/language-pragmas-split-and-sorted-out.hs +4/−0
- corpora/vendored/other/language-pragmas-split-and-sorted.hs +2/−0
- corpora/vendored/signature/abstract-data-with-a-kind-out.hsig +7/−0
- corpora/vendored/signature/abstract-data-with-a-kind.hsig +6/−0
- corpora/vendored/signature/class-and-instances-out.hsig +10/−0
- corpora/vendored/signature/class-and-instances.hsig +10/−0
- corpora/vendored/signature/comments-after-the-last-declaration-out.hsig +10/−0
- corpora/vendored/signature/comments-after-the-last-declaration.hsig +10/−0
- corpora/vendored/signature/comments-in-a-signature-out.hsig +10/−0
- corpora/vendored/signature/comments-in-a-signature.hsig +10/−0
- corpora/vendored/signature/consecutive-type-signatures-out.hsig +7/−0
- corpora/vendored/signature/consecutive-type-signatures.hsig +5/−0
- corpora/vendored/signature/documented-declarations-out.hsig +7/−0
- corpora/vendored/signature/documented-declarations.hsig +7/−0
- corpora/vendored/signature/everything-above-the-keyword-out.hsig +8/−0
- corpora/vendored/signature/everything-above-the-keyword.hsig +8/−0
- corpora/vendored/signature/exports-listed-out.hsig +12/−0
- corpora/vendored/signature/exports-listed.hsig +12/−0
- corpora/vendored/signature/fixity-declaration-out.hsig +7/−0
- corpora/vendored/signature/fixity-declaration.hsig +7/−0
- corpora/vendored/signature/type-family-out.hsig +11/−0
- corpora/vendored/signature/type-family.hsig +11/−0
- corpora/vendored/signature/type-role-annotation-out.hsig +7/−0
- corpora/vendored/signature/type-role-annotation.hsig +7/−0
- src/Tilia/Cabal/Package.hs +72/−39
- src/Tilia/Cabal/Plan.hs +824/−0
- src/Tilia/Cabal/Project.hs +14/−5
- src/Tilia/Cabal/Target.hs +35/−60
- src/Tilia/Comments.hs +58/−36
- src/Tilia/Comments/Attach.hs +3/−11
- src/Tilia/Comments/Place.hs +6/−8
- src/Tilia/Cpp.hs +73/−106
- src/Tilia/Cpp/Directives.hs +22/−131
- src/Tilia/Cpp/Fragment.hs +3/−3
- src/Tilia/Cpp/Macros.hs +4/−7
- src/Tilia/Cpp/Merge.hs +34/−20
- src/Tilia/Cpp/Place.hs +17/−7
- src/Tilia/Diff.hs +5/−20
- src/Tilia/Doc.hs +1/−5
- src/Tilia/Doc/Combinators.hs +0/−2
- src/Tilia/Doc/Internal.hs +12/−32
- src/Tilia/Editor.hs +21/−19
- src/Tilia/Equivalence.hs +2/−5
- src/Tilia/Fixity.hs +9/−0
- src/Tilia/Fixity/Archive.hs +104/−0
- src/Tilia/Fixity/ByHand.hs +0/−33
- src/Tilia/Fixity/Cabal.hs +13/−32
- src/Tilia/Fixity/Cache.hs +51/−26
- src/Tilia/Fixity/Debug.hs +10/−3
- src/Tilia/Fixity/HiFile.hs +5/−12
- src/Tilia/Fixity/Interface.hs +4/−16
- src/Tilia/Fixity/PackageDb.hs +4/−8
- src/Tilia/Fixity/Plan.hs +932/−1895
- src/Tilia/Format.hs +75/−24
- src/Tilia/Imports.hs +8/−10
- src/Tilia/Memo.hs +118/−0
- src/Tilia/Palette.hs +2/−2
- src/Tilia/Parser.hs +7/−37
- src/Tilia/Pragma.hs +25/−17
- src/Tilia/Render.hs +8/−5
- src/Tilia/Render/Body.hs +0/−85
- src/Tilia/Render/Class.hs +11/−22
- src/Tilia/Render/Context.hs +16/−12
- src/Tilia/Render/Data.hs +9/−20
- src/Tilia/Render/Declaration.hs +5/−1
- src/Tilia/Render/Expression.hs +133/−74
- src/Tilia/Render/Haddock.hs +2/−23
- src/Tilia/Render/Header.hs +50/−49
- src/Tilia/Render/Layout.hs +7/−7
- src/Tilia/Render/Literal.hs +3/−3
- src/Tilia/Render/Name.hs +2/−2
- src/Tilia/Render/Operator.hs +9/−4
- src/Tilia/Render/Pattern.hs +6/−11
- src/Tilia/Render/Pragma.hs +1/−1
- src/Tilia/Render/Signature.hs +7/−8
- src/Tilia/Render/Type.hs +24/−14
- src/Tilia/Run.hs +13/−6
- src/Tilia/Source.hs +5/−3
- src/Tilia/Source/Lines.hs +24/−1
- src/Tilia/Span.hs +5/−0
- src/Tilia/Span/Ghc.hs +7/−31
- src/Tilia/Utils.hs +28/−0
- tests/Main.hs +1/−10
- tests/Tilia/Cabal/PackageSpec.hs +1/−1
- tests/Tilia/Comments/PlaceSpec.hs +1/−7
- tests/Tilia/CommentsSpec.hs +3/−11
- tests/Tilia/Corpus.hs +28/−35
- tests/Tilia/CorpusSpec.hs +32/−32
- tests/Tilia/Cpp/PropertiesSpec.hs +3/−2
- tests/Tilia/CppSpec.hs +28/−2
- tests/Tilia/Doc/BodySpec.hs +1/−1
- tests/Tilia/Doc/CombinatorsSpec.hs +1/−1
- tests/Tilia/Doc/InternalSpec.hs +1/−1
- tests/Tilia/Doc/PropertiesSpec.hs +1/−1
- tests/Tilia/EditorSpec.hs +137/−25
- tests/Tilia/EquivalenceSpec.hs +2/−12
- tests/Tilia/Fixity/CacheSpec.hs +6/−9
- tests/Tilia/Fixity/DebugSpec.hs +5/−6
- tests/Tilia/Fixity/DependenciesSpec.hs +5/−3
- tests/Tilia/Fixity/HiFileSpec.hs +4/−8
- tests/Tilia/Fixity/PlanSpec.hs +67/−71
- tests/Tilia/FixitySpec.hs +1/−11
- tests/Tilia/PragmaSpec.hs +22/−15
- tests/Tilia/RenderSpec.hs +30/−358
- tests/Tilia/RunSpec.hs +1/−5
- tests/Tilia/TestConfig.hs +38/−6
- tests/Tilia/TestUtils.hs +222/−0
- tests/Tilia/WithProjectPlan.hs +7/−7
- tilia.cabal +37/−74
CHANGELOG.md view
@@ -1,3 +1,41 @@+## Tilia 0.1.2.0++* A comment written beside the `..` of an item of an import or export list,+ as in `T ({- c -} ..)`, stays inside its parentheses rather than moving to+ the item after it. [Issue 254](https://github.com/mrkkrp/tilia/issues/254).+* A fixity declaration written without a precedence, such as+ ``infixr `cons` ``, comes out without one rather than with the `9` it+ defaults to. [Issue 267](https://github.com/mrkkrp/tilia/issues/267).+* The `where` of a module header written once after the `#endif` of a+ conditional around the export list stays there rather than being copied+ into each branch, and comments written right before or after it stay on+ their side of it. [Issue 266](https://github.com/mrkkrp/tilia/issues/266).+* Code that one branch of a conditional puts a prefix in front of, such as+ `again $`, comes out once after the `#endif` rather than in each branch+ where it is written at the column of the prefix.+ [Issue 263](https://github.com/mrkkrp/tilia/issues/263).+* A comment carrying on the remark that ends the line above stays with that+ remark where it is lined up under it and the code under the comment begins+ at the same column, or where it is indented further than the remark,+ rather than going with the code that follows.+ [Issue 255](https://github.com/mrkkrp/tilia/issues/255).+* With `UnboxedTuples` on, as with `UnboxedSums` already, an operator that+ begins with `#` keeps the space after an opening parenthesis, as in+ `( #<| )` and the section `( #<| x)`, and `#` alone the space before a+ closing one, as in `(x # )`, rather than coming out as `(#` or `#)`, which+ do not parse. [Issue 273](https://github.com/mrkkrp/tilia/issues/273).+* A comment under a quasi-quotation written over several lines stays out of+ it, rather than being moved before its `|]`, which changed what was+ quoted. [Issue 274](https://github.com/mrkkrp/tilia/issues/274).+* A module that begins with a byte order mark, which GHC skips, is+ formatted and keeps the mark, and fixities are taken from a project module+ that begins with one, rather than its not parsing.+ [Issue 275](https://github.com/mrkkrp/tilia/issues/275).+* `Tilia.Editor.compiledSession` builds a session from what a program that+ has compiled the project already knows, such as a language server, so that+ formatting needs neither Cabal nor the sources of dependencies.+ [Issue 234](https://github.com/mrkkrp/tilia/issues/234).+ ## Tilia 0.1.1.0 * Keep code that every configuration prints alike out of the branches of a
README.md view
@@ -45,6 +45,7 @@ * `lib:tilia` or `tilia:lib:tilia` * `exe:tilia` or `tilia:exe:tilia` * `test:tests` or `tilia:test:tests`, or just `tests`+* `bench:bench` or `tilia:bench:bench`, or just `bench` It may be surprising that we talk about components rather than individual files. Well, formatting a Haskell module, fortunately or unfortunately,
app/Main.hs view
@@ -17,7 +17,7 @@ import GHC.IO.Encoding (TextEncoding (textEncodingName)) import Options.Applicative import Paths_tilia (version)-import System.Directory (makeRelativeToCurrentDirectory)+import System.Directory (getCurrentDirectory, makeRelativeToCurrentDirectory) import System.Exit (ExitCode (..)) import System.Exit qualified import System.IO@@ -43,7 +43,7 @@ import Tilia.Editor (editorSession, formatBuffer) import Tilia.Fixity.Debug (renderFixityNotes) import Tilia.Format- ( FormatError (Unreadable),+ ( FormatError (NoProject, Unreadable), PlanSource (..), Session, describeFormatError,@@ -106,7 +106,7 @@ =<< filesOfComponents root components session <- newSession- "."+ root ( case optBuildPlan of Nothing -> PlanFromCabal (mapMaybe componentInPlan components) Just givenPlan -> GivenPlan givenPlan@@ -217,8 +217,7 @@ componentsFor :: Palette -> Target -> IO (ProjectRoot, [Component]) componentsFor palette target = findProjectRoot "." >>= \case- Nothing ->- die 2 palette "no cabal.project or .cabal file at or above the working directory"+ Nothing -> dieFormatting palette . NoProject =<< getCurrentDirectory Just root -> componentsOfTarget root target >>= \case Left problem ->
bench/Tilia/Bench/Cases.hs view
@@ -22,13 +22,11 @@ import Data.Traversable (for) import GHC.LanguageExtensions.Type (Extension) import Tilia.Corpus (Example (..))-import Tilia.Cpp (everyBranch, formatWithCpp, usesCpp)-import Tilia.Doc (defaultRenderOptions, printDoc)+import Tilia.Cpp (usesCpp) import Tilia.Format (rewritten)-import Tilia.Parser (parseModule, parserConfigFor)-import Tilia.Pragma (effectiveExtensions)-import Tilia.Render (defaultRenderConfig, renderModule)-import Tilia.TestConfig (exampleRenderConfig)+import Tilia.Parser (parseModule)+import Tilia.TestConfig (formatCppExample, printExample)+import Tilia.TestUtils (effectiveExtensionsFor, parserConfigFor) -- | What part of a run a benchmark measures. data Stage@@ -147,10 +145,10 @@ -- | Modules made to stress one part of the formatter each. syntheticSubjects :: [Subject] syntheticSubjects =- [ subjectOf [] False "synthetic/comments" commented,- subjectOf [] False "synthetic/imports" imported,- subjectOf [] False "synthetic/conditionals" conditional,- subjectOf [] False "synthetic/nesting" nested+ [ subjectOf Nothing False "synthetic/comments" commented,+ subjectOf Nothing False "synthetic/imports" imported,+ subjectOf Nothing False "synthetic/conditionals" conditional,+ subjectOf Nothing False "synthetic/nesting" nested ] where commented =@@ -205,7 +203,7 @@ -- | A module to format, and to check what that printed where it is -- checked.-subjectOf :: [Extension] -> Bool -> Text -> Text -> Subject+subjectOf :: Maybe [Extension] -> Bool -> Text -> Text -> Subject subjectOf package isChecked name source = Subject { subjectName = name,@@ -219,29 +217,19 @@ where path = T.unpack name parser = parserConfigFor package- cpp = usesCpp (effectiveExtensions package source) source+ cpp = usesCpp (effectiveExtensionsFor package source) source tree | cpp = Nothing | otherwise = either (const Nothing) Just (parseModule parser path source) -- | What formatting a module prints, as the corpus does it, or 'Nothing' -- where it is declined.-formatted :: [Extension] -> FilePath -> Text -> Maybe Text+formatted :: Maybe [Extension] -> FilePath -> Text -> Maybe Text formatted package path source- | usesCpp (effectiveExtensions package source) source =- either (const Nothing) Just (formatWithCpp parser configured path source)- | otherwise = case parseModule parser path source of- Left _ -> Nothing- Right p ->- Just- ( printDoc- defaultRenderOptions- (renderModule (exampleRenderConfig package source (pure p)) p)- )- where- parser = parserConfigFor package- configured =- maybe- defaultRenderConfig- (exampleRenderConfig package source)- (everyBranch parser path source)+ | usesCpp (effectiveExtensionsFor package source) source =+ either (const Nothing) Just (formatCppExample package path source)+ | otherwise =+ either+ (const Nothing)+ (Just . printExample package source)+ (parseModule (parserConfigFor package) path source)
bench/Tilia/Bench/Fixity.hs view
@@ -30,11 +30,12 @@ doesFileExist, removePathForcibly, )-import System.Environment (lookupEnv, setEnv, unsetEnv) import System.FilePath ((<.>), (</>)) import System.Process (getCurrentPid) import Text.Printf (printf) import Tilia.Bench.Cases (Benchmark (..), Stage (..))+import Tilia.Cabal.Package (newPackageReader)+import Tilia.Cabal.Plan import Tilia.Corpus (Corpus (..), Source (..), corpusCache, hackagePackages) import Tilia.Fixity.Cabal (packageModules) import Tilia.Fixity.Cache (Cache, PlanToken (..), openCache)@@ -46,6 +47,7 @@ readInstalledPackages, ) import Tilia.Fixity.Plan+import Tilia.TestUtils (withEnvironment) -- | The packages of the Hackage corpus whose every module is read. resolved :: [Text]@@ -207,7 +209,8 @@ cache <- cacheUnder (dir </> "cache") interfaces <- interfacesOf decoded let reading kept modules = do- resolver <- newResolverWith [FromSource] kept [] tarballs plan+ packageReader <- newPackageReader (plannedCompiler plan)+ resolver <- newResolverWith [FromSource] kept [] tarballs packageReader plan pure $ do answers <- traverse (askModule resolver) modules length <$> evaluate (force answers)@@ -248,12 +251,10 @@ -- | A cache of fixities in a directory. cacheUnder :: FilePath -> IO Cache-cacheUnder dir = do- before <- lookupEnv "XDG_CACHE_HOME"- setEnv "XDG_CACHE_HOME" dir- cache <- openCache (Do #useCache) (PlanToken "benchmarks")- maybe (unsetEnv "XDG_CACHE_HOME") (setEnv "XDG_CACHE_HOME") before- pure cache+cacheUnder dir =+ withEnvironment+ [("XDG_CACHE_HOME", dir)]+ (openCache (Do #useCache) (PlanToken "benchmarks")) -- | The interfaces of every module of the installed packages named, each -- package by its name and version.
bench/bench.record view
@@ -31,13 +31,13 @@ # stage allocated instructions benchmark check 272354648 575981358 Agda-2.8.0/src/full/Agda/Syntax/Translation/ConcreteToAbstract.hs check 411823456 855523727 ShellCheck-0.11.0/src/ShellCheck/Analytics.hs-check 260526152 461063479 aeson-2.3.1.0/src/Data/Aeson/KeyMap.hs+check 260497296 461063479 aeson-2.3.1.0/src/Data/Aeson/KeyMap.hs check 196550504 511610069 blaze-html-0.9.2.0/src/Text/Blaze/Html5/Attributes.hs check 63623696 127394098 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Transformations/Alt.hs check 84949968 171830970 distributed-process-0.7.8/src/Control/Distributed/Process/Node.hs check 127362696 266504214 esqueleto-3.6.0.3/test/PostgreSQL/Test.hs check 506858784 923788756 haxl-2.5.1.1/Haxl/Core/Monad.hs-check 215891816 451467945 idris-1.3.4/src/Idris/Elab/Term.hs+check 215914784 451467945 idris-1.3.4/src/Idris/Elab/Term.hs check 333601000 572292120 lifted-base-0.2.3.12/Control/Exception/Lifted.hs check 60171824 128139113 megaparsec-9.8.1/Text/Megaparsec/Internal.hs check 81898560 173856514 optparse-applicative-0.19.0.0/tests/test.hs@@ -61,61 +61,61 @@ decode 42252744 55499725 ghc-internal-9.1003.0 decode 11189560 15486875 template-haskell-2.22.0.0 decode 6097120 6605095 text-2.1.3-format 586993080 1135630753 Agda-2.8.0/src/full/Agda/Syntax/Translation/ConcreteToAbstract.hs-format 4059772616 8036874388 QuickCheck-2.18.0.0/src/Test/QuickCheck/Arbitrary.hs+format 587068744 1136108572 Agda-2.8.0/src/full/Agda/Syntax/Translation/ConcreteToAbstract.hs+format 4060865072 8041341103 QuickCheck-2.18.0.0/src/Test/QuickCheck/Arbitrary.hs format 907373856 1723990920 ShellCheck-0.11.0/src/ShellCheck/Analytics.hs-format 330306320 571456793 aeson-2.3.1.0/src/Data/Aeson/KeyMap.hs-format 174818320 326282752 blaze-html-0.9.2.0/src/Text/Blaze/Html5/Attributes.hs-format 125893136 239428886 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Transformations/Alt.hs-format 181975648 344891996 distributed-process-0.7.8/src/Control/Distributed/Process/Node.hs-format 313302256 590497159 esqueleto-3.6.0.3/test/PostgreSQL/Test.hs-format 772624688 1542323569 haxl-2.5.1.1/Haxl/Core/Monad.hs-format 505622568 964281131 idris-1.3.4/src/Idris/Elab/Term.hs+format 330450600 571670315 aeson-2.3.1.0/src/Data/Aeson/KeyMap.hs+format 175030840 326850369 blaze-html-0.9.2.0/src/Text/Blaze/Html5/Attributes.hs+format 125920744 239525469 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Transformations/Alt.hs+format 182428296 345883719 distributed-process-0.7.8/src/Control/Distributed/Process/Node.hs+format 313363824 590647912 esqueleto-3.6.0.3/test/PostgreSQL/Test.hs+format 773029152 1543414928 haxl-2.5.1.1/Haxl/Core/Monad.hs+format 505953528 965039048 idris-1.3.4/src/Idris/Elab/Term.hs format 1340529160 2711040392 lens-5.3.6/src/Control/Lens/Wrapped.hs-format 403478464 737112328 lifted-base-0.2.3.12/Control/Exception/Lifted.hs-format 115314000 221071613 megaparsec-9.8.1/Text/Megaparsec/Internal.hs+format 403596632 737287954 lifted-base-0.2.3.12/Control/Exception/Lifted.hs+format 115350240 221131009 megaparsec-9.8.1/Text/Megaparsec/Internal.hs format 152138968 278467498 optparse-applicative-0.19.0.0/tests/test.hs-format 54506128 96618849 pandoc-3.10.2/src/Text/Pandoc/Class/PandocPure.hs-format 377550600 719032510 pandoc-3.10.2/src/Text/Pandoc/Readers/Markdown.hs-format 466875216 881659030 pandoc-3.10.2/src/Text/Pandoc/Writers/Powerpoint/Output.hs-format 374789216 709072870 purescript-0.15.15/src/Language/PureScript/Errors.hs-format 157590744 293012445 purescript-0.15.15/src/Language/PureScript/TypeChecker/Types.hs-format 129266384 233552107 recursion-schemes-5.2.3/src/Data/Functor/Foldable.hs-format 173044184 323789142 servant-server-0.20.3.0/src/Servant/Server/Internal.hs-format 174955608 327851739 swagger2-2.9.1/src/Data/Swagger/Internal.hs-format 716397560 1506784284 synthetic/comments-format 107750736 237375293 synthetic/conditionals-format 75942320 112110399 synthetic/imports+format 54554184 96700081 pandoc-3.10.2/src/Text/Pandoc/Class/PandocPure.hs+format 377676272 719453674 pandoc-3.10.2/src/Text/Pandoc/Readers/Markdown.hs+format 466875216 881809887 pandoc-3.10.2/src/Text/Pandoc/Writers/Powerpoint/Output.hs+format 375024840 709592247 purescript-0.15.15/src/Language/PureScript/Errors.hs+format 157667592 293149746 purescript-0.15.15/src/Language/PureScript/TypeChecker/Types.hs+format 129432696 233813267 recursion-schemes-5.2.3/src/Data/Functor/Foldable.hs+format 173127584 323770122 servant-server-0.20.3.0/src/Servant/Server/Internal.hs+format 175020056 327970368 swagger2-2.9.1/src/Data/Swagger/Internal.hs+format 716528888 1507106872 synthetic/comments+format 107796160 237491710 synthetic/conditionals+format 77608272 114637794 synthetic/imports format 99934472 207607344 synthetic/nesting-format 838934536 1651811978 text-2.1.4/src/Data/Text/Internal/Fusion/CaseMapping.hs-format 223463632 390867496 text-2.1.4/tests/Tests/Properties/Folds.hs-format 139006432 258150558 time-1.16.0.1/test/main/Test/Format/ParseTime.hs-format 139530880 263037198 tls-2.4.3/Network/TLS/Extension.hs-format 1012732608 1984898669 unordered-containers-0.2.21/Data/HashMap/Internal.hs-format 509614800 951730877 unpacked-containers-0/src/Map/Internal.hs-format 551766528 1043391813 vector-0.13.2.0/src/Data/Vector/Generic.hs-format 139402192 260910396 xmonad-0.18.1/src/XMonad/Operations.hs-recall 5159192 11629716 aeson-2.3.1.0-recall 2222736 5117209 conduit-1.3.6.1-recall 1711296 3256971 free-5.2-recall 11087912 34729465 lens-5.3.6-recall 1794808 3797182 megaparsec-9.8.1-recall 2130864 4316158 mtl-2.3.2-recall 1290456 2773511 optics-0.4.2.1-recall 1533048 3627916 pandoc-types-1.23.1.2-recall 2303032 4716532 semigroupoids-6.0.2-recall 3296464 6712375 servant-0.20.3.0-recall 5195216 9137639 text-2.1.4-recall 4064960 8696024 vector-0.13.2.0-resolve 1871048824 3295554158 aeson-2.3.1.0-resolve 444504256 757669620 conduit-1.3.6.1-resolve 487648992 826960067 free-5.2-resolve 3076558528 5353588555 lens-5.3.6-resolve 216507192 378740037 megaparsec-9.8.1-resolve 233565064 403415365 mtl-2.3.2-resolve 65725248 132703282 optics-0.4.2.1-resolve 78157216 145313962 pandoc-types-1.23.1.2-resolve 138235048 248322418 semigroupoids-6.0.2-resolve 219372680 404689330 servant-0.20.3.0-resolve 1185759448 2051011736 text-2.1.4-resolve 2471261160 4205683979 vector-0.13.2.0+format 839857072 1653646287 text-2.1.4/src/Data/Text/Internal/Fusion/CaseMapping.hs+format 223534696 391051730 text-2.1.4/tests/Tests/Properties/Folds.hs+format 139006432 258193642 time-1.16.0.1/test/main/Test/Format/ParseTime.hs+format 139928120 263889581 tls-2.4.3/Network/TLS/Extension.hs+format 1012993016 1985300102 unordered-containers-0.2.21/Data/HashMap/Internal.hs+format 509562512 951647762 unpacked-containers-0/src/Map/Internal.hs+format 551911832 1043593184 vector-0.13.2.0/src/Data/Vector/Generic.hs+format 139491808 261133367 xmonad-0.18.1/src/XMonad/Operations.hs+recall 5154088 11631440 aeson-2.3.1.0+recall 2222200 5118396 conduit-1.3.6.1+recall 1709976 3278498 free-5.2+recall 11088696 34739875 lens-5.3.6+recall 1793600 3797411 megaparsec-9.8.1+recall 2129136 4316346 mtl-2.3.2+recall 1289808 2773580 optics-0.4.2.1+recall 1532472 3627916 pandoc-types-1.23.1.2+recall 2301760 4717796 semigroupoids-6.0.2+recall 3293336 6712858 servant-0.20.3.0+recall 5188496 9120737 text-2.1.4+recall 4061800 8698087 vector-0.13.2.0+resolve 1810656152 3180580388 aeson-2.3.1.0+resolve 435325168 738034252 conduit-1.3.6.1+resolve 471290256 797760851 free-5.2+resolve 2974846416 5176070738 lens-5.3.6+resolve 210109008 366801169 megaparsec-9.8.1+resolve 225682064 388972813 mtl-2.3.2+resolve 64290384 128790701 optics-0.4.2.1+resolve 77403992 142729351 pandoc-types-1.23.1.2+resolve 133167224 239714598 semigroupoids-6.0.2+resolve 213362072 390356497 servant-0.20.3.0+resolve 1159785776 1994326073 text-2.1.4+resolve 2408709680 4066724655 vector-0.13.2.0
corpora/hackage/hackage.manifest view
@@ -22,7 +22,7 @@ formatted a5e37499e9c4 Agda-2.8.0/src/full/Agda/Compiler/MAlonzo/Encode.hs formatted be07bfaffa2e Agda-2.8.0/src/full/Agda/Compiler/MAlonzo/HaskellTypes.hs formatted aa62f795f1b2 Agda-2.8.0/src/full/Agda/Compiler/MAlonzo/Misc.hs-formatted 70bbea601ddf Agda-2.8.0/src/full/Agda/Compiler/MAlonzo/Pragmas.hs+formatted 3102bb30026e Agda-2.8.0/src/full/Agda/Compiler/MAlonzo/Pragmas.hs formatted 76490fc3bd40 Agda-2.8.0/src/full/Agda/Compiler/MAlonzo/Pretty.hs formatted 92ef6a29a48a Agda-2.8.0/src/full/Agda/Compiler/MAlonzo/Primitives.hs formatted cd156da2fa25 Agda-2.8.0/src/full/Agda/Compiler/MAlonzo/Strict.hs@@ -42,7 +42,7 @@ formatted 4795b9a43709 Agda-2.8.0/src/full/Agda/Compiler/Treeless/Simplify.hs formatted 80fddf5d858f Agda-2.8.0/src/full/Agda/Compiler/Treeless/Subst.hs formatted 83543a8ec338 Agda-2.8.0/src/full/Agda/Compiler/Treeless/Uncase.hs-formatted 24aeb7ff3365 Agda-2.8.0/src/full/Agda/Compiler/Treeless/Unused.hs+formatted 9bc8388d8beb Agda-2.8.0/src/full/Agda/Compiler/Treeless/Unused.hs formatted 3985ec175c5f Agda-2.8.0/src/full/Agda/ImpossibleTest.hs formatted 4bfae407a40f Agda-2.8.0/src/full/Agda/Interaction/AgdaTop.hs formatted f35eb355b24e Agda-2.8.0/src/full/Agda/Interaction/Base.hs@@ -52,7 +52,7 @@ formatted e4f2d0d2088e Agda-2.8.0/src/full/Agda/Interaction/CommandLine.hs formatted f2285f3b69f7 Agda-2.8.0/src/full/Agda/Interaction/EmacsCommand.hs formatted 555155a56d86 Agda-2.8.0/src/full/Agda/Interaction/EmacsTop.hs-formatted 720e5305288c Agda-2.8.0/src/full/Agda/Interaction/ExitCode.hs+formatted 05a9823fff57 Agda-2.8.0/src/full/Agda/Interaction/ExitCode.hs formatted e06156bc2afa Agda-2.8.0/src/full/Agda/Interaction/FindFile.hs formatted 3170eb1921b4 Agda-2.8.0/src/full/Agda/Interaction/Highlighting/Common.hs formatted 1fb0462a1b45 Agda-2.8.0/src/full/Agda/Interaction/Highlighting/Dot.hs@@ -202,7 +202,7 @@ formatted 59bd9e3da33e Agda-2.8.0/src/full/Agda/TypeChecking/DiscrimTree.hs formatted c17cb27af1b5 Agda-2.8.0/src/full/Agda/TypeChecking/DiscrimTree/Types.hs formatted ee105b08c0e0 Agda-2.8.0/src/full/Agda/TypeChecking/DisplayForm.hs-formatted fa3908c1bf4b Agda-2.8.0/src/full/Agda/TypeChecking/DropArgs.hs+formatted 2c03c6d0d27b Agda-2.8.0/src/full/Agda/TypeChecking/DropArgs.hs formatted c4bcc3ec3efc Agda-2.8.0/src/full/Agda/TypeChecking/Empty.hs formatted 546e2690c8bd Agda-2.8.0/src/full/Agda/TypeChecking/Empty.hs-boot formatted d796fe01f1e9 Agda-2.8.0/src/full/Agda/TypeChecking/Errors.hs@@ -473,7 +473,7 @@ formatted 09230f4d022f QuickCheck-2.18.0.0/src/Test/QuickCheck/Monadic.hs formatted a94b9d13da2f QuickCheck-2.18.0.0/src/Test/QuickCheck/Monoids.hs formatted 0c4f8c706edf QuickCheck-2.18.0.0/src/Test/QuickCheck/Poly.hs-formatted 7ad2b4286dd4 QuickCheck-2.18.0.0/src/Test/QuickCheck/Property.hs+formatted 3c937d9861f1 QuickCheck-2.18.0.0/src/Test/QuickCheck/Property.hs formatted d248979763fb QuickCheck-2.18.0.0/src/Test/QuickCheck/Random.hs formatted 67a6c49f2d76 QuickCheck-2.18.0.0/src/Test/QuickCheck/State.hs formatted d6690aaf2529 QuickCheck-2.18.0.0/src/Test/QuickCheck/Test.hs@@ -612,7 +612,7 @@ formatted e865ae48f11b ansi-terminal-1.1.5/Setup.hs formatted 075adef817c3 ansi-terminal-1.1.5/app/Example.hs formatted 5cffc9cf8231 ansi-terminal-1.1.5/src/System/Console/ANSI.hs-formatted 46d19416edeb ansi-terminal-1.1.5/unix/System/Console/ANSI/Internal.hs+formatted 820497cc6023 ansi-terminal-1.1.5/unix/System/Console/ANSI/Internal.hs formatted 1cc713a3f254 ansi-terminal-1.1.5/win/System/Console/ANSI/Internal.hs formatted 1aaa3e2316a9 ansi-terminal-1.1.5/win/System/Console/ANSI/Windows/Foreign.hs formatted e763680dce5b ansi-terminal-1.1.5/win/System/Console/ANSI/Windows/Win32/Types.hs@@ -651,7 +651,7 @@ formatted 8fa6e462f95f attoparsec-0.14.4/benchmarks/Links.hs formatted f582abe3e244 attoparsec-0.14.4/benchmarks/Numbers.hs formatted 696be472825c attoparsec-0.14.4/benchmarks/Sets.hs-formatted 0d64d700acb4 attoparsec-0.14.4/benchmarks/TextFastSet.hs+formatted f19602a9ee09 attoparsec-0.14.4/benchmarks/TextFastSet.hs formatted 2b16e18cd8c0 attoparsec-0.14.4/benchmarks/Warp.hs formatted 0328e73e6175 attoparsec-0.14.4/benchmarks/warp-3.0.1.1/Network/Wai/Handler/Warp/ReadInt.hs formatted 1f7d3a417fb5 attoparsec-0.14.4/benchmarks/warp-3.0.1.1/Network/Wai/Handler/Warp/RequestHeader.hs@@ -663,13 +663,13 @@ formatted f21e47830b1f attoparsec-0.14.4/internal/Data/Attoparsec/Internal/Compat.hs formatted 4498fcbfa61f attoparsec-0.14.4/internal/Data/Attoparsec/Internal/Fhthagn.hs formatted bc12b2ada3b5 attoparsec-0.14.4/internal/Data/Attoparsec/Text/Buffer.hs-formatted b2e14e3c02d3 attoparsec-0.14.4/internal/Data/Attoparsec/Text/FastSet.hs+formatted ecad246a959e attoparsec-0.14.4/internal/Data/Attoparsec/Text/FastSet.hs formatted ccaeb87485db attoparsec-0.14.4/tests/QC.hs formatted c4481214d6c0 attoparsec-0.14.4/tests/QC/Buffer.hs formatted ae7fa7b38a4b attoparsec-0.14.4/tests/QC/ByteString.hs formatted 455e4866502a attoparsec-0.14.4/tests/QC/Combinator.hs formatted 2bd326ae7162 attoparsec-0.14.4/tests/QC/Common.hs-formatted 7558d60104bb attoparsec-0.14.4/tests/QC/IPv6/Internal.hs+formatted 394bfa5dcceb attoparsec-0.14.4/tests/QC/IPv6/Internal.hs formatted 0692feda416d attoparsec-0.14.4/tests/QC/IPv6/Types.hs formatted 08452f3d9508 attoparsec-0.14.4/tests/QC/Rechunked.hs formatted 0bab40e54197 attoparsec-0.14.4/tests/QC/Simple.hs@@ -787,9 +787,9 @@ formatted 001f1d1eb163 aws-0.25.3/tests/Utils.hs formatted 10172e7983b9 base64-bytestring-1.2.1.0/Data/ByteString/Base64.hs formatted 8d1de483b9c4 base64-bytestring-1.2.1.0/Data/ByteString/Base64/Internal.hs-formatted dae9abac8bd0 base64-bytestring-1.2.1.0/Data/ByteString/Base64/Lazy.hs+formatted 20780be77581 base64-bytestring-1.2.1.0/Data/ByteString/Base64/Lazy.hs formatted efdde917444c base64-bytestring-1.2.1.0/Data/ByteString/Base64/URL.hs-formatted 0d8bf6b77f56 base64-bytestring-1.2.1.0/Data/ByteString/Base64/URL/Lazy.hs+formatted 4ea9b39db86f base64-bytestring-1.2.1.0/Data/ByteString/Base64/URL/Lazy.hs formatted e865ae48f11b base64-bytestring-1.2.1.0/Setup.hs formatted 0328dc0c3c7c base64-bytestring-1.2.1.0/benchmarks/BM.hs formatted e856b328c01f base64-bytestring-1.2.1.0/tests/Tests.hs@@ -1466,18 +1466,18 @@ formatted 62f91f308c2e brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Backend.hs formatted b114e1735e96 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/BackendUtils.hs formatted 4db6008c162f brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Config.hs-formatted b3a115fdcb04 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Config/Types.hs+formatted 47cac5b50849 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Config/Types.hs formatted 3479d640d39c brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Config/Types/Instances.hs formatted 3a7e4e0507b6 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/ExactPrintUtils.hs formatted 5d4339023e83 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/LayouterBasics.hs formatted e6236562836a brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/DataDecl.hs-formatted ee47e6ccde82 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/Decl.hs+formatted 4259510f3316 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/Decl.hs formatted 85310fee66ef brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/Expr.hs formatted 63ac93aa854f brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/Expr.hs-boot formatted f730e71a6728 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/IE.hs formatted 4647ea64ed39 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/Import.hs formatted 455e49f342e4 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/Module.hs-formatted 1f83e29fdd09 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/Pattern.hs+formatted 94a22988945f brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/Pattern.hs formatted d46ed5a9675b brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/Stmt.hs formatted 08c3e5d27dd4 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/Stmt.hs-boot formatted 757402eae174 brittany-0.14.0.2/source/library/Language/Haskell/Brittany/Internal/Layouters/Type.hs@@ -1941,7 +1941,7 @@ formatted 891834d5aec2 fay-0.24.2.0/src/Fay/Compiler/Desugar.hs formatted 570ca805808c fay-0.24.2.0/src/Fay/Compiler/Desugar/Name.hs formatted fe53147a7e33 fay-0.24.2.0/src/Fay/Compiler/Desugar/Types.hs-formatted cd957c0884e8 fay-0.24.2.0/src/Fay/Compiler/Exp.hs+formatted de04ffc1e73c fay-0.24.2.0/src/Fay/Compiler/Exp.hs formatted cfc0f0f46b43 fay-0.24.2.0/src/Fay/Compiler/FFI.hs formatted 69fd6dd80a66 fay-0.24.2.0/src/Fay/Compiler/GADT.hs formatted bcb9d437929e fay-0.24.2.0/src/Fay/Compiler/Import.hs@@ -2240,7 +2240,7 @@ formatted 92f9ffbc940f free-5.2/src/Control/Monad/Free/Ap.hs formatted 93c96d44b956 free-5.2/src/Control/Monad/Free/Church.hs formatted 39bf4e33f6e2 free-5.2/src/Control/Monad/Free/Class.hs-formatted d6318fb07edb free-5.2/src/Control/Monad/Free/TH.hs+formatted ff2776be0a4c free-5.2/src/Control/Monad/Free/TH.hs formatted a51821c826a6 free-5.2/src/Control/Monad/Trans/Free.hs formatted b9554e2bf6b2 free-5.2/src/Control/Monad/Trans/Free/Ap.hs formatted 91e1c571caac free-5.2/src/Control/Monad/Trans/Free/Church.hs@@ -2713,7 +2713,7 @@ formatted a8f154205e55 idris-1.3.4/src/Idris/AbsSyntax.hs formatted f6be69b820bf idris-1.3.4/src/Idris/AbsSyntaxTree.hs formatted 9f032fa98b74 idris-1.3.4/src/Idris/Apropos.hs-formatted 1b17f73d1ed1 idris-1.3.4/src/Idris/CaseSplit.hs+formatted dc91bae3387b idris-1.3.4/src/Idris/CaseSplit.hs formatted 887228791bf4 idris-1.3.4/src/Idris/Chaser.hs formatted a47646c618f2 idris-1.3.4/src/Idris/CmdOptions.hs formatted 4da7f931a59f idris-1.3.4/src/Idris/Colours.hs@@ -2724,11 +2724,11 @@ formatted 32e1f1dc7a41 idris-1.3.4/src/Idris/Core/DeepSeq.hs formatted 3b956c85cb93 idris-1.3.4/src/Idris/Core/Elaborate.hs formatted d0216dc5a258 idris-1.3.4/src/Idris/Core/Evaluate.hs-formatted 9e811f74925b idris-1.3.4/src/Idris/Core/Execute.hs+formatted 5c128b375f3d idris-1.3.4/src/Idris/Core/Execute.hs formatted a0c632cd26d6 idris-1.3.4/src/Idris/Core/ProofState.hs formatted 0ccd6a932fe8 idris-1.3.4/src/Idris/Core/ProofTerm.hs formatted cd52f6495e8c idris-1.3.4/src/Idris/Core/TT.hs-formatted d6069959015f idris-1.3.4/src/Idris/Core/Typecheck.hs+formatted 86a2b33d069a idris-1.3.4/src/Idris/Core/Typecheck.hs formatted 7872856d0301 idris-1.3.4/src/Idris/Core/Unify.hs formatted 01a710f67630 idris-1.3.4/src/Idris/Core/WHNF.hs formatted 36e22c48009d idris-1.3.4/src/Idris/Coverage.hs@@ -2740,7 +2740,7 @@ formatted d3d91df82106 idris-1.3.4/src/Idris/Docs.hs formatted a485f5642886 idris-1.3.4/src/Idris/Docstrings.hs formatted a1c474156864 idris-1.3.4/src/Idris/Elab/AsPat.hs-formatted 0c12951dd00e idris-1.3.4/src/Idris/Elab/Clause.hs+formatted fc2ff424b9e3 idris-1.3.4/src/Idris/Elab/Clause.hs formatted 9e5c2909e062 idris-1.3.4/src/Idris/Elab/Data.hs formatted 8513eb951865 idris-1.3.4/src/Idris/Elab/Implementation.hs formatted 9349c5236898 idris-1.3.4/src/Idris/Elab/Interface.hs@@ -2749,7 +2749,7 @@ formatted 229f49cf1c9d idris-1.3.4/src/Idris/Elab/Record.hs formatted 187fb9ddcd79 idris-1.3.4/src/Idris/Elab/Rewrite.hs formatted 77ebc33633f8 idris-1.3.4/src/Idris/Elab/RunElab.hs-formatted 5ca384198b92 idris-1.3.4/src/Idris/Elab/Term.hs+formatted 711d76a30f2c idris-1.3.4/src/Idris/Elab/Term.hs formatted 43565a6d9bac idris-1.3.4/src/Idris/Elab/Transform.hs formatted cf8cbbec4fe7 idris-1.3.4/src/Idris/Elab/Type.hs formatted b3e66f72a9ac idris-1.3.4/src/Idris/Elab/Utils.hs@@ -2780,9 +2780,9 @@ formatted de7138f89659 idris-1.3.4/src/Idris/Parser/Helpers.hs formatted edc130411af1 idris-1.3.4/src/Idris/Parser/Ops.hs formatted 3f33ff4561d4 idris-1.3.4/src/Idris/Parser/Stack.hs-formatted 7f8889e1a85a idris-1.3.4/src/Idris/PartialEval.hs+formatted d415f9474dc1 idris-1.3.4/src/Idris/PartialEval.hs formatted 58ad84ded5d8 idris-1.3.4/src/Idris/Primitives.hs-formatted 429ec8a4efee idris-1.3.4/src/Idris/ProofSearch.hs+formatted 03bf99a63aa4 idris-1.3.4/src/Idris/ProofSearch.hs formatted 168d70e142b3 idris-1.3.4/src/Idris/Prover.hs formatted ad0ff1beaf54 idris-1.3.4/src/Idris/Providers.hs formatted c47d07c305a7 idris-1.3.4/src/Idris/REPL.hs@@ -2790,7 +2790,7 @@ formatted 586418ee0cf6 idris-1.3.4/src/Idris/REPL/Commands.hs formatted f36f6bde407f idris-1.3.4/src/Idris/REPL/Parser.hs formatted a35f4ee64226 idris-1.3.4/src/Idris/Reflection.hs-formatted d8e303fd1eef idris-1.3.4/src/Idris/Termination.hs+formatted 434ab5e35b0e idris-1.3.4/src/Idris/Termination.hs formatted 6837e439e7d6 idris-1.3.4/src/Idris/Transforms.hs formatted 06befdb6fc0c idris-1.3.4/src/Idris/TypeSearch.hs formatted 528c0d503ddd idris-1.3.4/src/Idris/Unlit.hs@@ -2811,7 +2811,7 @@ formatted 72999d5c814e intero-0.1.40/src/GhciTypes.hs declined - intero-0.1.40/src/InteractiveUI.hs formatted d3b894941b5e intero-0.1.40/src/Intero/Compat.hs-partly-checked f1e8bf4ae69f intero-0.1.40/src/Main.hs+partly-checked 650cf9fb288c intero-0.1.40/src/Main.hs formatted 13b22f1a2f96 intero-0.1.40/src/test/Main.hs formatted 4fe73bbfe233 leksah-0.16.2.2/data/leksah-welcome/src/Main.hs formatted 1cefb44a49bc leksah-0.16.2.2/data/leksah-welcome/test/Main.hs@@ -2856,7 +2856,7 @@ formatted 575af73a6da8 leksah-0.16.2.2/src/IDE/Pane/PackageEditor.hs formatted f0501c777408 leksah-0.16.2.2/src/IDE/Pane/PackageFlags.hs formatted b852a84764b6 leksah-0.16.2.2/src/IDE/Pane/Search.hs-formatted fb6b3817d585 leksah-0.16.2.2/src/IDE/Pane/SourceBuffer.hs+formatted 1833ee2b3dc9 leksah-0.16.2.2/src/IDE/Pane/SourceBuffer.hs formatted 17fd33faa199 leksah-0.16.2.2/src/IDE/Pane/Trace.hs formatted 42dcc1c61a8c leksah-0.16.2.2/src/IDE/Pane/Variables.hs formatted 402db82d0213 leksah-0.16.2.2/src/IDE/Pane/WebKit/Documentation.hs@@ -3281,10 +3281,10 @@ formatted 9366da9eb866 pandoc-3.10.2/src/Text/Pandoc/Writers/Docx/Table.hs formatted a7b1f18b1b94 pandoc-3.10.2/src/Text/Pandoc/Writers/Docx/Types.hs formatted 393f60564bb0 pandoc-3.10.2/src/Text/Pandoc/Writers/DokuWiki.hs-formatted 63bfa4a35c66 pandoc-3.10.2/src/Text/Pandoc/Writers/EPUB.hs+formatted 34d053463a70 pandoc-3.10.2/src/Text/Pandoc/Writers/EPUB.hs formatted 18a7003d6454 pandoc-3.10.2/src/Text/Pandoc/Writers/FB2.hs formatted c4b4f9d5a6e4 pandoc-3.10.2/src/Text/Pandoc/Writers/GridTable.hs-formatted 52d1f99c1ff0 pandoc-3.10.2/src/Text/Pandoc/Writers/HTML.hs+formatted 98453ee060f9 pandoc-3.10.2/src/Text/Pandoc/Writers/HTML.hs formatted 6dff5ff11e27 pandoc-3.10.2/src/Text/Pandoc/Writers/Haddock.hs formatted c124279e4c0c pandoc-3.10.2/src/Text/Pandoc/Writers/ICML.hs formatted dd0566d62914 pandoc-3.10.2/src/Text/Pandoc/Writers/Ipynb.hs@@ -3302,7 +3302,7 @@ formatted a4eb629d0029 pandoc-3.10.2/src/Text/Pandoc/Writers/LaTeX/Types.hs formatted f86bcaf0241e pandoc-3.10.2/src/Text/Pandoc/Writers/LaTeX/Util.hs formatted 85fcfddb12db pandoc-3.10.2/src/Text/Pandoc/Writers/Man.hs-formatted b9f4a40b59a1 pandoc-3.10.2/src/Text/Pandoc/Writers/Markdown.hs+formatted d09a03d82e74 pandoc-3.10.2/src/Text/Pandoc/Writers/Markdown.hs formatted a7a53336237a pandoc-3.10.2/src/Text/Pandoc/Writers/Markdown/Inline.hs formatted c57101b9ec66 pandoc-3.10.2/src/Text/Pandoc/Writers/Markdown/Table.hs formatted 02c7eb593d88 pandoc-3.10.2/src/Text/Pandoc/Writers/Markdown/Types.hs@@ -3315,7 +3315,7 @@ formatted 780b39e3c620 pandoc-3.10.2/src/Text/Pandoc/Writers/OOXML.hs formatted 4c4123778a13 pandoc-3.10.2/src/Text/Pandoc/Writers/OPML.hs formatted 11f3bed781a4 pandoc-3.10.2/src/Text/Pandoc/Writers/OpenDocument.hs-formatted 7d585a078c42 pandoc-3.10.2/src/Text/Pandoc/Writers/Org.hs+formatted 4bae1768478d pandoc-3.10.2/src/Text/Pandoc/Writers/Org.hs formatted 74e7486aeef1 pandoc-3.10.2/src/Text/Pandoc/Writers/Powerpoint.hs formatted 9b93eaeae21e pandoc-3.10.2/src/Text/Pandoc/Writers/Powerpoint/Output.hs formatted 3cbe72b20538 pandoc-3.10.2/src/Text/Pandoc/Writers/Powerpoint/Presentation.hs@@ -4368,7 +4368,7 @@ formatted 015e3efd2d86 stack-9.9.9/src/System/Process/PagerEditor.hs formatted bc60fdf030d4 stack-9.9.9/src/System/Process/Read.hs formatted c40dad9ce8ee stack-9.9.9/src/System/Process/Run.hs-formatted e9412f975a02 stack-9.9.9/src/main/Main.hs+formatted 9366d8a339a8 stack-9.9.9/src/main/Main.hs formatted 4eec70869c80 stack-9.9.9/src/main/Plugins.hs formatted 50b246d28182 stack-9.9.9/src/main/Plugins/Commands.hs formatted 65c8c298f7aa stack-9.9.9/src/test/Spec.hs@@ -4567,7 +4567,7 @@ formatted a0bdad468ca4 text-2.1.4/scripts/CaseMapping.hs formatted 57e97be5ba61 text-2.1.4/scripts/SpecialCasing.hs formatted d775bb623580 text-2.1.4/scripts/UnicodeData.hs-partly-checked 0abe70d97042 text-2.1.4/src/Data/Text.hs+partly-checked 1e55021d157f text-2.1.4/src/Data/Text.hs formatted d3ff1f08422c text-2.1.4/src/Data/Text/Array.hs formatted 4970b89f4e7e text-2.1.4/src/Data/Text/Encoding.hs formatted d6231f7ebe99 text-2.1.4/src/Data/Text/Encoding/Error.hs@@ -4582,20 +4582,20 @@ formatted ab51e88834e1 text-2.1.4/src/Data/Text/Internal/Builder/RealFloat/Functions.hs formatted 7fa192160c9b text-2.1.4/src/Data/Text/Internal/ByteStringCompat.hs formatted 4815054ed2f6 text-2.1.4/src/Data/Text/Internal/Encoding.hs-formatted 43a1a670b3db text-2.1.4/src/Data/Text/Internal/Encoding/Fusion.hs+formatted 389325b86129 text-2.1.4/src/Data/Text/Internal/Encoding/Fusion.hs formatted 88e5c54a218b text-2.1.4/src/Data/Text/Internal/Encoding/Fusion/Common.hs formatted 875961737225 text-2.1.4/src/Data/Text/Internal/Encoding/Utf16.hs formatted 48ea4745d7e8 text-2.1.4/src/Data/Text/Internal/Encoding/Utf32.hs-formatted 999f28ae046f text-2.1.4/src/Data/Text/Internal/Encoding/Utf8.hs+formatted 26b2c3b56203 text-2.1.4/src/Data/Text/Internal/Encoding/Utf8.hs formatted da5c175cb49c text-2.1.4/src/Data/Text/Internal/Fusion.hs formatted 9ae139203b50 text-2.1.4/src/Data/Text/Internal/Fusion/CaseMapping.hs formatted 82caff784f76 text-2.1.4/src/Data/Text/Internal/Fusion/Common.hs formatted a3b0b51a0759 text-2.1.4/src/Data/Text/Internal/Fusion/Size.hs formatted 3c7c18d05ebf text-2.1.4/src/Data/Text/Internal/Fusion/Types.hs-formatted 32b50634031b text-2.1.4/src/Data/Text/Internal/IO.hs+formatted 54f6c2576b08 text-2.1.4/src/Data/Text/Internal/IO.hs formatted e2bb47936e11 text-2.1.4/src/Data/Text/Internal/IsAscii.hs formatted 538632ba614a text-2.1.4/src/Data/Text/Internal/Lazy.hs-formatted 76efe1116cd2 text-2.1.4/src/Data/Text/Internal/Lazy/Encoding/Fusion.hs+formatted 24939d0b9b8b text-2.1.4/src/Data/Text/Internal/Lazy/Encoding/Fusion.hs formatted c627413119e8 text-2.1.4/src/Data/Text/Internal/Lazy/Fusion.hs formatted a3c76abdf09c text-2.1.4/src/Data/Text/Internal/Lazy/Search.hs formatted e22062ee35e8 text-2.1.4/src/Data/Text/Internal/Measure.hs@@ -4621,7 +4621,7 @@ formatted 4053796b9111 text-2.1.4/src/Data/Text/Lazy/Read.hs formatted 95185ba2e9b4 text-2.1.4/src/Data/Text/Read.hs formatted a764e2fbcaba text-2.1.4/src/Data/Text/Show.hs-formatted 5aa1e6b706a4 text-2.1.4/src/Data/Text/Unsafe.hs+formatted ce35262cd7ed text-2.1.4/src/Data/Text/Unsafe.hs formatted 8c35444bb7ce text-2.1.4/tests/LiteralRuleTest.hs formatted 6c129d5c79c5 text-2.1.4/tests/Tests.hs formatted 8c9ba4deec7f text-2.1.4/tests/Tests/Lift.hs@@ -4644,7 +4644,7 @@ formatted f4e9f36f6be9 text-2.1.4/tests/Tests/SlowFunctions.hs formatted 375091fbe743 text-2.1.4/tests/Tests/Utils.hs formatted e865ae48f11b th-abstraction-0.7.2.0/Setup.hs-formatted 48b10de7e3db th-abstraction-0.7.2.0/src/Language/Haskell/TH/Datatype.hs+formatted ebfea5ec7f1c th-abstraction-0.7.2.0/src/Language/Haskell/TH/Datatype.hs formatted b67b7b9c968e th-abstraction-0.7.2.0/src/Language/Haskell/TH/Datatype/Internal.hs formatted 505471744185 th-abstraction-0.7.2.0/src/Language/Haskell/TH/Datatype/TyVarBndr.hs formatted fad455f51be7 th-abstraction-0.7.2.0/test/Harness.hs@@ -4966,7 +4966,7 @@ formatted e97eca400518 uuid-types-1.0.6.1/src/Data/UUID/Types.hs formatted a09b001e6a41 uuid-types-1.0.6.1/src/Data/UUID/Types/Internal.hs formatted d63508f44841 uuid-types-1.0.6.1/src/Data/UUID/Types/Internal/Builder.hs-formatted 14a92dc8286f uuid-types-1.0.6.1/tests/TestUUID.hs+formatted ed86f094a194 uuid-types-1.0.6.1/tests/TestUUID.hs formatted e865ae48f11b vector-0.13.2.0/Setup.hs formatted cb65eb293bd5 vector-0.13.2.0/benchlib/Bench/Vector/Algo/AwShCC.hs formatted 8a7c35b680c0 vector-0.13.2.0/benchlib/Bench/Vector/Algo/FindIndexR.hs@@ -5033,7 +5033,7 @@ formatted 385f1aecc6ac vector-algorithms-0.9.1.0/src/Data/Vector/Algorithms/Insertion.hs formatted c2cb20f40fd9 vector-algorithms-0.9.1.0/src/Data/Vector/Algorithms/Intro.hs formatted 4a50f751547a vector-algorithms-0.9.1.0/src/Data/Vector/Algorithms/Merge.hs-formatted 0d30a08d3940 vector-algorithms-0.9.1.0/src/Data/Vector/Algorithms/Optimal.hs+formatted f86bd27d6c5a vector-algorithms-0.9.1.0/src/Data/Vector/Algorithms/Optimal.hs formatted b7b0f718deb2 vector-algorithms-0.9.1.0/src/Data/Vector/Algorithms/Radix.hs formatted 47f1d28b6145 vector-algorithms-0.9.1.0/src/Data/Vector/Algorithms/Search.hs formatted 0a376532e9cb vector-algorithms-0.9.1.0/src/Data/Vector/Algorithms/Tim.hs
+ corpora/vendored/declaration/value/fixity-local-to-a-binding-out.hs view
@@ -0,0 +1,29 @@+-- A fixity declared among local bindings decides the layout of the operator+-- they bind.+module Setting.Fallback where++import Text.Read (readMaybe)++data Setting = Number Int | Switch Bool | Word String++parsed :: String -> Maybe Setting+parsed input =+ Number+ <$> readMaybe input+ <!> Switch+ <$> readMaybe input+ <!> pure (Word input)+ where+ infixl 3 <!>+ Nothing <!> y = y+ x <!> _ = x++firstOf :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int+firstOf a b c =+ let infixr 3 <!>+ Nothing <!> y = y+ x <!> _ = x+ in negate+ <$> a+ <!> b+ <!> c
+ corpora/vendored/declaration/value/fixity-local-to-a-binding.hs view
@@ -0,0 +1,29 @@+-- A fixity declared among local bindings decides the layout of the operator+-- they bind.+module Setting.Fallback where++import Text.Read (readMaybe)++data Setting = Number Int | Switch Bool | Word String++parsed :: String -> Maybe Setting+parsed input =+ Number+ <$> readMaybe input+ <!> Switch+ <$> readMaybe input+ <!> pure (Word input)+ where+ infixl 3 <!>+ Nothing <!> y = y+ x <!> _ = x++firstOf :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int+firstOf a b c =+ let infixr 3 <!>+ Nothing <!> y = y+ x <!> _ = x+ in negate+ <$> a+ <!> b+ <!> c
+ corpora/vendored/declaration/value/fixity-without-a-precedence-out.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE ExplicitNamespaces #-}++-- A fixity declaration written without a precedence stays without one.+module Rope.Join where++infixr `joinedTo`++infixl 5 `besides`++infix <~>, `against`++infixr type :~++class Splice r where+ infixl `splice`+ splice :: r -> r -> r++data a :~ b = a :~ b++joinedTo, besides, (<~>), against :: String -> String -> String+joinedTo = (++)+besides = (++)+(<~>) = (++)+against = (++)
+ corpora/vendored/declaration/value/fixity-without-a-precedence.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE ExplicitNamespaces #-}++-- A fixity declaration written without a precedence stays without one.+module Rope.Join where++infixr `joinedTo`+infixl 5 `besides`+infix <~>, `against`+infixr type :~++class Splice r where+ infixl `splice`+ splice :: r -> r -> r++data a :~ b = a :~ b++joinedTo, besides, (<~>), against :: String -> String -> String+joinedTo = (++)+besides = (++)+(<~>) = (++)+against = (++)
+ corpora/vendored/declaration/value/layout-as-written-out.hs view
@@ -0,0 +1,8 @@+module M where++onOneLine x = (x, x)++spreadOut x =+ ( x,+ x+ )
+ corpora/vendored/declaration/value/layout-as-written.hs view
@@ -0,0 +1,8 @@+module M where++onOneLine x = (x, x)++spreadOut x =+ ( x,+ x+ )
+ corpora/vendored/declaration/value/operator-beginning-with-a-hash-out.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE UnboxedTuples #-}++-- With unboxed tuples on, an operator that begins with # keeps the space+-- after an opening parenthesis, and # alone the space before a closing one,+-- so that neither reads as half of an unboxed tuple.+module Builder.Append+ ( ( #<| ),+ (<|#),+ ( # ),+ prepended,+ )+where++( #<| ), (<|#), ( # ) :: String -> String -> String+x #<| y = x ++ y+x <|# y = y ++ x+x # y = x ++ " " ++ y++infixr 5 #<|++prepended :: [String] -> [String]+prepended =+ map ( #<| "!") . map ("?" <|#) . map ("a" # ) . foldr (( #<| ) . pure) []
+ corpora/vendored/declaration/value/operator-beginning-with-a-hash.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE UnboxedTuples #-}++-- With unboxed tuples on, an operator that begins with # keeps the space+-- after an opening parenthesis, and # alone the space before a closing one,+-- so that neither reads as half of an unboxed tuple.+module Builder.Append+ ( ( #<| ),+ (<|#),+ ( # ),+ prepended,+ )+where++( #<| ), (<|#), ( # ) :: String -> String -> String+x #<| y = x ++ y+x <|# y = y ++ x+x # y = x ++ " " ++ y++infixr 5 #<|++prepended :: [String] -> [String]+prepended =+ map ( #<| "!") . map ("?" <|#) . map ("a" # ) . foldr (( #<| ) . pure) []
+ corpora/vendored/declaration/value/operator-chains-out.hs view
@@ -0,0 +1,18 @@+module M where++atTheLoosest =+ a * b+ + c * d++capturedLocally =+ a+ * b+ + c+ * d+ where+ infixl 8 ++ x + y = x++blockHandedOn = g $ do+ h+ i
+ corpora/vendored/declaration/value/operator-chains.hs view
@@ -0,0 +1,16 @@+module M where++atTheLoosest =+ a * b+ + c * d++capturedLocally =+ a * b+ + c * d+ where+ infixl 8 ++ x + y = x++blockHandedOn = g $ do+ h+ i
+ corpora/vendored/import/qualified-after-the-module-name-out.hs view
@@ -0,0 +1,6 @@+{-# LANGUAGE ImportQualifiedPost #-}++module M where++import Data.Map qualified as M+import Data.Set qualified as S
+ corpora/vendored/import/qualified-after-the-module-name.hs view
@@ -0,0 +1,4 @@+{-# LANGUAGE ImportQualifiedPost #-}+module M where+import Data.Map qualified as M+import qualified Data.Set as S
+ corpora/vendored/import/qualified-before-the-module-name-out.hs view
@@ -0,0 +1,4 @@+module M where++import qualified Data.Map as M+import qualified Data.Set as S
+ corpora/vendored/import/qualified-before-the-module-name.hs view
@@ -0,0 +1,3 @@+module M where+import Data.Map qualified as M+import qualified Data.Set as S
+ corpora/vendored/other/comment-beside-a-wildcard-out.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE PatternSynonyms #-}++-- A comment written beside the wildcard of an item stays inside its+-- parentheses.+module Ledger.Entry+ ( Entry ({- fields too -} ..),+ Side (Debit, {- and the rest -} ..),+ total,+ )+where++import Ledger.Money (Money (.. {- abstract upstream -}), add)++data Entry = Entry{entrySide :: Side, entryAmount :: Money}++data Side = Debit | Credit++total :: [Entry] -> Money+total = foldr (add . entryAmount) mempty
+ corpora/vendored/other/comment-beside-a-wildcard.hs view
@@ -0,0 +1,18 @@+{-# LANGUAGE PatternSynonyms #-}++-- A comment written beside the wildcard of an item stays inside its+-- parentheses.+module Ledger.Entry+ ( Entry({- fields too -}..)+ , Side(Debit, {- and the rest -} ..)+ , total+ ) where++import Ledger.Money (Money(.. {- abstract upstream -}), add)++data Entry = Entry { entrySide :: Side, entryAmount :: Money }++data Side = Debit | Credit++total :: [Entry] -> Money+total = foldr (add . entryAmount) mempty
+ corpora/vendored/other/comment-carried-on-under-a-remark-and-more-out.hs view
@@ -0,0 +1,20 @@+-- A comment under the remark that ends the line above, lined up with it or+-- indented further, carries the remark on, even where the code under it+-- begins at the same column.+data AttrTable a -- for all attribute identifiers not contained in the+ -- finite map the value is 'undef'+ = SoftTable a -- updated attributes+ | FrozenTable a++data Part+ = Literal String -- a literal string+ | Variable String -- some input; the+ -- string names the variable+ | Optional Part++decode :: String -> Either String String+decode b = -- returning an Either means the whole result+ -- is in memory at once anyway+ case reverse b of+ [] -> Left "empty"+ s -> Right s
+ corpora/vendored/other/comment-carried-on-under-a-remark-and-more.hs view
@@ -0,0 +1,20 @@+-- A comment under the remark that ends the line above, lined up with it or+-- indented further, carries the remark on, even where the code under it+-- begins at the same column.+data AttrTable a = -- for all attribute identifiers not contained in the+ -- finite map the value is 'undef'+ SoftTable a -- updated attributes+ | FrozenTable a++data Part+ = Literal String -- a literal string+ | Variable String -- some input; the+ -- string names the variable+ | Optional Part++decode :: String -> Either String String+decode b = -- returning an Either means the whole result+ -- is in memory at once anyway+ case reverse b of+ [] -> Left "empty"+ s -> Right s
+ corpora/vendored/other/comment-under-a-quasi-quotation-out.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE QuasiQuotes #-}++-- A comment under a quasi-quotation written over several lines stays out of+-- it.+module Physics.Body (setAngle, reindexed) where++setAngle :: Ptr Body -> Double -> IO ()+setAngle bodyPtr angle = [C.block| void {+ cpBodySetAngle($(cpBody* bodyPtr), $(double angle));+ } |]+ -- FIXME reindex++reindexed :: Ptr Body -> IO ()+reindexed bodyPtr = do+ [C.block| void {+ cpSpaceReindexShapesForBody($(cpBody* bodyPtr));+ } |]+ -- once per body+ pure ()
+ corpora/vendored/other/comment-under-a-quasi-quotation.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE QuasiQuotes #-}++-- A comment under a quasi-quotation written over several lines stays out of+-- it.+module Physics.Body (setAngle, reindexed) where++setAngle :: Ptr Body -> Double -> IO ()+setAngle bodyPtr angle = [C.block| void {+ cpBodySetAngle($(cpBody* bodyPtr), $(double angle));+ } |]+ -- FIXME reindex++reindexed :: Ptr Body -> IO ()+reindexed bodyPtr = do+ [C.block| void {+ cpSpaceReindexShapesForBody($(cpBody* bodyPtr));+ } |]+ -- once per body+ pure ()
+ corpora/vendored/other/comments-beside-an-alternative-and-a-bracket-out.hs view
@@ -0,0 +1,16 @@+module M where++atTheEnd = case x of+ a -> -- why+ b++onItsOwnLine = case x of+ a ->+ -- why+ b++beforeTheClosingBracket =+ [ a,+ b+ -- and that is all+ ]
+ corpora/vendored/other/comments-beside-an-alternative-and-a-bracket.hs view
@@ -0,0 +1,16 @@+module M where++atTheEnd = case x of+ a -> -- why+ b++onItsOwnLine = case x of+ a ->+ -- why+ b++beforeTheClosingBracket =+ [ a,+ b+ -- and that is all+ ]
+ corpora/vendored/other/comments-beside-the-where-of-the-header-out.hs view
@@ -0,0 +1,10 @@+-- Comments written before and after the where of the module header stay on+-- their side of it.+module Ledger.Book+-- everything is exported+where {- until the API settles -}++import Ledger.Entry (Entry)++opening :: Entry -> Int+opening _ = 0
+ corpora/vendored/other/comments-beside-the-where-of-the-header.hs view
@@ -0,0 +1,10 @@+-- Comments written before and after the where of the module header stay on+-- their side of it.+module Ledger.Book+ -- everything is exported+ where {- until the API settles -}++import Ledger.Entry (Entry)++opening :: Entry -> Int+opening _ = 0
+ corpora/vendored/other/comments-kept-where-written-out.hs view
@@ -0,0 +1,17 @@+module M where++-- a note+between = 1++inside = g+ where+ -- about g+ g = 1++selfDelimiting =+ {- here -} 1++notOnOneLine =+ ( -- here+ 1+ )
+ corpora/vendored/other/comments-kept-where-written.hs view
@@ -0,0 +1,17 @@+module M where++-- a note+between = 1++inside = g+ where+ -- about g+ g = 1++selfDelimiting =+ {- here -} 1++notOnOneLine =+ ( -- here+ 1+ )
+ corpora/vendored/other/comments-where-they-are-hard-to-put-back-out.hs view
@@ -0,0 +1,25 @@+module M where++-- | A heading++-- and a note under it+data T = T+ { a :: Int, -- the first+ -- the second+ b :: Int+ }++f x -- what to do with this?+ | x > 0 = g x+ | otherwise = h x+ where+ g = id++ -- about h+ h = negate++xs =+ [ one,+ {- inline -} two+ -- last+ ]
+ corpora/vendored/other/comments-where-they-are-hard-to-put-back.hs view
@@ -0,0 +1,25 @@+module M where++-- | A heading++-- and a note under it+data T = T+ { a :: Int, -- the first+ -- the second+ b :: Int+ }++f x -- what to do with this?+ | x > 0 = g x+ | otherwise = h x+ where+ g = id++ -- about h+ h = negate++xs =+ [ one,+ {- inline -} two+ -- last+ ]
+ corpora/vendored/other/cpp/code-after-an-optional-prefix-out.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE CPP #-}++-- Code that one branch puts a prefix in front of, written at the column of+-- the prefix, comes out once after the conditional.+module Properties.Json (jsonIso, label, retried) where++jsonIso :: Proxy a -> Property+jsonIso _ =+#if MIN_VERSION_QuickCheck(2,9,0)+ again $+#endif+ MkProperty $+ arbitrary >>= \x ->+ counterexample "decoded" (Just x == decode (encode x))++label :: String -> Property -> Property+label s =+#ifndef LAZY_LABELS+ s `seq`+#endif+ mapTotalResult $+ \res -> res{labels = s : labels res}++-- Written in each branch, it stays in each.+retried :: Property -> Property+retried p =+#if MIN_VERSION_QuickCheck(2,9,0)+ again $+ once p+#else+ once p+#endif
+ corpora/vendored/other/cpp/code-after-an-optional-prefix.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE CPP #-}++-- Code that one branch puts a prefix in front of, written at the column of+-- the prefix, comes out once after the conditional.+module Properties.Json (jsonIso, label, retried) where++jsonIso :: Proxy a -> Property+jsonIso _ =+#if MIN_VERSION_QuickCheck(2,9,0)+ again $+#endif+ MkProperty $+ arbitrary >>= \x ->+ counterexample "decoded" (Just x == decode (encode x))++label :: String -> Property -> Property+label s =+#ifndef LAZY_LABELS+ s `seq`+#endif+ mapTotalResult $+ \res -> res {labels = s : labels res}++-- Written in each branch, it stays in each.+retried :: Property -> Property+retried p =+#if MIN_VERSION_QuickCheck(2,9,0)+ again $+ once p+#else+ once p+#endif
+ corpora/vendored/other/cpp/where-after-an-export-list-in-a-conditional-out.hs view
@@ -0,0 +1,16 @@+{-# LANGUAGE CPP #-}++-- | Shapes, with an export list unless the doctests read the module.+module Shapes+#ifndef DOCTEST+ ( Shape (..),+ area,+ )+#endif+where++data Shape = Square Double | Circle Double++area :: Shape -> Double+area (Square a) = a * a+area (Circle r) = pi * r * r
+ corpora/vendored/other/cpp/where-after-an-export-list-in-a-conditional.hs view
@@ -0,0 +1,15 @@+{-# LANGUAGE CPP #-}+-- | Shapes, with an export list unless the doctests read the module.+module Shapes+#ifndef DOCTEST+ ( Shape (..)+ , area+ )+#endif+where++data Shape = Square Double | Circle Double++area :: Shape -> Double+area (Square a) = a * a+area (Circle r) = pi * r * r
+ corpora/vendored/other/extension-pack-before-what-it-enables-out.hs view
@@ -0,0 +1,4 @@+{-# LANGUAGE GHC2021 #-}+{-# LANGUAGE ApplicativeDo #-}++module M where
+ corpora/vendored/other/extension-pack-before-what-it-enables.hs view
@@ -0,0 +1,3 @@+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE GHC2021 #-}+module M where
+ corpora/vendored/other/extension-taken-back-from-the-edition-out.hs view
@@ -0,0 +1,6 @@+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE NoStarIsType #-}++module M (type (*)) where++import GHC.TypeLits (type (*))
+ corpora/vendored/other/extension-taken-back-from-the-edition.hs view
@@ -0,0 +1,4 @@+{-# LANGUAGE NoStarIsType #-}+{-# LANGUAGE TypeOperators #-}+module M (type (*)) where+import GHC.TypeLits (type (*))
+ corpora/vendored/other/language-pragmas-split-and-sorted-out.hs view
@@ -0,0 +1,4 @@+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}++module M where
+ corpora/vendored/other/language-pragmas-split-and-sorted.hs view
@@ -0,0 +1,2 @@+{-# LANGUAGE OverloadedStrings, GADTs #-}+module M where
+ corpora/vendored/signature/abstract-data-with-a-kind-out.hsig view
@@ -0,0 +1,7 @@+signature Shape where++import Data.Kind (Type)++data Shape :: Type++data Pair :: Type -> Type -> Type
+ corpora/vendored/signature/abstract-data-with-a-kind.hsig view
@@ -0,0 +1,6 @@+signature Shape where++import Data.Kind (Type)++data Shape :: Type+data Pair :: Type -> Type -> Type
+ corpora/vendored/signature/class-and-instances-out.hsig view
@@ -0,0 +1,10 @@+signature Ordered where++data Item++class Sized a where+ size :: a -> Int++instance Sized Item++instance Eq Item
+ corpora/vendored/signature/class-and-instances.hsig view
@@ -0,0 +1,10 @@+signature Ordered where++data Item++class Sized a where+ size :: a -> Int++instance Sized Item++instance Eq Item
+ corpora/vendored/signature/comments-after-the-last-declaration-out.hsig view
@@ -0,0 +1,10 @@+signature Ledger where++data Entry++instance Show Entry++post :: Entry -> IO () -- posted before the period closes,+ -- and never after++-- the journal is append-only
+ corpora/vendored/signature/comments-after-the-last-declaration.hsig view
@@ -0,0 +1,10 @@+signature Ledger where++data Entry++instance Show Entry++post :: Entry -> IO () -- posted before the period closes,+ -- and never after++-- the journal is append-only
+ corpora/vendored/signature/comments-in-a-signature-out.hsig view
@@ -0,0 +1,10 @@+signature Ledger where++-- the amounts are always in minor units+data Amount++data Entry -- one line of the journal++-- posted before the period closes,+-- and never after+post :: Entry -> IO ()
+ corpora/vendored/signature/comments-in-a-signature.hsig view
@@ -0,0 +1,10 @@+signature Ledger where++-- the amounts are always in minor units+data Amount++data Entry -- one line of the journal++-- posted before the period closes,+-- and never after+post :: Entry -> IO ()
+ corpora/vendored/signature/consecutive-type-signatures-out.hsig view
@@ -0,0 +1,7 @@+signature Store where++data Handle++open :: FilePath -> IO Handle++close :: Handle -> IO ()
+ corpora/vendored/signature/consecutive-type-signatures.hsig view
@@ -0,0 +1,5 @@+signature Store where++data Handle+open :: FilePath -> IO Handle+close :: Handle -> IO ()
+ corpora/vendored/signature/documented-declarations-out.hsig view
@@ -0,0 +1,7 @@+signature Clock where++-- | A reading of the wall clock.+data Instant++-- | The moment this was asked for.+now :: IO Instant
+ corpora/vendored/signature/documented-declarations.hsig view
@@ -0,0 +1,7 @@+signature Clock where++-- | A reading of the wall clock.+data Instant++-- | The moment this was asked for.+now :: IO Instant
+ corpora/vendored/signature/everything-above-the-keyword-out.hsig view
@@ -0,0 +1,8 @@+{-# LANGUAGE RoleAnnotations #-}++-- the parser leaves these in its own state, not in the tree+signature Buffer where++type role Buffer nominal++data Buffer a
+ corpora/vendored/signature/everything-above-the-keyword.hsig view
@@ -0,0 +1,8 @@+{-# LANGUAGE RoleAnnotations #-}++-- the parser leaves these in its own state, not in the tree+signature Buffer where++type role Buffer nominal++data Buffer a
+ corpora/vendored/signature/exports-listed-out.hsig view
@@ -0,0 +1,12 @@+signature Codec+ ( Codec,+ encode,+ decode,+ )+where++data Codec++encode :: Codec -> String++decode :: String -> Maybe Codec
+ corpora/vendored/signature/exports-listed.hsig view
@@ -0,0 +1,12 @@+signature Codec+ ( Codec,+ encode,+ decode,+ )+where++data Codec++encode :: Codec -> String++decode :: String -> Maybe Codec
+ corpora/vendored/signature/fixity-declaration-out.hsig view
@@ -0,0 +1,7 @@+signature Combine where++data Chunk++infixr 5 <+>++(<+>) :: Chunk -> Chunk -> Chunk
+ corpora/vendored/signature/fixity-declaration.hsig view
@@ -0,0 +1,7 @@+signature Combine where++data Chunk++infixr 5 <+>++(<+>) :: Chunk -> Chunk -> Chunk
+ corpora/vendored/signature/type-family-out.hsig view
@@ -0,0 +1,11 @@+{-# LANGUAGE TypeFamilies #-}++signature Element where++data Container++type family Elem c++type Name = String++elems :: Container -> [Elem Container]
+ corpora/vendored/signature/type-family.hsig view
@@ -0,0 +1,11 @@+{-# LANGUAGE TypeFamilies #-}++signature Element where++data Container++type family Elem c++type Name = String++elems :: Container -> [Elem Container]
+ corpora/vendored/signature/type-role-annotation-out.hsig view
@@ -0,0 +1,7 @@+{-# LANGUAGE RoleAnnotations #-}++signature Cache where++type role Cache nominal representational++data Cache k v
+ corpora/vendored/signature/type-role-annotation.hsig view
@@ -0,0 +1,7 @@+{-# LANGUAGE RoleAnnotations #-}++signature Cache where++type role Cache nominal representational++data Cache k v
src/Tilia/Cabal/Package.hs view
@@ -8,6 +8,8 @@ module Tilia.Cabal.Package ( PackageProblem (..), describePackageProblem,+ readPackageDescription,+ declaredModules, PackageReader, newPackageReader, ComponentSection (..),@@ -18,6 +20,7 @@ benchmarkBranches, addedSources, sourceExtensions,+ isDeclaredModule, joined, setupScript, )@@ -27,7 +30,7 @@ import Data.ByteString qualified as BS import Data.Generics.Schemes (listify) import Data.IORef-import Data.List (isSuffixOf, sortOn)+import Data.List (sortOn) import Data.List.NonEmpty qualified as NE import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map@@ -68,7 +71,7 @@ import GHC.Driver.Session qualified as GHC import GHC.LanguageExtensions.Type (Extension) import Language.Haskell.Extension qualified as Cabal-import System.Directory (canonicalizePath, doesDirectoryExist, listDirectory)+import System.Directory (canonicalizePath, doesDirectoryExist) import System.FilePath ( dropExtension, equalFilePath,@@ -79,7 +82,8 @@ takeExtension, (</>), )-import Tilia.Pragma (lookupExtension)+import Tilia.Cabal.Project (cabalFilesIn)+import Tilia.Pragma (applyExtensionName, lookupEdition) import Tilia.Process (readProgramOutput) import Tilia.Utils (attempted, quietly) @@ -132,7 +136,7 @@ findCabalFile covering from >>= \case Nothing -> pure (Left NoPackageFile) Just cabalFile ->- componentsOf compiler described cabalFile >>= \case+ componentBranchesOf compiler described cabalFile >>= \case Left problem -> pure (Left problem) Right components | equalFilePath file (takeDirectory cabalFile </> setupScript) ->@@ -172,15 +176,24 @@ covered = not . null . covering named c = any (declares c) (covering c) declares c d =- let path = under d- in Set.member path (componentEntries c)- || ( takeExtension path `elem` sourceExtensions- && Set.member (dropExtension path) (componentModules c)- )+ isDeclaredModule (componentModules c) (componentEntries c) (under d) under d = joinPath (drop (length (splitDirectories d)) (splitDirectories file)) nearness = maximum . fmap length . covering covering c = [d | d <- componentDirs c, d `covers` file] +-- | Is a file, named relative to a source directory, one of the modules or+-- entry points declared there?+isDeclaredModule ::+ -- | The modules, without their extensions.+ Set FilePath ->+ -- | The entry points.+ Set FilePath ->+ FilePath ->+ Bool+isDeclaredModule modules entries path =+ takeExtension path `elem` sourceExtensions+ && (Set.member path entries || Set.member (dropExtension path) modules)+ -- | Whether a file is somewhere under a directory. covers :: FilePath -> FilePath -> Bool covers directory file = go (splitDirectories directory) (splitDirectories file)@@ -209,9 +222,8 @@ case Map.lookup directory known of Just answer -> settle passed answer Nothing -> do- entries <- quietly [] (listDirectory directory)- case filter (".cabal" `isSuffixOf`) entries of- (named : _) -> settle (directory : passed) (Just (directory </> named))+ cabalFilesIn directory >>= \case+ (found : _) -> settle (directory : passed) (Just found) [] -> let parent = takeDirectory directory in if parent == directory@@ -222,12 +234,12 @@ pure answer -- | What a @.cabal@ file amounts to.-componentsOf ::+componentBranchesOf :: IO (Maybe Version) -> IORef (Map FilePath (Either PackageProblem [ComponentBranch])) -> FilePath -> IO (Either PackageProblem [ComponentBranch])-componentsOf compiler ref cabalFile = do+componentBranchesOf compiler ref cabalFile = do known <- readIORef ref case Map.lookup cabalFile known of Just answer -> pure answer@@ -237,18 +249,26 @@ pure answer where settle =- attempted (BS.readFile cabalFile) >>= \case- Left why -> pure (Left (PackageUnreadable cabalFile why))- Right bytes ->- case snd (runParseResult (parseGenericPackageDescription bytes)) of- Left (_, complaints) ->- pure (Left (PackageMalformed cabalFile (fmap said (NE.toList complaints))))- Right description -> do- valueOf <- conditionValues compiler description- Right- <$> traverse- (componentBranch (takeDirectory cabalFile))- (sectionsInForce valueOf description)+ readPackageDescription cabalFile >>= \case+ Left problem -> pure (Left problem)+ Right description -> do+ valueOf <- conditionValues compiler description+ Right+ <$> traverse+ (componentBranch (takeDirectory cabalFile))+ (sectionsInForce valueOf description)++-- | Read and parse a @.cabal@ file, or say why it cannot be.+readPackageDescription :: FilePath -> IO (Either PackageProblem GenericPackageDescription)+readPackageDescription cabalFile =+ attempted (BS.readFile cabalFile) >>= \case+ Left why -> pure (Left (PackageUnreadable cabalFile why))+ Right bytes ->+ pure $ case snd (runParseResult (parseGenericPackageDescription bytes)) of+ Left (_, complaints) ->+ Left (PackageMalformed cabalFile (fmap said (NE.toList complaints)))+ Right description -> Right description+ where said = T.pack . showPError cabalFile -- | The value a build of a package on this platform gives each variable its@@ -314,14 +334,29 @@ <> ownSection <> ComponentSection (builtSettings valueOf) [] [] )- <$> concat- [ foldMap libraryBranches (condLibrary described),- concatMap (libraryBranches . snd) (condSubLibraries described),- concatMap (executableBranches . snd) (condExecutables described),- concatMap (suiteBranches . snd) (condTestSuites described),- concatMap (benchmarkBranches . snd) (condBenchmarks described)- ]+ <$> packageBranches described +-- | Every branch of every component of a package, in the order declared.+packageBranches :: GenericPackageDescription -> [BranchSections]+packageBranches described =+ concat+ [ foldMap libraryBranches (condLibrary described),+ concatMap (libraryBranches . snd) (condSubLibraries described),+ concatMap (executableBranches . snd) (condExecutables described),+ concatMap (suiteBranches . snd) (condTestSuites described),+ concatMap (benchmarkBranches . snd) (condBenchmarks described)+ ]++-- | Every module any branch of a package declares, with where it would be+-- written relative to the package, without an extension.+declaredModules :: GenericPackageDescription -> [(Text, FilePath)]+declaredModules described =+ [ (T.intercalate "." (fmap T.pack (splitDirectories sub <> [name])), joined d (joined sub name))+ | branch <- packageBranches described,+ (d, section) <- addedSources branch,+ (sub, name) <- sectionModules section+ ]+ -- | What one branch of a component declares. data ComponentSection = ComponentSection { -- | Its build settings.@@ -476,13 +511,11 @@ where edition = ghcLanguage (fromMaybe Cabal.Haskell98 (defaultLanguage bi)) apply acc = \case- Cabal.EnableExtension e- | Just on <- named e, on `notElem` acc -> acc <> [on]- Cabal.DisableExtension e- | Just off <- named e -> filter (/= off) acc+ Cabal.EnableExtension e -> applyExtensionName acc (named e)+ Cabal.DisableExtension e -> applyExtensionName acc ("No" <> named e) _ -> acc- named = lookupExtension . T.pack . show+ named = T.pack . show -- | GHC's name for a language edition, when it has one. ghcLanguage :: Cabal.Language -> Maybe GHC.Language-ghcLanguage l = lookup (show l) [(show e, e) | e <- [minBound .. maxBound]]+ghcLanguage = lookupEdition . T.pack . show
+ src/Tilia/Cabal/Plan.hs view
@@ -0,0 +1,824 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++-- | A project's build plan, and the @cabal@ runs that make it ready to read.+module Tilia.Cabal.Plan+ ( -- * Build plans+ PlanPackage (..),+ PackageSource (..),+ isFetchable,+ sourceHashOf,+ cacheKey,+ BuildPlan (..),+ readBuildPlan,+ readGivenPlan,+ fetchUninstalled,+ getInstalledPackages,+ tokenForEnvAndBuildPlan,+ tokenForBuildPlan,+ macrosOf,+ versionMacros,+ plannedCompiler,+ generatedModules,++ -- * Readiness+ Readiness (..),+ PlanComponent (..),+ spellPlanComponent,+ plannedComponents,+ planPathFor,+ checkReadiness,+ plannedTarballs,+ packageCacheRoot,+ guessedPackageCacheRoot,+ Futility (..),+ undiscoveredFutility,+ futilityFor,+ prepareWith,+ loadPlan,+ )+where++import Control.Monad (filterM, join, void)+import Data.Aeson+ ( FromJSON (..),+ Value,+ decodeStrict,+ eitherDecodeFileStrict,+ withObject,+ (.:),+ (.:?),+ )+import Data.Aeson.Types (parseMaybe)+import Data.Choice (Choice, isTrue)+import Data.Foldable (toList, traverse_)+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 (fromMaybe, listToMaybe)+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 Data.Time (UTCTime, getCurrentTime)+import Distribution.Version (Version, mkVersion)+import GHC.IO.Handle (hDuplicate)+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 (hClose, hFlush, stderr)+import System.Info qualified+import System.Process+ ( StdStream (CreatePipe, Inherit, UseHandle),+ createProcess,+ cwd,+ proc,+ std_err,+ std_in,+ std_out,+ waitForProcess,+ )+import Tilia.Cabal.Project (cabalFilesIn)+import Tilia.Cpp.Macros (Macros (..))+import Tilia.Fixity.Cache+import Tilia.Fixity.PackageDb+import Tilia.Process (readProgramOutput)+import Tilia.Utils (quietly, readFileText, sha256Of)++----------------------------------------------------------------------------+-- 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 :: Cache -> IO [InstalledPackage]+getInstalledPackages cache =+ cachedInstalled cache >>= \case+ Just packages -> pure packages+ Nothing -> do+ found <- readInstalledPackages+ storeInstalled cache found+ pure (installedPackages found)++-- | 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+ . sha256Of+ $ 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++-- | How a package's cached answers are filed.+cacheKey :: PlanPackage -> Text+cacheKey p =+ ppName p <> "-" <> ppVersion p <> maybe "" (("-" <>) . T.take 16) (sourceHashOf p)++-- | 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 =+ decodePlan path >>= traverse (checkedOutIn (takeDirectory (takeDirectory path)))++-- | 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 =+ fmap (\plan -> plan{bpPackages = fmap rooted (bpPackages plan)})+ <$> decodePlan path+ where+ rooted p = case ppSource p of+ LocalPackage dir+ | isRelative dir ->+ p{ppSource = LocalPackage (root </> dir)}+ _ -> p++-- | Decode a plan, or say why it cannot be.+decodePlan :: FilePath -> IO (Either Text BuildPlan)+decodePlan path =+ doesFileExist path >>= \case+ False -> pure (Left ("no build plan at " <> T.pack path))+ True -> either (Left . T.pack) Right <$> eitherDecodeFileStrict path++-- | 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 =+ ( versionMacros+ (numberedVersion =<< T.stripPrefix "ghc-" (bpCompiler plan))+ (Map.mapMaybe sole versions)+ )+ { macroPlannedPackages =+ Just (Set.fromList [underscored (ppName p) | p <- bpPackages plan])+ }+ where+ versions =+ Map.fromListWith+ Set.union+ [ (ppName p, Set.singleton v)+ | p <- bpPackages plan,+ Just v <- [numberedVersion (ppVersion p)]+ ]+ sole vs = case Set.toList vs of+ [v] -> Just v+ _ -> Nothing++-- | The version macros the compiler's version and each package's settle.+versionMacros ::+ -- | The compiler's version.+ Maybe [Integer] ->+ -- | Each package's version, by name.+ Map Text [Integer] ->+ Macros+versionMacros given versions =+ mempty+ { macroExactVersions =+ Map.fromList+ ( [ ("MIN_VERSION_" <> underscored name, version)+ | (name, version) <- Map.toList versions+ ]+ <> [("MIN_VERSION_GLASGOW_HASKELL", v) | v <- toList compiler]+ ),+ macroExactNumbers =+ 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)+ ]+ ],+ macroUndefined =+ if null compiler+ then Set.empty+ else Set.fromList ["__MHS__", "__HUGS__"]+ }+ where+ compiler = case given of+ Just parts@(_ : _ : _) -> Just (take 4 (parts <> repeat 0))+ _ -> Nothing+ nth i xs = if i < length xs then xs !! i else 0++-- | The version of GHC a plan is for, where it is for GHC.+plannedCompiler :: BuildPlan -> Maybe Version+plannedCompiler plan =+ mkVersion . fmap fromInteger+ <$> (numberedVersion =<< T.stripPrefix "ghc-" (bpCompiler plan))++-- | 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.+spellPlanComponent :: PlanComponent -> Text+spellPlanComponent 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+ solved <- cachedSolveTime =<< openCache caching =<< tokenForBuildPlan plan+ newer <- filesNewerThanPlan plan projectDir solved+ let covered = plannedComponents plan+ missing = [spellPlanComponent 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+ cache <- openCache caching =<< tokenForBuildPlan plan+ tarballs <- filter (isFetchable . fst) <$> plannedTarballs cache plan+ absent <- fmap fst <$> filterM (fmap not . doesFileExist . snd) tarballs+ if null absent+ then pure []+ else do+ installed <- getInstalledPackages cache+ pure (filter (not . builtAlready installed) absent)++-- | 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, and+-- since the last solve, which leaves a plan it would not change as it was.+filesNewerThanPlan :: BuildPlan -> FilePath -> Maybe UTCTime -> IO [FilePath]+filesNewerThanPlan plan projectDir solved = quietly [] $ do+ written <- getModificationTime (planPathFor projectDir)+ let planTime = maybe written (max written) solved+ atRoot <- quietly [] (listDirectory projectDir)+ atRootPackage <- cabalFilesIn projectDir+ inPackages <- concat <$> traverse cabalFilesIn (localDirs plan)+ let candidates =+ [projectDir </> f | f <- atRoot, f `elem` projectFiles]+ <> atRootPackage+ <> 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)]+ isNewerThan planTime path = quietly Nothing $ do+ t <- getModificationTime path+ pure (if t > planTime then Just path else Nothing)++-- | 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 (),+ -- | Remember when a solve started, so that no change made before it+ -- makes the plan stale.+ rememberSolveTime :: UTCTime -> 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 ()),+ rememberSolveTime = 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),+ rememberSolveTime = \started -> withCache () (`storeSolveTime` started)+ }+ where+ withCache fallback use =+ readBuildPlan (planPathFor projectDir) >>= \case+ Left _ -> pure fallback+ Right plan -> use =<< openCache caching =<< tokenForBuildPlan plan++-- | Do whatever is missing, 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 = do+ started <- getCurrentTime+ tryWholeProject ["build", ":all", "--dry-run"] >>= \case+ Left err -> pure (Left err)+ Right () -> do+ rememberSolveTime futility started+ 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+ -- A pipe closed at once rather than our standard input, which in an+ -- editor's process carries what the editor says to it.+ (toChild, _, _, running) <-+ createProcess+ (proc "cabal" args)+ { cwd = Just projectDir,+ std_in = CreatePipe,+ std_out = UseHandle passed,+ std_err = Inherit+ }+ traverse_ hClose toChild+ 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+ prepareWith+ caching+ downloading+ (runCabal projectDir)+ (futilityFor caching projectDir)+ 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: under the package cache @cabal@ last named, unless+-- a tarball it downloads is not there, when @cabal@ is asked again.+plannedTarballs :: Cache -> BuildPlan -> IO [(PlanPackage, FilePath)]+plannedTarballs cache plan =+ cachedPackageCache cache >>= \case+ Just remembered -> do+ tarballs <- under remembered+ absent <-+ filterM+ (fmap not . doesFileExist . snd)+ (filter (isFetchable . fst) tarballs)+ if null absent then pure tarballs else asked+ Nothing -> asked+ where+ asked = do+ cacheRoot <- packageCacheRoot+ storePackageCache cache cacheRoot+ under cacheRoot+ under cacheRoot = do+ repos <- quietly [] (Data.List.sort <$> listDirectory cacheRoot)+ traverse+ (\p -> (,) p <$> tarballFor cacheRoot repos p)+ [p | p <- bpPackages plan, not (isLocal p)]+ 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
src/Tilia/Cabal/Project.hs view
@@ -7,17 +7,20 @@ Marker (..), markerFile, findProjectRoot,+ cabalFilesIn, ) where -import Data.List (isSuffixOf)+import Control.Monad (filterM)+import Data.List (isSuffixOf, sort) import Data.Maybe (listToMaybe) import System.Directory ( canonicalizePath, doesDirectoryExist,+ doesFileExist, listDirectory, )-import System.FilePath (takeDirectory)+import System.FilePath (takeDirectory, takeFileName, (</>)) import Tilia.Utils (quietly) -- | A project, and what marked it out.@@ -61,9 +64,15 @@ markersIn directory = quietly ([], []) $ do entries <- listDirectory directory+ packages <- cabalFilesIn directory pure ( [ProjectRoot directory ProjectFile | "cabal.project" `elem` entries],- [ ProjectRoot directory (PackageFile named)- | named <- take 1 (filter (".cabal" `isSuffixOf`) entries)- ]+ [ProjectRoot directory (PackageFile (takeFileName p)) | p <- take 1 packages] )++-- | The @.cabal@ files sitting directly in a directory, in order.+cabalFilesIn :: FilePath -> IO [FilePath]+cabalFilesIn directory = quietly [] $ do+ entries <- listDirectory directory+ let named = sort (filter (".cabal" `isSuffixOf`) entries)+ filterM doesFileExist (fmap (directory </>) named)
src/Tilia/Cabal/Target.hs view
@@ -22,7 +22,6 @@ import Data.ByteString.Char8 qualified as BS8 import Data.Char (toLower) import Data.List (isPrefixOf, isSuffixOf, sort)-import Data.List.NonEmpty qualified as NE import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Set (Set)@@ -31,7 +30,6 @@ import Data.Text qualified as T import Data.Text.Encoding qualified as T import Distribution.Fields.Field (Field (..), FieldLine (..), Name (..))-import Distribution.Fields.ParseResult (runParseResult) import Distribution.Fields.Parser (readFields) import Distribution.PackageDescription ( GenericPackageDescription (..),@@ -39,33 +37,32 @@ unPackageName, unUnqualComponentName, )-import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)-import Distribution.Parsec (showPError) import Distribution.Types.PackageId (PackageIdentifier (..)) import System.Directory (doesFileExist, listDirectory) import System.FilePath- ( dropExtension,- dropTrailingPathSeparator,+ ( dropTrailingPathSeparator, normalise, takeDirectory,- takeExtension, (</>), ) import Tilia.Cabal.Package ( ComponentSection (..),+ PackageProblem, addedSources, benchmarkBranches,+ describePackageProblem, executableBranches,+ isDeclaredModule, joined, libraryBranches,+ readPackageDescription, setupScript,- sourceExtensions, suiteBranches, )-import Tilia.Cabal.Project (Marker (..), ProjectRoot (..), markerFile)-import Tilia.Fixity.Plan (PlanComponent (..))+import Tilia.Cabal.Plan (PlanComponent (..))+import Tilia.Cabal.Project (Marker (..), ProjectRoot (..), cabalFilesIn, markerFile) import Tilia.Ignore (belowRoot, ignoreFilesAbove, isIgnored)-import Tilia.Utils (attempted, quietly)+import Tilia.Utils (fieldWords, quietly) -- | Which components a run was asked for. data Target@@ -147,18 +144,14 @@ -- | Is a directory entry one of the files wanted there? admits :: Wanted -> FilePath -> Bool-admits Wanted{..} entry =- takeExtension entry `elem` sourceExtensions- && ( Set.member entry wantedEntries- || Set.member (dropExtension entry) wantedModules- )+admits Wanted{..} = isDeclaredModule wantedModules wantedEntries -- | Why a run could not work out what to format. data TargetProblem = -- | A @cabal.project@ naming packages, none of which could be found. NoPackages FilePath- | -- | A @.cabal@ file that would not parse, and what the parser said.- Unparseable FilePath [Text]+ | -- | A @.cabal@ file that could not be read or parsed, and why.+ UnreadablePackage PackageProblem | -- | A target naming no component the project holds. NoSuchTarget Text [Text] deriving (Eq, Show)@@ -168,8 +161,7 @@ describeTargetProblem = \case NoPackages file -> T.pack file <> " names no packages that exist"- Unparseable file complaints ->- T.pack file <> " does not parse:" <> foldMap ("\n " <>) complaints+ UnreadablePackage problem -> describePackageProblem problem NoSuchTarget asked available -> "no component matches " <> asked@@ -261,7 +253,7 @@ directories = Map.unionsWith (<>)- [ Map.mapKeys (joined (directoryOf (componentRoot c))) (componentSources c)+ [ Map.mapKeys (joined (asDirectory (componentRoot c))) (componentSources c) | c <- components ] present (directory, wanted) = do@@ -272,8 +264,8 @@ -- | A directory spelled so that a path can be appended to it without -- further normalisation.-directoryOf :: FilePath -> FilePath-directoryOf = dropTrailingPathSeparator . normalise+asDirectory :: FilePath -> FilePath+asDirectory = dropTrailingPathSeparator . normalise -- | The @.cabal@ files the project is made of. --@@ -303,11 +295,7 @@ [T.unwords [T.decodeUtf8Lenient value | FieldLine _ value <- ls]] | otherwise -> [] Section _ _ inner -> packagesIn inner- entries =- filter (not . T.null)- . fmap T.strip- . concatMap (T.split (== ','))- . T.words+ entries = fieldWords -- | Turn one entry of a @packages@ field into the @.cabal@ files it names. packageToCabalFile :: FilePath -> Text -> IO [FilePath]@@ -348,40 +336,27 @@ before `isPrefixOf` name && after `isSuffixOf` drop (length before) name --- | The @.cabal@ files sitting directly in a directory.-cabalFilesIn :: FilePath -> IO [FilePath]-cabalFilesIn directory = quietly [] $ do- entries <- listDirectory directory- let named = sort (filter (".cabal" `isSuffixOf`) entries)- filterM doesFileExist (fmap (directory </>) named)- -- | Every component one @.cabal@ file declares. componentsInCabalFile :: FilePath -> IO (Either TargetProblem [Component]) componentsInCabalFile cabalFile =- attempted (BS.readFile cabalFile) >>= \case- Left why -> pure (Left (Unparseable cabalFile [why]))- Right bytes ->- case snd (runParseResult (parseGenericPackageDescription bytes)) of- Left (_, complaints) ->- pure (Left (Unparseable cabalFile (fmap said (NE.toList complaints))))- where- said = T.pack . showPError cabalFile- Right described -> do- let root = takeDirectory cabalFile- declared = declaredComponents root described- hasSetup <- quietly False (doesFileExist (root </> setupScript))- pure . Right $- declared- <> [ Component- { componentPackage = packageOf described,- componentKind = Setup,- componentName = "setup",- componentRoot = root,- componentSources =- Map.singleton "." (Wanted Set.empty (Set.singleton setupScript))- }- | hasSetup- ]+ readPackageDescription cabalFile >>= \case+ Left problem -> pure (Left (UnreadablePackage problem))+ Right described -> do+ let root = takeDirectory cabalFile+ declared = declaredComponents root described+ hasSetup <- quietly False (doesFileExist (root </> setupScript))+ pure . Right $+ declared+ <> [ Component+ { componentPackage = packageOf described,+ componentKind = Setup,+ componentName = "setup",+ componentRoot = root,+ componentSources =+ Map.singleton "." (Wanted Set.empty (Set.singleton setupScript))+ }+ | hasSetup+ ] -- | The name of a package. packageOf :: GenericPackageDescription -> Text@@ -422,7 +397,7 @@ componentSources = Map.unionsWith (<>)- [ sourcesIn (directoryOf d) declared+ [ sourcesIn (asDirectory d) declared | branch <- branches, (d, declared) <- addedSources branch ]
src/Tilia/Comments.hs view
@@ -16,11 +16,14 @@ holdsOff, continuesHaddock, carriedOnFrom,+ carriesOn, widenTrigger, asOrdinary, triggerEscaped, opensHaddock, opensSectionHeading,+ afterOpener,+ triggerOn, commentsWithin, Pragma (..), commentPragma,@@ -34,10 +37,10 @@ import Data.Maybe (isJust, listToMaybe, mapMaybe) import Data.Text (Text) import Data.Text qualified as T-import GHC.Parser.Annotation qualified as GHC+import GHC.Parser.Lexer qualified as GHC import GHC.Types.SrcLoc qualified as GHC import Tilia.Source.Lines (Lines, blankAt, directiveOnLine, lineAt, lineTexts)-import Tilia.Span (Span (..), endPoint, startPoint)+import Tilia.Span (Span (..), covers) import Tilia.Span.Ghc (spanOfReal) -- | One comment.@@ -95,22 +98,21 @@ commentsOf :: -- | The module's lines, which every comment is read against. Lines ->- -- | The module's comments, in source order.- [GHC.LEpaComment] ->+ -- | The module's comment tokens, in source order, each where the lexer+ -- found it.+ [(GHC.RealSrcSpan, GHC.Token)] -> [Comment]-commentsOf ls = fmap (uncurry (mkComment ls)) . mapMaybe located- where- located (GHC.L anchor (GHC.EpaComment tok _)) = case anchor of- GHC.EpaSpan (GHC.RealSrcSpan s _) -> Just (s, tok)- _ -> Nothing+commentsOf ls = mapMaybe (uncurry (mkComment ls)) --- | Build a comment from a token and the span it occupied.-mkComment :: Lines -> GHC.RealSrcSpan -> GHC.EpaCommentTok -> Comment-mkComment ls spn tok = case tok of- GHC.EpaLineComment t -> commentAt ls s LineComment (T.pack t)- GHC.EpaBlockComment t -> commentAt ls s BlockComment (T.pack t)- GHC.EpaDocComment _ -> commentAt ls s DocComment (sliceSpan (lineTexts ls) s)- GHC.EpaDocOptions t -> commentAt ls s LineComment (T.pack t)+-- | Build a comment from a token and the span it occupied, where the token+-- is a comment.+mkComment :: Lines -> GHC.RealSrcSpan -> GHC.Token -> Maybe Comment+mkComment ls spn = \case+ GHC.ITlineComment t _ -> Just (commentAt ls s LineComment (T.pack t))+ GHC.ITblockComment t _ -> Just (commentAt ls s BlockComment (T.pack t))+ GHC.ITdocComment _ _ -> Just (commentAt ls s DocComment (sliceSpan (lineTexts ls) s))+ GHC.ITdocOptions t _ -> Just (commentAt ls s LineComment (T.pack t))+ _ -> Nothing where s = spanOfReal spn @@ -255,12 +257,27 @@ (_ :| []) -> True _ -> False +-- | Does a comment under a remark carry it on, rather than go with the code+-- after it: is it no further left than the remark, or does that code begin+-- further left than the comment?+carriesOn ::+ -- | The column the remark begins at.+ Int ->+ -- | The comment.+ Span ->+ -- | Where the code after it begins, if any follows.+ Maybe Span ->+ Bool+carriesOn remark comment after =+ remark <= spanStartColumn comment+ || all (\r -> spanStartColumn r < spanStartColumn comment) after+ -- | The line whose trailing comment a comment with the line to itself -- carries on, by being lined up under that line or that comment, or under -- comments that carry it on in turn, and the column that comment begins at. carriedOnFrom :: [Comment] -> Comment -> Maybe (Int, Int) carriedOnFrom cs = \c ->- if commentTrailing c+ if commentTrailing c || commentFollowed c then Nothing else go@@ -286,7 +303,7 @@ ] go column line above | Just remark <- IntMap.lookup line remarks,- above == ContentAt column || remark == column =+ above == ContentAt column || remark <= column = Just (line, remark) | Just (col, above') <- IntMap.lookup line ownLineComments, col == column,@@ -372,13 +389,9 @@ -- the point of escaping—so anything wanting to know whether a comment -- started life as a Haddock has to ask this. triggerEscaped :: Comment -> Bool-triggerEscaped c = case openerWidth headLine of- Nothing -> False- Just n -> case T.uncons (T.dropWhile (== ' ') (T.drop n headLine)) of- Just ('\\', rest) -> triggered rest- _ -> False- where- headLine = NE.head (commentBody c)+triggerEscaped c = case T.uncons =<< afterOpener (NE.head (commentBody c)) of+ Just ('\\', rest) -> triggered rest+ _ -> False -- | Does this text begin with one of the characters that opens a Haddock? triggered :: Text -> Bool@@ -399,15 +412,17 @@ splitTrigger :: Text -> Maybe (Text, Text) splitTrigger l = do n <- openerWidth l- let (opener, afterOpener) = T.splitAt n l- (gap, rest) = T.span (== ' ') afterOpener- (trigger, body) <- case T.uncons rest of- Just ('|', b) -> Just ("|", b)- Just ('^', b) -> Just ("^", b)- Just ('*', _) -> Just (T.span (== '*') rest)- _ -> Nothing- pure (opener <> gap <> trigger, body)+ let (opener, rest) = T.splitAt n l+ (gap, inside) = T.span (== ' ') rest+ trigger <- triggerOn inside+ pure (opener <> gap <> trigger, T.drop (T.length trigger) inside) +-- | What follows the @--@ or @{-@ that opens a comment, and the spaces after+-- it, if the line opens one.+afterOpener :: Text -> Maybe Text+afterOpener l = (\n -> T.dropWhile (== ' ') (T.drop n l)) <$> openerWidth l+{-# INLINE afterOpener #-}+ -- | How many characters open a comment, if it opens one. openerWidth :: Text -> Maybe Int openerWidth l@@ -415,11 +430,18 @@ | "{-" `T.isPrefixOf` l = Just 2 | otherwise = Nothing +-- | The trigger at the start of what follows a comment's opener: @|@, @^@,+-- or a run of @*@.+triggerOn :: Text -> Maybe Text+triggerOn inside = case T.uncons inside of+ Just ('|', _) -> Just "|"+ Just ('^', _) -> Just "^"+ Just ('*', _) -> Just (T.takeWhile (== '*') inside)+ _ -> Nothing+ -- | The comments written inside a region. commentsWithin :: Span -> [Comment] -> [Comment]-commentsWithin s = filter (within . commentSpan)- where- within c = startPoint s <= startPoint c && endPoint c <= endPoint s+commentsWithin s = filter (covers s . commentSpan) -- | A compiler pragma, which is written as a block comment but is not one. --
src/Tilia/Comments/Attach.hs view
@@ -162,7 +162,7 @@ DLocated s d -> let (mine, p') = claimPlaced s p (d', p'') = go p' d- write = foldMap (uncurry (writtenAs margin (isEmptyAnchor s)))+ write = foldMap (uncurry (writtenAs margin (zeroWidth s))) before' = (Before,) <$> heldOffFrom d [c | (Before, c) <- mine] after' =@@ -190,11 +190,8 @@ in (DVariant a' b', p') d -> (d, p) --- | Hold the last comment off a Haddock about to be written under it.------ Only a comment written as @--@ lines needs holding off: the lexer would--- read it and the Haddock under it as one comment. A @{- … -}@ ends at its--- own bracket and may sit against whatever follows.+-- | Hold the last comment, if written as @--@ lines, off a Haddock about to+-- be written under it. heldOffFrom :: Doc -> [Comment] -> [Comment] heldOffFrom d cs = case unsnoc cs of Just (earlier, c)@@ -227,11 +224,6 @@ DBreak -> ([], layout == Broken) DSoftBreak -> ([], layout == Broken) _ -> ([], False)---- | Is this region an 'emptyAnchor' rather than one covering something--- written?-isEmptyAnchor :: Span -> Bool-isEmptyAnchor s = startPoint s == endPoint s -- | Render one 'Comment' where it was placed. writtenAs ::
src/Tilia/Comments/Place.hs view
@@ -27,6 +27,7 @@ ( Above (..), Comment (..), carriedOnFrom,+ carriesOn, closesItself, commentTrailing, singleLine,@@ -158,7 +159,7 @@ onThatLine = IntMap.findWithDefault [] line regionsByEndLine candidate r = endPoint r <= startPoint here- && startPoint r /= endPoint r+ && not (zeroWidth r) && not (fencedOff r) writtenAgainst = maybe False (`Set.member` regionEndPoints) stopsAt@@ -175,18 +176,15 @@ next = snd <$> Map.lookupGE (endPoint here) regionsByStartPoint continues- | nothingBelowItLinesUp,- Just (anchor, remark) <- carriedOn c =+ | Just (anchor, remark) <- carriedOn c,+ carriesOn remark here next = (,linedUpWith remark) <$> endingOn anchor | otherwise = Nothing linedUpWith remark- | remark == spanStartColumn here = After SpillUnderPrevious+ | remark <= spanStartColumn here = After SpillUnderPrevious | otherwise = After SpillAtIndentation - nothingBelowItLinesUp =- all (\r -> spanStartColumn r < spanStartColumn here) next- under | Just (top, bottom) <- IntMap.lookup (spanStartLine here) runs, Just (line, column) <- commentNextLine bottom,@@ -204,7 +202,7 @@ onThatLine = IntMap.findWithDefault [] line regionsByEndLine lineEnd = maximum (fmap endPoint onThatLine) linedUp r =- startPoint r /= endPoint r+ not (zeroWidth r) && endPoint r == lineEnd && (startsUnderIt r || holdsTheLine r) && not (fencedOff r)
src/Tilia/Cpp.hs view
@@ -19,17 +19,8 @@ -- * Splitting Guard (..), Configurations (..),- configurations,- leaves, branchLeaves, correspondingBranches,- linearLeaves,- countLeaves,- answeredLeaves,- answeredLinearLeaves,-- -- * Diagnostics- regions, ) where @@ -42,7 +33,6 @@ import Data.IntMap.Strict qualified as IntMap import Data.List (sort) import Data.List.NonEmpty (NonEmpty, nonEmpty)-import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Maybe (catMaybes, fromMaybe, isNothing, listToMaybe, mapMaybe, maybeToList) import Data.Monoid (Any (..))@@ -55,7 +45,7 @@ import Tilia.Cpp.Fragment (bodyOf, fragmentText, fragmentsOf, linesHeld, reassembled) import Tilia.Cpp.Merge (Settled (..), combine, merge) import Tilia.Cpp.Place (CommentSummary, restoreUnprinted, summarizeComments)-import Tilia.Doc (defaultRenderOptions, printDoc)+import Tilia.Doc (printDoc) import Tilia.Doc.Internal ( Doc (..), Layout (..),@@ -82,11 +72,7 @@ dropping, linesOf, )-import Tilia.Span- ( Span,- spanEndLine,- spanStartLine,- )+import Tilia.Span (spanEndLine, spanStartLine) ---------------------------------------------------------------------------- -- Formatting@@ -113,18 +99,22 @@ evalState ( runExceptT ( formatAllConfigs- parser- known- path- (reading parser known path (noAnswers source) source)+ formatter+ (reading formatter (noAnswers source) source) ) ) (configurationBudget * linesHeld source) formatted <-- printDoc defaultRenderOptions+ printDoc <$> restoreUnprinted source found document formatted <$ traverse_ (Left . RuledOutBranch) (ruledOutBranch formatted) where+ formatter =+ Formatter+ { formatterParser = parser,+ formatterRender = known,+ formatterPath = path+ } known = render { rcImportBarriers = maybe [] (importBarriers parser source) groups,@@ -198,6 +188,16 @@ TooManyConfigurations -> refuse TooManyConfigurations _ -> pure Nothing +-- | What every configuration of a module is parsed and printed with.+data Formatter = Formatter+ { -- | What to parse a configuration with.+ formatterParser :: ParserConfig,+ -- | What to print it with.+ formatterRender :: RenderConfig,+ -- | The file this is, for the positions in a parse error.+ formatterPath :: FilePath+ }+ -- | A configuration, with what its conditionals come to worked out once -- for everything that asks. data Reading = Reading@@ -219,18 +219,14 @@ -- | Read a configuration. reading ::- -- | What to parse a configuration with.- ParserConfig ->- -- | What to print it with.- RenderConfig ->- -- | The file this is, for the positions in a parse error.- FilePath ->+ -- | What to parse and print each configuration with.+ Formatter -> -- | How this configuration was reached. Reached -> -- | Input text. Text -> Reading-reading parser render path reached source =+reading formatter reached source = Reading { readingReached = reached, readingSource = source,@@ -238,10 +234,11 @@ readingApart = do apart <- withForest (`variations` source) let baseline = without (vaBaselineDropped apart) reached- pure (apart, reading parser render path baseline (vaBaseline apart)),- readingPragma = withForest (\f -> splitOnPragma parser f source),+ pure (apart, reading formatter baseline (vaBaseline apart)),+ readingPragma =+ withForest (\f -> splitOnPragma (formatterParser formatter) f source), readingFormatted =- formatSingleConfig parser render path reached (withoutOpaque source)+ formatSingleConfig formatter reached (withoutOpaque source) } where forest = readConditionals source@@ -266,16 +263,12 @@ -- | Format every configuration of a module, and merge them into one -- document, with what they found besides their code. formatAllConfigs ::- -- | What to parse a configuration with.- ParserConfig ->- -- | What to print it with.- RenderConfig ->- -- | The file this is, for the positions in a parse error.- FilePath ->+ -- | What to parse and print each configuration with.+ Formatter -> -- | The configuration. Reading -> Spending (Doc, CommentSummary)-formatAllConfigs parser render path r = do+formatAllConfigs formatter r = do forest <- except (readingForest r) case readingApart r of Nothing -> do@@ -289,10 +282,10 @@ base <- if isNothing pragma || affordable then- attempt (formatAllConfigs parser render path baseline)+ attempt (formatAllConfigs formatter baseline) else pure Nothing fragmented <- case (pragma, base) of- (Nothing, Just b) -> join <$> attempt (inFragments parser render path reached forest source b)+ (Nothing, Just b) -> join <$> attempt (inFragments formatter reached forest source b) _ -> pure Nothing linear <- case (fragmented, base) of (Nothing, Just b) | affordable -> oneAtATime apart b@@ -300,13 +293,13 @@ case fragmented <|> linear of Just found -> pure found Nothing -> case maybeToList pragma <> forest of- g : _ -> together parser render path reached (configurationsOn g forest source)+ g : _ -> together formatter reached (configurationsOn g forest source) [] -> error "Tilia: a module that varies has a conditional to split on" where reached = readingReached r source = readingSource r oneAtATime apart base = do- varied <- attempt (separately parser render path reached apart base)+ varied <- attempt (separately formatter reached apart base) pure $ do (merged, found) <- varied d <- combine Broken (fst base) (zip (fmap cfgWholes (vaGroups apart)) merged)@@ -383,12 +376,8 @@ -- | Vary each conditional on its own, holding the others at their first -- branch. separately ::- -- | What to parse a configuration with.- ParserConfig ->- -- | What to print it with.- RenderConfig ->- -- | The file this is, for the positions in a parse error.- FilePath ->+ -- | What to parse and print each configuration with.+ Formatter -> -- | How this configuration was reached. Reached -> -- | The conditionals to vary, and the baseline to hold them against.@@ -396,10 +385,10 @@ -- | What the baseline was formatted to. (Doc, CommentSummary) -> Spending ([Doc], CommentSummary)-separately parser render path reached v (baseDoc, baseFound) = do+separately formatter reached v (baseDoc, baseFound) = do groups <- aheadOf (catMaybes (concat readings)) . for readings . traverse $- maybe (pure (baseDoc, mempty)) (formatAllConfigs parser render path)+ maybe (pure (baseDoc, mempty)) (formatAllConfigs formatter) let merged = zipWith (mergeOf (reachedLines reached))@@ -410,7 +399,7 @@ readings = [ [ if t == vaBaseline v then Nothing- else Just (reading parser render path (answering c i reached) t)+ else Just (reading formatter (answering c i reached) t) | (i, t) <- zip [0 ..] (cfgTexts c) ] | c <- vaGroups v@@ -421,12 +410,8 @@ -- -- 'Nothing' where the module cannot be taken apart like that. inFragments ::- -- | What to parse a configuration with.- ParserConfig ->- -- | What to print it with.- RenderConfig ->- -- | The file this is, for the positions in a parse error.- FilePath ->+ -- | What to parse and print each configuration with.+ Formatter -> -- | How this configuration was reached. Reached -> -- | The outermost conditionals.@@ -436,17 +421,17 @@ -- | What the baseline was formatted to. (Doc, CommentSummary) -> Spending (Maybe (Doc, CommentSummary))-inFragments parser render path reached forest source (baseDoc, baseFound) =+inFragments formatter reached forest source (baseDoc, baseFound) = case parts of Nothing -> pure Nothing Just (body, ps) -> do let readings =- [ reading parser render path (without gone reached) t+ [ reading formatter (without gone reached) t | (_, (t, gone)) <- ps ] formatted <- aheadOf readings $- traverse (formatAllConfigs parser render path) readings+ traverse (formatAllConfigs formatter) readings pure $ do doc <- reassembled body (zip (fmap fst ps) (fmap fst formatted)) pure (doc, baseFound <> foldMap snd formatted)@@ -459,8 +444,8 @@ (fragmentsOf body forest) pure (body, ps) --- | The outermost conditional around a @LANGUAGE@ or @OPTIONS@ pragma that--- changes how the rest of the module is parsed, if there is one.+-- | The outermost conditional around a @LANGUAGE@ pragma that changes how+-- the rest of the module is parsed, if there is one. -- -- Formatting a fragment on its own takes the rest of the module to be -- parsed alike in every configuration.@@ -488,35 +473,32 @@ in any pragma (take (to - from + 1) (drop (from - 1) written)) pragma l = let u = T.toUpper l- in "{-#" `T.isInfixOf` u- && ("LANGUAGE" `T.isInfixOf` u || "OPTIONS" `T.isInfixOf` u)+ in "{-#" `T.isInfixOf` u && "LANGUAGE" `T.isInfixOf` u -- | Vary the conditionals together, one group at a time. together ::- -- | What to parse a configuration with.- ParserConfig ->- -- | What to print it with.- RenderConfig ->- -- | The file this is, for the positions in a parse error.- FilePath ->+ -- | What to parse and print each configuration with.+ Formatter -> -- | How this configuration was reached. Reached -> -- | The group to split on, and the branch texts to split it into. Configurations -> Spending (Doc, CommentSummary)-together parser render path reached c = do- (found, blind) <-- formatted differentlyRead `catchE` \case- ConfigurationNotParsed answers _- | not differentlyRead,- isNothing (implied answers) ->- formatted True- e -> refuse e- if blind then fst <$> formatted False else pure found+together formatter reached c+ | differentlyRead = settledOr (fst <$> formatted False)+ | otherwise =+ (fst <$> formatted False) `catchE` \case+ e@(ConfigurationNotParsed answers _)+ | isNothing (implied answers) -> settledOr (refuse e)+ e -> refuse e where- readingOf = reading parser render path+ settledOr unsettled = do+ (found, blind) <- formatted True+ if blind then unsettled else pure found+ readingOf = reading formatter differentlyRead = or (zipWith (\a b -> not (readAlike parser a b)) texts (drop 1 texts))+ parser = formatterParser formatter texts = cfgTexts c formatted settle = do let (settledBy, answers) =@@ -526,7 +508,7 @@ if null (unconditionalErrors t) then Just . (,) i- <$> formatAllConfigs parser render path (readingOf r t)+ <$> formatAllConfigs formatter (readingOf r t) else pure Nothing docs <- except (traverse (complete (fmap (fmap fst) found)) (zip [0 ..] texts))@@ -597,22 +579,23 @@ -- | Format one configuration's code with the ordinary printer, and gather -- what else it holds. formatSingleConfig ::- -- | What to parse it with.- ParserConfig ->- -- | What to print it with.- RenderConfig ->- -- | The file this is, for the positions in a parse error.- FilePath ->+ -- | What to parse and print it with.+ Formatter -> -- | How this configuration was reached. Reached -> -- | The configuration itself, with no directives left in it. Text -> Either CppError (Doc, CommentSummary)-formatSingleConfig parser render path reached text =- case parseConfiguration parser path (reachedLines reached) text of+formatSingleConfig formatter reached text =+ case parseConfiguration+ (formatterParser formatter)+ (formatterPath formatter)+ (reachedLines reached)+ text of Left e -> Left (ConfigurationNotParsed (reachedAnswers reached) e) Right parsed ->- let (document, loose) = renderConfiguration render parsed+ let (document, loose) =+ renderConfiguration (formatterRender formatter) parsed in Right (document, summarizeComments loose (comments (pmSource parsed))) -- | How a configuration was reached, and what to call it.@@ -715,19 +698,3 @@ (cfgGuards c) (cfgWholes c) []--------------------------------------------------------------------------------- Diagnostics---- | Every region a document records provenance for, with what was printed--- there.------ The outermost wins where a span appears twice, which is the one 'walk'--- would have given a comment to.-regions :: Doc -> Map Span Doc-regions = Map.fromListWith (\_ outer -> outer) . go- where- go = \case- DLocated s d -> (s, d) : go d- DVariant a _ -> go a- d -> foldChildren go d
src/Tilia/Cpp/Directives.hs view
@@ -15,21 +15,16 @@ -- * Splitting Guard (..), Configurations (..),- configurations, configurationsOn, Varied (..), untouched,- leaves, branchLeaves, implied, settledBranch, correspondingBranches, ruledOutBranch, unconditionalErrors,- linearLeaves,- countLeaves,- answeredLeaves,- answeredLinearLeaves,+ Assignment, resolved, -- * The directives themselves@@ -63,7 +58,13 @@ import Tilia.Cpp.Macros (Macros, guardHolds, impliedBy) import Tilia.Doc.Internal (Conditional (..)) import Tilia.Parser (ParseError, describeParseError)-import Tilia.Source (directiveOnLine)+import Tilia.Source.Lines+ ( conditionalKeywords,+ continuesGroup,+ directiveOnLine,+ opaqueKeywords,+ opensGroup,+ ) import Tilia.Span ( Span, mkSpan,@@ -81,10 +82,7 @@ -- | Blank out the directive lines, keeping every branch. blankCpp :: Text -> Text-blankCpp source =- blanking- ([(dLine d, dLastLine d) | d <- directives source] <> [(n, n) | (n, _) <- macroLines source])- source+blankCpp = withoutDirectives directives -- | Blank out every branch the macros rule out, and the conditionals that -- ask about them.@@ -119,9 +117,16 @@ -- | A module with the opaque directives and the lines using its macros -- blanked out. withoutOpaque :: Text -> Text-withoutOpaque source =+withoutOpaque = withoutDirectives opaqueDirectives++-- | A module with the directives a scan finds and the lines using its+-- macros blanked out.+withoutDirectives :: (Text -> [Directive]) -> Text -> Text+withoutDirectives scan source = blanking- ([(dLine d, dLastLine d) | d <- opaqueDirectives source] <> [(n, n) | (n, _) <- macroLines source])+ ( [(dLine d, dLastLine d) | d <- scan source]+ <> [(n, n) | (n, _) <- macroLines source]+ ) source -- | Why a module using the preprocessor could not be formatted.@@ -129,7 +134,8 @@ = -- | A conditional that does not make sense: the line and the keyword of -- the directive that gives it away, and what is wrong with it. MalformedConditional Int Text Malformation- | -- | More configurations than 'configurationBudget' allows.+ | -- | Configurations that would format the module's lines more times over+ -- than 'Tilia.Cpp.configurationBudget' allows. TooManyConfigurations | -- | A configuration the parser rejected, and which one it was. ConfigurationNotParsed [([Guard], Int)] ParseError@@ -212,14 +218,6 @@ } deriving (Eq, Show) --- | Split a module on its first outermost conditional, and on every other--- one written behind the same directives, wherever in the module it sits.-configurations :: Text -> Maybe Configurations-configurations source = do- forest <- scanConditionals source- gs <- listToMaybe forest- pure (configurationsOn gs forest source)- -- | Split a module on one of its conditionals, and on every other one -- written behind the same directives. configurationsOn ::@@ -259,10 +257,6 @@ where reaches (from, to) = spanStartLine s <= to && from <= spanEndLine s --- | Every configuration of a module, with every conditional resolved.-leaves :: Text -> Either CppError [Text]-leaves = fmap (fmap snd) . answeredLeaves- -- | One configuration for every branch of every conditional, and no more. branchLeaves :: Text -> Either CppError [Text] branchLeaves source = do@@ -436,90 +430,9 @@ groups = [gsWhole g | forest <- maybeToList (scanConditionals source), g <- allGroups forest] encloses n (from, to) = from < n && n < to --- | The configurations reached by varying one conditional at a time.-linearLeaves :: Text -> Either CppError [Text]-linearLeaves = fmap (fmap snd) . answeredLinearLeaves---- | How many configurations a module has, without building any of them,--- and so counting those no definition of the macros gives.-countLeaves :: Text -> Either CppError Integer-countLeaves source = do- forest <- readConditionals source- pure (sum [across answers forest | answers <- combinations (afforded forest)])- where- across answers = product . fmap (one answers)- one answers gs = case lookup (gsGuards gs) answers of- Just i -> across answers (nestedIn gs i)- Nothing -> sum [across answers (nestedIn gs i) | i <- [0 .. gsCount gs - 1]]- combinations = traverse (\(g, k) -> [(g, i) | i <- [0 .. k - 1]])- afforded forest = go 1 (repeated forest)- where- go _ [] = []- go n ((g, k) : rest)- | n * toInteger k <= guardsToTie = (g, k) : go (n * toInteger k) rest- | otherwise = []---- | The guards a module asks more than once, and how many answers each has.-repeated :: [GroupSpec] -> [([Guard], Int)]-repeated forest = distinct Map.empty [q | q@(g, _) <- asked forest, twice g]- where- asked gss = concat [(gsGuards gs, gsCount gs) : asked (concat (gsNested gs)) | gs <- gss]- times = Map.fromListWith (+) [(g, 1 :: Int) | (g, _) <- asked forest]- twice g = Map.findWithDefault 0 g times >= 2-- distinct _ [] = []- distinct seen (q@(g, _) : rest)- | Map.member g seen = distinct seen rest- | otherwise = q : distinct (Map.insert g () seen) rest---- | How many combinations of answers 'countLeaves' will enumerate.-guardsToTie :: Integer-guardsToTie = 4096---- | Every configuration some definition of the macros gives, and the--- answers that reach it.-answeredLeaves :: Text -> Either CppError [(Assignment, Text)]-answeredLeaves =- fmap (filter (isJust . implied . Map.toList . fst)) . go Map.empty- where- go answers source = case configurations source of- Nothing | not (null (unconditionalErrors source)) -> Right []- Nothing -> (\t -> [(answers, t)]) <$> resolved source- Just c ->- concat- <$> traverse- (\(i, t) -> go (Map.insert (cfgGuards c) i answers) t)- (zip [0 ..] (cfgTexts c))- -- | Which branch every question was answered with to reach a configuration. type Assignment = Map [Guard] Int --- | The same, labelled by the answers that reach each one, and for the same--- reason as 'answeredLeaves'.-answeredLinearLeaves :: Text -> Either CppError [(Assignment, Text)]-answeredLinearLeaves =- fmap (filter (isJust . implied . Map.toList . fst)) . go Map.empty- where- go answers source = case configurations source of- Nothing -> (\t -> [(answers, t)]) <$> resolved source- Just c -> case zip [0 ..] (cfgTexts c) of- [] -> Right []- (i, first) : rest ->- (<>)- <$> go (Map.insert (cfgGuards c) i answers) first- <*> traverse (held answers (cfgGuards c)) rest- where- held before gs (i, t) = answeredBaseline (Map.insert gs i before) t---- | The configuration in which every question still to be asked takes its--- first branch, and the answers that gives.-answeredBaseline :: Assignment -> Text -> Either CppError (Assignment, Text)-answeredBaseline answers source = case configurations source of- Nothing -> (answers,) <$> resolved source- Just c -> case cfgTexts c of- [] -> Right (answers, source)- t : _ -> answeredBaseline (Map.insert (cfgGuards c) 0 answers) t- -- | A module with every conditional answered, and its other directives -- taken out, or why its conditionals do not make sense. resolved :: Text -> Either CppError Text@@ -551,8 +464,7 @@ go = \case [] -> [] (n, l) : ls- | Just (keyword, body) <- directiveOnLine l,- keyword `elem` directiveKeywords ->+ | Just (keyword, body) <- directiveOnLine l -> let (continued, rest) = continuation l ls in Directive { dLine = n,@@ -616,30 +528,9 @@ scanConditionals :: Text -> Maybe [GroupSpec] scanConditionals = either (const Nothing) Just . readConditionals --- | The directives that ask a question, and so split a module in two.-conditionalKeywords :: [Text]-conditionalKeywords = opensGroup <> continuesGroup <> ["endif"]---- | The keywords that open a group or continue one.-opensGroup, continuesGroup :: [Text]-opensGroup = ["if", "ifdef", "ifndef"]-continuesGroup = ["elif", "elifdef", "elifndef", "else"]- -- | Does this line begin with a preprocessor directive? isDirective :: Text -> Bool-isDirective l = case directiveOnLine l of- Just (keyword, _) -> keyword `elem` directiveKeywords- Nothing -> False---- | Every directive the C preprocessor takes, whether or not this module--- can do anything with the ones it names.-directiveKeywords :: [Text]-directiveKeywords = conditionalKeywords <> opaqueKeywords---- | The unconditional directives that do not split the source code.-opaqueKeywords :: [Text]-opaqueKeywords =- ["define", "undef", "include", "line", "error", "warning", "pragma"]+isDirective = isJust . directiveOnLine -- | One conditional group, read off the directives that make it up. data GroupSpec = GroupSpec
src/Tilia/Cpp/Fragment.hs view
@@ -121,7 +121,7 @@ [ Fragment gs (any above gs) (runAt (lo - 1)) (runAt hi) | (gs, (lo, hi)) <- clustered . sortOn (fst . snd) $- [(g, reach (bodyRuns body) (gsWhole g)) | g <- forest]+ [(g, runsTouched (bodyRuns body) (gsWhole g)) | g <- forest] ] where above = (<= bodyHeadEnd body) . fst . gsWhole@@ -131,8 +131,8 @@ -- | The runs a conditional's lines touch, or where it falls between two of -- them.-reach :: [Run] -> (Int, Int) -> (Int, Int)-reach runs (a, b) =+runsTouched :: [Run] -> (Int, Int) -> (Int, Int)+runsTouched runs (a, b) = case [i | (i, r) <- zip [0 ..] runs, runFrom r <= b, a <= runTo r] of [] -> let p = length (takeWhile ((< a) . runTo) runs) in (p, p) is -> (minimum is, maximum is + 1)
src/Tilia/Cpp/Macros.hs view
@@ -226,10 +226,7 @@ firstThat f = foldr (\x acc -> maybe acc Just (f x)) Nothing readDigits = T.foldl' (\n c -> n * 10 + toInteger (fromEnum c - fromEnum '0')) 0 --- | An expression, and what is left of the tokens after it.-type Reading = Maybe (Expr, [Token])--orExpr :: [Token] -> Reading+orExpr :: [Token] -> Maybe (Expr, [Token]) orExpr ts = do (left, rest) <- andExpr ts more left rest@@ -240,7 +237,7 @@ more (Or left right) rest' rest -> Just (left, rest) -andExpr :: [Token] -> Reading+andExpr :: [Token] -> Maybe (Expr, [Token]) andExpr ts = do (left, rest) <- compared ts more left rest@@ -251,7 +248,7 @@ more (And left right) rest' rest -> Just (left, rest) -compared :: [Token] -> Reading+compared :: [Token] -> Maybe (Expr, [Token]) compared ts = do (left, rest) <- unary ts case rest of@@ -284,7 +281,7 @@ exactly = And (Reaches n [m]) (Not (Reaches n [m + 1])) reaching _ _ _ = Nothing -unary :: [Token] -> Reading+unary :: [Token] -> Maybe (Expr, [Token]) unary = \case Punct "!" : rest -> do (e, rest') <- unary rest
src/Tilia/Cpp/Merge.hs view
@@ -22,8 +22,8 @@ import Data.Text (Text) import Data.Text qualified as T import Tilia.Cpp.Directives (Guard (..), Varied (..), untouched)-import Tilia.Cpp.Place (regionOf)-import Tilia.Doc (defaultRenderOptions, printDoc)+import Tilia.Cpp.Place (foldRegions, regionOf)+import Tilia.Doc (printDoc) import Tilia.Doc.Combinators qualified as Doc import Tilia.Doc.Internal ( Conditional (..),@@ -44,7 +44,7 @@ wrap, ) import Tilia.Source (Lines, lineAt)-import Tilia.Span (Span (..), covers, endOf, endPoint, meets, startOf, startPoint)+import Tilia.Span (Span (..), covers, endOf, meets, startOf, zeroWidth) -- | A conditional other than the one asked about that answering a question -- settles, for some answers at least.@@ -132,9 +132,26 @@ Nothing -> case spineAt l d of parts@(_ : _ : _) -> Just . factored middle l $- [if k == inside then parts else [e] | (k, e) <- zip [0 ..] xs]+ [ if k == inside then tucked inside l parts else [e]+ | (k, e) <- zip [0 ..] xs+ ] _ -> Nothing + tucked inside l parts = case unsnoc parts of+ Just (front, DNest _ y)+ | e : _ <-+ [ e+ | e <- xs,+ regionOf e == regionOf y,+ fmap spanStartColumn (regionOf e) == column,+ printDoc (DGroup l y)+ == printDoc e+ ] ->+ front <> [e]+ _ -> parts+ where+ column = spanStartColumn <$> listToMaybe (drop inside spans)+ factored within layout ss = let same (x, _) (y, _) = agree varied layout x y common as bs =@@ -240,7 +257,7 @@ byLines | not (any continuing ss), not (null (foldr1 Set.intersection (foldMap writtenAt <$> ss))),- (ls, cuts) <- unzip (fmap (linesOf layout) ss),+ (ls, cuts) <- unzip (fmap (printedLines layout) ss), or cuts = [factored whole layout ls] | otherwise = []@@ -318,7 +335,7 @@ lineEnding h hs | all ((== printed h) . printed) hs = layout | otherwise = Flat- printed = printDoc defaultRenderOptions+ printed = printDoc endingWith t d = fromMaybe (d <> t) (inAlternatives d) where@@ -447,9 +464,9 @@ agreeOn varied layout x y = case (x, y) of (DLocated s x', DLocated t y') -> s == t- && ( untouched varied (reach s x')- && untouched varied (reach t y')- && startPoint s /= endPoint s+ && ( untouched varied (spanWithHaddock s x')+ && untouched varied (spanWithHaddock t y')+ && not (zeroWidth s) || inside x' y' ) (DCppChoice ws bs x', DCppChoice ws' cs y') ->@@ -480,8 +497,8 @@ -- | A region's span, stretched over the span it prints first, which the -- Haddock of a constructor, a field or an argument is, written above or -- below it.-reach :: Span -> Doc -> Span-reach s d = maybe s (<> s) (firstMarked d)+spanWithHaddock :: Span -> Doc -> Span+spanWithHaddock s d = maybe s (<> s) (firstMarked d) where firstMarked = \case DLocated h x -> firstMarked x <|> Just h@@ -759,8 +776,8 @@ -- | The lines a run of documents prints and the space between them, each in -- the wrappers it is printed in, and whether that cuts an element of the -- run.-linesOf :: Layout -> [Doc] -> ([Doc], Bool)-linesOf layout xs = (plain . mconcat <$> filter (not . null) ps, cut)+printedLines :: Layout -> [Doc] -> ([Doc], Bool)+printedLines layout xs = (plain . mconcat <$> filter (not . null) ps, cut) where (ps, _, cut) = partsOf layout Within xs @@ -805,7 +822,7 @@ d -> getAny (foldChildren (Any . bare) d) -- | Where a part of a run of documents begins.-data Place+data PartStart = -- | On a line, after what the line already holds. Within | -- | Between two lines.@@ -818,7 +835,7 @@ -- | The layout the run is printed in. Layout -> -- | Where the run begins.- Place ->+ PartStart -> -- | The run. [Doc] -> -- | The parts in order, the first of them what the run adds to the part@@ -826,7 +843,7 @@ -- run ends, which is where what follows it begins; and whether an element -- of the run holds what is printed on more than one line, so that the cut -- went through it.- ([[Doc]], Place, Bool)+ ([[Doc]], PartStart, Bool) partsOf layout = walk [[]] False where walk ls cut place = \case@@ -874,10 +891,7 @@ -- | Where the regions a document holds were written. writtenAt :: Doc -> Set Span-writtenAt = \case- DLocated s d -> Set.insert s (writtenAt d)- DFence s d -> Set.insert s (writtenAt d)- d -> foldChildren writtenAt d+writtenAt = foldRegions (\s _ -> Set.insert s) -- | Does this document print nothing but space and what ends a line? blank :: Doc -> Bool
src/Tilia/Cpp/Place.hs view
@@ -8,6 +8,8 @@ summarizeComments, restoreUnprinted, regionOf,+ regionsIn,+ foldRegions, ) where @@ -108,8 +110,8 @@ summaryHaddocks = Set.fromList [commentSpan c | c <- every, holdsOff c] } --- | Place comments into a document is the result of merging different--- configurations.+-- | Put the conditionals, comments, opaque directives and macro lines back+-- into the document the configurations were merged into. restoreUnprinted :: -- | The module as written. Text ->@@ -378,12 +380,20 @@ -- | Every region a document records, and what it holds. regionsIn :: Doc -> [(Span, Doc)]-regionsIn = \case- DLocated s d -> (s, d) : regionsIn d- DFence s d -> (s, d) : regionsIn d- d -> foldChildren regionsIn d+regionsIn = foldRegions (\s d inside -> (s, d) : inside) --- | The smallest span covering every region a document records.+-- | Fold the regions a document records, giving each its span, what it+-- holds, and what the fold made of the regions inside it.+foldRegions :: (Monoid m) => (Span -> Doc -> m -> m) -> Doc -> m+foldRegions f = go+ where+ go = \case+ DLocated s d -> f s d (go d)+ DFence s d -> f s d (go d)+ d -> foldChildren go d+{-# INLINE foldRegions #-}++-- | The smallest span covering the outermost regions a document records. regionOf :: Doc -> Maybe Span regionOf = \case DLocated s _ -> Just s
src/Tilia/Diff.hs view
@@ -34,7 +34,7 @@ -- | After. Text -> Text-diff palette = unified palette (Just roomFor) []+diff palette = unified palette [] -- | The whole of a unified diff of one file against its formatted self, -- headed the way @git diff@ heads one.@@ -50,7 +50,6 @@ diffInFull palette path = unified palette- Nothing [paint palette Place ("diff --git " <> before <> " " <> after)] (before, after) where@@ -60,11 +59,9 @@ -- | One line of the comparison, with the number it has on each side. data Line = Line !Mark !Int !Int !Text --- | A unified diff, with whatever heading and limit the caller wants.+-- | A unified diff, with whatever heading the caller wants. unified :: Palette ->- -- | How many lines are worth printing, where there is a limit at all.- Maybe Int -> -- | Whatever goes above the two file names. [Text] -> -- | What to call the two sides.@@ -74,28 +71,20 @@ -- | After. Text -> Text-unified palette limit above (beforeName, afterName) before after+unified palette above (beforeName, afterName) before after | null hunks, before /= after = "(the two differ only in how they end their lines)" | null hunks = "(the two are identical as text, so the difference is in something\ \ the text does not show)"- | otherwise = T.intercalate "\n" (above <> heading <> shown)+ | otherwise = T.intercalate "\n" (above <> heading <> body) where heading = [ paint palette (Header Gone) ("--- " <> beforeName), paint palette (Header New) ("+++ " <> afterName) ] - shown = case limit of- Just room | length body > room -> take room body <> [omitted room]- _ -> body- where- omitted room =- paint palette Meta $- "… and " <> T.pack (show (length body - room)) <> " more lines"- body = concatMap render hunks render range = hunkHeading range : fmap line (slice range)@@ -113,7 +102,7 @@ [] -> "0,0" line (Line mark _ _ text) = case mark of- Context -> paint palette Unchanged (" " <> text)+ Context -> paint palette Kept (" " <> text) Removed -> paint palette Gone ("-" <> text) Added -> paint palette New ("+" <> text) @@ -134,10 +123,6 @@ -- | How many unchanged lines to show either side of a change. margin :: Int margin = 3---- | How many lines of diff are worth printing before it stops being read.-roomFor :: Int-roomFor = 60 -- | Join hunks that have grown into one another. merge :: [(Int, Int)] -> [(Int, Int)]
src/Tilia/Doc.hs view
@@ -1,8 +1,6 @@ -- | Turning a printed document into source text. module Tilia.Doc ( Doc,- RenderOptions (..),- defaultRenderOptions, printDoc, ) where@@ -10,11 +8,9 @@ import Data.Text (Text) import Tilia.Doc.Internal ( Doc,- RenderOptions (..),- defaultRenderOptions, render, ) -- | Render a document to source text.-printDoc :: RenderOptions -> Doc -> Text+printDoc :: Doc -> Text printDoc = render
src/Tilia/Doc/Combinators.hs view
@@ -224,8 +224,6 @@ attach Hanging body = space <> body attach Normal body = breakOrSpace <> indent body --- | 'Hanging' if the span was a single line in the input, 'Normal'- ---------------------------------------------------------------------------- -- Indentation
src/Tilia/Doc/Internal.hs view
@@ -25,8 +25,6 @@ TrailingWhitespace (..), -- * Rendering- RenderOptions (..),- defaultRenderOptions, render, ) where@@ -295,16 +293,9 @@ ---------------------------------------------------------------------------- -- Rendering --- | Knobs for 'render'.-newtype RenderOptions = RenderOptions- { -- | Columns per indentation step.- roIndentStep :: Int- }- deriving (Eq, Show)---- | Two columns per step.-defaultRenderOptions :: RenderOptions-defaultRenderOptions = RenderOptions{roIndentStep = 2}+-- | Columns per indentation step.+indentStep :: Int+indentStep = 2 -- | What the engine carries while walking a document. data Env = Env@@ -312,8 +303,6 @@ envIndent :: !Int, -- | The layout of the innermost group. envLayout :: !Layout,- -- | Columns per indentation step.- envIndentStep :: !Int, -- | Whether what is written now is a note, which goes to the margin -- right above a preprocessor directive. envNote :: !Bool@@ -414,14 +403,13 @@ } -- | Turn a document into text.-render :: RenderOptions -> Doc -> Text-render opts doc = finish (roIndentStep opts) (go env doc emptyOut)+render :: Doc -> Text+render doc = finish (go env doc emptyOut) where env = Env { envIndent = 0, envLayout = Broken,- envIndentStep = roIndentStep opts, envNote = False } @@ -450,7 +438,7 @@ DHardBreak -> breakLine (envIndent env) DVerbatimBreak lineStart trailing -> verbatimBreakLine (envNote env) lineStart trailing DCat a b -> go env b . go env a- DNest n d -> go env{envIndent = envIndent env + n * envIndentStep env} d+ DNest n d -> go env{envIndent = envIndent env + n * indentStep} d DAlign d -> \out -> go env{envIndent = max (envIndent env) (outColumn out)} d out DCppMarginNote d -> \out ->@@ -698,24 +686,16 @@ -- | Assemble the final text: one trailing newline, no blank lines at the -- end, no trailing whitespace anywhere.-finish ::- -- | Columns per indentation step.- Int ->- Out ->- Text-finish step out =+finish :: Out -> Text+finish out = case dropWhile (T.null . lineBody) (outLines (breakLine 0 out)) of [] -> ""- ls -> T.unlines (lineText <$> notesToMargin (continued step (reverse ls)))+ ls -> T.unlines (lineText <$> notesToMargin (continued (reverse ls))) -- | Indent each alternative of a conditional that continues the line before -- the conditional further than that line.-continued ::- -- | Columns per indentation step.- Int ->- [Line] ->- [Line]-continued step = walk [] 0 False+continued :: [Line] -> [Line]+continued = walk [] 0 False where walk frames previous pending = \case [] -> []@@ -736,7 +716,7 @@ | lineContinues l, not (linePinned l), at <= before ->- (before, before - at + step) : rest+ (before, before - at + indentStep) : rest where at = lineIndent l + sum (fmap snd rest) _ -> frames
src/Tilia/Editor.hs view
@@ -4,6 +4,8 @@ -- | Definitions to support editor integrations. module Tilia.Editor ( editorSession,+ Compiled (..),+ compiledSession, formatBuffer, ) where@@ -18,9 +20,11 @@ componentsOfTarget, ) import Tilia.Format- ( FormatError,+ ( Compiled (..),+ FormatError (NoProject), PlanSource (..), Session,+ compiledSession, newSession, sessionRoot, )@@ -53,25 +57,23 @@ downloading checkAst checkIdempotence- debugFixity = do- planSource <- case givenPlan of- Just plan -> pure (GivenPlan plan)- Nothing -> PlanFromCabal <$> components- newSession- file- planSource- caching- downloading- checkAst- checkIdempotence- debugFixity- where- components =- findProjectRoot file >>= \case- Nothing -> pure []- Just root ->- either (const []) (mapMaybe componentInPlan)+ debugFixity =+ findProjectRoot file >>= \case+ Nothing -> pure (Left (NoProject file))+ Just root -> do+ planSource <- case givenPlan of+ Just plan -> pure (GivenPlan plan)+ Nothing ->+ PlanFromCabal . either (const []) (mapMaybe componentInPlan) <$> componentsOfTarget root Everything+ newSession+ root+ planSource+ caching+ downloading+ checkAst+ checkIdempotence+ debugFixity -- | Format given text on the assumption it comes from the specified -- 'FilePath'.
src/Tilia/Equivalence.hs view
@@ -68,6 +68,7 @@ ) import Tilia.Imports (normalizeImports) import Tilia.Parser (ParsedModule (..))+import Tilia.Pragma (languageNames) import Tilia.Source (comments, sourceLines) import Tilia.Span (Span (..)) import Tilia.Span.Ghc (spanOf, spansOf)@@ -561,9 +562,5 @@ where entries p | pragmaName p == "LANGUAGE" =- [ ("LANGUAGE", extension)- | e <- T.splitOn "," (pragmaBody p),- let extension = T.strip e,- not (T.null extension)- ]+ [("LANGUAGE", extension) | extension <- languageNames (pragmaBody p)] | otherwise = [(pragmaName p, pragmaBody p)]
src/Tilia/Fixity.hs view
@@ -11,6 +11,7 @@ Fixity (..), defaultFixity, spellFixity,+ readPrecedence, -- * Module declarations declaredFixities,@@ -83,6 +84,7 @@ import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T+import Data.Text.Read qualified as T import GHC.Generics (Generic) import GHC.Hs hiding (Fixity, OpName) import GHC.Types.Fixity qualified as GHC@@ -123,6 +125,13 @@ -- | What an operator with no declaration in scope means: @infixl 9@. defaultFixity :: Fixity defaultFixity = Fixity LeftAssoc 9++-- | Read a precedence, which can be -1: GHC gives @->@ one, below anything+-- the report allows anyone to write.+readPrecedence :: Text -> Maybe Int+readPrecedence t = case T.signed T.decimal t of+ Right (p, rest) | T.null rest -> Just p+ _ -> Nothing -- | A fixity, written the way it would be declared. spellFixity :: Fixity -> Text
+ src/Tilia/Fixity/Archive.hs view
@@ -0,0 +1,104 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++-- | What a package's source tarball holds that a module could be read out+-- of.+module Tilia.Fixity.Archive+ ( Archive (..),+ readArchive,+ InArchive (..),+ moduleIn,+ pathOfModule,+ moduleEndings,+ writtenForHsc,+ )+where++import Codec.Archive.Tar qualified as Tar+import Codec.Compression.GZip qualified as GZip+import Control.DeepSeq (NFData, force)+import Control.Exception (evaluate)+import Data.ByteString qualified as BS+import Data.ByteString.Lazy qualified as BL+import Data.List (isSuffixOf)+import Data.Maybe (listToMaybe, mapMaybe)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Encoding qualified as T+import GHC.Generics (Generic)+import Tilia.Fixity.Cabal (cabalFileAtTop, entryPosixPath, sourceDirs)+import Tilia.Utils (quietly)++-- | What a tarball holds that a module could be read out of.+data Archive = Archive+ { -- | The package's @.cabal@ file, the first one at the top.+ archiveCabal :: Maybe Text,+ -- | Every file that could hold a module, by where it sits, in the order+ -- the tarball has them.+ archiveFiles :: [(FilePath, BS.ByteString)]+ }+ deriving (Generic)++instance NFData Archive++-- | Read a tarball, keeping what a module could be read out of.+readArchive :: FilePath -> IO (Maybe Archive)+readArchive tarball = quietly Nothing $ do+ bytes <- BL.readFile tarball+ Just <$> evaluate (force (sweep Nothing [] (Tar.read (GZip.decompress bytes))))+ where+ sweep cabal found = \case+ Tar.Next entry rest+ | Tar.NormalFile content _ <- Tar.entryContent entry,+ cabalFileAtTop (entryPosixPath entry),+ Nothing <- cabal ->+ sweep (Just (T.decodeUtf8Lenient (BL.toStrict content))) found rest+ | Tar.NormalFile content _ <- Tar.entryContent entry,+ any (`isSuffixOf` entryPosixPath entry) moduleEndings ->+ sweep cabal ((entryPosixPath entry, BL.toStrict content) : found) rest+ | otherwise -> sweep cabal found rest+ _ -> Archive cabal (reverse found)++-- | Find a module in what a tarball holds and say what was found.+moduleIn :: Text -> Archive -> Maybe InArchive+moduleIn modName archive = listToMaybe (mapMaybe pick moduleEndings)+ where+ dirs = maybe [] sourceDirs (archiveCabal archive)+ pick ending =+ inArchive ending . T.decodeUtf8Lenient . snd+ <$> listToMaybe (under sfx matching <> matching)+ where+ sfx = "/" <> pathOfModule modName <> ending+ matching = [f | f <- archiveFiles archive, sfx `isSuffixOf` fst f]+ under sfx matching = [f | d <- dirs, f <- matching, inDir sfx d (fst f)]+ inDir sfx d path+ | d == "." = takeWhile (/= '/') path <> sfx == path+ | otherwise = ("/" <> T.unpack d <> sfx) `isSuffixOf` path++-- | Where a module's file is under the directory that holds it, without an+-- extension.+pathOfModule :: Text -> FilePath+pathOfModule = T.unpack . T.replace "." "/"++-- | 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 it is taken to declare nothing.+ 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"
− src/Tilia/Fixity/ByHand.hs
@@ -1,33 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---- | Fixities for the modules written for @hsc2hs@, which are not read.-module Tilia.Fixity.ByHand- ( hscFixities,- )-where--import Data.Map.Strict (Map)-import Data.Map.Strict qualified as Map-import Data.Text (Text)-import Tilia.Fixity---- | What the modules written for @hsc2hs@ declare.------ This is the whole answer for an @.hsc@, given instead of reading it, and--- a module absent from here declares nothing rather than being unreadable.-hscFixities :: Map Text (Map OpName Fixity)-hscFixities =- Map.fromList- [ -- @addSignal@ and @deleteSignal@ take a signal on the left and a set- -- on the right, so a chain of them only typechecks to the right, and- -- the module says so with a bare @infixr@—precedence 9, as the Report- -- has it when none is written.- entry- "System.Posix.Signals"- [ ("addSignal", RightAssoc, 9),- ("deleteSignal", RightAssoc, 9)- ]- ]- where- entry name ops =- (name, Map.fromList [(OpName o, Fixity d p) | (o, d, p) <- ops])
src/Tilia/Fixity/Cabal.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} --- | Reading a package's exposed modules out of its @.cabal@ file.+-- | Reading a package's modules out of its @.cabal@ file. module Tilia.Fixity.Cabal ( packageModules, cabalFileInArchive,@@ -26,10 +26,10 @@ import Data.Text.Encoding qualified as T import GHC.Driver.Session qualified as GHC import GHC.LanguageExtensions.Type (Extension)-import Tilia.Pragma (lookupExtension)-import Tilia.Utils (quietly)+import Tilia.Pragma (applyExtensionName, lookupEdition)+import Tilia.Utils (fieldWords, quietly) --- | The modules a package exposes, read from the @.cabal@ file in its+-- | The modules a package holds, read from the @.cabal@ file in its -- source tarball. -- -- 'Nothing' if the tarball cannot be read or holds no @.cabal@ file.@@ -71,10 +71,7 @@ modulesUnder field = concatMap moduleNames . fieldsNamed field . T.lines where- moduleNames =- filter looksLikeModule- . concatMap (T.split (== ','))- . T.words+ moduleNames = filter looksLikeModule . fieldWords looksLikeModule m = case T.uncons m of Just (c, _) -> c `elem` ['A' .. 'Z'] Nothing -> False@@ -83,38 +80,22 @@ sourceDirs :: Text -> [Text] sourceDirs contents = Data.List.nub (named <> ["."]) where- named =- filter (not . T.null)- . fmap T.strip- . concatMap (T.split (== ','))- . concatMap T.words- . fieldsNamed "hs-source-dirs"- $ T.lines contents+ named = concatMap fieldWords (fieldsNamed "hs-source-dirs" (T.lines contents)) -- | The extensions a package puts in force, read from its @.cabal@ file. declaredExtensions :: Text -> [Extension] declaredExtensions contents =- foldl apply baseline named+ foldl applyExtensionName baseline named where ls = T.lines contents- baseline = case mapMaybe languageNamed (fieldsNamed "default-language" ls) of- [] -> GHC.languageExtensions (Just GHC.Haskell98)- editions ->- Data.List.nub (concatMap (GHC.languageExtensions . Just) editions)+ baseline =+ case mapMaybe lookupEdition (fieldsNamed "default-language" ls) of+ [] -> GHC.languageExtensions (Just GHC.Haskell98)+ editions ->+ Data.List.nub (concatMap (GHC.languageExtensions . Just) editions) named =- concatMap (T.split (== ',')) . concatMap T.words $+ concatMap fieldWords $ fieldsNamed "extensions" ls <> fieldsNamed "default-extensions" ls- apply acc written = case T.strip written of- name- | Just off <- T.stripPrefix "No" name,- Just extension <- lookupExtension off ->- filter (/= extension) acc- | Just extension <- lookupExtension name,- extension `notElem` acc ->- acc <> [extension]- | otherwise -> acc- languageNamed written =- lookup (T.unpack (T.strip written)) [(show e, e) | e <- [minBound .. maxBound]] -- | The values of every field with the given name, wherever it appears and -- however deeply it is nested.
src/Tilia/Fixity/Cache.hs view
@@ -3,7 +3,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} --- | Package-level cache.+-- | Answers remembered on disk between runs. module Tilia.Fixity.Cache ( Cache, PlanToken (..),@@ -40,7 +40,6 @@ import Data.Text (Text) import Data.Text qualified as T import Data.Text.IO qualified as T-import Data.Text.Read qualified as T import Data.Time (UTCTime) import Data.Time.Format (defaultTimeLocale, formatTime) import Data.Time.Format.ISO8601 (iso8601ParseM, iso8601Show)@@ -107,16 +106,16 @@ traverse_ remember found pure found --- | The modules a package exposes, if that was worked out before.+-- | The modules a package holds, if that was worked out before. cachedModules :: Cache -> Text -> IO (Maybe [Text]) cachedModules cache package =- readIfPresent (at cache ["modules", package]) $+ readIfPresent (at cache (modulesFile package)) $ filter (not . T.null) . T.lines --- | Remember what a package exposes.+-- | Remember which modules a package holds. storeModules :: Cache -> Text -> [Text] -> IO () storeModules cache package =- writeAtomically (at cache ["modules", package]) . T.unlines+ writeAtomically (at cache (modulesFile package)) . T.unlines -- | What was established about a module before, if anything. cachedEstablished ::@@ -130,7 +129,7 @@ -- left unsettled was left so under another plan. IO (Maybe Established) cachedEstablished cache package modName =- fmap join . readIfPresent (at cache ["established", package, modName]) $+ fmap join . readIfPresent (at cache (establishedFile package modName)) $ assembled . fmap T.words . T.lines where assembled entries = do@@ -179,7 +178,7 @@ Established -> IO () storeEstablished cache package modName established =- writeAtomically path . renderLines $+ writeAtomically (at cache (establishedFile package modName)) . renderLines $ fmap (("fixity" :) . renderFixity) (Map.toList fixities) <> [ ["unsettled", tokenOf cache] <> chain <> renderNamespaced names | (chain, names) <- Map.toList (establishedUnsettled established)@@ -201,7 +200,6 @@ Just kids /= Map.lookup (OpName parent) written ] where- path = at cache ["established", package, modName] fixities = establishedFixities established certain = establishedCertain established written = Map.map (Set.map snd) (certainMembers certain)@@ -218,7 +216,7 @@ -- | What it said, or 'Nothing' if nothing is remembered for that stamp. IO (Maybe (Maybe (NonEmpty ModuleSummary))) cachedSummaries cache key stamp =- fmap join . readIfPresent (at cache ["summaries", key]) $ \contents ->+ fmap join . readIfPresent (at cache (summariesFile key)) $ \contents -> case T.lines contents of (header : rest) | header == readFrom stamp -> parseSummaries rest _ -> Nothing@@ -236,7 +234,7 @@ Maybe (NonEmpty ModuleSummary) -> IO () storeSummaries cache key stamp summaries =- writeAtomically (at cache ["summaries", key]) . T.unlines $+ writeAtomically (at cache (summariesFile key)) . T.unlines $ readFrom stamp : renderSummaries summaries -- | The first line of a remembered summary: what it was read from, and the@@ -370,7 +368,7 @@ -- see it. cachedInstalled :: Cache -> IO (Maybe [InstalledPackage]) cachedInstalled cache = quietly Nothing $ do- readIfPresent (at cache ["installed", tokenOf cache]) T.lines >>= \case+ readIfPresent (at cache (installedFile cache)) T.lines >>= \case Nothing -> pure Nothing Just ls -> do let (databases, packages) = span (T.isPrefixOf "db ") ls@@ -404,7 +402,7 @@ | null databases = pure () | otherwise = quietly () $ do stamps <- traverse stampOf databases- writeAtomically (at cache ["installed", tokenOf cache]) . renderLines $+ writeAtomically (at cache (installedFile cache)) . renderLines $ [["db", stamp, T.pack path] | (path, stamp) <- zip databases stamps] <> concatMap packageLines (installedPackages found) where@@ -423,12 +421,12 @@ -- and left the plan exactly as before. cachedFutileSolve :: Cache -> IO Bool cachedFutileSolve cache =- isJust <$> readIfPresent (at cache ["solves", tokenOf cache]) (const ())+ isJust <$> readIfPresent (at cache (solvesFile cache)) (const ()) -- | Remember that solving again did not widen the plan. storeFutileSolve :: Cache -> IO () storeFutileSolve cache =- writeAtomically (at cache ["solves", tokenOf cache]) ""+ writeAtomically (at cache (solvesFile cache)) "" -- | The packages an earlier run was still short of after asking @cabal@ to -- fetch them.@@ -436,38 +434,36 @@ cachedFutileFetch cache = fromMaybe [] <$> readIfPresent- (at cache ["fetches", tokenOf cache])+ (at cache (fetchesFile cache)) (filter (not . T.null) . T.lines) -- | Remember what fetching left missing. storeFutileFetch :: Cache -> [Text] -> IO () storeFutileFetch cache =- writeAtomically (at cache ["fetches", tokenOf cache]) . T.unlines+ writeAtomically (at cache (fetchesFile cache)) . T.unlines -- | When the project was last solved under this plan. cachedSolveTime :: Cache -> IO (Maybe UTCTime) cachedSolveTime cache = join <$> readIfPresent- (at cache ["solve-times", tokenOf cache])+ (at cache (solveTimeFile cache)) (iso8601ParseM . T.unpack . T.strip) -- | Remember when the project was solved under this plan. storeSolveTime :: Cache -> UTCTime -> IO () storeSolveTime cache =- writeAtomically (at cache ["solve-times", tokenOf cache])- . T.pack- . iso8601Show+ writeAtomically (at cache (solveTimeFile cache)) . T.pack . iso8601Show -- | Where @cabal@ last said it keeps the sources it downloads. cachedPackageCache :: Cache -> IO (Maybe FilePath) cachedPackageCache cache =- readIfPresent (at cache ["package-cache", tokenOf cache]) (T.unpack . T.strip)+ readIfPresent (at cache (packageCacheFile cache)) (T.unpack . T.strip) -- | Remember where @cabal@ keeps the sources it downloads. storePackageCache :: Cache -> FilePath -> IO () storePackageCache cache =- writeAtomically (at cache ["package-cache", tokenOf cache]) . T.pack+ writeAtomically (at cache (packageCacheFile cache)) . T.pack -- | Render a fixity declaration as fields. renderFixity :: ((Namespace, OpName), Fixity) -> [Text]@@ -498,9 +494,6 @@ "r" -> Just RightAssoc "n" -> Just NoAssoc _ -> Nothing- readPrecedence t = case T.signed T.decimal t of- Right (p, rest) | T.null rest -> Just p- _ -> Nothing -- | Render a namespace as 'Text'. renderNamespace :: Namespace -> Text@@ -551,6 +544,38 @@ at cache path = case cache of Cache root _ -> Just (foldl (</>) root (fmap T.unpack path)) NoCache -> Nothing++-- | The path of the modules a package exposes.+modulesFile :: Text -> [Text]+modulesFile package = ["modules", package]++-- | The path of what reading a module established.+establishedFile :: Text -> Text -> [Text]+establishedFile package modName = ["established", package, modName]++-- | The path of what each configuration of a project module says.+summariesFile :: Text -> [Text]+summariesFile key = ["summaries", key]++-- | The path of the packages the compiler can see.+installedFile :: Cache -> [Text]+installedFile cache = ["installed", tokenOf cache]++-- | The path of the mark that solving again did not widen the plan.+solvesFile :: Cache -> [Text]+solvesFile cache = ["solves", tokenOf cache]++-- | The path of what fetching left missing.+fetchesFile :: Cache -> [Text]+fetchesFile cache = ["fetches", tokenOf cache]++-- | The path of when the project was last solved.+solveTimeFile :: Cache -> [Text]+solveTimeFile cache = ["solve-times", tokenOf cache]++-- | The path of the directory @cabal@ downloads sources into.+packageCacheFile :: Cache -> [Text]+packageCacheFile cache = ["package-cache", tokenOf cache] -- | The token a cache is kept under. tokenOf :: Cache -> Text
src/Tilia/Fixity/Debug.hs view
@@ -13,9 +13,10 @@ where import Data.Choice (Choice)-import Data.List.NonEmpty (NonEmpty)+import Data.List.NonEmpty (NonEmpty, nonEmpty) import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map+import Data.Maybe (mapMaybe) import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T@@ -24,6 +25,7 @@ Established (..), Fixity (..), Import (..),+ ModuleChain (..), OpName (..), Provenance (..), Resolution (..),@@ -37,6 +39,7 @@ reachUnqualified, spellDisagreement, spellFixity,+ spellModuleChain, spellUnreadIn, ) import Tilia.Palette (Color (Operator, Place), Palette, paint)@@ -191,9 +194,13 @@ | null (noteUnsettled i) -> operators n | otherwise -> operators n <> ", not settling every name" <> through (noteUnsettled i) - through ways = case filter (not . null) ways of+ through ways = case mapMaybe nonEmpty ways of [] -> ""- below -> ", through " <> T.intercalate " or " (fmap (T.intercalate " → " . fmap named) below)+ below ->+ ", through "+ <> T.intercalate+ " or "+ (fmap (spellModuleChain palette . ModuleChain) below) qualification i = case (noteQualified i, noteAlias i) of (True, Just alias) -> " qualified as " <> named alias
src/Tilia/Fixity/HiFile.hs view
@@ -12,7 +12,8 @@ ) where -import Data.Array (Array, bounds, listArray, (!))+import Control.Monad (unless)+import Data.Array (Array, bounds, inRange, listArray, (!)) import Data.Bits (shiftL, testBit, (.&.), (.|.)) import Data.ByteString (ByteString) import Data.ByteString qualified as BS@@ -87,7 +88,7 @@ hiFile :: Get HiFile hiFile = do magic <- word32be- unless' (magic == 0x1face64) (failure ("not a 64-bit interface file: " <> T.pack (show magic)))+ unless (magic == 0x1face64) (failure ("not a 64-bit interface file: " <> T.pack (show magic))) version <- string series <- case take 3 version of "910" -> pure Series910@@ -95,11 +96,11 @@ "914" -> pure Series914 _ -> failure ("unsupported interface version " <> T.pack version) _way <- string- unless' (series == Series914) fingerprint+ unless (series == Series914) fingerprint _extFields <- pointer series stringsAt <- pointer series namesAt <- pointer series- unless' (series == Series910) (() <$ pointer series)+ unless (series == Series910) (() <$ pointer series) strings <- lookingAt stringsAt dictionary names <- lookingAt namesAt symbolTable let tables = Tables strings names (keyedNames series)@@ -181,10 +182,6 @@ | inRange (bounds strings) i = pure (T.decodeUtf8Lenient (strings ! i)) | otherwise = failure "string index out of range" --- | Whether an index is within bounds.-inRange :: (Int, Int) -> Int -> Bool-inRange (lo, hi) i = lo <= i && i <= hi- -- | A unit, which must be a real one: instantiated units come of Backpack. unit :: Get () unit =@@ -302,10 +299,6 @@ listOf get = do n <- sleb mapM (const get) [1 .. n]---- | Run an action unless the condition holds.-unless' :: Bool -> Get () -> Get ()-unless' condition action = if condition then pure () else action -- | Reading from a position in a byte string. newtype Get a = Get{runGet :: ByteString -> Int -> Either Text (a, Int)}
src/Tilia/Fixity/Interface.hs view
@@ -22,7 +22,6 @@ import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T-import Data.Text.Read qualified as T import GHC.Generics (Generic) import Tilia.Fixity import Tilia.Fixity.HiFile (HiExport (..), HiFile (..), HiName (..), decodeHiFile)@@ -81,8 +80,7 @@ ], interfaceReexports = [ (m, op)- | names <- exports,- (Just m, _, op) <- names,+ | (Just m, _, op) <- concatMap mentioned hiExports, m /= hiModule ], interfaceMembers = Map.map (Set.map snd) members,@@ -94,7 +92,6 @@ } } where- exports = concatMap resolved hiExports exported = concatMap exportedNames hiExports exportedHere = Set.fromList [(namespace, op) | (_, namespace, op) <- exported]@@ -109,11 +106,9 @@ exportedNames = \case Avail n -> maybeToList (known n) AvailTC _ ns -> mapMaybe known ns- resolved = \case- Avail n -> maybe [] (pure . pure) (known n)- AvailTC p ns ->- let kids = mapMaybe known ns- in maybe (fmap pure kids) (\named -> [named : kids]) (known p)+ mentioned = \case+ Avail n -> maybeToList (known n)+ AvailTC p ns -> maybeToList (known p) <> mapMaybe known ns known = \case HiName m namespace op -> Just (Just m, namespace, op) BuiltInSyntax namespace op -> Just (Nothing, namespace, op)@@ -213,13 +208,6 @@ p <- readPrecedence precedence pure (OpName op, Fixity d p) _ -> Nothing- where- -- Not one digit: GHC gives @->@ a precedence of -1, below anything the- -- report allows anyone to write, and drops it into a fixities line like- -- any other.- readPrecedence t = case T.signed T.decimal t of- Right (p, rest) | T.null rest -> Just p- _ -> Nothing -- | Split an exports section into its entries, keeping the members an entry -- wears in braces with the name they belong to.
src/Tilia/Fixity/PackageDb.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} --- | Querying the compiler on which package exposes a module and where its+-- | Querying the compiler on which package holds a module and where its -- compiled interfaces are. module Tilia.Fixity.PackageDb ( InstalledPackage (..),@@ -22,7 +22,7 @@ import System.Directory (doesDirectoryExist, findExecutable) import System.FilePath ((</>)) import Tilia.Process (readProgramOutput)-import Tilia.Utils (quietly)+import Tilia.Utils (fieldWords, quietly) -- | An installed package. data InstalledPackage = InstalledPackage@@ -130,7 +130,7 @@ -- | The modules an @exposed-modules@ field names that the package holds. moduleNames :: Text -> [Text]-moduleNames = go . moduleTokens+moduleNames = go . fieldWords where go = \case (_ : "from" : _ : rest) -> go rest@@ -144,17 +144,13 @@ -- | The modules an @exposed-modules@ field names that another package -- holds, each with its name there. reexportedModules :: Text -> [(Text, Text)]-reexportedModules = go . moduleTokens+reexportedModules = go . fieldWords where go = \case (m : "from" : original : rest) -> (m, T.takeWhileEnd (/= ':') original) : go rest (_ : rest) -> go rest [] -> []---- | The words of a field that lists modules.-moduleTokens :: Text -> [Text]-moduleTokens = filter (not . T.null) . concatMap (T.split (== ',')) . T.words -- | Split a record into its fields. parseFields :: Text -> Map.Map Text Text
src/Tilia/Fixity/Plan.hs view
@@ -8,1898 +8,935 @@ -- 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,- plannedCompiler,-- -- * Readiness- Readiness (..),- PlanComponent (..),- spellComponent,- plannedComponents,- planPathFor,- checkReadiness,- plannedTarballs,- packageCacheRoot,- guessedPackageCacheRoot,- Futility (..),- undiscoveredFutility,- futilityFor,- prepareWith,- loadPlan,-- -- * Resolving- Route (..),- Resolver (..),- newResolver,- newResolverVia,- newResolverWith,- withReexports,- scopeFor,- )-where--import Codec.Archive.Tar qualified as Tar-import Codec.Compression.GZip qualified as GZip-import Control.Applicative ((<|>))-import Control.Concurrent- ( MVar,- ThreadId,- getNumCapabilities,- modifyMVar,- modifyMVar_,- myThreadId,- newEmptyMVar,- newMVar,- readMVar,- tryPutMVar,- tryReadMVar,- )-import Control.Concurrent.QSem (newQSem, signalQSem, waitQSem)-import Control.DeepSeq (NFData, force)-import Control.Exception (bracket_, evaluate, onException)-import Control.Monad (filterM, 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, isNothing, 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 Data.Time (UTCTime, getCurrentTime)-import Data.Unique (Unique, newUnique)-import Distribution.Version (Version, mkVersion)-import GHC.Generics (Generic)-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 (hClose, hFlush, stderr)-import System.Info qualified-import System.Process- ( StdStream (CreatePipe, Inherit, UseHandle),- createProcess,- cwd,- proc,- std_err,- std_in,- 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.ByHand (hscFixities)-import Tilia.Fixity.Cabal- ( cabalFileAtTop,- containedModules,- declaredExtensions,- entryPosixPath,- packageModules,- sourceDirs,- )-import Tilia.Fixity.Cache-import Tilia.Fixity.HiFile (primopFixities)-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 :: Cache -> IO [InstalledPackage]-getInstalledPackages cache =- cachedInstalled cache >>= \case- Just packages -> pure packages- Nothing -> do- found <- readInstalledPackages- storeInstalled cache found- pure (installedPackages found)---- | 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 =- decodePlan path >>= traverse (checkedOutIn (takeDirectory (takeDirectory path)))---- | 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 =- fmap (\plan -> plan{bpPackages = fmap rooted (bpPackages plan)})- <$> decodePlan path- where- rooted p = case ppSource p of- LocalPackage dir- | isRelative dir ->- p{ppSource = LocalPackage (root </> dir)}- _ -> p---- | Decode a plan, or say why it cannot be.-decodePlan :: FilePath -> IO (Either Text BuildPlan)-decodePlan path =- doesFileExist path >>= \case- False -> pure (Left ("no build plan at " <> T.pack path))- True -> either (Left . T.pack) Right <$> eitherDecodeFileStrict path---- | 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 =- mempty- { macroExactVersions =- Map.fromList- ( [ ("MIN_VERSION_" <> underscored name, version)- | (name, [version]) <- Map.toList (Map.map Set.toList versions)- ]- <> [("MIN_VERSION_GLASGOW_HASKELL", v) | v <- toList compiler]- ),- macroExactNumbers =- 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)- ]- ],- macroUndefined =- if null compiler- then Set.empty- else Set.fromList ["__MHS__", "__HUGS__"],- macroPlannedPackages =- Just (Set.fromList [underscored (ppName p) | p <- bpPackages plan])- }- 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 version of GHC a plan is for, where it is for GHC.-plannedCompiler :: BuildPlan -> Maybe Version-plannedCompiler plan =- mkVersion . fmap fromInteger- <$> (numberedVersion =<< T.stripPrefix "ghc-" (bpCompiler plan))---- | 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- solved <- cachedSolveTime =<< openCache caching =<< tokenForBuildPlan plan- newer <- filesNewerThanPlan plan projectDir solved- 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- cache <- openCache caching =<< tokenForBuildPlan plan- tarballs <- filter (isFetchable . fst) <$> plannedTarballs cache plan- absent <- fmap fst <$> filterM (fmap not . doesFileExist . snd) tarballs- if null absent- then pure []- else do- installed <- getInstalledPackages cache- pure (filter (not . builtAlready installed) absent)---- | 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, and--- since the last solve, which leaves a plan it would not change as it was.-filesNewerThanPlan :: BuildPlan -> FilePath -> Maybe UTCTime -> IO [FilePath]-filesNewerThanPlan plan projectDir solved = quietly [] $ do- written <- getModificationTime (planPathFor projectDir)- let planTime = maybe written (max written) solved- 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)---- | 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 (),- -- | Remember when a solve started, so that no change made before it- -- makes the plan stale.- rememberSolveTime :: UTCTime -> 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 ()),- rememberSolveTime = 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),- rememberSolveTime = \started -> withCache () (`storeSolveTime` started)- }- where- withCache fallback use =- readBuildPlan (planPathFor projectDir) >>= \case- Left _ -> pure fallback- Right plan -> use =<< openCache caching =<< tokenForBuildPlan plan---- | Do whatever is missing, 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 = do- started <- getCurrentTime- tryWholeProject ["build", ":all", "--dry-run"] >>= \case- Left err -> pure (Left err)- Right () -> do- rememberSolveTime futility started- 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- -- A pipe closed at once rather than our standard input, which in an- -- editor's process carries what the editor says to it.- (toChild, _, _, running) <-- createProcess- (proc "cabal" args)- { cwd = Just projectDir,- std_in = CreatePipe,- std_out = UseHandle passed,- std_err = Inherit- }- traverse_ hClose toChild- 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- prepareWith- caching- downloading- (runCabal projectDir)- (futilityFor caching projectDir)- 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: under the package cache @cabal@ last named, unless--- a tarball it downloads is not there, when @cabal@ is asked again.-plannedTarballs :: Cache -> BuildPlan -> IO [(PlanPackage, FilePath)]-plannedTarballs cache plan =- cachedPackageCache cache >>= \case- Just remembered -> do- tarballs <- under remembered- absent <-- filterM- (fmap not . doesFileExist . snd)- (filter (isFetchable . fst) tarballs)- if null absent then pure tarballs else asked- Nothing -> asked- where- asked = do- cacheRoot <- packageCacheRoot- storePackageCache cache cacheRoot- under cacheRoot- under cacheRoot = do- repos <- quietly [] (Data.List.sort <$> listDirectory cacheRoot)- traverse- (\p -> (,) p <$> tarballFor cacheRoot repos p)- [p | p <- bpPackages plan, not (isLocal p)]- 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.-newtype Resolver = Resolver- { -- | What reading a module established about the names it exports.- askModule :: Text -> IO Established- }---- | Build a new 'Resolver'.------ Answers are remembered on disk between runs by "Tilia.Fixity.Cache", so a--- package is decompressed and parsed once per machine rather than once per--- file.-newResolver ::- -- | The build plan to use.- BuildPlan ->- IO Resolver-newResolver = 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- cache <- openCache caching =<< tokenForBuildPlan plan- tarballs <-- if FromSource `elem` routes- then plannedTarballs cache plan- else pure []- installed <- getInstalledPackages cache- newResolverWith routes cache installed tarballs plan---- | 'newResolverVia', given what it would find out from the machine.-newResolverWith ::- -- | Which readings to try, in order.- [Route] ->- -- | Where to remember answers between runs.- Cache ->- -- | Every package the compiler can see.- [InstalledPackage] ->- -- | Every planned package that might have a tarball, and where it would- -- be.- [(PlanPackage, FilePath)] ->- -- | The build plan to use.- BuildPlan ->- IO Resolver-newResolverWith routes cache installed tarballs plan = do- index <- buildModuleIndex cache installed tarballs- let interfaces = interfaceIndex installed- local <- localModules plan- flights <- newMVar (Flights Map.empty Map.empty)- answersRead <- newMemo- askPackage <- newPackageReader (plannedCompiler plan)- extensionsRead <- newMemo- summariesRead <- newMemo- archivesRead <- newMemo- interfacesRead <- newMemo- reading <- newQSem =<< getNumCapabilities- let interfaceOf modName =- memoized flights interfacesRead Nothing modName $- case Map.lookup modName primopFixities of- Just declared -> pure (Just (asInterface declared))- Nothing -> case Map.lookup modName interfaces of- Nothing -> pure Nothing- Just (_, path) ->- bracket_ (waitQSem reading) (signalQSem reading) $- readInterface modName path- let workings =- Workings- { wkRoutes = routes,- wkCache = cache,- wkLocal = local,- wkIndex = index,- wkInterfaces = interfaces,- wkInterfaceOf = interfaceOf,- wkReach = reach,- wkSummariesOf = summariesOf,- wkModuleInArchive = moduleInArchive,- wkGenerated = generatedModules plan,- wkReexported =- Map.fromList [pair | i <- installed, pair <- ipReexports i]- }- -- The stand-in is what 'withReexports' makes of a module it is in the- -- middle of reading.- reach visiting modName- | modName `Set.member` visiting = pure mempty- | otherwise =- memoized flights answersRead mempty modName $- resolveModule workings visiting modName- summariesOf modName text =- memoized flights summariesRead Nothing modName $ do- extensions <- extensionsOf modName- let summarized =- evaluate . force $- configurationsOf- (macrosOf plan)- (Just extensions)- modName- text- stamp =- digestOf $- T.intercalate "\n" [T.pack (show extensions), macrosRead, text]- case digestOf . T.pack <$> Map.lookup modName local of- Nothing -> summarized- Just key ->- join- <$> recalled- (cachedSummaries cache key stamp)- (storeSummaries cache key stamp)- (Just <$> summarized)- macrosRead = T.pack (show (macrosOf plan))- extensionsOf modName- | Just path <- Map.lookup modName local =- either (const []) id <$> askPackage path- | Just (package, tarball) <- Map.lookup modName index =- memoized flights extensionsRead [] package $- foldMap declaredExtensions . (archiveCabal =<<) <$> archiveOf tarball- | otherwise = pure []- archiveOf tarball =- memoized flights archivesRead Nothing (T.pack tarball) (readArchive tarball)- moduleInArchive tarball modName = (moduleIn modName =<<) <$> archiveOf tarball- pure Resolver{askModule = reach Set.empty}---- | Answers filed under the names they are about, each worked out once.-data Memo v = Memo Unique (IORef (Map Text (MVar v)))---- | An empty 'Memo'.-newMemo :: IO (Memo v)-newMemo = Memo <$> newUnique <*> newIORef Map.empty---- | Which thread is working out which answer, and which answer each waiting--- thread waits for, across all of a resolver's tables.-data Flights = Flights- { flightOwners :: Map (Unique, Text) ThreadId,- flightWaits :: Map ThreadId (Unique, Text)- }---- | Look an answer up in a table, working it out and filing it the first--- time.-memoized ::- -- | Who is working out what.- MVar Flights ->- -- | Where the answers are filed.- Memo v ->- -- | The answer for a thread that cannot wait.- v ->- -- | What the answer is about.- Text ->- -- | Work that is being memoized.- IO v ->- IO v-memoized flights (Memo table answers) standIn key work =- Map.lookup key <$> readIORef answers >>= \case- Just slot -> tryReadMVar slot >>= maybe (waitFor slot) pure- Nothing -> do- me <- myThreadId- claimed <- modifyMVar flights $ \fl -> do- filed <- readIORef answers- case Map.lookup key filed of- Just slot -> pure (fl, Left slot)- Nothing -> do- slot <- newEmptyMVar- writeIORef answers (Map.insert key slot filed)- pure- ( fl{flightOwners = Map.insert (table, key) me (flightOwners fl)},- Right slot- )- case claimed of- Left slot -> waitFor slot- Right slot -> do- answer <- work `onException` file slot standIn- answer <$ file slot answer- where- file slot answer = modifyMVar_ flights $ \fl -> do- _ <- tryPutMVar slot answer- pure fl{flightOwners = Map.delete (table, key) (flightOwners fl)}- waitFor slot = do- me <- myThreadId- settled <- modifyMVar flights $ \fl ->- tryReadMVar slot >>= \case- Just answer -> pure (fl, Just answer)- Nothing- | waitedOnBy fl me (table, key) -> pure (fl, Just standIn)- | otherwise ->- pure- ( fl{flightWaits = Map.insert me (table, key) (flightWaits fl)},- Nothing- )- case settled of- Just answer -> pure answer- Nothing -> do- answer <- readMVar slot- modifyMVar_ flights $ \fl ->- pure fl{flightWaits = Map.delete me (flightWaits fl)}- pure answer---- | Is this answer being worked out by the given thread, or by one that--- waits for it, directly or through others?-waitedOnBy :: Flights -> ThreadId -> (Unique, Text) -> Bool-waitedOnBy fl me = go- where- go key = case Map.lookup key (flightOwners fl) of- Nothing -> False- Just owner- | owner == me -> True- | otherwise -> maybe False go (Map.lookup owner (flightWaits fl))---- | Work out what a module can see, using a resolver to reach its imports.------ This is the join between the pure half of "Tilia.Fixity" and the half--- that touches the disk: the imports are resolved first, and the scope is--- then computed from the answers. Note that a name an import leaves--- unsettled stays unsettled, which is what lets 'Tilia.Fixity.lookupFixity'--- distinguish a conclusion from a guess.-scopeFor ::- -- | What can be asked about the modules it imports.- Resolver ->- -- | Whether @ImplicitPrelude@ is on, which the module's own pragmas- -- and its package's @default-extensions@ decide between them.- Choice "implicitPrelude" ->- -- | The configurations of the module whose scope is wanted, already- -- parsed.- NonEmpty (HsModule GhcPs) ->- -- | Everything that module can see, and what it could not find out.- IO Scope-scopeFor resolver implicitPrelude configurations = do- let imports = moduleImports implicitPrelude configurations- answers <-- inParallel- (\m -> (m,) <$> askModule resolver m)- (fmap importModule imports)- let table = Map.fromList answers- pure $- resolveScope- implicitPrelude- (\m -> Map.findWithDefault unreadable m table)- configurations---- | Everything a resolver consults, and the way back into it.------ None of it changes from one module to the next, which is why--- '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 :: 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 Established,- -- | What each configuration of a module says, given its text, worked- -- out once a run.- wkSummariesOf :: Text -> Text -> IO (Maybe (NonEmpty ModuleSummary)),- -- | What a tarball holds for a module, each tarball read once a run.- wkModuleInArchive :: FilePath -> Text -> IO (Maybe InArchive),- -- | The modules @cabal@ writes itself, which are therefore in no- -- package's sources. See 'generatedModules'.- wkGenerated :: Set Text,- -- | The modules an installed package exposes that another one holds,- -- each with its name there.- wkReexported :: Map Text Text- }---- | What reading a module establishes, by the cheapest route that settles--- it.-resolveModule ::- -- | Where to look, and how to get back to the resolver.- Workings ->- -- | Modules currently being resolved further up the call chain.- Set Text ->- -- | The module to resolve.- Text ->- IO Established-resolveModule- Workings- { wkRoutes,- wkCache,- wkLocal,- wkIndex,- wkInterfaces,- wkInterfaceOf,- wkReach,- wkSummariesOf,- wkModuleInArchive,- wkGenerated,- wkReexported- }- visiting- modName- | Just path <- Map.lookup modName wkLocal,- writtenForHsc path =- pure (hscDeclares modName)- | Just path <- Map.lookup modName wkLocal =- readFileText path >>= \case- Nothing -> pure unreadable- Just source ->- fromSummaries (wkReach visiting') modName =<< wkSummariesOf modName source- | Just original <- Map.lookup modName wkReexported,- Map.notMember modName wkInterfaces =- wkReach visiting' original- | otherwise = answered <$> firstAnswer (fmap taking wkRoutes)- where- visiting' = Set.insert modName visiting-- taking = \case- FromInterface -> viaInterface- FromSource -> viaArchive-- -- A route that read some of the module is kept in case no later one- -- reads all of it.- firstAnswer = go Nothing- where- go partial [] = pure (fromMaybe unreadable partial)- go partial (route : rest) =- route >>= \case- Just established- | settlesEverything established -> pure established- | established /= unreadable -> go (partial <|> Just established) rest- _ -> go partial rest-- viaInterface = case Map.lookup modName wkInterfaces of- Nothing -> pure Nothing- Just (key, _) ->- throughCache key (Just <$> fromInterface wkInterfaceOf modName)-- viaArchive = case Map.lookup modName wkIndex of- Nothing -> pure Nothing- Just (package, tarball) ->- throughCache package $- fromSource- wkModuleInArchive- wkSummariesOf- (wkReach visiting')- tarball- modName-- answered established- | settlesEverything established = established- | Set.member modName wkGenerated = settledAs Map.empty established- | otherwise = established- throughCache key =- recalled- (cachedEstablished wkCache key modName)- (storeEstablished wkCache key modName)---- | Which package and tarball holds each module.-buildModuleIndex ::- -- | Where to remember each package's module list.- Cache ->- -- | What the compiler says is installed. Empty if @ghc-pkg@ could not be- -- run, in which case every package falls back to its @.cabal@ file.- [InstalledPackage] ->- -- | Every package that might have a tarball, and where it would be.- [(PlanPackage, FilePath)] ->- -- | For each module, the package that 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-" <> digestOf (T.pack dir)---- | A name for some text, short enough to file something under.-digestOf :: Text -> Text-digestOf =- T.take 24 . T.decodeUtf8Lenient . B16.encode . SHA256.hash . T.encodeUtf8---- | Present a fixity map as an 'Interface'.-asInterface :: Fixities -> Interface-asInterface fixities =- Interface- { interfaceDeclares = fixities,- interfaceReexports = [],- interfaceMembers = Map.empty,- interfaceExports = Nothing- }---- | What a compiled interface says a module exports, with the fixities of--- what it passes on read from where each is declared.-fromInterface ::- -- | A module's interface, if it has one.- (Text -> IO (Maybe Interface)) ->- -- | The module to read.- Text ->- IO Established-fromInterface interfaceOf modName =- interfaceOf modName >>= \case- Nothing -> pure unreadable- Just iface -> do- declarers <-- inParallel- asked- (distinct (fmap fst (interfaceReexports iface)))- let declarer m = join (lookup m declarers)- pure- Established- { establishedFixities =- Map.union (interfaceDeclares iface) . Map.unions $- [ Map.filterWithKey- (\(_, o) _ -> o == op)- (interfaceDeclares declared)- | (m, op) <- interfaceReexports iface,- Just declared <- [declarer m]- ],- establishedUnsettled =- Map.fromListWith- Set.union- [ ([m], Set.fromList [(InTypes, op), (InTerms, op)])- | (m, op) <- interfaceReexports iface,- Nothing <- [declarer m]- ],- establishedUntold = Set.empty,- establishedCertain = fromMaybe mempty (interfaceExports iface),- establishedMembers = interfaceMembers iface- }- where- asked m = do- interface <- interfaceOf m- pure (m, interface)- distinct = Map.keys . Map.fromList . fmap (,())---- | A package's module list from the @.cabal@ file in its tarball.-fromCabalFile ::- -- | Where to remember the answer.- Cache ->- -- | What to file it under. Carries the hash the tarball was verified- -- against, so a changed tarball misses rather than matching stale data.- Text ->- -- | The tarball to read the @.cabal@ file out of.- FilePath ->- -- | The package it belongs to, consulted for the hash to verify against.- PlanPackage ->- -- | The modules it exposes, or 'Nothing' if the tarball is absent, fails- -- verification, or holds no @.cabal@ file.- IO (Maybe [Text])-fromCabalFile cache key tarball p =- recalled (cachedModules cache key) (storeModules cache key) $- verified p tarball >>= \case- False -> pure Nothing- True -> packageModules tarball---- | 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 what a module exports out of a tarball, following re-exports.-fromSource ::- -- | What a tarball holds for a module.- (FilePath -> Text -> IO (Maybe InArchive)) ->- -- | What each configuration of a module says, given its text.- (Text -> Text -> IO (Maybe (NonEmpty ModuleSummary))) ->- -- | How to reach another module, for chasing re-exports. This is- -- 'resolveModule' tied back on itself, with the visiting set already- -- extended.- (Text -> IO Established) ->- -- | The tarball holding this module's source.- FilePath ->- -- | The module to read.- Text ->- -- | What reading it established, or 'Nothing' if there is no archive to- -- read it from.- IO (Maybe Established)-fromSource moduleInArchive summariesOf reach tarball modName =- doesFileExist tarball >>= \case- False -> pure Nothing- True ->- fmap Just $- moduleInArchive tarball modName >>= \case- Nothing -> pure unreadable- Just ForHsc -> pure (hscDeclares modName)- Just (Haskell source) ->- fromSummaries reach modName =<< summariesOf modName source---- | What a module exports, out of what each of its configurations says.-fromSummaries ::- -- | How to reach another module, for chasing re-exports. This is- -- 'resolveModule' tied back on itself, with the visiting set already- -- extended.- (Text -> IO Established) ->- -- | The name the module was looked up under.- Text ->- -- | What each configuration of it says, or 'Nothing' where none of them- -- parses.- Maybe (NonEmpty ModuleSummary) ->- IO Established-fromSummaries reach modName = \case- Nothing -> pure unreadable- Just summaries ->- agreeing <$> traverse (withReexports reach modName) summaries---- | One answer from every configuration of a module that counts.------ A module may declare a fixity in one configuration and a different one in--- another. Which of them holds depends on how the module is compiled, which--- is not ours to decide, so disagreement leaves the name unsettled.--- Agreement across the ones that count is an answer, and a stronger one--- than the blanked text could give.------ A configuration that leaves names unsettled is passed over where another--- settles every name, because almost every one of those is a branch meant--- for a different platform. Where none does, every configuration counts,--- and a name any of them leaves unsettled stays unsettled.-agreeing :: NonEmpty Established -> Established-agreeing answers =- settledOnly- Established- { establishedFixities = Map.mapMaybe sole declared,- establishedUnsettled =- Map.unionsWith- Set.union- ( maybe- Map.empty- (Map.singleton [])- disagreed- : fmap establishedUnsettled (toList counted)- ),- establishedUntold = foldMap establishedUntold counted,- establishedCertain = foldr1 inEvery (fmap establishedCertain counted),- establishedMembers =- Map.unionsWith Set.union (fmap establishedMembers counted)- }- where- counted =- fromMaybe answers $- NE.nonEmpty (NE.filter settlesEverything answers)- declared =- Map.unionsWith- (<>)- [Map.map pure (establishedFixities a) | a <- toList counted]- sole (fixity :| rest) =- if all (== fixity) rest then Just fixity else Nothing- disagreed = inhabited (Map.keysSet (Map.filter (isNothing . sole) declared))- inEvery a b =- Certain- { certainNames = Set.intersection (certainNames a) (certainNames b),- certainMembers =- Map.intersectionWith- Set.intersection- (certainMembers a)- (certainMembers b)- }---- | Where each module that lives in a directory rather than an archive is.-localModules :: BuildPlan -> IO (Map Text FilePath)-localModules plan =- Map.unions <$> traverse forPackage (concatMap directoryOf (bpPackages plan))- where- directoryOf p = case ppSource p of- LocalPackage dir -> [dir]- CheckedOut dir -> [dir]- _ -> []- forPackage dir = quietly Map.empty $ do- 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 exports, out of what it declares and what its imports--- bring in.-withReexports ::- -- | How to reach another module, for names this one only passes on.- (Text -> IO Established) ->- -- | The name this module was looked up under.- Text ->- -- | The module.- ModuleSummary ->- IO Established-withReexports reach modName summary =- case summaryExports summary of- Nothing -> pure own- Just items -> do- listed <-- reached $ \i ->- any (suppliedBy i) (concatMap named items)- || ( not (importQualified i)- && importAlias i- `elem` [m | ExportModule m <- items]- )- let reexported =- Map.fromList- [ ((qualifier, parent), membersFrom listed qualifier parent)- | ExportAll qualifier parent <- items,- Map.notMember parent declared- ]- members =- [ (qualifier, kid)- | ((qualifier, _), Just kids) <- Map.toList reexported,- kid <- Set.toList kids,- Set.notMember kid defined- ]- more <- reached $ \i ->- Map.notMember (importModule i) listed && any (suppliedBy i) members- let answers = Map.union listed more- parts = fmap (exported answers reexported) items- pure . together $- mempty- { establishedFixities =- Map.restrictKeys- (summaryFixities summary)- (foldMap (certainNames . establishedCertain) parts),- establishedMembers = summaryListedMembers summary- }- : parts- where- imports = summaryImports summary- declared = summaryDeclaredMembers summary- defined =- Set.map snd (summaryNames summary)- <> Set.map snd (Map.keysSet (summaryFixities summary))- own =- mempty- { establishedFixities = summaryFixities summary,- establishedCertain = Certain (summaryNames summary) declared,- establishedMembers = Map.map (Set.map snd) declared- }- reached wanted =- Map.fromList- <$> traverse- (\m -> (m,) <$> reach m)- (Set.toList (Set.fromList [importModule i | i <- imports, wanted i]))- suppliedBy i (qualifier, op) = maySupply Map.empty qualifier op i- named = \case- ExportName _ qualifier op ->- [(qualifier, op) | Set.notMember op defined]- ExportAll qualifier parent ->- [(qualifier, parent) | Set.notMember parent defined]- ExportSome qualifier parent kids ->- [(qualifier, op) | op <- parent : kids, Set.notMember op defined]- ExportModule _ -> []-- exported answers reexported = \case- ExportName namespace qualifier op ->- settled answers True qualifier (namespace, op)- <> certainly (Set.singleton (namespace, op)) Map.empty- ExportAll qualifier parent- | Just kids <- Map.lookup parent declared -> withMembers parent kids- | otherwise ->- let found = settled answers True qualifier (InTypes, parent)- kids =- Map.findWithDefault Nothing (qualifier, parent) reexported- in found- <> mempty{establishedUntold = Map.keysSet (establishedUnsettled found)}- <> foldMap (member answers qualifier parent) (foldMap Set.toList kids)- <> case kids of- Nothing -> certainly (Set.singleton (InTypes, parent)) Map.empty- Just known ->- withMembers parent (certainMembersOf answers qualifier parent)- <> mempty{establishedMembers = Map.singleton parent known}- ExportSome qualifier parent kids ->- settled answers True qualifier (InTypes, parent)- <> foldMap (member answers qualifier parent) kids- <> withMembers- parent- ( Set.filter- ((`elem` kids) . snd)- ( fromMaybe- (certainMembersOf answers qualifier parent)- (Map.lookup parent declared)- )- )- ExportModule m ->- (if Just m == summaryName summary || m == modName then own else mempty)- <> foldMap- (whole answers)- [i | i <- imports, not (importQualified i), importAlias i == m]-- certainly names members = mempty{establishedCertain = Certain names members}- withMembers parent kids =- certainly (Set.insert (InTypes, parent) kids) (Map.singleton parent kids)- member answers qualifier parent kid- | Set.member kid defined = mempty- | otherwise =- case [ (i, established, certainKids)- | (i, established) <- candidates answers qualifier parent,- decides established (InTypes, parent) i,- let certainKids =- Set.filter- ((== kid) . snd)- ( Map.findWithDefault- Set.empty- parent- (certainMembers (establishedCertain established))- ),- not (Set.null certainKids)- ] of- (i, established, certainKids) : _ ->- foldMap (from [(i, established)]) certainKids- [] ->- foldMap- ( \namespace ->- settled answers False qualifier (namespace, kid)- )- [InTypes, InTerms]-- candidates answers qualifier op =- [ (i, established)- | i <- imports,- Just established <- [Map.lookup (importModule i) answers],- maySupply (establishedMembers established) qualifier op i- ]-- settled answers certain qualifier name@(_, op)- | Set.member op defined = mempty- | otherwise = case [ (i, established)- | certain,- (i, established) <- found,- decides established name i- ] of- decider : _ -> from [decider] name- [] -> from found name- where- found = candidates answers qualifier op-- from imported name =- case [ importModule i : chain- | (i, established) <- imported,- chain <- unsettledThrough established name- ] of- [] ->- mempty- { establishedFixities =- Map.fromList- ( take- 1- [ (name, fixity)- | (_, a) <- imported,- Just fixity <- [Map.lookup name (establishedFixities a)]- ]- )- }- chains ->- mempty- { establishedUnsettled =- Map.fromList [(chain, Set.singleton name) | chain <- chains]- }-- membersFrom answers qualifier parent =- case mapMaybe- (Map.lookup parent . establishedMembers . snd)- (candidates answers qualifier parent) of- [] -> Nothing- kids -> Just (Set.unions kids)-- certainMembersOf answers qualifier parent =- Set.unions- [ Set.filter (\kid -> certainlyBrings (establishedMembers established) kid i) kids- | (i, established) <- candidates answers qualifier parent,- let certain = establishedCertain established,- Set.member (InTypes, parent) (certainNames certain),- Just kids <- [Map.lookup parent (certainMembers certain)]- ]-- whole answers i =- Established- { establishedFixities =- Map.filterWithKey- (\(_, op) _ -> admitted op)- (establishedFixities established),- establishedUnsettled =- Map.mapKeys- (importModule i :)- ( Map.mapMaybe- (inhabited . Set.filter (admitted . snd))- (establishedUnsettled established)- ),- establishedUntold =- Set.map- (importModule i :)- (establishedUntold established),- establishedCertain =- Certain- { certainNames = Set.filter included (certainNames certain),- certainMembers =- Map.map- (Set.filter included)- ( Map.filterWithKey- (\parent _ -> included (InTypes, parent))- (certainMembers certain)- )- },- establishedMembers = establishedMembers established- }- where- established = Map.findWithDefault mempty (importModule i) answers- certain = establishedCertain established- admitted op = maySupply (establishedMembers established) Nothing op i- included name = certainlyBrings (establishedMembers established) name i---- | The parts of what a module exports, together.------ A name one part certainly exports with a settled fixity is the one every--- other part exports under that name too, or the module would not compile,--- so no part leaves it unsettled.-together :: [Established] -> Established-together parts =- settledOnly- combined- { establishedUnsettled =- Map.mapMaybe- (inhabited . (`Set.difference` certain))- (establishedUnsettled combined)- }- where- combined = mconcat parts- certain =- Set.unions- [ Set.filter- (null . unsettledThrough part)- (certainNames (establishedCertain part))- | part <- parts- ]---- | An answer without the fixities of the names it leaves unsettled.-settledOnly :: Established -> Established-settledOnly established- | settlesEverything established = established- | otherwise =- established- { establishedFixities =- Map.filterWithKey- (\name _ -> null (unsettledThrough established name))- (establishedFixities established)- }---- | An answer whose fixities come from elsewhere, and settle every name.-settledAs :: Fixities -> Established -> Established-settledAs fixities established =- established- { establishedFixities = fixities,- establishedUnsettled = Map.empty,- establishedUntold = Set.empty- }---- | A set, unless it is empty.-inhabited :: Set a -> Maybe (Set a)-inhabited names- | Set.null names = Nothing- | otherwise = Just names---- | Every configuration the preprocessor allows of a module's text that is--- Haskell, parsed and summarized.-configurationsOf ::- -- | What the plan settles about the questions its conditionals ask.- Macros ->- -- | What extensions the module's package puts in force.- Maybe [Extension] ->- -- | The module's name, for the parser to put in its errors.- Text ->- -- | Its text.- Text ->- Maybe (NonEmpty ModuleSummary)-configurationsOf macros extensions modName text =- NE.nonEmpty . mapMaybe parsed- =<< whatParsed (branchLeaves (withoutRuledOut macros text))- where- parsed leaf =- summarize (hasImplicitPrelude (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)---- | What a tarball holds that a module could be read out of.-data Archive = Archive- { -- | The package's @.cabal@ file, the first one at the top.- archiveCabal :: Maybe Text,- -- | Every file that could hold a module, by where it sits, in the order- -- the tarball has them.- archiveFiles :: [(FilePath, BS.ByteString)]- }- deriving (Generic)--instance NFData Archive---- | Read a tarball, keeping what a module could be read out of.-readArchive :: FilePath -> IO (Maybe Archive)-readArchive tarball = quietly Nothing $ do- bytes <- BL.readFile tarball- Just <$> evaluate (force (sweep Nothing [] (Tar.read (GZip.decompress bytes))))- where- sweep cabal found = \case- Tar.Next entry rest- | Tar.NormalFile content _ <- Tar.entryContent entry,- cabalFileAtTop (entryPosixPath entry),- Nothing <- cabal ->- sweep (Just (T.decodeUtf8Lenient (BL.toStrict content))) found rest- | Tar.NormalFile content _ <- Tar.entryContent entry,- any (`isSuffixOf` entryPosixPath entry) moduleEndings ->- sweep cabal ((entryPosixPath entry, BL.toStrict content) : found) rest- | otherwise -> sweep cabal found rest- _ -> Archive cabal (reverse found)---- | Find a module in what a tarball holds and say what was found.-moduleIn :: Text -> Archive -> Maybe InArchive-moduleIn modName archive = listToMaybe (mapMaybe pick moduleEndings)- where- dirs = maybe [] sourceDirs (archiveCabal archive)- pick ending =- inArchive ending . T.decodeUtf8Lenient . snd- <$> listToMaybe (under sfx matching <> matching)- where- sfx = "/" <> T.unpack (T.replace "." "/" modName) <> ending- matching = [f | f <- archiveFiles archive, sfx `isSuffixOf` fst f]- under sfx matching = [f | d <- dirs, f <- matching, inDir sfx d (fst f)]- inDir sfx d path- | d == "." = takeWhile (/= '/') path <> sfx == path- | otherwise = ("/" <> T.unpack d <> sfx) `isSuffixOf` path---- | 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 =- mempty- { establishedFixities =- maybe Map.empty inBothNamespaces (Map.lookup modName hscFixities)- }+ ( Route (..),+ Resolver (..),+ newResolver,+ newResolverVia,+ newResolverWith,+ newResolverFrom,+ withReexports,+ scopeFor,+ )+where++import Control.Applicative ((<|>))+import Control.Concurrent (getNumCapabilities)+import Control.Concurrent.QSem (newQSem, signalQSem, waitQSem)+import Control.DeepSeq (force)+import Control.Exception (bracket_, evaluate)+import Control.Monad (filterM, join)+import Crypto.Hash.SHA256 qualified as SHA256+import Data.ByteString.Base16 qualified as B16+import Data.ByteString.Lazy qualified as BL+import Data.Choice (Choice, fromBool, pattern Do)+import Data.Foldable (toList)+import Data.List.NonEmpty (NonEmpty ((:|)))+import Data.List.NonEmpty qualified as NE+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Maybe (catMaybes, fromMaybe, isNothing, mapMaybe)+import Data.Set (Set)+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as T+import Data.Text.Encoding qualified as T+import GHC.Hs (HsModule)+import GHC.Hs.Extension (GhcPs)+import GHC.LanguageExtensions.Type (Extension (ImplicitPrelude))+import System.Directory (doesFileExist)+import System.FilePath ((</>))+import Tilia.Cabal.Package (PackageReader, declaredModules, newPackageReader, readPackageDescription)+import Tilia.Cabal.Plan+import Tilia.Cabal.Project (cabalFilesIn)+import Tilia.Cpp.Directives (branchLeaves, withoutRuledOut)+import Tilia.Cpp.Macros (Macros (..))+import Tilia.Fixity+import Tilia.Fixity.Archive (Archive (..), InArchive (..), moduleEndings, moduleIn, pathOfModule, readArchive, writtenForHsc)+import Tilia.Fixity.Cabal (declaredExtensions, packageModules)+import Tilia.Fixity.Cache+import Tilia.Fixity.HiFile (HiFile, primopFixities)+import Tilia.Fixity.Interface+import Tilia.Fixity.PackageDb+import Tilia.Memo (memoized, newFlights, newMemo)+import Tilia.Parser+import Tilia.Pragma (effectiveExtensions)+import Tilia.Utils (byteOrderMark, inParallel, quietly, readFileText, sha256Of)++----------------------------------------------------------------------------+-- Resolving++-- | Where a module's fixities can be read from.+data Route+ = -- | The compiled interface the package database points at. Cheap, and+ -- authoritative where it exists, since it is the compiler's own account+ -- of what it settled on.+ FromInterface+ | -- | The module's source, out of the package's tarball in Cabal's+ -- package cache. Slower, and the only route for a package that is+ -- planned but not built.+ FromSource+ deriving (Eq, Show)++-- | What can be asked about a module, once a plan says where to look.+newtype Resolver = Resolver+ { -- | What reading a module established about the names it exports.+ askModule :: Text -> IO Established+ }++-- | Build a new 'Resolver'.+--+-- Answers are remembered on disk between runs by "Tilia.Fixity.Cache", so a+-- package is decompressed and parsed once per machine rather than once per+-- file.+newResolver ::+ -- | The build plan to use.+ BuildPlan ->+ IO Resolver+newResolver plan = do+ packageReader <- newPackageReader (plannedCompiler plan)+ newResolverVia (Do #useCache) [FromInterface, FromSource] packageReader plan++-- | 'newResolver', restricted to the routes given.+newResolverVia ::+ -- | Whether to use the cache.+ Choice "useCache" ->+ -- | Which readings to try, in order.+ [Route] ->+ -- | What the package of each of the project's own modules puts in force.+ PackageReader ->+ -- | The build plan to use.+ BuildPlan ->+ IO Resolver+newResolverVia caching routes packageReader plan = do+ cache <- openCache caching =<< tokenForBuildPlan plan+ tarballs <-+ if FromSource `elem` routes+ then plannedTarballs cache plan+ else pure []+ installed <- getInstalledPackages cache+ newResolverWith routes cache installed tarballs packageReader plan++-- | 'newResolverVia', given what it would find out from the machine.+newResolverWith ::+ -- | Which readings to try, in order.+ [Route] ->+ -- | Where to remember answers between runs.+ Cache ->+ -- | Every package the compiler can see.+ [InstalledPackage] ->+ -- | Every planned package that might have a tarball, and where it would+ -- be.+ [(PlanPackage, FilePath)] ->+ -- | What the package of each of the project's own modules puts in force.+ PackageReader ->+ -- | The build plan to use.+ BuildPlan ->+ IO Resolver+newResolverWith routes cache installed tarballs packageReader plan = do+ index <- buildModuleIndex cache installed tarballs+ let interfaces = interfaceIndex installed+ local <- localModules plan+ flights <- newFlights+ answersRead <- newMemo+ extensionsRead <- newMemo+ archivesRead <- newMemo+ interfacesRead <- newMemo+ reading <- newQSem =<< getNumCapabilities+ let interfaceOf modName =+ memoized flights interfacesRead Nothing modName $+ case Map.lookup modName primopFixities of+ Just declared -> pure (Just (asInterface declared))+ Nothing -> case Map.lookup modName interfaces of+ Nothing -> pure Nothing+ Just (_, path) ->+ bracket_ (waitQSem reading) (signalQSem reading) $+ readInterface modName path+ let workings =+ Workings+ { wkRoutes = routes,+ wkCache = cache,+ wkLocal = local,+ wkIndex = index,+ wkInterfaces = interfaces,+ wkInterfaceOf = interfaceOf,+ wkReach = reach,+ wkSummariesOf = summariesOf,+ wkModuleInArchive = moduleInArchive,+ wkGenerated = generatedModules plan,+ wkReexported =+ Map.fromListWith+ (\_ first' -> first')+ [pair | i <- installed, pair <- ipReexports i]+ }+ -- The stand-in is what 'withReexports' makes of a module it is in the+ -- middle of reading.+ reach visiting modName+ | modName `Set.member` visiting = pure mempty+ | otherwise =+ memoized flights answersRead mempty modName $+ resolveModule workings visiting modName+ summariesOf modName text =+ extensionsOf modName >>= \case+ Nothing -> pure Nothing+ Just extensions -> do+ let summarized =+ evaluate . force $+ configurationsOf (macrosOf plan) extensions modName text+ stamp =+ digestOf . T.intercalate "\n" $+ [T.pack (show extensions), macrosRead, text]+ case digestOf . T.pack <$> Map.lookup modName local of+ Nothing -> summarized+ Just key ->+ join+ <$> recalled+ (cachedSummaries cache key stamp)+ (storeSummaries cache key stamp)+ (Just <$> summarized)+ macrosRead = T.pack (show (macrosOf plan))+ extensionsOf modName+ | Just path <- Map.lookup modName local =+ either (const Nothing) Just <$> packageReader path+ | Just (package, tarball) <- Map.lookup modName index =+ memoized flights extensionsRead Nothing package $+ fmap declaredExtensions . (archiveCabal =<<) <$> archiveOf tarball+ | otherwise = pure Nothing+ archiveOf tarball =+ memoized flights archivesRead Nothing (T.pack tarball) (readArchive tarball)+ moduleInArchive tarball modName = (moduleIn modName =<<) <$> archiveOf tarball+ pure Resolver{askModule = reach Set.empty}++-- | A 'Resolver' that reads every module out of the interface given for it,+-- for a program that has compiled the project and holds its interfaces.+newResolverFrom ::+ -- | The interface of a module, by name.+ (Text -> IO (Maybe HiFile)) ->+ IO Resolver+newResolverFrom given = do+ flights <- newFlights+ interfacesRead <- newMemo+ answersRead <- newMemo+ let interfaceOf modName =+ memoized flights interfacesRead Nothing modName $+ case Map.lookup modName primopFixities of+ Just declared -> pure (Just (asInterface declared))+ Nothing ->+ (either (const Nothing) Just . fromHiFile modName =<<)+ <$> given modName+ pure+ Resolver+ { askModule = \modName ->+ memoized flights answersRead unreadable modName $+ fromInterface interfaceOf modName+ }++-- | Work out what a module can see, using a resolver to reach its imports.+--+-- This is the join between the pure half of "Tilia.Fixity" and the half+-- that touches the disk: the imports are resolved first, and the scope is+-- then computed from the answers. Note that a name an import leaves+-- unsettled stays unsettled, which is what lets 'Tilia.Fixity.lookupFixity'+-- distinguish a conclusion from a guess.+scopeFor ::+ -- | What can be asked about the modules it imports.+ Resolver ->+ -- | Whether @ImplicitPrelude@ is on, which the module's own pragmas+ -- and its package's @default-extensions@ decide between them.+ Choice "implicitPrelude" ->+ -- | The configurations of the module whose scope is wanted, already+ -- parsed.+ NonEmpty (HsModule GhcPs) ->+ -- | Everything that module can see, and what it could not find out.+ IO Scope+scopeFor resolver implicitPrelude configurations = do+ let imports = moduleImports implicitPrelude configurations+ answers <-+ inParallel+ (\m -> (m,) <$> askModule resolver m)+ (fmap importModule imports)+ let table = Map.fromList answers+ pure $+ resolveScope+ implicitPrelude+ (\m -> Map.findWithDefault unreadable m table)+ configurations++-- | Everything a resolver consults, and the way back into it.+--+-- None of it changes from one module to the next, which is why+-- 'newResolverWith' builds it once and hands it over whole. What comes back+-- out of that is the 'Resolver'; this is what is behind it.+data Workings = Workings+ { -- | Which readings to try, in the order given.+ wkRoutes :: [Route],+ -- | Where to remember answers between runs.+ wkCache :: Cache,+ -- | The modules of the project's own packages, which are read straight+ -- from disk rather than out of an archive.+ wkLocal :: Map Text FilePath,+ -- | Which package holds each module, and the tarball to find it in; the+ -- package is the cache key, which carries the hash the tarball was+ -- verified against.+ wkIndex :: Map Text (Text, FilePath),+ -- | What to file an answer read out of each module's interface under.+ wkInterfaces :: Map Text (Text, FilePath),+ -- | A module's interface, read at most once a run.+ wkInterfaceOf :: Text -> IO (Maybe Interface),+ -- | How to reach another module. Tied back on itself by+ -- 'newResolverWith', so that the memo it keeps covers the recursive+ -- calls too.+ wkReach :: Set Text -> Text -> IO Established,+ -- | What each configuration of a module says, given its text, worked+ -- out once a run.+ wkSummariesOf :: Text -> Text -> IO (Maybe (NonEmpty ModuleSummary)),+ -- | What a tarball holds for a module, each tarball read once a run.+ wkModuleInArchive :: FilePath -> Text -> IO (Maybe InArchive),+ -- | The modules @cabal@ writes itself, which are therefore in no+ -- package's sources. See 'generatedModules'.+ wkGenerated :: Set Text,+ -- | The modules an installed package exposes that another one holds,+ -- each with its name there.+ wkReexported :: Map Text Text+ }++-- | What reading a module establishes, by the cheapest route that settles+-- it.+resolveModule ::+ -- | Where to look, and how to get back to the resolver.+ Workings ->+ -- | Modules currently being resolved further up the call chain.+ Set Text ->+ -- | The module to resolve.+ Text ->+ IO Established+resolveModule+ Workings+ { wkRoutes,+ wkCache,+ wkLocal,+ wkIndex,+ wkInterfaces,+ wkInterfaceOf,+ wkReach,+ wkSummariesOf,+ wkModuleInArchive,+ wkGenerated,+ wkReexported+ }+ visiting+ modName+ | Just path <- Map.lookup modName wkLocal,+ writtenForHsc path =+ pure mempty+ | Just path <- Map.lookup modName wkLocal =+ readFileText path >>= \case+ Nothing -> pure unreadable+ Just source ->+ fromSummaries (wkReach visiting') modName+ =<< wkSummariesOf modName (snd (byteOrderMark source))+ | Just original <- Map.lookup modName wkReexported,+ Map.notMember modName wkInterfaces =+ wkReach visiting' original+ | otherwise = answered <$> firstAnswer (fmap taking wkRoutes)+ where+ visiting' = Set.insert modName visiting++ taking = \case+ FromInterface -> viaInterface+ FromSource -> viaArchive++ -- A route that read some of the module is kept in case no later one+ -- reads all of it.+ firstAnswer = go Nothing+ where+ go partial [] = pure (fromMaybe unreadable partial)+ go partial (route : rest) =+ route >>= \case+ Just established+ | settlesEverything established -> pure established+ | established /= unreadable -> go (partial <|> Just established) rest+ _ -> go partial rest++ viaInterface = case Map.lookup modName wkInterfaces of+ Nothing -> pure Nothing+ Just (key, _) ->+ throughCache key (Just <$> fromInterface wkInterfaceOf modName)++ viaArchive = case Map.lookup modName wkIndex of+ Nothing -> pure Nothing+ Just (package, tarball) ->+ throughCache package $+ fromSource+ wkModuleInArchive+ wkSummariesOf+ (wkReach visiting')+ tarball+ modName++ answered established+ | settlesEverything established = established+ | Set.member modName wkGenerated =+ established+ { establishedFixities = Map.empty,+ establishedUnsettled = Map.empty,+ establishedUntold = Set.empty+ }+ | otherwise = established+ throughCache key =+ recalled+ (cachedEstablished wkCache key modName)+ (storeEstablished wkCache key modName)++-- | Which package and tarball holds each module.+buildModuleIndex ::+ -- | Where to remember each package's module list.+ Cache ->+ -- | What the compiler says is installed. Empty if @ghc-pkg@ could not be+ -- run, in which case every package falls back to its @.cabal@ file.+ [InstalledPackage] ->+ -- | Every package that might have a tarball, and where it would be.+ [(PlanPackage, FilePath)] ->+ -- | For each module, the package that holds it (as a cache key) and+ -- the tarball holding its source.+ IO (Map Text (Text, FilePath))+buildModuleIndex cache installed tarballs =+ Map.fromListWith (\_ first' -> first') . concat <$> traverse one tarballs+ where+ byNameVersion =+ Map.fromList [((ipName i, ipVersion i), ipModules i) | i <- installed]+ one (p, tarball) = do+ let key = cacheKey p+ let exposed = Map.lookup (ppName p, ppVersion p) byNameVersion+ held <- fromCabalFile cache key tarball p+ let modules = case (exposed, held) of+ (Nothing, Nothing) -> []+ (a, b) -> concat (catMaybes [a, b])+ pure [(m, (key, tarball)) | m <- modules]++-- | Where each installed module's compiled interface is.+interfaceIndex :: [InstalledPackage] -> Map Text (Text, FilePath)+interfaceIndex installed =+ Map.fromListWith+ (\_ first' -> first')+ [ (m, (key, dir </> pathOfModule m <> ".hi"))+ | i <- installed,+ dir <- ipImportDirs i,+ -- Bound out here so that the directory is hashed once rather than+ -- once for each of the modules found in it.+ let key = keyFor dir,+ m <- ipModules i+ ]+ where+ keyFor dir = "interface-" <> digestOf (T.pack dir)++-- | A name for some text, short enough to file something under.+digestOf :: Text -> Text+digestOf = T.take 24 . sha256Of++-- | Present a fixity map as an 'Interface'.+asInterface :: Fixities -> Interface+asInterface fixities =+ Interface+ { interfaceDeclares = fixities,+ interfaceReexports = [],+ interfaceMembers = Map.empty,+ interfaceExports = Nothing+ }++-- | What a compiled interface says a module exports, with the fixities of+-- what it passes on read from where each is declared.+fromInterface ::+ -- | A module's interface, if it has one.+ (Text -> IO (Maybe Interface)) ->+ -- | The module to read.+ Text ->+ IO Established+fromInterface interfaceOf modName =+ interfaceOf modName >>= \case+ Nothing -> pure unreadable+ Just iface -> do+ declarers <-+ inParallel+ asked+ (distinct (fmap fst (interfaceReexports iface)))+ let declarer m = join (lookup m declarers)+ pure+ Established+ { establishedFixities =+ Map.union (interfaceDeclares iface) . Map.unions $+ [ Map.filterWithKey+ (\(_, o) _ -> o == op)+ (interfaceDeclares declared)+ | (m, op) <- interfaceReexports iface,+ Just declared <- [declarer m]+ ],+ establishedUnsettled =+ Map.fromListWith+ Set.union+ [ ([m], Set.fromList [(InTypes, op), (InTerms, op)])+ | (m, op) <- interfaceReexports iface,+ Nothing <- [declarer m]+ ],+ establishedUntold = Set.empty,+ establishedCertain = fromMaybe mempty (interfaceExports iface),+ establishedMembers = interfaceMembers iface+ }+ where+ asked m = do+ interface <- interfaceOf m+ pure (m, interface)+ distinct = Map.keys . Map.fromList . fmap (,())++-- | A package's module list from the @.cabal@ file in its tarball.+fromCabalFile ::+ -- | Where to remember the answer.+ Cache ->+ -- | What to file it under. Carries the hash the tarball was verified+ -- against, so a changed tarball misses rather than matching stale data.+ Text ->+ -- | The tarball to read the @.cabal@ file out of.+ FilePath ->+ -- | The package it belongs to, consulted for the hash to verify against.+ PlanPackage ->+ -- | The modules it holds, or 'Nothing' if the tarball is absent, fails+ -- verification, or holds no @.cabal@ file.+ IO (Maybe [Text])+fromCabalFile cache key tarball p =+ recalled (cachedModules cache key) (storeModules cache key) $+ verified p tarball >>= \case+ False -> pure Nothing+ True -> packageModules tarball++-- | Does the tarball hash to what the plan says it should?+verified :: PlanPackage -> FilePath -> IO Bool+verified p tarball = case sourceHashOf p of+ Nothing -> pure True+ Just expected ->+ quietly False $ do+ actual <- sha256OfFile tarball+ pure (actual == T.toLower expected)++-- | The SHA-256 of a file, as lower-case hex.+sha256OfFile :: FilePath -> IO Text+sha256OfFile path = do+ bytes <- BL.readFile path+ pure (T.decodeUtf8Lenient (B16.encode (SHA256.hashlazy bytes)))++-- | Read what a module exports out of a tarball, following re-exports.+fromSource ::+ -- | What a tarball holds for a module.+ (FilePath -> Text -> IO (Maybe InArchive)) ->+ -- | What each configuration of a module says, given its text.+ (Text -> Text -> IO (Maybe (NonEmpty ModuleSummary))) ->+ -- | How to reach another module, for chasing re-exports. This is+ -- 'resolveModule' tied back on itself, with the visiting set already+ -- extended.+ (Text -> IO Established) ->+ -- | The tarball holding this module's source.+ FilePath ->+ -- | The module to read.+ Text ->+ -- | What reading it established, or 'Nothing' if there is no archive to+ -- read it from.+ IO (Maybe Established)+fromSource moduleInArchive summariesOf reach tarball modName =+ doesFileExist tarball >>= \case+ False -> pure Nothing+ True ->+ fmap Just $+ moduleInArchive tarball modName >>= \case+ Nothing -> pure unreadable+ Just ForHsc -> pure mempty+ Just (Haskell source) ->+ fromSummaries reach modName =<< summariesOf modName source++-- | What a module exports, out of what each of its configurations says.+fromSummaries ::+ -- | How to reach another module, for chasing re-exports. This is+ -- 'resolveModule' tied back on itself, with the visiting set already+ -- extended.+ (Text -> IO Established) ->+ -- | The name the module was looked up under.+ Text ->+ -- | What each configuration of it says, or 'Nothing' where none of them+ -- parses.+ Maybe (NonEmpty ModuleSummary) ->+ IO Established+fromSummaries reach modName = \case+ Nothing -> pure unreadable+ Just summaries ->+ agreeing <$> traverse (withReexports reach modName) summaries++-- | One answer from every configuration of a module that counts.+--+-- A module may declare a fixity in one configuration and a different one in+-- another. Which of them holds depends on how the module is compiled, which+-- is not ours to decide, so disagreement leaves the name unsettled.+-- Agreement across the ones that count is an answer, and a stronger one+-- than the blanked text could give.+--+-- A configuration that leaves names unsettled is passed over where another+-- settles every name, because almost every one of those is a branch meant+-- for a different platform. Where none does, every configuration counts,+-- and a name any of them leaves unsettled stays unsettled.+agreeing :: NonEmpty Established -> Established+agreeing answers =+ settledOnly+ Established+ { establishedFixities = Map.mapMaybe sole declared,+ establishedUnsettled =+ Map.unionsWith+ Set.union+ ( maybe+ Map.empty+ (Map.singleton [])+ disagreed+ : fmap establishedUnsettled (toList counted)+ ),+ establishedUntold = foldMap establishedUntold counted,+ establishedCertain = foldr1 inEvery (fmap establishedCertain counted),+ establishedMembers =+ Map.unionsWith Set.union (fmap establishedMembers counted)+ }+ where+ counted =+ fromMaybe answers $+ NE.nonEmpty (NE.filter settlesEverything answers)+ declared =+ Map.unionsWith+ (<>)+ [Map.map pure (establishedFixities a) | a <- toList counted]+ sole (fixity :| rest) =+ if all (== fixity) rest then Just fixity else Nothing+ disagreed = inhabited (Map.keysSet (Map.filter (isNothing . sole) declared))+ inEvery a b =+ Certain+ { certainNames = Set.intersection (certainNames a) (certainNames b),+ certainMembers =+ Map.intersectionWith+ Set.intersection+ (certainMembers a)+ (certainMembers b)+ }++-- | Where each module that lives in a directory rather than an archive is.+localModules :: BuildPlan -> IO (Map Text FilePath)+localModules plan =+ Map.unions <$> traverse forPackage (concatMap directoryOf (bpPackages plan))+ where+ directoryOf p = case ppSource p of+ LocalPackage dir -> [dir]+ CheckedOut dir -> [dir]+ _ -> []+ forPackage dir = quietly Map.empty $ do+ cabalFilesIn dir >>= \case+ [] -> pure Map.empty+ (cabalFile : _) ->+ readPackageDescription cabalFile >>= \case+ Left _ -> pure Map.empty+ Right described ->+ Map.fromListWith (\_ first' -> first') . concat+ <$> traverse (locate dir) (declaredModules described)+ locate dir (m, unwritten) = do+ found <- filterM doesFileExist [dir </> unwritten <> e | e <- moduleEndings]+ pure [(m, path) | path <- take 1 found]++-- | What a module exports, out of what it declares and what its imports+-- bring in.+withReexports ::+ -- | How to reach another module, for names this one only passes on.+ (Text -> IO Established) ->+ -- | The name this module was looked up under.+ Text ->+ -- | The module.+ ModuleSummary ->+ IO Established+withReexports reach modName summary =+ case summaryExports summary of+ Nothing -> pure own+ Just items -> do+ listed <-+ reached $ \i ->+ any (suppliedBy i) (concatMap named items)+ || ( not (importQualified i)+ && importAlias i+ `elem` [m | ExportModule m <- items]+ )+ let reexported =+ Map.fromList+ [ ((qualifier, parent), membersFrom listed qualifier parent)+ | ExportAll qualifier parent <- items,+ Map.notMember parent declared+ ]+ members =+ [ (qualifier, kid)+ | ((qualifier, _), Just kids) <- Map.toList reexported,+ kid <- Set.toList kids,+ Set.notMember kid defined+ ]+ more <- reached $ \i ->+ Map.notMember (importModule i) listed && any (suppliedBy i) members+ let answers = Map.union listed more+ parts = fmap (exported answers reexported) items+ pure . together $+ mempty+ { establishedFixities =+ Map.restrictKeys+ (summaryFixities summary)+ (foldMap (certainNames . establishedCertain) parts),+ establishedMembers = summaryListedMembers summary+ }+ : parts+ where+ imports = summaryImports summary+ declared = summaryDeclaredMembers summary+ defined =+ Set.map snd (summaryNames summary)+ <> Set.map snd (Map.keysSet (summaryFixities summary))+ own =+ mempty+ { establishedFixities = summaryFixities summary,+ establishedCertain = Certain (summaryNames summary) declared,+ establishedMembers = Map.map (Set.map snd) declared+ }+ reached wanted =+ Map.fromList+ <$> traverse+ (\m -> (m,) <$> reach m)+ (Set.toList (Set.fromList [importModule i | i <- imports, wanted i]))+ suppliedBy i (qualifier, op) = maySupply Map.empty qualifier op i+ named = \case+ ExportName _ qualifier op ->+ [(qualifier, op) | Set.notMember op defined]+ ExportAll qualifier parent ->+ [(qualifier, parent) | Set.notMember parent defined]+ ExportSome qualifier parent kids ->+ [(qualifier, op) | op <- parent : kids, Set.notMember op defined]+ ExportModule _ -> []++ exported answers reexported = \case+ ExportName namespace qualifier op ->+ settled answers True qualifier (namespace, op)+ <> certainly (Set.singleton (namespace, op)) Map.empty+ ExportAll qualifier parent+ | Just kids <- Map.lookup parent declared -> withMembers parent kids+ | otherwise ->+ let found = settled answers True qualifier (InTypes, parent)+ kids =+ Map.findWithDefault Nothing (qualifier, parent) reexported+ in found+ <> mempty{establishedUntold = Map.keysSet (establishedUnsettled found)}+ <> foldMap (member answers qualifier parent) (foldMap Set.toList kids)+ <> case kids of+ Nothing -> certainly (Set.singleton (InTypes, parent)) Map.empty+ Just known ->+ withMembers parent (certainMembersOf answers qualifier parent)+ <> mempty{establishedMembers = Map.singleton parent known}+ ExportSome qualifier parent kids ->+ settled answers True qualifier (InTypes, parent)+ <> foldMap (member answers qualifier parent) kids+ <> withMembers+ parent+ ( Set.filter+ ((`elem` kids) . snd)+ ( fromMaybe+ (certainMembersOf answers qualifier parent)+ (Map.lookup parent declared)+ )+ )+ ExportModule m ->+ (if Just m == summaryName summary || m == modName then own else mempty)+ <> foldMap+ (whole answers)+ [i | i <- imports, not (importQualified i), importAlias i == m]++ certainly names members = mempty{establishedCertain = Certain names members}+ withMembers parent kids =+ certainly (Set.insert (InTypes, parent) kids) (Map.singleton parent kids)+ member answers qualifier parent kid+ | Set.member kid defined = mempty+ | otherwise =+ case [ (i, established, certainKids)+ | (i, established) <- candidates answers qualifier parent,+ decides established (InTypes, parent) i,+ let certainKids =+ Set.filter+ ((== kid) . snd)+ ( Map.findWithDefault+ Set.empty+ parent+ (certainMembers (establishedCertain established))+ ),+ not (Set.null certainKids)+ ] of+ (i, established, certainKids) : _ ->+ foldMap (from [(i, established)]) certainKids+ [] ->+ foldMap+ ( \namespace ->+ settled answers False qualifier (namespace, kid)+ )+ [InTypes, InTerms]++ candidates answers qualifier op =+ [ (i, established)+ | i <- imports,+ Just established <- [Map.lookup (importModule i) answers],+ maySupply (establishedMembers established) qualifier op i+ ]++ settled answers certain qualifier name@(_, op)+ | Set.member op defined = mempty+ | otherwise = case [ (i, established)+ | certain,+ (i, established) <- found,+ decides established name i+ ] of+ decider : _ -> from [decider] name+ [] -> from found name+ where+ found = candidates answers qualifier op++ from imported name =+ case [ importModule i : chain+ | (i, established) <- imported,+ chain <- unsettledThrough established name+ ] of+ [] ->+ mempty+ { establishedFixities =+ Map.fromList+ ( take+ 1+ [ (name, fixity)+ | (_, a) <- imported,+ Just fixity <- [Map.lookup name (establishedFixities a)]+ ]+ )+ }+ chains ->+ mempty+ { establishedUnsettled =+ Map.fromList [(chain, Set.singleton name) | chain <- chains]+ }++ membersFrom answers qualifier parent =+ case mapMaybe+ (Map.lookup parent . establishedMembers . snd)+ (candidates answers qualifier parent) of+ [] -> Nothing+ kids -> Just (Set.unions kids)++ certainMembersOf answers qualifier parent =+ Set.unions+ [ Set.filter (\kid -> certainlyBrings (establishedMembers established) kid i) kids+ | (i, established) <- candidates answers qualifier parent,+ let certain = establishedCertain established,+ Set.member (InTypes, parent) (certainNames certain),+ Just kids <- [Map.lookup parent (certainMembers certain)]+ ]++ whole answers i =+ Established+ { establishedFixities =+ Map.filterWithKey+ (\(_, op) _ -> admitted op)+ (establishedFixities established),+ establishedUnsettled =+ Map.mapKeys+ (importModule i :)+ ( Map.mapMaybe+ (inhabited . Set.filter (admitted . snd))+ (establishedUnsettled established)+ ),+ establishedUntold =+ Set.map+ (importModule i :)+ (establishedUntold established),+ establishedCertain =+ Certain+ { certainNames = Set.filter included (certainNames certain),+ certainMembers =+ Map.map+ (Set.filter included)+ ( Map.filterWithKey+ (\parent _ -> included (InTypes, parent))+ (certainMembers certain)+ )+ },+ establishedMembers = establishedMembers established+ }+ where+ established = Map.findWithDefault mempty (importModule i) answers+ certain = establishedCertain established+ admitted op = maySupply (establishedMembers established) Nothing op i+ included name = certainlyBrings (establishedMembers established) name i++-- | The parts of what a module exports, together.+--+-- A name one part certainly exports with a settled fixity is the one every+-- other part exports under that name too, or the module would not compile,+-- so no part leaves it unsettled.+together :: [Established] -> Established+together parts =+ settledOnly+ combined+ { establishedUnsettled =+ Map.mapMaybe+ (inhabited . (`Set.difference` certain))+ (establishedUnsettled combined)+ }+ where+ combined = mconcat parts+ certain =+ Set.unions+ [ Set.filter+ (null . unsettledThrough part)+ (certainNames (establishedCertain part))+ | part <- parts+ ]++-- | An answer without the fixities of the names it leaves unsettled.+settledOnly :: Established -> Established+settledOnly established+ | settlesEverything established = established+ | otherwise =+ established+ { establishedFixities =+ Map.filterWithKey+ (\name _ -> null (unsettledThrough established name))+ (establishedFixities established)+ }++-- | A set, unless it is empty.+inhabited :: Set a -> Maybe (Set a)+inhabited names+ | Set.null names = Nothing+ | otherwise = Just names++-- | Every configuration the preprocessor allows of a module's text that is+-- Haskell, parsed and summarized.+configurationsOf ::+ -- | What the plan settles about the questions its conditionals ask.+ Macros ->+ -- | What extensions the module's package puts in force.+ [Extension] ->+ -- | The module's name, for the parser to put in its errors.+ Text ->+ -- | Its text.+ Text ->+ Maybe (NonEmpty ModuleSummary)+configurationsOf macros extensions modName text =+ NE.nonEmpty . mapMaybe parsed+ =<< whatParsed (branchLeaves (withoutRuledOut macros text))+ where+ parsed leaf =+ summarize (hasImplicitPrelude extensions leaf) . pmModule+ <$> whatParsed (parseModule (ParserConfig extensions) named leaf)+ whatParsed = either (const Nothing) Just+ named = T.unpack modName++-- | Does this module see the Prelude without importing it?+hasImplicitPrelude :: [Extension] -> Text -> Choice "implicitPrelude"+hasImplicitPrelude extensions source =+ fromBool (ImplicitPrelude `elem` effectiveExtensions extensions source)
src/Tilia/Format.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} -- | Formatting a file, with everything the project can tell us about it. module Tilia.Format@@ -12,6 +13,8 @@ sessionRoot, PlanSource (..), newSession,+ Compiled (..),+ compiledSession, fixityNotesOf, formatSource, rewritten,@@ -30,6 +33,7 @@ import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T+import Data.Version (Version, versionBranch) import GHC.LanguageExtensions.Type (Extension (ImplicitPrelude)) import Tilia.Cabal.Package ( PackageProblem (..),@@ -37,6 +41,15 @@ describePackageProblem, newPackageReader, )+import Tilia.Cabal.Plan+ ( PlanComponent,+ fetchUninstalled,+ loadPlan,+ macrosOf,+ plannedCompiler,+ readGivenPlan,+ versionMacros,+ ) import Tilia.Cabal.Project (ProjectRoot (..), findProjectRoot) import Tilia.Cpp ( CppError (..),@@ -48,7 +61,7 @@ withoutRuledOut, ) import Tilia.Cpp.Macros (Macros)-import Tilia.Doc (defaultRenderOptions, printDoc)+import Tilia.Doc (printDoc) import Tilia.Equivalence (commentDifference, syntaxDifference) import Tilia.Fixity ( OpName,@@ -59,26 +72,21 @@ unknownOperators, ) import Tilia.Fixity.Debug (FixityNotes, fixityNotes)+import Tilia.Fixity.HiFile (HiFile) import Tilia.Fixity.Plan- ( PlanComponent,- Resolver (..),+ ( Resolver (..), Route (FromInterface, FromSource),- fetchUninstalled,- loadPlan,- macrosOf,+ newResolverFrom, newResolverVia,- plannedCompiler,- readGivenPlan, scopeFor, ) import Tilia.Palette (Color (Operator, Place), Palette, paint) import Tilia.Parser ( ParseError, ParsedModule,- ParserConfig,+ ParserConfig (..), describeParseError, parseModule,- parserConfigFor, pmGathered, pmModule, )@@ -92,7 +100,7 @@ NoProject FilePath | -- | A project, but no build plan we could read or produce, and why. NoBuildPlan FilePath Text- | -- | We failed to read .cabal file.+ | -- | The extensions in force in the file could not be settled, and why. NoPackage FilePath PackageProblem | -- | The file is not Haskell we can parse. NotParsed ParseError@@ -230,8 +238,8 @@ -- | Settle everything that does not depend on the file being formatted. newSession ::- -- | Where to start looking for the project.- FilePath ->+ -- | The project.+ ProjectRoot -> -- | Where the build plan comes from. PlanSource -> -- | Whether to read from and write to the cache.@@ -246,21 +254,21 @@ -- with 'fixityNotesOf'. Choice "debugFixity" -> IO (Either FormatError Session)-newSession start planSource caching downloading checkAst checkIdempotence debugFixity = runExceptT $ do- project <- need (NoProject start) =<< liftIO (findProjectRoot start)+newSession project planSource caching downloading checkAst checkIdempotence debugFixity = runExceptT $ do let root = prPath project- (plan, resolver) <- case planSource of+ (plan, routes) <- case planSource of PlanFromCabal components -> do plan <- orElse (NoBuildPlan root) =<< liftIO (loadPlan caching downloading components root)- (,) plan <$> liftIO (newResolverVia caching [FromInterface, FromSource] plan)+ pure (plan, [FromInterface, FromSource]) GivenPlan path -> do plan <- orElse (NoBuildPlan root) =<< liftIO (readGivenPlan root path) routes <- if isTrue downloading then [FromInterface, FromSource] <$ liftIO (fetchUninstalled caching root plan) else pure [FromInterface]- (,) plan <$> liftIO (newResolverVia caching routes plan)- askPackage <- liftIO (newPackageReader (plannedCompiler plan))+ pure (plan, routes)+ packageReader <- liftIO (newPackageReader (plannedCompiler plan))+ resolver <- liftIO (newResolverVia caching routes packageReader plan) notes <- if isTrue debugFixity then Just <$> liftIO (newIORef Map.empty)@@ -270,14 +278,57 @@ { sessionRoot = project, sessionResolver = resolver, sessionMacros = macrosOf plan,- sessionPackage = askPackage,+ sessionPackage = packageReader, sessionCheckAst = checkAst, sessionCheckIdempotence = checkIdempotence, sessionFixityNotes = notes }++-- | What compiling a file's component establishes, which a session+-- otherwise works out with Cabal, the package database, and the sources of+-- dependencies.+data Compiled = Compiled+ { -- | The extensions the component puts in force.+ compiledExtensions :: [Extension],+ -- | The version of the compiler.+ compiledWith :: Version,+ -- | The version of each package the component depends on, by name.+ compiledPackages :: Map Text Version,+ -- | The interface of a module the component can import, by name.+ compiledInterface :: Text -> IO (Maybe HiFile)+ }++-- | Settle everything that does not depend on the file being formatted from+-- what compiling its component establishes.+compiledSession ::+ -- | The file.+ FilePath ->+ -- | What compiling its component establishes.+ Compiled ->+ -- | Check AST equivalence.+ Choice "checkAst" ->+ -- | Check idempotence.+ Choice "checkIdempotence" ->+ IO (Either FormatError Session)+compiledSession file Compiled{..} checkAst checkIdempotence = runExceptT $ do+ project <-+ maybe (throwE (NoProject file)) pure =<< liftIO (findProjectRoot file)+ resolver <- liftIO (newResolverFrom compiledInterface)+ pure+ Session+ { sessionRoot = project,+ sessionResolver = resolver,+ sessionMacros =+ versionMacros+ (Just (numbers compiledWith))+ (fmap numbers compiledPackages),+ sessionPackage = \_ -> pure (Right compiledExtensions),+ sessionCheckAst = checkAst,+ sessionCheckIdempotence = checkIdempotence,+ sessionFixityNotes = Nothing+ } where- need :: FormatError -> Maybe a -> ExceptT FormatError IO a- need e = maybe (throwE e) pure+ numbers = fmap toInteger . versionBranch -- | What the run made of every file's operators, by file. --@@ -303,7 +354,7 @@ throwE (PositionPragmas path) package <- orElse (NoPackage path) =<< liftIO (sessionPackage session path) let resolver = sessionResolver session- config = parserConfigFor package+ config = ParserConfig package extensionsAndCpp text = let declared = effectiveExtensions package text in (Set.fromList declared, usesCpp declared text)@@ -344,7 +395,7 @@ maybe (orElse NotParsed (parseModule config path text)) pure already render <- renderConfigFor extensions (pure parsed) pure- ( printDoc defaultRenderOptions (renderModule render parsed),+ ( printDoc (renderModule render parsed), Just parsed ) let inForce@(_, cpp) = extensionsAndCpp source
src/Tilia/Imports.hs view
@@ -23,8 +23,8 @@ import GHC.Types.SourceText (StringLiteral (..)) import GHC.Types.SrcLoc import Tilia.Comments (Comment (..), commentTrailing, commentsWithin)-import Tilia.Span (endPoint, isSingleLine, startPoint)-import Tilia.Span.Ghc (bracketsSpan, spanOf, spanOfSrcSpan, tokenSpan)+import Tilia.Span (Span (..), endPoint, isSingleLine, startPoint)+import Tilia.Span.Ghc (bracketsSpan, spanOf, spanOfSrcSpan) -- | Sort and fold together a module's imports. normalizeImports ::@@ -49,7 +49,7 @@ (combineImports directives written) [((importIdentity implicitPrelude i, hiddenNames i, alone i), i) | i <- is] alone i- | any strands (spanOf i) = startLineOf i+ | any strands (spanOf i) = lineOf i | otherwise = 0 where strands s =@@ -58,9 +58,6 @@ (filter loose (commentsWithin s written)) loose = not . commentTrailing unanchored starts c = not (any (> endPoint (commentSpan c)) starts)- startLineOf i = case srcSpanStart (getLocA i) of- RealSrcLoc l _ -> srcLocLine l- _ -> 0 -- | The names an import hides, where it hides any, so that only imports -- hiding the same names are folded together.@@ -111,10 +108,11 @@ groupBy ((==) `on` fst) [(between i, i) | i <- xs] & fmap (fmap snd) where between i = length (takeWhile (< lineOf i) barriers)- lineOf i = case srcSpanStart (getLocA i) of- RealSrcLoc l _ -> srcLocLine l- _ -> 0 +-- | The line an item begins on, or 0 for one without a place in the input.+lineOf :: GenLocated SrcSpanAnnA a -> Int+lineOf = maybe 0 spanStartLine . spanOf+ -- | Sort by the keys, then replace each run of equal keys by one value -- folded out of it. foldRuns ::@@ -323,7 +321,7 @@ | onOneLine open close && not (onOneLine open' close') = (warning, (open', dots, comma, close')) | otherwise = (warning, (open, dots, comma, close))- onOneLine open close = all isSingleLine (tokenSpan open <> tokenSpan close)+ onOneLine open close = all isSingleLine (spanOf open <> spanOf close) -- | An entry with the names in its own brackets sorted and deduplicated. sortSubnames :: IE GhcPs -> IE GhcPs
+ src/Tilia/Memo.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE LambdaCase #-}++-- | Tables of answers, each worked out once however many threads ask for it.+module Tilia.Memo+ ( Memo,+ newMemo,+ Flights,+ newFlights,+ memoized,+ )+where++import Control.Concurrent+ ( MVar,+ ThreadId,+ modifyMVar,+ modifyMVar_,+ myThreadId,+ newEmptyMVar,+ newMVar,+ readMVar,+ tryPutMVar,+ tryReadMVar,+ )+import Control.Exception (onException)+import Data.IORef (IORef, newIORef, readIORef, writeIORef)+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Text (Text)+import Data.Unique (Unique, newUnique)++-- | Answers filed under the names they are about, each worked out once.+data Memo v = Memo Unique (IORef (Map Text (MVar v)))++-- | An empty 'Memo'.+newMemo :: IO (Memo v)+newMemo = Memo <$> newUnique <*> newIORef Map.empty++-- | Which thread is working out which answer, and which answer each waiting+-- thread waits for, across all of a resolver's tables.+data Flights = Flights+ { flightOwners :: Map (Unique, Text) ThreadId,+ flightWaits :: Map ThreadId (Unique, Text)+ }++-- | Nobody working out anything, and nobody waiting.+newFlights :: IO (MVar Flights)+newFlights = newMVar (Flights Map.empty Map.empty)++-- | Look an answer up in a table, working it out and filing it the first+-- time.+memoized ::+ -- | Who is working out what.+ MVar Flights ->+ -- | Where the answers are filed.+ Memo v ->+ -- | The answer for a thread that cannot wait.+ v ->+ -- | What the answer is about.+ Text ->+ -- | Work that is being memoized.+ IO v ->+ IO v+memoized flights (Memo table answers) standIn key work =+ Map.lookup key <$> readIORef answers >>= \case+ Just slot -> tryReadMVar slot >>= maybe (waitFor slot) pure+ Nothing -> do+ me <- myThreadId+ claimed <- modifyMVar flights $ \fl -> do+ filed <- readIORef answers+ case Map.lookup key filed of+ Just slot -> pure (fl, Left slot)+ Nothing -> do+ slot <- newEmptyMVar+ writeIORef answers (Map.insert key slot filed)+ pure+ ( fl{flightOwners = Map.insert (table, key) me (flightOwners fl)},+ Right slot+ )+ case claimed of+ Left slot -> waitFor slot+ Right slot -> do+ answer <- work `onException` file slot standIn+ answer <$ file slot answer+ where+ file slot answer = modifyMVar_ flights $ \fl -> do+ _ <- tryPutMVar slot answer+ pure fl{flightOwners = Map.delete (table, key) (flightOwners fl)}+ waitFor slot = do+ me <- myThreadId+ settled <- modifyMVar flights $ \fl ->+ tryReadMVar slot >>= \case+ Just answer -> pure (fl, Just answer)+ Nothing+ | waitedOnBy fl me (table, key) -> pure (fl, Just standIn)+ | otherwise ->+ pure+ ( fl{flightWaits = Map.insert me (table, key) (flightWaits fl)},+ Nothing+ )+ case settled of+ Just answer -> pure answer+ Nothing -> do+ answer <- readMVar slot+ modifyMVar_ flights $ \fl ->+ pure fl{flightWaits = Map.delete me (flightWaits fl)}+ pure answer++-- | Is this answer being worked out by the given thread, or by one that+-- waits for it, directly or through others?+waitedOnBy :: Flights -> ThreadId -> (Unique, Text) -> Bool+waitedOnBy fl me = go+ where+ go key = case Map.lookup key (flightOwners fl) of+ Nothing -> False+ Just owner+ | owner == me -> True+ | otherwise -> maybe False go (Map.lookup owner (flightWaits fl))
src/Tilia/Palette.hs view
@@ -39,7 +39,7 @@ | -- | A line a change adds. New | -- | A line a change leaves alone.- Unchanged+ Kept | -- | Something that went well. Good | -- | Something that did not, without being wrong.@@ -64,7 +64,7 @@ Meta -> "\ESC[36m" Gone -> "\ESC[31m" New -> "\ESC[32m"- Unchanged -> "\ESC[39m"+ Kept -> "\ESC[39m" Good -> "\ESC[32m" Middling -> "\ESC[33m" Bad -> "\ESC[31m"
src/Tilia/Parser.hs view
@@ -12,8 +12,6 @@ ParseError (..), describeParseError, ParserConfig (..),- defaultParserConfig,- parserConfigFor, ghcLibParserVersion, ) where@@ -22,6 +20,7 @@ import Data.IntMap.Strict (IntMap) import Data.IntMap.Strict qualified as IntMap import Data.List (isSuffixOf, nub, sortOn)+import Data.Maybe (listToMaybe, mapMaybe) import Data.Text (Text) import Data.Text qualified as T import Data.Text.Encoding qualified as T@@ -33,7 +32,6 @@ import GHC.Hs.Extension (GhcPs) import GHC.LanguageExtensions.Type (Extension) import GHC.Parser qualified as GHC-import GHC.Parser.Annotation (LEpaComment, getLocA) import GHC.Parser.Lexer qualified as GHC import GHC.Types.Error qualified as GHC import GHC.Types.SrcLoc qualified as GHC@@ -43,8 +41,8 @@ import Tilia.Gathered (Gathered (..), gathered) import Tilia.Pragma (effectiveExtensions) import Tilia.Source (Lines, Source, SourceType (..), Written (..), lineTexts, linesOf, sourceOf)-import Tilia.Span (Span (..))-import Tilia.Span.Ghc (spanOfReal)+import Tilia.Span (Span (..), startPoint)+import Tilia.Span.Ghc (spanOf) -- | A parsed module, together with the comments found in it. data ParsedModule = ParsedModule@@ -185,12 +183,12 @@ -- | Lex every comment, in source order, and the line each quasi-quotation -- written over several lines ends on, by the line it begins on, from the -- state a parse starts in.-lexed :: GHC.PState -> ([LEpaComment], IntMap Int)+lexed :: GHC.PState -> ([(GHC.RealSrcSpan, GHC.Token)], IntMap Int) lexed s = case GHC.lexTokenStream (GHC.options s) (GHC.buffer s) loc of GHC.PFailed _ -> error "Tilia: a module that parses cannot fail to lex" GHC.POk _ tokens ->- ( [ GHC.commentToAnnotation (GHC.L l t)+ ( [ (l, t) | GHC.L (GHC.RealSrcSpan l _) t <- tokens, isComment t ],@@ -292,18 +290,8 @@ -- | The start of the first thing that is not part of the header. headerEndOf :: HsModule GhcPs -> Maybe Span headerEndOf hsModule =- foldl' earliest Nothing $- fmap getLocA (hsmodImports hsModule)- <> fmap getLocA (hsmodDecls hsModule)- where- earliest acc l = case GHC.srcSpanToRealSrcSpan l of- Nothing -> acc- Just s ->- let this = spanOfReal s- in Just (maybe this (keepEarlier this) acc)- keepEarlier a b- | (spanStartLine a, spanStartColumn a) <= (spanStartLine b, spanStartColumn b) = a- | otherwise = b+ listToMaybe . sortOn startPoint $+ mapMaybe spanOf (hsmodImports hsModule) <> mapMaybe spanOf (hsmodDecls hsModule) -- | Why a module did not parse. data ParseError = ParseError@@ -323,24 +311,6 @@ { -- | Extensions to enable before parsing. pcExtensions :: [Extension] }---- | What to parse with when there is no package to ask.-defaultParserConfig :: ParserConfig-defaultParserConfig = parserConfigFor []---- | What to parse with, given whatever the package had to say.-parserConfigFor ::- -- | What the package puts in force, or nothing if there is no package.- [Extension] ->- -- | The resulting parser config.- ParserConfig-parserConfigFor package =- ParserConfig- { pcExtensions =- if null package- then GHC.languageExtensions (Just GHC.GHC2021)- else package- } -- | What a file's name says it holds. sourceTypeOf :: FilePath -> SourceType
src/Tilia/Pragma.hs view
@@ -5,6 +5,9 @@ ( movesPositions, effectiveExtensions, lookupExtension,+ lookupEdition,+ applyExtensionName,+ languageNames, ) where @@ -31,28 +34,33 @@ -- | The module's source, read here for its @LANGUAGE@ pragmas alone. Text -> [Extension]-effectiveExtensions package- | null package = pragmasOver onUnlessRefused- | otherwise = pragmasOver package---- | The extensions that are on until something says otherwise.-onUnlessRefused :: [Extension]-onUnlessRefused = [ImplicitPrelude]---- | Apply a module's @LANGUAGE@ pragmas to a starting set.-pragmasOver :: [Extension] -> Text -> [Extension]-pragmasOver initial = foldl' apply initial . concatMap pragmaNames . pragmaBodies+effectiveExtensions package =+ foldl' applyExtensionName package . concatMap pragmaNames . pragmaBodies where- apply acc name = case T.stripPrefix "No" name >>= lookupExtension of- Just off -> filter (/= off) acc- Nothing -> case lookupExtension name of- Just on | on `notElem` acc -> acc <> [on]- _ -> acc pragmaNames body = let (keyword, names) = T.break (== ' ') body in if T.toUpper keyword == "LANGUAGE"- then filter (not . T.null) (fmap T.strip (T.splitOn "," names))+ then languageNames names else []++-- | Turn on the extension a name spells, or off where the name puts @No@ in+-- front of one, leaving the set as it is for a name that spells neither.+applyExtensionName :: [Extension] -> Text -> [Extension]+applyExtensionName acc name =+ case T.stripPrefix "No" name >>= lookupExtension of+ Just off -> filter (/= off) acc+ Nothing -> case lookupExtension name of+ Just on | on `notElem` acc -> acc <> [on]+ _ -> acc++-- | The extensions a @LANGUAGE@ pragma names, as written.+languageNames :: Text -> [Text]+languageNames = filter (not . T.null) . fmap T.strip . T.splitOn ","++-- | Parse the name of an edition of the language, such as @GHC2021@.+lookupEdition :: Text -> Maybe GHC.Language+lookupEdition name =+ lookup name [(T.pack (show e), e) | e <- [minBound .. maxBound]] -- | Parse an extension name. lookupExtension :: Text -> Maybe Extension
src/Tilia/Render.hs view
@@ -11,7 +11,7 @@ import Data.IntMap.Strict qualified as IntMap import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map-import Data.Maybe (isJust, listToMaybe)+import Data.Maybe (listToMaybe) import Data.Set (Set) import Data.Set qualified as Set import Data.Text (Text)@@ -38,7 +38,7 @@ import Tilia.Parser (ParsedModule (..)) import Tilia.Render.Context import Tilia.Render.Declaration (decls, declsKeepingGroups)-import Tilia.Render.Expression (hsCmd, hsExprIn, untypedSplice)+import Tilia.Render.Expression (hsExprIn, untypedSplice) import Tilia.Render.Haddock (haddockSpans) import Tilia.Render.Header (HeaderPragma (..), hsModule, takeHeaderPragmas, takeStackHeader) import Tilia.Render.Signature (sigDecl)@@ -109,6 +109,7 @@ (comments (pmSource parsed)) (hsmodImports m) }+ carryingOn = carriedOnFrom loose ctx = Ctx { ctxExtensions = rcExtensions settings,@@ -118,8 +119,11 @@ ctxSource = pmSource parsed, ctxLineComments = indexOn (filter (not . closesItself) loose), ctxCarryingOn =- Set.fromList- (startPoint . commentSpan <$> filter (isJust . carriedOnFrom loose) loose),+ Map.fromList+ [ (startPoint (commentSpan c), remark)+ | c <- loose,+ Just (_, remark) <- [carryingOn c]+ ], ctxHaddocks = indexOn haddocks, ctxKnot = knot }@@ -221,7 +225,6 @@ knot = Knot { knotExpr = hsExprIn,- knotCmd = hsCmd, knotSplice = untypedSplice, knotSig = sigDecl, knotDecls = decls,
− src/Tilia/Render/Body.hs
@@ -1,85 +0,0 @@-{-# LANGUAGE LambdaCase #-}---- | Hanging vs non-hanging constructs.-module Tilia.Render.Body- ( ExprBody (..),- CmdBody (..),- CmdTopBody (..),- exprHangs,- operatorName,- cmdTopHangs,- )-where--import GHC.Hs-import GHC.Types.Name.Occurrence (occNameString)-import GHC.Types.Name.Reader (RdrName, rdrNameOcc)-import GHC.Types.SrcLoc (GenLocated (..), unLoc)-import Tilia.Doc.Body-import Tilia.Doc.Combinators-import Tilia.Render.Context-import Tilia.Span-import Tilia.Span.Ghc---- | An expression standing as the body of an enclosing construct.-data ExprBody = ExprBody Ctx Site (LHsExpr GhcPs)--instance Body ExprBody where- printBody (ExprBody ctx site e) = knotExpr (ctxKnot ctx) ctx site e- bodyPlacement (ExprBody _ _ e) = exprHangs (unLoc e)---- | A command standing as the body of an enclosing construct.-data CmdBody = CmdBody Ctx Site (LHsCmd GhcPs)--instance Body CmdBody where- printBody (CmdBody ctx site c) = knotCmd (ctxKnot ctx) ctx site c- bodyPlacement (CmdBody _ _ c) = cmdHangs (unLoc c)---- | A command at the top of an arrow form.-data CmdTopBody = CmdTopBody Ctx Site (LHsCmdTop GhcPs)--instance Body CmdTopBody where- printBody (CmdTopBody ctx site l) =- at ctx l (\(HsCmdTop _ cmd) -> knotCmd (ctxKnot ctx) ctx site cmd)- bodyPlacement (CmdTopBody _ _ l) = cmdTopHangs (unLoc l)---- | Does this expression absorb the line break that introduces it?-exprHangs :: HsExpr GhcPs -> Placement-exprHangs = \case- HsDo _ (DoExpr _) _ -> Hanging- HsDo _ (MDoExpr _) _ -> Hanging- HsCase{} -> Hanging- HsLam _ lamVariant mg -> case lamVariant of- LamCase -> Hanging- LamCases -> Hanging- LamSingle -> case mg of- MG _ (L _ [L _ (Match _ _ (L _ ps@(_ : _)) _)])- | maybe False isSingleLine (spansOf ps) -> Hanging- _ -> Normal- HsProc _ p _- | maybe False isSingleLine (spanOf p) -> Hanging- | otherwise -> Normal- HsApp _ _ y -> exprHangs (unLoc y)- OpApp _ x op y- | Just n <- operatorName op,- occNameString (rdrNameOcc n) == "$" ->- exprHangs (unLoc y)- | otherwise -> exprHangs (unLoc x)- _ -> Normal---- | Does this command absorb the line break that introduces it?-cmdHangs :: HsCmd GhcPs -> Placement-cmdHangs = \case- HsCmdDo{} -> Hanging- HsCmdCase{} -> Hanging- HsCmdLam{} -> Hanging- _ -> Normal--cmdTopHangs :: HsCmdTop GhcPs -> Placement-cmdTopHangs (HsCmdTop _ c) = cmdHangs (unLoc c)---- | The name of an operator, when the expression standing as one is a name.-operatorName :: LHsExpr GhcPs -> Maybe RdrName-operatorName e = case unLoc e of- HsVar _ (L _ n) -> Just n- _ -> Nothing
src/Tilia/Render/Class.hs view
@@ -18,6 +18,7 @@ where import Data.Choice (fromBool, pattern Do)+import Data.Foldable (fold) import Data.Function (on) import Data.List (sortBy) import Data.Maybe (isNothing)@@ -54,13 +55,13 @@ txt "class" <> layoutFrom ctx wholeHeadSpan head' <> body where headSpan = spanOf tyCon <> spansOf hsq_explicit- whereSpan = tokenSpan (acd_where anns)+ whereSpan = spanOf (acd_where anns) wholeHeadSpan = foldMap spanOf ctxt <> headSpan <> spansOf fdeps head' = breakOrSpace <> indent- ( foldMap (classContext ctx) ctxt+ ( foldMap (leftContext ctx) ctxt <> layoutFrom ctx headSpan classHead <> indent (funDeps ctx fdeps) <> includeUnless@@ -102,12 +103,6 @@ isCTuple (Exact n) arity = isCTupleTyConName n && n == cTupleTyConName arity isCTuple _ _ = False --- | A context on a class head, with the @=>@ that follows it.-classContext :: Ctx -> LHsContext GhcPs -> Doc-classContext ctx ctxt- | null (unLoc ctxt) = mempty- | otherwise = context ctx ctxt <> joinedBy "=>"- -- | The functional dependencies of a class. funDeps :: Ctx -> [LHsFunDep GhcPs] -> Doc funDeps _ [] = mempty@@ -131,7 +126,7 @@ txt "instance" <> layoutFrom ctx headSpan head' <> body where headSpan = foldMap spanOf warning <> spanOf cid_poly_ty- whereSpan = tokenSpan (acid_where anns)+ whereSpan = spanOf (acid_where anns) head' = foldMap (\w -> breakOrSpace <> at ctx w warningTxt) warning@@ -201,7 +196,7 @@ dataFamInstDecl ctx style (DataFamInstDecl FamEqn{..}) = dataDecl ctx- style+ (fromBool (style == Free)) feqn_tycon feqn_pats typeArgSpan@@ -212,10 +207,7 @@ where -- @data instance forall k (a :: k). D a = …@ binds its variables ahead -- of the head, exactly as a type family instance does.- outerBinders = case feqn_bndrs of- HsOuterImplicit NoExtField -> mempty- HsOuterExplicit _ bndrs ->- forallBndrs ctx Invisible (tyVarBndr ctx) bndrs <> breakOrSpace+ outerBinders = outerBndrs ctx breakOrSpace feqn_bndrs -- | A @data family@ or @type family@ declaration. famDecl :: Ctx -> FamilyStyle -> FamilyDecl GhcPs -> Doc@@ -249,12 +241,11 @@ (isNothing resultSig && isNothing fdInjectivityAnn) space <> indent- ( sequence_' resultSig+ ( fold resultSig <> space <> foldMap (at_ ctx (injectivityAnn ctx)) fdInjectivityAnn ) - sequence_' = maybe mempty id resultSig = familyResultSig ctx fdResultSig equations = case closedEqns of@@ -290,12 +281,10 @@ tyFamInstEqn ctx FamEqn{..} = binders <> nest (if hasBinders then 1 else 0) (lhs <> rhs) where- (binders, hasBinders) = case feqn_bndrs of- HsOuterImplicit NoExtField -> (mempty, False)- HsOuterExplicit _ bndrs ->- ( forallBndrs ctx Invisible (tyVarBndr ctx) bndrs <> breakOrSpace,- not (null bndrs)- )+ binders = outerBndrs ctx breakOrSpace feqn_bndrs+ hasBinders = case feqn_bndrs of+ HsOuterExplicit _ (_ : _) -> True+ _ -> False lhs = layoutFrom ctx (spanOf feqn_tycon <> foldMap typeArgSpan feqn_pats) $
src/Tilia/Render/Context.hs view
@@ -14,6 +14,7 @@ -- * Extensions extensionOn,+ negativeLiteralGap, -- * Fixities operatorFixity,@@ -50,12 +51,12 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Hs hiding (Fixity)-import GHC.LanguageExtensions.Type (Extension)+import GHC.LanguageExtensions.Type (Extension (NegativeLiterals)) import GHC.Types.Name.Occurrence (occNameString) import GHC.Types.Name.Reader (RdrName (..), rdrNameOcc) import GHC.Types.SrcLoc (GenLocated (..)) import GHC.Types.SrcLoc qualified as GHC-import Tilia.Comments (Comment (..), CommentStyle (..), commentTrailing)+import Tilia.Comments (Comment (..), CommentStyle (..), carriesOn, commentTrailing) import Tilia.Doc.Combinators import Tilia.Fixity ( Fixity,@@ -93,8 +94,9 @@ ctxCaptured :: Map Span Fixity, -- | The comments that take whole lines, by starting position. ctxLineComments :: Map (Int, Int) Comment,- -- | Where each comment that carries on a trailing comment begins.- ctxCarryingOn :: Set (Int, Int),+ -- | The column of the trailing comment each comment that carries one on+ -- begins at, by where that comment begins.+ ctxCarryingOn :: Map (Int, Int) Int, -- | The module as its author wrote it. ctxSource :: Source, -- | The author's own text for each Haddock, by starting position.@@ -105,10 +107,8 @@ -- | The backward edges of the rendering knot. data Knot = Knot- { -- | Expressions, needed by types, patterns and bodies.+ { -- | Expressions, needed by patterns. knotExpr :: Ctx -> Site -> LHsExpr GhcPs -> Doc,- -- | Commands, needed by bodies.- knotCmd :: Ctx -> Site -> LHsCmd GhcPs -> Doc, -- | Splices, needed by types and patterns, defined with expressions. knotSplice :: Ctx -> SpliceDecoration -> HsUntypedSplice GhcPs -> Doc, -- | Signature declarations, needed by @let@ and @where@ bodies.@@ -174,6 +174,11 @@ extensionOn :: Ctx -> Extension -> Bool extensionOn ctx e = Set.member e (ctxExtensions ctx) +-- | With @NegativeLiterals@ on, @- 1@ and @-1@ are different, so the minus+-- in front of a literal has to keep its distance.+negativeLiteralGap :: Ctx -> Doc+negativeLiteralGap ctx = includeWhen (extensionOn ctx NegativeLiterals) space+ ---------------------------------------------------------------------------- -- Fixities @@ -218,11 +223,10 @@ Map.dropWhileAntitone (< endPoint a) (ctxLineComments ctx) printedAfter c = commentTrailing c- || ( Set.member (startPoint s) (ctxCarryingOn ctx)- && spanStartColumn b < spanStartColumn s- )- where- s = commentSpan c+ || maybe+ False+ (\remark -> carriesOn remark (commentSpan c) (Just b))+ (Map.lookup (startPoint (commentSpan c)) (ctxCarryingOn ctx)) -- | Is a comment going to be printed between the two spans? commentPrintedBetween :: Ctx -> Maybe Span -> Maybe Span -> Bool
src/Tilia/Render/Data.hs view
@@ -38,8 +38,9 @@ dataDecl :: -- | The context. Ctx ->- -- | The family style.- FamilyStyle ->+ -- | Whether @instance@ follows the keyword, as it does in a family+ -- instance that stands on its own.+ Choice "instanceWord" -> -- | The type constructor. LocatedN RdrName -> -- | What it is applied to.@@ -55,18 +56,17 @@ Doc -> HsDataDefn GhcPs -> Doc-dataDecl ctx style tyCon tyVars tyVarSpan renderTyVar fixity outerBinders HsDataDefn{..} =- txt (keyword <> instanceWord) <> header <> constructors <> derivings+dataDecl ctx instanceWord tyCon tyVars tyVarSpan renderTyVar fixity outerBinders HsDataDefn{..} =+ txt (keyword <> if isTrue instanceWord then " instance" else "")+ <> header+ <> constructors+ <> derivings where keyword = case dd_cons of NewTypeCon _ -> "newtype" DataTypeCons False _ -> "data" DataTypeCons True _ -> "type data" - instanceWord = case style of- Associated -> ""- Free -> " instance"- headSpan = spanOf tyCon <> foldMap tyVarSpan tyVars wholeHeadSpan = headSpan@@ -195,11 +195,7 @@ <> indent (layoutFrom ctx sigSpan (brokenIfDocumented ctx documented signature)) signature =- outerBndrs ctx (unLoc con_outer_bndrs)- <> ( case unLoc con_outer_bndrs of- HsOuterImplicit{} -> mempty- HsOuterExplicit{} -> breakOrSpace- )+ outerBndrs ctx breakOrSpace (unLoc con_outer_bndrs) <> foldMap (\tele -> forallTelescope ctx tele <> breakOrSpace) con_inner_bndrs <> foldMap (\qs -> context ctx qs <> joinedBy "=>")@@ -310,13 +306,6 @@ PrefixCon xs -> spansOf (fmap cdf_type xs) RecCon l -> spanOf l InfixCon x y -> spansOf (fmap cdf_type [x, y])---- | A context standing to the left of a @=>@, with the arrow and the break--- after it.-leftContext :: Ctx -> LHsContext GhcPs -> Doc-leftContext ctx = \case- L _ [] -> mempty- ctxt -> context ctx ctxt <> joinedBy "=>" -- | One @deriving@ clause, with the strategy it was written with. derivingClause :: Ctx -> HsDerivingClause GhcPs -> Doc
src/Tilia/Render/Declaration.hs view
@@ -1,5 +1,8 @@+{-# LANGUAGE DataKinds #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RecordWildCards #-} -- | Declarations: dispatching to the right printer, and grouping.@@ -9,6 +12,7 @@ ) where +import Data.Choice (pattern Don't) import Data.List (sort) import Data.List.NonEmpty (NonEmpty (..), (<|)) import Data.List.NonEmpty qualified as NE@@ -277,7 +281,7 @@ DataDecl{..} -> dataDecl ctx- Associated+ (Don't #instanceWord) tcdLName (hsq_explicit tcdTyVars) spanOf
src/Tilia/Render/Expression.hs view
@@ -12,7 +12,6 @@ module Tilia.Render.Expression ( hsExpr, hsExprIn,- hsCmd, valDecl, untypedSplice, )@@ -33,8 +32,8 @@ import GHC.LanguageExtensions.Type (Extension (..)) import GHC.Types.Basic (Boxity (..)) import GHC.Types.Fixity (LexicalFixity (..))-import GHC.Types.Name.Occurrence (isVarOcc)-import GHC.Types.Name.Reader (RdrName, mkVarUnqual, rdrNameOcc)+import GHC.Types.Name.Occurrence (isVarOcc, occNameString)+import GHC.Types.Name.Reader (RdrName (..), mkVarUnqual, rdrNameOcc) import GHC.Types.SourceText import GHC.Types.SrcLoc ( GenLocated (..),@@ -50,7 +49,6 @@ ( Fixity, Namespace (..), )-import Tilia.Render.Body import Tilia.Render.Context import Tilia.Render.Layout import Tilia.Render.Literal (stringLiteral)@@ -89,14 +87,26 @@ <> indent ( atSpan ctx- (tokenSpan at' <> spanOf (hswc_body a))+ (spanOf at' <> spanOf (hswc_body a)) (txt "@" <> hsTypeBody ctx (spanOf (hswc_body a)) (unLoc (hswc_body a))) ) OpApp _ x op y -> exprChain ctx site x op y- NegApp _ e _ -> txt "-" <> negationGap ctx e <> hsExpr ctx e+ NegApp _ e _ ->+ txt "-"+ <> includeWhen (isLiteral e) (negativeLiteralGap ctx)+ <> hsExpr ctx e HsPar _ e -> layoutWithin ctx here (spanOf e) $- parensWith (closingFor site) (insideBrackets here (hsExpr ctx e))+ parensWith (closingFor site) (insideBrackets here (apart (hsExpr ctx e)))+ where+ apart d = case unLoc e of+ SectionR _ (L _ (HsVar _ (L _ (Unqual o)))) _+ | unboxedParens, take 1 (occNameString o) == "#" -> space <> d+ SectionL _ _ (L _ (HsVar _ (L _ (Unqual o))))+ | unboxedParens, occNameString o == "#" -> d <> space+ _ -> d+ unboxedParens =+ extensionOn ctx UnboxedTuples || extensionOn ctx UnboxedSums SectionL _ x op -> hsExpr ctx x <> breakOrSpace <> indent (hsExpr ctx op) SectionR _ op x -> hsExpr ctx op <> breakOrSpace <> indent (hsExpr ctx x) ExplicitTuple _ args boxity -> tuple ctx here (closingFor site) boxity args@@ -210,16 +220,12 @@ bodyIn :: BodyOf body b -> Bracing -> LocatedA body -> b bodyIn mkBody bracing = mkBody (withBracing bracing plainSite) --- | @-@ in front of a literal needs a space when @NegativeLiterals@ is on,--- since @- 1@ and @-1@ then parse differently.-negationGap :: Ctx -> LHsExpr GhcPs -> Doc-negationGap ctx e =- includeWhen (extensionOn ctx NegativeLiterals && isLiteral) space- where- isLiteral = case unLoc e of- HsLit{} -> True- HsOverLit{} -> True- _ -> False+-- | Is this expression a literal?+isLiteral :: LHsExpr GhcPs -> Bool+isLiteral e = case unLoc e of+ HsLit{} -> True+ HsOverLit{} -> True+ _ -> False -- | A function applied to arguments. application :: Ctx -> Site -> LHsExpr GhcPs -> LHsExpr GhcPs -> Doc@@ -263,9 +269,7 @@ | any isMissing args = flat (brackets' (sepBy comma (fmap arg args))) | otherwise = brackets' (insideBrackets here (commaSep (fmap arg args))) where- brackets' = case boxity of- Boxed -> parensWith closing- Unboxed -> unboxedWith closing+ brackets' = boxityBrackets boxity closing isMissing = \case Missing _ -> True _ -> False@@ -304,7 +308,7 @@ <> includeUnless hfbPun ( space- <> maybe id located (tokenSpan =<< hfbAnn) (txt "=")+ <> keywordAt ctx (spanOf =<< hfbAnn) "=" <> attach placement (hsExpr ctx hfbRHS) ) where@@ -351,7 +355,7 @@ renderExprChain :: Ctx -> Site -> OpChain (LHsExpr GhcPs) (LHsExpr GhcPs) -> Doc renderExprChain ctx site = \case Operand e -> hsExprIn ctx site e- chain@(Chain operands@(firstOne :| rest) operators) ->+ chain@(Chain operands@(firstOne :| _) operators) -> layoutFrom ctx (chainSpan spanOf chain) $ if trailing then laidOut MayBrace@@ -364,13 +368,11 @@ chainPlacement exprHangs (chainSpan spanOf firstOne) (NE.last operands) laidOut bracing = renderExprChain ctx (withBracing bracing site) firstOne- <> pieces bracing firstOne operators rest- pieces bracing previous (o : os) (operand : more) =- let isLast = null more- operandSite =+ <> foldr (piece bracing) mempty (links operands operators)+ piece bracing (previous, o, operand, isLast) rest' =+ let operandSite = withBracing (if isLast then siteBracing site else bracing) plainSite rendered = renderExprChain ctx operandSite operand- rest' = pieces bracing operand os more in if trailing then space@@ -387,7 +389,6 @@ (hsExpr ctx o) rendered <> rest'- pieces _ _ _ _ = mempty tailPlacement isLast before o operand | isLast, not (maybe True isSingleLine (chainSpan spanOf operand)),@@ -467,7 +468,7 @@ BodyStmt _ body _ _ -> printBody (mkBody site body) BindStmt arrow p f -> hsPatClosing ctx (closingFor site) p- <> nest 1 (space <> maybe id located (uniTokenSpan arrow) (txt "<-"))+ <> nest 1 (space <> keywordAt ctx (spanOf arrow) "<-") <> layoutFrom ctx (fmap endOf (spanOf p) <> spanOf f)@@ -537,14 +538,12 @@ items (siteBracing site) $ keepBlanks (separatedByBlank ctx)- [(spanOf stmt, item place stmt) | (place, stmt) <- places xs]- item place stmt = case place of- Last -> rendered (siteBracing site)- Only -> rendered (siteBracing site)- _ -> mayBraceWhenFlat (siteBracing site) rendered- where- rendered bracing =- at_ ctx (stmtBody ctx (blockSite bracing) mkBody) stmt+ [ (spanOf stmt, item position stmt)+ | (position, stmt) <- positioned xs+ ]+ item position stmt =+ blockItem position (siteBracing site) $ \bracing ->+ at_ ctx (stmtBody ctx (blockSite bracing) mkBody) stmt blockSite bracing = plainSite{siteInBlock = True, siteBracing = bracing} @@ -595,11 +594,11 @@ MatchGroup GhcPs (LocatedA body) -> Doc caseOf ctx site mkBody EpAnnHsCase{..} scrutinee mg =- maybe id located (tokenSpan hsCaseAnnCase) (txt "case")+ keywordAt ctx (spanOf hsCaseAnnCase) "case" <> space <> hsExpr ctx scrutinee <> space- <> indent (maybe id located (tokenSpan hsCaseAnnOf) (txt "of"))+ <> indent (keywordAt ctx (spanOf hsCaseAnnOf) "of") <> breakOrSpace <> bracedForSite site alternatives where@@ -675,17 +674,17 @@ Doc letIn ctx bodyOf nested (letToken, inToken) binds body = align $- written (tokenSpan letToken) (txt "let")+ written (spanOf letToken) (txt "let") <> space <> align ( localBinds ctx NoBrace binds- <> foldMap (emptyAnchor . startOf) (tokenSpan inToken)+ <> foldMap (emptyAnchor . startOf) (spanOf inToken) )- <> written (tokenSpan inToken) (separator <> txt "in")+ <> written (spanOf inToken) (separator <> txt "in") <> (if chained then breakOrSpace else space) <> align (printBody (bodyOf body)) where- chained = nested && not (sameLine (tokenSpan inToken) (spanOf body))+ chained = nested && not (sameLine (spanOf inToken) (spanOf body)) separator = if chained then space else variant space (hardBreak <> txt " ") written s d = if chained then fenceWithin ctx s d else d @@ -696,19 +695,17 @@ anchored ann . align . items bracing $ keepBlanks (separatedByBlank ctx)- [(spanOf item, rendered place item) | (place, item) <- places sorted]+ [ (spanOf item, rendered position item)+ | (position, item) <- positioned sorted+ ] where sorted = sortBy (leftmost_smallest `on` getLocA) (fmap (fmap Left) binds <> fmap (fmap Right) sigs)- rendered place item = case place of- Last -> render NoBrace- Only -> render NoBrace- _ -> mayBraceWhenFlat NoBrace render- where- render b =- at_ ctx (either (valDecl ctx b) (knotSig (ctxKnot ctx) ctx)) item+ rendered position item =+ blockItem position NoBrace $ \b ->+ at_ ctx (either (valDecl ctx b) (knotSig (ctxKnot ctx) ctx)) item HsValBinds _ _ -> error "Tilia: renamer-only local bindings" HsIPBinds ann (IPBinds _ xs) -> anchored ann (items bracing (fmap (at_ ctx implicitBind) xs))@@ -821,7 +818,7 @@ _ -> Nothing render = \case Operand c -> at ctx c (cmdTop ctx site)- chain@(Chain operands@(firstOne :| rest) operators) ->+ chain@(Chain operands@(firstOne :| _) operators) -> layoutFrom ctx (chainSpan spanOf chain) $ mayBraceWhenFlat (if placement == Hanging then MayBrace else NoBrace) laidOut where@@ -831,23 +828,16 @@ (chainSpan spanOf firstOne) (NE.last operands) laidOut bracing =- renderIn bracing firstOne <> pieces bracing firstOne operators rest- pieces bracing previous (o : os) (operand : more) =+ renderIn bracing firstOne+ <> foldMap (piece bracing) (links operands operators)+ piece bracing (previous, o, operand, isLast) = attachOperator placement (chainSpan spanOf previous) (spanOf o) (chainSpan spanOf operand) (hsExpr ctx o)- ( renderIn- ( if null more- then siteBracing site- else bracing- )- operand- )- <> pieces bracing operand os more- pieces _ _ _ _ = mempty+ (renderIn (if isLast then siteBracing site else bracing) operand) renderIn bracing = \case Operand c -> at ctx c (cmdTop ctx (withBracing bracing site)) inner -> render inner@@ -897,19 +887,15 @@ Doc matchGroup ctx bracing mkBody style MG{..} | isCaseStyle style, null (unLoc mg_alts) = txt "{}"- | otherwise = items blockBracing (fmap rendered (places (unLoc mg_alts)))+ | otherwise = items blockBracing (fmap rendered (positioned (unLoc mg_alts))) where blockBracing = case style of CaseStyle -> ifEmpty LambdaCaseStyle -> ifEmpty _ -> NoBrace ifEmpty = if null (unLoc mg_alts) then MayBrace else bracing- rendered (place, m) = case place of- Last -> written bracing- Only -> written bracing- _ -> mayBraceWhenFlat bracing written- where- written b = at_ ctx (renderMatch b) m+ rendered (position, m) =+ blockItem position bracing (\b -> at_ ctx (renderMatch b) m) renderMatch b m@Match{..} = match ctx@@ -1100,7 +1086,7 @@ [] -> atSpan ctx (grhsSpan grhs) (printBody bound) _ -> maybe id located (spanOf lgrhs) . layoutFrom ctx (grhsSpan grhs) $- maybe id fence headSpan guardHead+ fenceWithin ctx headSpan guardHead <> nest (if parentPlacement == Normal then 1 else 0) (attach placement (printBody bound))@@ -1112,7 +1098,7 @@ <> align (commaSep (fmap (align . hsStmt ctx) guards)) <> space <> indent (txt separator)- headSpan = (<>) <$> (tokenSpan =<< ga_vbar) <*> separatorSpan+ headSpan = (<>) <$> (spanOf =<< ga_vbar) <*> separatorSpan bound = bodyIn mkBody bracing body separator = case style of EqualsSign -> "="@@ -1123,7 +1109,7 @@ not (remarkBefore && remarkAfter) = bodyPlacement bound | otherwise = Normal- separatorSpan = either annSpan annSpan ga_sep+ separatorSpan = either spanOf spanOf ga_sep remarkBefore = lineCommentWrittenBetween ctx (spansOf guards) separatorSpan remarkAfter = lineCommentWrittenBetween ctx separatorSpan (nextLine <$> separatorSpan)@@ -1196,7 +1182,7 @@ txt "[" <> name ctx quoter <> txt "|"- <> at ctx str (verbatim . T.pack . unpackFS)+ <> verbatim (T.pack (unpackFS (unLoc str))) <> txt "|]" -- | A Template Haskell splice, typed or not.@@ -1242,6 +1228,14 @@ ClosedTypeFamily Nothing -> True _ -> False +-- | An item of a block: the last one keeps the block's bracing, and any+-- other may brace itself when the block is flat.+blockItem :: Position -> Bracing -> (Bracing -> Doc) -> Doc+blockItem position bracing render = case position of+ Last -> render bracing+ Only -> render bracing+ _ -> mayBraceWhenFlat bracing render+ -- | Let a block brace itself when the surrounding layout is flat. mayBraceWhenFlat :: Bracing -> (Bracing -> Doc) -> Doc mayBraceWhenFlat whenBroken render =@@ -1255,3 +1249,68 @@ | siteInBlock site && not (siteApplicand site) = mayBraceWhenFlat (siteBracing site) render | otherwise = render (siteBracing site)++----------------------------------------------------------------------------+-- Bodies++-- | An expression standing as the body of an enclosing construct.+data ExprBody = ExprBody Ctx Site (LHsExpr GhcPs)++instance Body ExprBody where+ printBody (ExprBody ctx site e) = hsExprIn ctx site e+ bodyPlacement (ExprBody _ _ e) = exprHangs (unLoc e)++-- | A command standing as the body of an enclosing construct.+data CmdBody = CmdBody Ctx Site (LHsCmd GhcPs)++instance Body CmdBody where+ printBody (CmdBody ctx site c) = hsCmd ctx site c+ bodyPlacement (CmdBody _ _ c) = cmdHangs (unLoc c)++-- | A command at the top of an arrow form.+data CmdTopBody = CmdTopBody Ctx Site (LHsCmdTop GhcPs)++instance Body CmdTopBody where+ printBody (CmdTopBody ctx site l) = at ctx l (cmdTop ctx site)+ bodyPlacement (CmdTopBody _ _ l) = cmdTopHangs (unLoc l)++-- | Does this expression absorb the line break that introduces it?+exprHangs :: HsExpr GhcPs -> Placement+exprHangs = \case+ HsDo _ (DoExpr _) _ -> Hanging+ HsDo _ (MDoExpr _) _ -> Hanging+ HsCase{} -> Hanging+ HsLam _ lamVariant mg -> case lamVariant of+ LamCase -> Hanging+ LamCases -> Hanging+ LamSingle -> case mg of+ MG _ (L _ [L _ (Match _ _ (L _ ps@(_ : _)) _)])+ | maybe False isSingleLine (spansOf ps) -> Hanging+ _ -> Normal+ HsProc _ p _+ | maybe False isSingleLine (spanOf p) -> Hanging+ | otherwise -> Normal+ HsApp _ _ y -> exprHangs (unLoc y)+ OpApp _ x op y+ | Just n <- operatorName op,+ occNameString (rdrNameOcc n) == "$" ->+ exprHangs (unLoc y)+ | otherwise -> exprHangs (unLoc x)+ _ -> Normal++-- | Does this command absorb the line break that introduces it?+cmdHangs :: HsCmd GhcPs -> Placement+cmdHangs = \case+ HsCmdDo{} -> Hanging+ HsCmdCase{} -> Hanging+ HsCmdLam{} -> Hanging+ _ -> Normal++cmdTopHangs :: HsCmdTop GhcPs -> Placement+cmdTopHangs (HsCmdTop _ c) = cmdHangs (unLoc c)++-- | The name of an operator, when the expression standing as one is a name.+operatorName :: LHsExpr GhcPs -> Maybe RdrName+operatorName e = case unLoc e of+ HsVar _ (L _ n) -> Just n+ _ -> Nothing
src/Tilia/Render/Haddock.hs view
@@ -14,7 +14,6 @@ ) where -import Control.Applicative ((<|>)) import Data.Char (isSpace) import Data.Data (Data) import Data.Generics.Schemes (listify)@@ -26,6 +25,7 @@ import Data.Text qualified as T import GHC.Hs import GHC.Types.SrcLoc (GenLocated (..), getLoc, unLoc)+import Tilia.Comments (afterOpener, triggerOn) import Tilia.Doc.Combinators import Tilia.Gathered (Gathered (..)) import Tilia.Render.Context@@ -88,7 +88,7 @@ | blockForm -> (writtenAt rebuiltBlock, False) | otherwise -> (writtenAt rebuilt, False) where- writtenAt = maybe id fence (spanOfSrcSpan l)+ writtenAt = fenceWithin ctx (spanOfSrcSpan l) emptyBlock = txt (blockOpener style) <> space <> txt "-}" rebuilt = sepBy hardBreak (zipWith line' (True : repeat False) written')@@ -134,9 +134,6 @@ maybe False isBlockForm (writtenHaddock ctx (spanOfSrcSpan (getLoc doc))) -- | The anchor of a named documentation chunk.------ Unlike a Haddock this carries no text of its own, so there is nothing to--- reuse and nothing to report a position for. docSectionName :: String -> Text docSectionName n = "-- $" <> T.pack n @@ -171,24 +168,6 @@ Caret -> "^" Section n -> T.replicate n "*" Chunk n -> "$" <> T.pack n---- | What follows the @--@ or @{-@ that opens a comment, with the spaces--- after it removed.-afterOpener :: Text -> Maybe Text-afterOpener firstLine = T.stripStart <$> opened (T.stripStart firstLine)- where- opened t = T.stripPrefix "--" t <|> T.stripPrefix "{-" t---- | The trigger an opened comment carries, for the triggers that are a run--- of one character.-triggerOn :: Text -> Maybe Text-triggerOn inside = do- (c, rest) <- T.uncons inside- case c of- '|' -> Just "|"- '^' -> Just "^"- '*' -> Just (T.cons c (T.takeWhile (== '*') rest))- _ -> Nothing -- | Was the reused text a block comment? isBlockForm :: NonEmpty Text -> Bool
src/Tilia/Render/Header.hs view
@@ -15,16 +15,17 @@ import Data.Function (on) import Data.List (sortOn) import Data.List.NonEmpty qualified as NE+import Data.Maybe (isJust) import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T-import GHC.Driver.Flags (Language) import GHC.Hs import GHC.LanguageExtensions.Type (Extension (..)) import GHC.Types.PkgQual (RawPkgQual (..)) import GHC.Types.SrcLoc (GenLocated (..), unLoc) import Tilia.Comments (Comment (..), Pragma (..), commentPragma) import Tilia.Doc.Combinators+import Tilia.Pragma (languageNames, lookupEdition) import Tilia.Render.Context import Tilia.Render.Declaration (decls) import Tilia.Render.Haddock@@ -41,8 +42,6 @@ hpSpan :: Span, -- | How many preprocessor directives the header has above it. hpRun :: Int,- -- | Where it sorts.- hpOrder :: PragmaOrder, -- | @LANGUAGE@, @OPTIONS_GHC@ or @OPTIONS_HADDOCK@. hpName :: Text, -- | One extension, or the whole of an options string.@@ -108,23 +107,23 @@ HeaderPragma { hpSpan = commentSpan c, hpRun = directivesAbove (spanStartLine (commentSpan c)),- hpOrder = orderOf p, hpName = pragmaName p, hpBody = pragmaBody p } headerPragma c = do p <- commentPragma c- _ <- lookupOrder (pragmaName p)+ _ <- pragmaOrder (pragmaName p) (pragmaBody p) if inHeader headerEnd (commentSpan c) then Just p else Nothing- orderOf p = case pragmaName p of- "LANGUAGE" -> LanguageOrder (classifyExtension (pragmaBody p))- other -> maybe OptionsGhcOrder id (lookupOrder other)- lookupOrder = \case- "LANGUAGE" -> Just (LanguageOrder Enabling)- "OPTIONS_GHC" -> Just OptionsGhcOrder- "OPTIONS_HADDOCK" -> Just OptionsHaddockOrder- _ -> Nothing +-- | Where a pragma sorts, given its name and its body, or 'Nothing' for one+-- the header does not hoist.+pragmaOrder :: Text -> Text -> Maybe PragmaOrder+pragmaOrder named body = case named of+ "LANGUAGE" -> Just (LanguageOrder (classifyExtension body))+ "OPTIONS_GHC" -> Just OptionsGhcOrder+ "OPTIONS_HADDOCK" -> Just OptionsHaddockOrder+ _ -> Nothing+ -- | Was this written above everything the compiler reads as code? inHeader :: Maybe Span -> Span -> Bool inHeader headerEnd s = case headerEnd of@@ -153,14 +152,15 @@ pragmaBlock :: [HeaderPragma] -> Doc pragmaBlock = foldMap render . dedupe . sortOn order . concatMap split where- order p = (hpRun p, hpOrder p, extension p)+ order p = (hpRun p, orderOf p, extension p) extension p = if hpName p == "LANGUAGE" then hpBody p else ""- key p = (hpRun p, hpOrder p, hpBody p)+ key p = (hpRun p, orderOf p, hpBody p)+ orderOf p = pragmaOrder (hpName p) (hpBody p) dedupe = fmap NE.head . NE.groupBy ((==) `on` key) split p | hpName p == "LANGUAGE" =- [ p{hpBody = body, hpOrder = LanguageOrder (classifyExtension body)}- | body <- fmap T.strip (T.splitOn "," (hpBody p))+ [ p{hpBody = body}+ | body <- languageNames (hpBody p) ] | otherwise = [p] @@ -185,9 +185,7 @@ -- | Does this name a whole edition of the language rather than one -- extension of it? namesAnEdition :: Text -> Bool-namesAnEdition t = any spelledTheSame [minBound .. maxBound]- where- spelledTheSame edition = t == T.pack (show (edition :: Language))+namesAnEdition = isJust . lookupEdition -- | A whole module. hsModule ::@@ -235,8 +233,8 @@ ( breakOrSpace <> foldMap warning hsmodDeprecMessage <> foldMap exports' hsmodExports- <> txt "where" )+ <> keywordAt ctx (spanOf (am_where (anns hsmodAnn))) "where" <> hardBreak documentation = foldMap (haddock ctx Pipe Closed) hsmodHaddockModHeader warning w = at ctx w warningTxt <> breakOrSpace@@ -258,12 +256,16 @@ importExportItems ctx xs = variant (laidOut False) (laidOut True) where laidOut broken' =- sepBy breakOrSpace (zipWith (item broken') (Nothing : fmap Just xs) (places xs))- item broken' previous (place, x) =- gapAbove place (unLoc <$> previous) (unLoc x)- <> align (at ctx (widenToDoc x) (ieItem ctx (spanOf x) (comma' broken' place)))- gapAbove place previous here- | place == First || place == Only = mempty+ sepBy+ breakOrSpace+ (zipWith (item broken') (Nothing : fmap Just xs) (positioned xs))+ item broken' previous (position, x) =+ gapAbove position (unLoc <$> previous) (unLoc x)+ <> align (at ctx (widenToDoc x) (ieItem ctx (spanOf x) withComma))+ where+ withComma = comma' broken' position+ gapAbove position previous here+ | position == First || position == Only = mempty | isSection here = hardBreak | isPipe here, maybe False runsOn previous = hardBreak | otherwise = mempty@@ -277,9 +279,9 @@ IEDoc{} -> True IEDocNamed{} -> True _ -> False- comma' broken' place+ comma' broken' position | broken' = True- | otherwise = place == First || place == Middle+ | otherwise = position == First || position == Middle -- | Widen an item's span to take in the documentation printed with it, so -- that a documented item is laid out as one thing.@@ -291,31 +293,25 @@ -- | One item of an import or export list. ieItem :: Ctx -> Maybe Span -> Bool -> IE GhcPs -> Doc ieItem ctx here withComma = \case- IEVar warning n doc ->- exportWarning warning- <> at ctx n (wrappedName ctx)- <> comma'- <> itemDocumentation doc- IEThingAbs warning n doc ->- exportWarning warning- <> at ctx n (wrappedName ctx)- <> comma'- <> itemDocumentation doc- IEThingAll (warning, _) n doc ->+ IEVar warning n doc -> plain warning n doc+ IEThingAbs warning n doc -> plain warning n doc+ IEThingAll (warning, (_, dots, _)) n doc -> exportWarning warning <> at ctx n (wrappedName ctx) <> space- <> txt "(..)"+ <> txt "("+ <> keywordAt ctx (spanOf dots) ".."+ <> txt ")" <> comma' <> itemDocumentation doc- IEThingWith (warning, (open, _, _, close)) n wildcard members doc ->+ IEThingWith (warning, (open, dots, _, close)) n wildcard members doc -> align ( exportWarning warning <> at ctx n (wrappedName ctx) <> layoutWithin ctx here- (tokenSpan open <> tokenSpan close <|> here)+ (spanOf open <> spanOf close <|> here) ( breakOrSpace <> indent (parens (insideBrackets here (commaSep (align <$> withWildcard)))) )@@ -328,7 +324,7 @@ NoIEWildcard -> rendered IEWildcard n' -> let (before, after) = splitAt n' rendered- in before <> [txt ".."] <> after+ in before <> [keywordAt ctx (spanOf dots) ".."] <> after IEModuleContents (warning, _) m -> exportWarning warning <> at ctx m (moduleHeadName ctx) <> comma' IEGroup NoExtField n str -> haddock ctx (Section n) Open str@@ -337,6 +333,11 @@ Just written -> sepBy (verbatimBreak AtIndent TrimWhitespace) (fmap txt (NE.toList written)) Nothing -> txt (docSectionName n) where+ plain warning n doc =+ exportWarning warning+ <> at ctx n (wrappedName ctx)+ <> comma'+ <> itemDocumentation doc comma' = includeWhen withComma comma exportWarning = foldMap (\w -> at ctx w warningTxt <> breakOrSpace)@@ -364,12 +365,12 @@ -- and its import list, each a node of its own past the keyword's column. importDecl :: Ctx -> ImportDecl GhcPs -> Doc importDecl ctx ImportDecl{..} =- keywordAt ctx (tokenSpan (importDeclAnnImport annotation)) "import"+ keywordAt ctx (spanOf (importDeclAnnImport annotation)) "import" <> indent ( space <> includeWhen (ideclSource == IsBoot) (txt "{-# SOURCE #-}") <> space- <> includeWhen ideclSafe (keywordAt ctx (tokenSpan =<< importDeclAnnSafe annotation) "safe")+ <> includeWhen ideclSafe (keywordAt ctx (spanOf =<< importDeclAnnSafe annotation) "safe") <> space <> levelBefore <> space@@ -386,7 +387,7 @@ ( \a -> indent ( space- <> keywordAt ctx (tokenSpan =<< importDeclAnnAs annotation) "as"+ <> keywordAt ctx (spanOf =<< importDeclAnnAs annotation) "as" <> space <> at ctx a outputable )@@ -395,7 +396,7 @@ <> foldMap (indent . (space <>)) importList where annotation = anns (ideclAnn ideclExt)- qualified = keywordAt ctx (tokenSpan =<< importDeclAnnQualified annotation) "qualified"+ qualified = keywordAt ctx (spanOf =<< importDeclAnnQualified annotation) "qualified" qualifiedLast = extensionOn ctx ImportQualifiedPost isQualified = isImportDeclQualified ideclQualified packageQualifier = case ideclPkgQual of@@ -410,7 +411,7 @@ importList = flip fmap ideclImportList $ \(interpretation, L listLoc xs) -> let hidden = case interpretation of Exactly -> mempty- EverythingBut -> keywordAt ctx (tokenSpan (fst (al_rest (anns listLoc)))) "hiding"+ EverythingBut -> keywordAt ctx (spanOf (fst (al_rest (anns listLoc)))) "hiding" listSpan = spanOfSrcSpan (locA listLoc) parensSpan = bracketsSpan (al_brackets (anns listLoc)) <|> listSpan in layoutWithin ctx listSpan parensSpan $
src/Tilia/Render/Layout.hs view
@@ -9,8 +9,8 @@ items, itemsSepBy, keepBlanks,- Place (..),- places,+ Position (..),+ positioned, ) where @@ -59,7 +59,7 @@ gap previous (here, d) = includeWhen (blank previous here) hardBreak <> d -- | Where an item sits among its siblings.-data Place+data Position = Only | First | Middle@@ -67,10 +67,10 @@ deriving (Eq, Show) -- | Label each item of a list with its position.-places :: [a] -> [(Place, a)]-places [] = []-places [x] = [(Only, x)]-places (x : xs) = (First, x) : go xs+positioned :: [a] -> [(Position, a)]+positioned [] = []+positioned [x] = [(Only, x)]+positioned (x : xs) = (First, x) : go xs where go [] = [] go [y] = [(Last, y)]
src/Tilia/Render/Literal.hs view
@@ -18,7 +18,7 @@ import GHC.Data.FastString (FastString, unpackFS) import GHC.Parser.CharClass (is_space) import Tilia.Doc.Combinators-import Tilia.Render.Layout (Place (..), places)+import Tilia.Render.Layout (Position (..), positioned) -- | A string literal, from the text the author wrote. stringLiteral :: FastString -> Doc@@ -39,8 +39,8 @@ (fmap txt (litParts literal)) onOneLine = txt (joinParts (litParts literal)) acrossLines =- sepBy breakOrSpace (fmap continued (places (litParts literal)))- continued (place, s) = case place of+ sepBy breakOrSpace (fmap continued (positioned (litParts literal)))+ continued (position, s) = case position of Only -> txt s First -> txt s <> txt "\\" Middle -> txt "\\" <> txt s <> txt "\\"
src/Tilia/Render/Name.hs view
@@ -78,10 +78,10 @@ NameBackquotes{} -> backticks . itsOwn nann_name _ -> itsOwn nann_name _ -> id- itsOwn = atSpan ctx . annSpan+ itsOwn = atSpan ctx . spanOf inParens d = txt "(" <> d <> txt ")" spaceOutHash d- | extensionOn ctx UnboxedSums,+ | extensionOn ctx UnboxedTuples || extensionOn ctx UnboxedSums, Unqual (occNameString -> '#' : _) <- x = space <> d <> space | otherwise = d
src/Tilia/Render/Operator.hs view
@@ -7,11 +7,13 @@ flattenAround, associate, chainSpan,+ links, lastOperand, isSeparator, ) where +import Data.List (tails, zip4) import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NE import Data.Maybe (isNothing, mapMaybe)@@ -115,14 +117,17 @@ in (NE.cons (NE.reverse current, reverse currentOps) groups, op : splitters) | otherwise = go (NE.cons y current) (op : currentOps) rest +-- | Each operator of a chain level with the operands either side of it,+-- and whether the one after it is the last.+links :: NonEmpty (OpChain a op) -> [op] -> [(OpChain a op, op, OpChain a op, Bool)]+links (firstOne :| rest) operators =+ zip4 (firstOne : rest) operators rest (fmap null (drop 1 (tails rest)))+ -- | The region of the input a chain came from. chainSpan :: (a -> Maybe Span) -> OpChain a op -> Maybe Span chainSpan spanOfOperand = \case Operand x -> spanOfOperand x- Chain xs _ -> foldr1 join' (chainSpan spanOfOperand <$> xs)- where- join' (Just a) (Just b) = Just (a <> b)- join' a b = maybe b Just a+ Chain xs _ -> foldMap (chainSpan spanOfOperand) xs -- | The rightmost operand of a chain. lastOperand :: OpChain a op -> a
src/Tilia/Render/Pattern.hs view
@@ -12,6 +12,7 @@ hsPatClosing, fieldOcc, unboxedSum,+ boxityBrackets, ) where @@ -19,7 +20,6 @@ import Data.List.NonEmpty qualified as NE import Data.Maybe (isJust) import GHC.Hs-import GHC.LanguageExtensions.Type (Extension (..)) import GHC.Types.Basic (Arity, Boxity (..), ConTag) import GHC.Types.Name.Reader (RdrName) import GHC.Types.SrcLoc (GenLocated (..))@@ -81,7 +81,7 @@ ListPat _ ps -> bracketsWith closing (insideBrackets here (commaSep (fmap recur ps))) TuplePat _ ps boxity ->- tupleBrackets+ boxityBrackets boxity closing (insideBrackets here (commaSep (fmap (align . recur) ps)))@@ -97,7 +97,7 @@ SplicePat _ splice -> knotSplice (ctxKnot ctx) ctx DollarSplice splice LitPat _ lit -> outputable lit NPat _ v (isJust -> negated) _ ->- includeWhen negated (txt "-" <> negativeGap ctx)+ includeWhen negated (txt "-" <> negativeLiteralGap ctx) <> at ctx v (outputable . ol_val) NPlusKPat _ n k _ _ _ -> align $@@ -170,13 +170,8 @@ before = replicate (tag - 1) space after = replicate (arity - tag) space --- | The brackets a tuple pattern is written with.-tupleBrackets :: Boxity -> ClosingIndent -> Doc -> Doc-tupleBrackets = \case+-- | The brackets a tuple is written with.+boxityBrackets :: Boxity -> ClosingIndent -> Doc -> Doc+boxityBrackets = \case Boxed -> parensWith Unboxed -> unboxedWith---- | With @NegativeLiterals@ on, @- 1@ and @-1@ are different expressions, so--- the minus of a negated literal has to keep its distance.-negativeGap :: Ctx -> Doc-negativeGap ctx = includeWhen (extensionOn ctx NegativeLiterals) space
src/Tilia/Render/Pragma.hs view
@@ -56,7 +56,7 @@ Opaque _ -> txt "OPAQUE" NoUserInlinePrag -> mempty --- | The overlap pragma of an instance, and the separator after it.+-- | The overlap pragma of an instance. overlapMode :: Ctx -> Maybe (LocatedP OverlapMode) -> Maybe Doc overlapMode ctx mode = do m <- mode
src/Tilia/Render/Signature.hs view
@@ -36,7 +36,7 @@ import Tilia.Render.Pragma import Tilia.Render.Type import Tilia.Span (startOf)-import Tilia.Span.Ghc (spanOf, spansOf, tokenSpan)+import Tilia.Span.Ghc (spanOf, spansOf) -- | A signature declaration. sigDecl :: Ctx -> Sig GhcPs -> Doc@@ -45,7 +45,7 @@ PatSynSig _ names sigType -> patSynSig ctx names sigType ClassOpSig _ isDefault names sigType -> includeWhen isDefault (txt "default" <> space) <> typeSig ctx (Do #indentTail) names sigType- FixSig _ sig -> fixitySig ctx sig+ FixSig ((_, written), _) sig -> fixitySig ctx written sig InlineSig _ n prag -> inlineSig ctx n prag SpecSig _ n types prag -> specialiseSig ctx Nothing (noLocA (HsVar NoExtField n)) types prag@@ -104,13 +104,12 @@ where body = typeSig ctx (Don't #indentTail) names sigType --- | @infixl 5 <+>@.-fixitySig :: Ctx -> FixitySig GhcPs -> Doc-fixitySig ctx (FixitySig namespace names (Fixity precedence direction)) =+-- | @infixl 5 <+>@, with the precedence only where one is written.+fixitySig :: Ctx -> Maybe EpaLocation -> FixitySig GhcPs -> Doc+fixitySig ctx written (FixitySig namespace names (Fixity prec direction)) = txt keyword <> space- <> outputable precedence- <> space+ <> foldMap (const (outputable prec <> space)) written <> namespaceSpec namespace <> align (commaSep (fmap (name ctx) names)) where@@ -247,7 +246,7 @@ ruleDecls ctx (HsRules ((_, close), _) rules) = pragma "RULES" $ sepBy breakOrSpace (fmap (align . at_ ctx (ruleDecl ctx)) rules)- <> foldMap (emptyAnchor . startOf) (tokenSpan close)+ <> foldMap (emptyAnchor . startOf) (spanOf close) -- | One rule of a @RULES@ block. ruleDecl :: Ctx -> RuleDecl GhcPs -> Doc
src/Tilia/Render/Type.hs view
@@ -13,6 +13,7 @@ hsSigTypeBody, typeAscription, context,+ leftContext, contextOf, tyVarBndr, Visibility (..),@@ -158,12 +159,11 @@ renderChain :: Ctx -> OpChain (LHsType GhcPs) (LocatedN RdrName) -> Doc renderChain ctx = \case Operand t -> hsType ctx t- chain@(Chain (firstOne :| rest) operators) ->+ chain@(Chain operands@(firstOne :| _) operators) -> layoutFrom ctx (chainSpan spanOf chain) $- renderChain ctx firstOne- <> mconcat (zipWith3 piece (firstOne : rest) operators rest)+ renderChain ctx firstOne <> foldMap piece (links operands operators) where- piece previous op operand =+ piece (previous, op, operand, _) = attachOperator Normal (chainSpan spanOf previous)@@ -268,6 +268,13 @@ HsParTy _ inner -> unbracket inner _ -> t +-- | A context standing to the left of a @=>@, with the arrow and the break+-- after it, or nothing where the context is empty.+leftContext :: Ctx -> LHsContext GhcPs -> Doc+leftContext ctx ctxt+ | null (unLoc ctxt) = mempty+ | otherwise = context ctx ctxt <> joinedBy "=>"+ -- | A context over anything that can stand as a constraint. contextOf :: -- | Is this constraint nothing but a variable?@@ -360,11 +367,19 @@ HsForAllInvis _ bndrs -> forallBndrs ctx Invisible (tyVarBndr ctx) bndrs HsForAllVis _ bndrs -> forallBndrs ctx Visible (tyVarBndr ctx) bndrs --- | The binders a signature quantifies over, when it names them.-outerBndrs :: Ctx -> HsOuterTyVarBndrs Specificity GhcPs -> Doc-outerBndrs ctx = \case+-- | The binders a signature or an equation quantifies over, where it names+-- them.+outerBndrs ::+ (TyVarBndrFlag flag) =>+ Ctx ->+ -- | What separates them from what follows.+ Doc ->+ HsOuterTyVarBndrs flag GhcPs ->+ Doc+outerBndrs ctx separator = \case HsOuterImplicit _ -> mempty- HsOuterExplicit _ bndrs -> forallTelescope ctx (mkHsForAllInvisTele noAnn bndrs)+ HsOuterExplicit _ bndrs ->+ forallBndrs ctx Invisible (tyVarBndr ctx) bndrs <> separator -- | A type together with whatever it quantifies over. hsSigType :: Ctx -> LHsSigType GhcPs -> Doc@@ -373,12 +388,7 @@ -- | A signature type whose location the caller has already entered. hsSigTypeBody :: Ctx -> HsSigType GhcPs -> Doc hsSigTypeBody ctx HsSig{..} =- outerBndrs ctx sig_bndrs- <> ( case sig_bndrs of- HsOuterImplicit{} -> mempty- HsOuterExplicit{} -> afterBinders- )- <> hsType ctx sig_body+ outerBndrs ctx afterBinders sig_bndrs <> hsType ctx sig_body where afterBinders | typeIsDocumented (unLoc sig_body) = hardBreak
src/Tilia/Run.hs view
@@ -5,8 +5,6 @@ module Tilia.Run ( -- * Outcomes Outcome (..),- declined,- failed, differs, failIfDeclined, exitCodeOf,@@ -17,7 +15,6 @@ readAsUtf8, formattingOutcome, writeBack,- inParallel, -- * Report Report (..),@@ -46,7 +43,15 @@ ) import Tilia.Newline (NewlineStyle (Lf), getNewlineStyle, setNewlineStyle) import Tilia.Palette (Color (Bad, Good, Middling, Place), Palette, marker, paint)-import Tilia.Utils (asUtf8, attempted, inParallel, indent, lineWidth, wrapTo)+import Tilia.Utils+ ( asUtf8,+ attempted,+ byteOrderMark,+ inParallel,+ indent,+ lineWidth,+ wrapTo,+ ) ---------------------------------------------------------------------------- -- Outcomes@@ -118,9 +123,11 @@ Text -> IO Outcome outcomeOf session path before =- formatSource session path (setNewlineStyle Lf before) >>= \case+ formatSource session path (setNewlineStyle Lf source) >>= \case Left e -> pure (if refused e then Declined e else Failed e)- Right formatted -> pure (formattingOutcome before formatted)+ Right formatted -> pure (formattingOutcome before (mark <> formatted))+ where+ (mark, source) = byteOrderMark before -- | Read a source file as UTF-8. readAsUtf8 :: FilePath -> IO (Either Text Text)
src/Tilia/Source.hs view
@@ -27,7 +27,8 @@ import Data.IntMap.Strict (IntMap) import Data.IntMap.Strict qualified as IntMap-import GHC.Parser.Annotation (LEpaComment)+import GHC.Parser.Lexer qualified as GHC+import GHC.Types.SrcLoc qualified as GHC import Tilia.Comments (Comment, commentsOf) import Tilia.Source.Lines @@ -56,8 +57,9 @@ sourceOf :: -- | The lines of the module, as this configuration has them. Lines ->- -- | Every comment of the module, in source order.- [LEpaComment] ->+ -- | Every comment token of the module, in source order, each where the+ -- lexer found it.+ [(GHC.RealSrcSpan, GHC.Token)] -> -- | The line each quasi-quotation written over several lines ends on, by -- the line it begins on. IntMap Int ->
src/Tilia/Source/Lines.hs view
@@ -13,6 +13,10 @@ blankAt, directivePresentOnLine, directiveOnLine,+ conditionalKeywords,+ opensGroup,+ continuesGroup,+ opaqueKeywords, ) where @@ -82,8 +86,27 @@ directiveOnLine :: Text -> Maybe (Text, Text) directiveOnLine l = case T.uncons (T.stripStart l) of Just ('#', rest)- | not (T.null keyword) -> Just (keyword, body)+ | keyword `elem` directiveKeywords -> Just (keyword, body) where body = T.stripStart rest keyword = T.takeWhile isAsciiLower body _ -> Nothing++-- | Every directive the C preprocessor takes, whether or not this module+-- can do anything with the ones it names.+directiveKeywords :: [Text]+directiveKeywords = conditionalKeywords <> opaqueKeywords++-- | The directives that ask a question, and so split a module in two.+conditionalKeywords :: [Text]+conditionalKeywords = opensGroup <> continuesGroup <> ["endif"]++-- | The keywords that open a group or continue one.+opensGroup, continuesGroup :: [Text]+opensGroup = ["if", "ifdef", "ifndef"]+continuesGroup = ["elif", "elifdef", "elifndef", "else"]++-- | The unconditional directives that do not split the source code.+opaqueKeywords :: [Text]+opaqueKeywords =+ ["define", "undef", "include", "line", "error", "warning", "pragma"]
src/Tilia/Span.hs view
@@ -7,6 +7,7 @@ blankBetween, meets, covers,+ zeroWidth, startOf, endOf, startPoint,@@ -68,6 +69,10 @@ -- | Does the first cover all of the second? covers :: Span -> Span -> Bool covers a b = startPoint a <= startPoint b && endPoint b <= endPoint a++-- | Does a span cover nothing written, as an empty anchor's does?+zeroWidth :: Span -> Bool+zeroWidth s = startPoint s == endPoint s -- | A zero-width span at the start of the given one. startOf :: Span -> Span
src/Tilia/Span/Ghc.hs view
@@ -6,22 +6,12 @@ spanOfSrcSpan, spanOf, spansOf,- tokenSpan,- uniTokenSpan,- annSpan, bracketsSpan, ) where import Data.Maybe (mapMaybe)-import GHC.Parser.Annotation- ( AnnListBrackets (..),- EpToken,- EpUniToken (..),- HasLoc,- getEpTokenSrcSpan,- getHasLoc,- )+import GHC.Parser.Annotation (AnnListBrackets (..), HasLoc, getHasLoc) import GHC.Types.SrcLoc (GenLocated) import GHC.Types.SrcLoc qualified as GHC import Tilia.Span (Span, mkSpan)@@ -37,32 +27,18 @@ spanOfSrcSpan :: GHC.SrcSpan -> Maybe Span spanOfSrcSpan = fmap spanOfReal . GHC.srcSpanToRealSrcSpan --- | The span of a located thing.-spanOf :: (HasLoc l) => GenLocated l a -> Maybe Span+-- | Where something the compiler located was written: a node, a token or+-- an annotation.+spanOf :: (HasLoc l) => l -> Maybe Span spanOf = spanOfSrcSpan . getHasLoc --- | Where a keyword or a piece of punctuation was written.-tokenSpan :: EpToken sym -> Maybe Span-tokenSpan = spanOfSrcSpan . getEpTokenSrcSpan---- | Where a keyword or a piece of punctuation that has a Unicode spelling--- was written.-uniTokenSpan :: EpUniToken tok utok -> Maybe Span-uniTokenSpan = \case- EpUniTok l _ -> annSpan l- NoEpUniTok -> Nothing---- | Where an annotation says something was written.-annSpan :: (HasLoc l) => l -> Maybe Span-annSpan = spanOfSrcSpan . getHasLoc- -- | Where a list's brackets were written, from the opening one to the -- closing one. bracketsSpan :: AnnListBrackets -> Maybe Span bracketsSpan = \case- ListParens open close -> tokenSpan open <> tokenSpan close- ListBraces open close -> tokenSpan open <> tokenSpan close- ListSquare open close -> tokenSpan open <> tokenSpan close+ ListParens open close -> spanOf open <> spanOf close+ ListBraces open close -> spanOf open <> spanOf close+ ListSquare open close -> spanOf open <> spanOf close _ -> Nothing -- | The span covering every located thing in the list.
src/Tilia/Utils.hs view
@@ -6,7 +6,11 @@ module Tilia.Utils ( quietly, attempted,+ readFileText,+ fieldWords, asUtf8,+ byteOrderMark,+ sha256Of, lineWidth, indent, wrapTo,@@ -27,13 +31,16 @@ ) import Control.Exception (SomeException, displayException, try) import Control.Monad (replicateM)+import Crypto.Hash.SHA256 qualified as SHA256 import Data.ByteString qualified as BS+import Data.ByteString.Base16 qualified as B16 import Data.Foldable (for_, traverse_) import Data.IORef (atomicModifyIORef', newIORef, readIORef) import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Text qualified as T import Data.Text.Encoding qualified as T+import System.Directory (doesFileExist) -- | 'show' a value and take the result as 'Text'. tshow :: (Show a) => a -> Text@@ -46,6 +53,18 @@ Left (_ :: SomeException) -> pure fallback Right a -> pure a +-- | 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++-- | The words of a field's value, which whitespace and commas separate.+fieldWords :: Text -> [Text]+fieldWords = filter (not . T.null) . concatMap (T.split (== ',')) . T.words+ -- | Run an action and return its result or the exception it threw rendered -- as 'Text'. attempted :: IO a -> IO (Either Text a)@@ -59,6 +78,15 @@ asUtf8 bytes = case T.decodeUtf8' bytes of Right text -> Right text Left _ -> Left "it is not valid UTF-8"++-- | The byte order mark a text begins with, which GHC skips, if it has one,+-- and the rest of it.+byteOrderMark :: Text -> (Text, Text)+byteOrderMark t = T.splitAt (if "\xFEFF" `T.isPrefixOf` t then 1 else 0) t++-- | The SHA-256 of a text's UTF-8 encoding, in hexadecimal.+sha256Of :: Text -> Text+sha256Of = T.decodeUtf8Lenient . B16.encode . SHA256.hash . T.encodeUtf8 -- | The line width for the terminal output of this program. lineWidth :: Int
tests/Main.hs view
@@ -11,14 +11,12 @@ -- everything. module Main (main) where -import Data.Bits (xor)-import Data.Char (ord) import Data.List (intercalate, isPrefixOf)-import Data.Word (Word64) import Spec qualified import System.Environment (lookupEnv) import Test.Hspec.Runner (Config (..), Path, defaultConfig, hspecWith) import Text.Read (readMaybe)+import Tilia.TestUtils (fingerprint) main :: IO () main =@@ -58,13 +56,6 @@ -- is rather than by where it happens to fall in the order. spell :: Path -> String spell (groups, requirement) = intercalate "/" (groups <> [requirement])---- | FNV-1a, so that a test lands in the same share on every machine and--- every run, and adding one test does not move the others.-fingerprint :: String -> Word64-fingerprint = foldl' step 14695981039346656037- where- step h c = (h `xor` fromIntegral (ord c)) * 1099511628211 -- | Is this test one whose answer the compiler can change? leansOnCompiler :: Path -> Bool
tests/Tilia/Cabal/PackageSpec.hs view
@@ -14,7 +14,7 @@ import System.IO.Temp (withSystemTempDirectory) import Test.Hspec import Tilia.Cabal.Package (PackageProblem (..), newPackageReader)-import Tilia.Fixity.Plan (BuildPlan (..), plannedCompiler)+import Tilia.Cabal.Plan (BuildPlan (..), plannedCompiler) spec :: Spec spec = do
tests/Tilia/Comments/PlaceSpec.hs view
@@ -8,9 +8,8 @@ import Tilia.Comments (Comment (..), renderComment) import Tilia.Comments.Place import Tilia.Doc.Internal (Spill (..))-import Tilia.Parser-import Tilia.Source (comments) import Tilia.Span+import Tilia.TestUtils (commentsIn) spec :: Spec spec = do@@ -330,8 +329,3 @@ firstComment src = case commentsIn src of (c : _) -> c [] -> error "the test input had no comments"--commentsIn :: Text -> [Comment]-commentsIn src = case parseModule defaultParserConfig "test.hs" src of- Left _ -> error "the test input did not parse"- Right pm -> comments (pmSource pm)
tests/Tilia/CommentsSpec.hs view
@@ -13,8 +13,8 @@ import Tilia.Doc import Tilia.Doc.Combinators import Tilia.Parser-import Tilia.Source (comments) import Tilia.Span+import Tilia.TestUtils (commentsIn, parsed) spec :: Spec spec = do@@ -196,7 +196,7 @@ -- Helpers render :: Doc -> Text-render = printDoc defaultRenderOptions+render = printDoc one :: Text -> Comment one src = case commentsIn src of@@ -206,12 +206,6 @@ countOf :: Text -> Text -> Int countOf needle = length . T.breakOnAll needle -commentsIn :: Text -> [Comment]-commentsIn src =- case parseModule defaultParserConfig "test.hs" src of- Left _ -> error "the test input did not parse"- Right pm -> comments (pmSource pm)- bodies :: Text -> [[Text]] bodies = fmap (NE.toList . commentBody) . commentsIn @@ -226,6 +220,4 @@ followeds = fmap commentFollowed . commentsIn headerLine :: Text -> Maybe Int-headerLine src = case parseModule defaultParserConfig "test.hs" src of- Left _ -> error "the test input did not parse"- Right pm -> spanStartLine <$> pmHeaderEnd pm+headerLine = fmap spanStartLine . pmHeaderEnd . parsed
tests/Tilia/Corpus.hs view
@@ -25,7 +25,7 @@ import Codec.Archive.Tar qualified as Tar import Codec.Compression.GZip qualified as GZip-import Control.Exception (SomeException, try)+import Control.Exception (try) import Control.Monad (forM) import Data.ByteString.Lazy qualified as BL import Data.List (isPrefixOf, isSuffixOf, sort, stripPrefix)@@ -49,7 +49,8 @@ ) import System.Environment (lookupEnv) import System.FilePath (splitDirectories, takeDirectory, (</>))-import Tilia.Cabal.Package (newPackageReader)+import Tilia.Cabal.Package (newPackageReader, sourceExtensions)+import Tilia.Utils (quietly) ---------------------------------------------------------------------------- -- Corpora@@ -361,7 +362,8 @@ "declaration" </> "signature" </> "pattern" </> "multiline.hs", "declaration" </> "splice" </> "quasiquote.hs", "declaration" </> "value" </> "function" </> "quasi-quotes.hs",- "declaration" </> "value" </> "function" </> "operator-comments-0.hs"+ "declaration" </> "value" </> "function" </> "operator-comments-0.hs",+ "declaration" </> "signature" </> "fixity" </> "infix.hs" ] <> ormoluRecordBraces <> ormoluUnreadable@@ -887,9 +889,10 @@ -- for an example within one that happens to have none. exampleReference :: Maybe FilePath, -- | What the package around it puts in force, already resolved from its- -- @.cabal@ file. Empty for a corpus whose examples are not modules of a- -- package; see 'corpusInPackages'.- exampleExtensions :: [Extension]+ -- @.cabal@ file. 'Nothing' for a corpus whose examples are not modules+ -- of a package, see 'corpusInPackages', and for one whose package could+ -- not be read.+ exampleExtensions :: Maybe [Extension] } deriving (Eq, Show) @@ -898,20 +901,24 @@ -- -- Fetching happens once: an unpacked corpus is left in place and found -- again, and a download interrupted half way leaves nothing behind to be--- mistaken for a complete one. 'Left' is for the machine that cannot reach--- the network rather than for a defect, and callers are expected to say so--- and carry on rather than fail. A vendored corpus is already here and can--- never fail this way.+-- mistaken for a complete one. 'Left' says why a corpus that is not here+-- could not be fetched; a vendored corpus is always here.+--+-- The archive of a 'Fetched' corpus is deleted once unpacked; Hackage+-- releases keep theirs, which the fixity benchmarks read. obtain :: Corpus -> IO (Either Text [Example]) obtain corpus = case corpusSource corpus of Vendored dir -> Right <$> examplesIn corpus dir Fetched url root -> do home <- corpusCache let unpacked = home </> corpusName corpus+ archive = unpacked <> ".tar.gz" createDirectoryIfMissing True home- fetch url (home </> corpusName corpus <> ".tar.gz") unpacked >>= \case+ fetch url archive unpacked >>= \case Left problem -> pure (Left problem)- Right () -> Right <$> examplesIn corpus (unpacked </> root)+ Right () -> do+ removePathForcibly archive+ Right <$> examplesIn corpus (unpacked </> root) HackageReleases releases -> do home <- corpusCache let root = home </> corpusName corpus@@ -968,9 +975,8 @@ -- partial file under the real name would be taken for a complete download -- on the next run and never fetched again. ----- 'Left' is for the machine that cannot reach the network rather than for a--- defect, and callers are expected to say so and carry on. Only the fetch--- is caught: a file that cannot be written is a fault worth hearing about.+-- 'Left' says why the fetch failed. Only the fetch is caught: a file that+-- cannot be written is a fault worth hearing about. download :: (Url 'Https, Option 'Https) -> FilePath -> IO (Either Text ()) download (url, query) dest = try get >>= \case@@ -1018,7 +1024,7 @@ case Tar.entryContent entry of Tar.NormalFile content _ | Just path <- beneathTop (Tar.entryPath entry),- any (`isSuffixOf` path) (".cabal" : haskellExtensions) -> do+ any (`isSuffixOf` path) (".cabal" : sourceExtensions) -> do createDirectoryIfMissing True (takeDirectory (staging </> path)) BL.writeFile (staging </> path) content _ -> pure ()@@ -1070,36 +1076,23 @@ | suffix `isSuffixOf` name = Just (take (length name - length suffix) name) | otherwise = Nothing stemOf f =- fromMaybe f (listToMaybe (mapMaybe (`withoutSuffix` f) haskellExtensions))+ fromMaybe f (listToMaybe (mapMaybe (`withoutSuffix` f) sourceExtensions)) extensionOf f =- fromMaybe "" (listToMaybe (filter (`isSuffixOf` f) haskellExtensions))+ fromMaybe "" (listToMaybe (filter (`isSuffixOf` f) sourceExtensions)) -- | What each of a corpus's examples has in force before its own pragmas.-packageReaderFor :: Corpus -> IO (FilePath -> IO [Extension])+packageReaderFor :: Corpus -> IO (FilePath -> IO (Maybe [Extension])) packageReaderFor corpus- | not (corpusInPackages corpus) = pure (const (pure []))+ | not (corpusInPackages corpus) = pure (const (pure Nothing)) | otherwise = do reader <- newPackageReader Nothing- pure (fmap (either (const []) id) . reader)---- | The extensions an example may be written with.-haskellExtensions :: [String]-haskellExtensions = [".hs", ".hs-boot", ".hsig"]+ pure (fmap (either (const Nothing) Just) . reader) haskellFilesIn :: FilePath -> IO [FilePath] haskellFilesIn dir = do isDir <- doesDirectoryExist dir if not isDir- then pure [dir | any (`isSuffixOf` dir) haskellExtensions]+ then pure [dir | any (`isSuffixOf` dir) sourceExtensions] else do entries <- quietly [] (listDirectory dir) concat <$> traverse (haskellFilesIn . (dir </>)) entries--------------------------------------------------------------------------------- Helpers--quietly :: a -> IO a -> IO a-quietly fallback action =- try action >>= \case- Left (_ :: SomeException) -> pure fallback- Right a -> pure a
tests/Tilia/CorpusSpec.hs view
@@ -20,31 +20,26 @@ import Test.Hspec hiding (Example, after, before, example) import Tilia.Corpus import Tilia.Corpus.Manifest-import Tilia.Cpp- ( answeredLeaves,- answeredLinearLeaves,- countLeaves,- describeCppError,- everyBranch,- formatWithCpp,- usesCpp,- )-import Tilia.Diff (diff)-import Tilia.Doc (defaultRenderOptions, printDoc)+import Tilia.Cpp (describeCppError, usesCpp)+import Tilia.Diff qualified as Diff import Tilia.Equivalence (commentDifference, syntaxDifference)-import Tilia.Palette (Palette (Plain), paletteFor)+import Tilia.Palette (Color (Meta), Palette (Plain), paint, paletteFor) import Tilia.Parser ( ParseError (..), ParsedModule (..),- ParserConfig, parseModule,- parserConfigFor, )-import Tilia.Pragma (effectiveExtensions, movesPositions)-import Tilia.Render (RenderConfig, defaultRenderConfig, renderModule)+import Tilia.Pragma (movesPositions) import Tilia.Span (spanStartColumn, spanStartLine) import Tilia.Span.Ghc (spanOfSrcSpan)-import Tilia.TestConfig (exampleRenderConfig)+import Tilia.TestConfig (formatCppExample, printExample)+import Tilia.TestUtils+ ( answeredLeaves,+ answeredLinearLeaves,+ countLeaves,+ effectiveExtensionsFor,+ parserConfigFor,+ ) spec :: Spec spec = do@@ -274,6 +269,20 @@ readUtf8 :: FilePath -> IO (Maybe Text) readUtf8 path = either (const Nothing) Just . decodeUtf8' <$> BS.readFile path +-- | A unified diff, cut short after 60 lines, past which it stops being+-- read.+diff :: Palette -> (Text, Text) -> Text -> Text -> Text+diff palette names before after+ | length body > room = T.intercalate "\n" (heading <> take room body <> [omitted])+ | otherwise = whole+ where+ whole = Diff.diff palette names before after+ (heading, body) = splitAt 2 (T.lines whole)+ room = 60+ omitted =+ paint palette Meta $+ "… and " <> T.pack (show (length body - room)) <> " more lines"+ -- | Run a check, turning a crash into a failure rather than into a dead test -- run. guarded :: Result -> IO Result@@ -292,7 +301,7 @@ -- | Everything that can be established about one example without doing any -- more input or output.-checkPure :: Palette -> FilePath -> [Extension] -> Text -> Maybe Text -> Result+checkPure :: Palette -> FilePath -> Maybe [Extension] -> Text -> Maybe Text -> Result checkPure palette path package source expected | movesPositions source = Result Declined "a pragma that moves positions, which we do not rewrite" noDigest@@ -349,20 +358,17 @@ where told formatted outcome why = Result outcome why (digestOf formatted) config = parserConfigFor package- inForce = effectiveExtensions package source+ inForce = effectiveExtensionsFor package source parse = either (const Nothing) Just . parseModule config path- render parsed =- printDoc- defaultRenderOptions- (renderModule (exampleRenderConfig package source (pure parsed)) parsed)+ render = printExample package source ---------------------------------------------------------------------------- -- Checking an example that involved the preprocessor -- | Everything that can be established about an example with conditionals in -- it.-checkCpp :: Palette -> FilePath -> [Extension] -> Text -> Maybe Text -> Result-checkCpp palette path package source expected = case formatWithCpp parser render path source of+checkCpp :: Palette -> FilePath -> Maybe [Extension] -> Text -> Maybe Text -> Result+checkCpp palette path package source expected = case formatCppExample package path source of Left why -> Result Declined (describeCppError why) noDigest Right formatted -> case (countLeaves source, countLeaves formatted) of (Left why, _) ->@@ -408,7 +414,7 @@ (Right went, Right came) | (why : _) <- alongside went came -> told formatted Broken (why <> "\n" <> against formatted)- | otherwise -> case formatWithCpp parser (renderConfigFor parser path package formatted) path formatted of+ | otherwise -> case formatCppExample package path formatted of Left why -> told formatted Broken ("the output cannot be formatted again: " <> describeCppError why) Right settled@@ -463,7 +469,6 @@ parse = parseModule parser path parser = parserConfigFor package- render = renderConfigFor parser path package source -- | Why a parse failed, and where in the file, but not which file. --@@ -494,8 +499,3 @@ -- | How many configurations one example gets compared over. configurationsToCheck :: Integer configurationsToCheck = 64---- | What to print an example's configurations with.-renderConfigFor :: ParserConfig -> FilePath -> [Extension] -> Text -> RenderConfig-renderConfigFor parser path package source =- maybe defaultRenderConfig (exampleRenderConfig package source) (everyBranch parser path source)
tests/Tilia/Cpp/PropertiesSpec.hs view
@@ -12,11 +12,12 @@ import Test.Hspec hiding (after, before) import Test.Hspec.QuickCheck (modifyMaxSuccess) import Test.QuickCheck-import Tilia.Cpp (answeredLeaves, formatWithCpp, withoutRuledOut)+import Tilia.Cpp (formatWithCpp, withoutRuledOut) import Tilia.Cpp.Macros (Macros (..)) import Tilia.Equivalence (syntaxDifference)-import Tilia.Parser (defaultParserConfig, parseModule, pmModule)+import Tilia.Parser (parseModule, pmModule) import Tilia.Render (defaultRenderConfig)+import Tilia.TestUtils (answeredLeaves, defaultParserConfig) import Tilia.Utils (tshow) spec :: Spec
tests/Tilia/CppSpec.hs view
@@ -1,8 +1,10 @@+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -- | Whether documents printed from different configurations line up. module Tilia.CppSpec (spec) where +import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Maybe (isJust, maybeToList) import Data.Text (Text)@@ -10,11 +12,20 @@ import Test.Hspec import Tilia.Cpp import Tilia.Cpp.Directives (implied)+import Tilia.Cpp.Place (regionsIn) import Tilia.Doc.Internal (Doc) import Tilia.Equivalence (syntaxDifference)-import Tilia.Parser (defaultParserConfig, parseModule, pmModule)+import Tilia.Parser (parseModule, pmModule) import Tilia.Render (defaultRenderConfig, renderModule)+import Tilia.Source (directiveOnLine) import Tilia.Span (Span)+import Tilia.TestUtils+ ( configurations,+ countLeaves,+ defaultParserConfig,+ leaves,+ linearLeaves,+ ) -- | Format a module which uses the C preprocessor, configured with nothing. formatCpp :: Text -> Either Text Text@@ -242,7 +253,7 @@ (said (linearLeaves atDeclarations), said (leaves atDeclarations)) `shouldSatisfy` uncurry (==) - describe "directives the prototype cannot read" $ do+ describe "directives it cannot read" $ do it "refuses a module whose conditionals do not balance" $ formatCpp unbalanced `shouldBe` Left "the #ifdef at line 3 is never closed" @@ -265,6 +276,13 @@ formatCpp ruledOut `shouldBe` Left "the branch at line 6 is ruled out by a conditional around it that asks the same question" + describe "telling a directive line from code" $ do+ it "reads the keyword and everything after the hash" $+ directiveOnLine " # if FOO" `shouldBe` Just ("if", "if FOO")++ it "does not take a label at the start of a line for a directive" $+ directiveOnLine " #field" `shouldBe` Nothing+ ---------------------------------------------------------------------------- -- The modules the question is asked of @@ -730,3 +748,11 @@ documentOf source = case parseModule defaultParserConfig "<cpp>" source of Left _ -> Left ("did not parse:\n" <> T.unpack source) Right parsed -> Right (renderModule defaultRenderConfig parsed)++-- | Every region a document records provenance for, with what was printed+-- there.+--+-- The outermost wins where a span appears twice, which is the one+-- 'Tilia.Comments.Attach.attachComments' gives a comment to.+regions :: Doc -> Map Span Doc+regions = Map.fromListWith (\_ outer -> outer) . regionsIn
tests/Tilia/Doc/BodySpec.hs view
@@ -58,4 +58,4 @@ `shouldBe` Hanging out :: Doc -> Text-out = printDoc defaultRenderOptions+out = printDoc
tests/Tilia/Doc/CombinatorsSpec.hs view
@@ -89,4 +89,4 @@ out (flat (includeUnless True (txt "a"))) `shouldBe` "" out :: Doc -> Text-out = printDoc defaultRenderOptions+out = printDoc
tests/Tilia/Doc/InternalSpec.hs view
@@ -115,7 +115,7 @@ underPrevious = DHoldBack SpillUnderPrevious out :: Doc -> Text-out = printDoc defaultRenderOptions+out = printDoc directive :: Doc directive = DCppDirective (mkSpan (9, 1) (9, 6)) "if X"
tests/Tilia/Doc/PropertiesSpec.hs view
@@ -87,4 +87,4 @@ squash = T.filter (not . isSpace) out :: Doc -> Text-out = printDoc defaultRenderOptions+out = printDoc
tests/Tilia/EditorSpec.hs view
@@ -7,15 +7,26 @@ -- | Formatting what an editor holds of a module. module Tilia.EditorSpec (spec) where +import Data.ByteString qualified as BS import Data.Choice (pattern Don't)+import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Text qualified as T-import Data.Text.IO qualified as T+import Data.Text.Encoding qualified as T+import Data.Version (makeVersion)+import GHC.LanguageExtensions.Type (Extension (..)) import System.Directory (createDirectoryIfMissing) import System.FilePath (takeDirectory, (</>)) import System.IO.Temp (withSystemTempDirectory) import Test.Hspec-import Tilia.Editor (editorSession, formatBuffer)+import Tilia.Editor+ ( Compiled (..),+ compiledSession,+ editorSession,+ formatBuffer,+ )+import Tilia.Fixity (Direction (..), Fixity (..), Namespace (..), OpName (..))+import Tilia.Fixity.HiFile (HiExport (..), HiFile (..), HiName (..)) import Tilia.Format (FormatError, formatErrorExitCode) import Tilia.Run (Outcome (..)) @@ -41,11 +52,21 @@ buffer ("src" </> "Ops.hs") "module Ops where\r\nx=1\r\n" `shouldReturn` CameTo "module Ops where\r\n\r\nx = 1\r\n" + it "keeps the byte order mark the buffer begins with" $+ withProject $ \buffer ->+ buffer ("src" </> "Ops.hs") "\xFEFFmodule Ops where\nx=1\n"+ `shouldReturn` CameTo "\xFEFFmodule Ops where\n\nx = 1\n"+ it "takes fixities from the modules the buffer imports" $ withProject $ \buffer -> buffer ("src" </> "Uses.hs") "module Uses where\nimport Ops\nx=a <+> b\n" `shouldReturn` CameTo "module Uses where\n\nimport Ops\n\nx = a <+> b\n" + it "takes fixities from a module that begins with a byte order mark" $+ withProject $ \buffer ->+ buffer ("src" </> "Uses.hs") "module Uses where\nimport Marked\nx=a <-> b\n"+ `shouldReturn` CameTo "module Uses where\n\nimport Marked\n\nx = a <-> b\n"+ it "declines an operator from a module it cannot read" $ withProject $ \buffer -> buffer ("src" </> "Uses.hs") "module Uses where\nimport Nowhere\nx=a <?> b\n"@@ -83,6 +104,26 @@ (Don't #debugFixity) `shouldReturn` Just 2 + describe "given what compiling the project establishes" $ do+ it "takes fixities from the interfaces it is given" $+ withCompiled $ \buffer ->+ buffer ("src" </> "Uses.hs") "module Uses where\nimport Ops\nx=a <+> b\n"+ `shouldReturn` CameTo "module Uses where\n\nimport Ops\n\nx = a <+> b\n"++ it "declines an operator from a module it is given no interface of" $+ withCompiled $ \buffer ->+ buffer ("src" </> "Uses.hs") "module Uses where\nimport Nowhere\nx=a <?> b\n"+ `shouldReturn` CameDeclined 15++ it "parses with the extensions it is given rather than the package's" $+ withCompiled $ \buffer ->+ buffer ("src" </> "Uses.hs") "module Uses where\nf = \\case\n _ -> 1\n"+ `shouldReturn` CameTo "module Uses where\n\nf = \\case\n _ -> 1\n"++ it "leaves out the branches the versions it is given rule out" $+ withCompiled $ \buffer ->+ buffer ("src" </> "Uses.hs") versioned `shouldReturn` CameOut+ -- | A formatted module with a conditional around two alternatives, which do -- not parse one after the other. alternatives :: Text@@ -127,6 +168,26 @@ "#endif" ] +-- | A module with a branch for a version of the package other than the one+-- built, which uses an operator from a module only that version has.+versioned :: Text+versioned =+ T.unlines+ [ "{-# LANGUAGE CPP #-}",+ "",+ "module Uses where",+ "",+ "#if MIN_VERSION_fake(1,0,0)",+ "import Nowhere",+ "",+ "x = a <?> b",+ "#else",+ "import Ops",+ "",+ "x = a <+> b",+ "#endif"+ ]+ -- | What formatting a buffer came to, with errors told apart by the status -- they exit with. data Came@@ -152,24 +213,78 @@ failure :: FormatError -> Int failure = formatErrorExitCode --- | A project with a module declaring @infixl 6 <+>@, a @.tiliaignore@+-- | A project with a module declaring @infixl 6 <+>@, one beginning with a+-- byte order mark declaring @infixr 0 <->@, a @.tiliaignore@ -- excluding @src/Generated.hs@, and a plan for it trusted as up to date, -- handed to the test as a way to format a buffer as one of its files. withProject :: ((FilePath -> Text -> IO Came) -> Expectation) -> Expectation withProject act =+ inProject $ \dir project -> do+ let planFile = dir </> "elsewhere" </> "plan.json"+ write planFile $+ "{\"compiler-id\":\"ghc-0.0\",\"install-plan\":[\+ \{\"pkg-name\":\"fake\",\"pkg-version\":\"0.1.0.0\",\+ \\"pkg-src\":{\"type\":\"local\",\"path\":\"./.\"}}]}"+ act $ \file text -> do+ let path = project </> file+ editorSession+ path+ (Just planFile)+ (Don't #useCache)+ (Don't #download)+ (Don't #checkAst)+ (Don't #checkIdempotence)+ (Don't #debugFixity)+ >>= \case+ Left e -> pure (CameFailed (failure e))+ Right session -> came <$> formatBuffer session path text++-- | The project of 'withProject', with what compiling it would establish+-- given instead of a plan: @LambdaCase@ in force, version 0.1.0.0 of the+-- package, and the interfaces of @Ops@ and of an empty @Prelude@.+withCompiled :: ((FilePath -> Text -> IO Came) -> Expectation) -> Expectation+withCompiled act =+ inProject $ \_ project ->+ act $ \file text -> do+ let path = project </> file+ compiledSession path compiled (Don't #checkAst) (Don't #checkIdempotence)+ >>= \case+ Left e -> pure (CameFailed (failure e))+ Right session -> came <$> formatBuffer session path text+ where+ compiled =+ Compiled+ { compiledExtensions = [ImplicitPrelude, LambdaCase],+ compiledWith = makeVersion [9, 14, 1],+ compiledPackages = Map.singleton "fake" (makeVersion [0, 1, 0, 0]),+ compiledInterface = pure . (`Map.lookup` interfaces)+ }+ interfaces =+ Map.fromList+ [ ( "Ops",+ HiFile+ { hiModule = "Ops",+ hiExports = [Avail (HiName "Ops" InTerms (OpName "<+>"))],+ hiFixities = [(InTerms, OpName "<+>", Fixity LeftAssoc 6)]+ }+ ),+ ("Prelude", HiFile{hiModule = "Prelude", hiExports = [], hiFixities = []})+ ]++-- | A project with a module declaring @infixl 6 <+>@, one beginning with a+-- byte order mark declaring @infixr 0 <->@, and a @.tiliaignore@ excluding+-- @src/Generated.hs@, given to the test with the directory it is in.+inProject :: (FilePath -> FilePath -> Expectation) -> Expectation+inProject act = withSystemTempDirectory "tilia-editor" $ \dir -> do let project = dir </> "project"- planFile = dir </> "elsewhere" </> "plan.json"- write path text = do- createDirectoryIfMissing True (takeDirectory path)- T.writeFile path text write (project </> "fake.cabal") $ T.unlines [ "cabal-version: 2.4", "name: fake", "version: 0.1.0.0", "library",- " exposed-modules: Ops",+ " exposed-modules: Ops, Marked", " hs-source-dirs: src", " default-language: Haskell2010" ]@@ -180,21 +295,18 @@ "(<+>) :: a -> a -> a", "(<+>) = const" ]+ write (project </> "src" </> "Marked.hs") $+ T.unlines+ [ "\xFEFFmodule Marked ((<->)) where",+ "infixr 0 <->",+ "(<->) :: a -> a -> a",+ "(<->) = const"+ ] write (project </> ".tiliaignore") "src/Generated.hs\n"- write planFile $- "{\"compiler-id\":\"ghc-0.0\",\"install-plan\":[\- \{\"pkg-name\":\"fake\",\"pkg-version\":\"0.1.0.0\",\- \\"pkg-src\":{\"type\":\"local\",\"path\":\"./.\"}}]}"- act $ \file text -> do- let path = project </> file- editorSession- path- (Just planFile)- (Don't #useCache)- (Don't #download)- (Don't #checkAst)- (Don't #checkIdempotence)- (Don't #debugFixity)- >>= \case- Left e -> pure (CameFailed (failure e))- Right session -> came <$> formatBuffer session path text+ act dir project++-- | Write a file, with the directories it is in.+write :: FilePath -> Text -> IO ()+write path text = do+ createDirectoryIfMissing True (takeDirectory path)+ BS.writeFile path (T.encodeUtf8 text)
tests/Tilia/EquivalenceSpec.hs view
@@ -10,13 +10,8 @@ import GHC.Hs.Extension (GhcPs) import Test.Hspec import Tilia.Equivalence-import Tilia.Parser- ( ParsedModule,- defaultParserConfig,- describeParseError,- parseModule,- pmModule,- )+import Tilia.Parser (pmModule)+import Tilia.TestUtils (parsed) spec :: Spec spec = do@@ -168,8 +163,3 @@ treeOf :: Text -> HsModule GhcPs treeOf = pmModule . parsed--parsed :: Text -> ParsedModule-parsed source = case parseModule defaultParserConfig "Test.hs" source of- Left problem -> error (T.unpack (describeParseError problem))- Right m -> m
tests/Tilia/Fixity/CacheSpec.hs view
@@ -11,13 +11,13 @@ import Data.Set qualified as Set import Data.Text (Text) import System.Directory (getModificationTime, listDirectory, setModificationTime)-import System.Environment (setEnv, unsetEnv) import System.FilePath ((</>)) import System.IO.Temp (withSystemTempDirectory) import Test.Hspec import Tilia.Fixity import Tilia.Fixity.Cache import Tilia.Fixity.PackageDb (Installed (..), InstalledPackage (..))+import Tilia.TestUtils (withEnvironment) spec :: Spec spec = do@@ -26,9 +26,9 @@ describe "a cache not to be used" $ it "remembers nothing, and nothing is made on disk for it" $ withIsolatedDirectory $ \dir -> do- setEnv "XDG_CACHE_HOME" dir- cache <- openCache (Don't #useCache) (PlanToken "plan")- unsetEnv "XDG_CACHE_HOME"+ cache <-+ withEnvironment [("XDG_CACHE_HOME", dir)] $+ openCache (Don't #useCache) (PlanToken "plan") storeModules cache "thing-1.0" ["A"] cachedModules cache "thing-1.0" `shouldReturn` Nothing listDirectory dir `shouldReturn` []@@ -464,8 +464,5 @@ -- | Open a cache in a given directory, under a given token. open :: FilePath -> PlanToken -> IO Cache-open dir token = do- setEnv "XDG_CACHE_HOME" dir- cache <- openCache (Do #useCache) token- unsetEnv "XDG_CACHE_HOME"- pure cache+open dir token =+ withEnvironment [("XDG_CACHE_HOME", dir)] (openCache (Do #useCache) token)
tests/Tilia/Fixity/DebugSpec.hs view
@@ -23,7 +23,8 @@ ) import Tilia.Fixity.Debug (fixityNotes, renderFixityNotes) import Tilia.Palette (Palette (Plain))-import Tilia.Parser (defaultParserConfig, describeParseError, parseModule, pmModule)+import Tilia.Parser (pmModule)+import Tilia.TestUtils (parsed) import Tilia.Utils (lineWidth, visibleLength) spec :: Spec@@ -174,16 +175,14 @@ IO [Text] notesThrough chains world source = renderFixityNotes Plain . Map.singleton "M.hs"- <$> fixityNotes (Is #implicitPrelude) (pure . answerOf) scope (pure parsed)+ <$> fixityNotes (Is #implicitPrelude) (pure . answerOf) scope (pure module') where scope = resolveScope (Is #implicitPrelude) answerOf (pure hsModule) answerOf m = case exportsOf m of Just fixities -> mempty{establishedFixities = fixities} Nothing -> mempty{establishedUntold = Set.singleton (fromMaybe [] (lookup m chains))}- hsModule = pmModule parsed- parsed = case parseModule defaultParserConfig "M.hs" ("module M where\n" <> source) of- Left problem -> error (T.unpack (describeParseError problem))- Right m -> m+ hsModule = pmModule module'+ module' = parsed ("module M where\n" <> source) exportsOf m = do declared <- lookup m world inBothNamespaces . Map.fromList . fmap (\(op, fixity) -> (OpName op, fixity))
tests/Tilia/Fixity/DependenciesSpec.hs view
@@ -17,7 +17,7 @@ import Control.Monad (filterM) import Data.ByteString qualified as BS-import Data.Choice (pattern Do, pattern Don't, pattern Is)+import Data.Choice (pattern Don't, pattern Is) import Data.Foldable (for_) import Data.List (isSuffixOf, sort) import Data.Map.Strict qualified as Map@@ -31,6 +31,7 @@ import System.FilePath ((</>)) import Test.Hspec import Tilia.BootFixities (bootFixities)+import Tilia.Cabal.Plan import Tilia.Cpp (blankCpp) import Tilia.Fixity import Tilia.Fixity.Cache (PlanToken (..), openCache)@@ -39,6 +40,7 @@ import Tilia.Fixity.Plan import Tilia.Gathered (gathered) import Tilia.Parser+import Tilia.TestUtils (defaultParserConfig, routedResolver) import Tilia.WithProjectPlan ( Dependency (..), compilerShipped,@@ -53,8 +55,8 @@ withPlan :: BuildPlan -> Spec withPlan plan = do installed <- runIO readInstalledPackages- fromSource <- runIO (fixitiesThrough <$> newResolverVia (Do #useCache) [FromSource] plan)- fromInterface <- runIO (fixitiesThrough <$> newResolverVia (Do #useCache) [FromInterface] plan)+ fromSource <- runIO (fixitiesThrough <$> routedResolver [FromSource] plan)+ fromInterface <- runIO (fixitiesThrough <$> routedResolver [FromInterface] plan) resolver <- runIO (newResolver plan) let resolve = fixitiesThrough resolver own <- runIO ownModules
tests/Tilia/Fixity/HiFileSpec.hs view
@@ -11,17 +11,15 @@ import Control.Applicative ((<|>)) import Control.Exception (evaluate)-import Data.Bits (xor) import Data.ByteString qualified as BS-import Data.Char (ord) import Data.Foldable (for_) import Data.Map.Strict qualified as Map import Data.Set (Set) import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T-import Data.Word (Word64) import Test.Hspec+import Tilia.Cabal.Plan (BuildPlan) import Tilia.Fixity (Fixities, Namespace (..), OpName (..)) import Tilia.Fixity.HiFile ( HiExport (..),@@ -32,8 +30,8 @@ ) import Tilia.Fixity.Interface (Interface (..), fromHiFile, parseInterface) import Tilia.Fixity.PackageDb (readInstalledPackages)-import Tilia.Fixity.Plan (BuildPlan) import Tilia.Process (readProgramOutput)+import Tilia.TestUtils (fingerprint) import Tilia.Utils (inParallel) import Tilia.WithProjectPlan ( Dependency (..),@@ -102,12 +100,10 @@ -- | Whether a module is in the fixed eighth of the rest, which are checked -- against @ghc@ all the same. ----- Chosen by an FNV-1a hash of the name, so that it is the same modules on+-- Chosen by a 'fingerprint' of the name, so that it is the same modules on -- every run and machine. sampled :: Text -> Bool-sampled m = T.foldl' step 14695981039346656037 m `mod` 8 == (0 :: Word64)- where- step h c = (h `xor` fromIntegral (ord c)) * 1099511628211+sampled m = fingerprint (T.unpack m) `mod` 8 == 0 -- | Where a direct reading and the text disagree about what fixities depend -- on, beyond what the text cannot tell.
tests/Tilia/Fixity/PlanSpec.hs view
@@ -48,19 +48,22 @@ import System.Info qualified import System.Timeout (timeout) import Test.Hspec+import Tilia.Cabal.Plan hiding (checkReadiness, prepareWith)+import Tilia.Cabal.Plan qualified as Plan import Tilia.Cpp.Macros (Macros (..)) import Tilia.Fixity import Tilia.Fixity.Cache (PlanToken (..), openCache, storePackageCache)+import Tilia.Fixity.HiFile (HiExport (..), HiFile (..), HiName (..)) import Tilia.Fixity.PackageDb ( Installed (..), InstalledPackage (..), compilerIdentity, readInstalledPackages, )-import Tilia.Fixity.Plan hiding (checkReadiness, prepareWith)-import Tilia.Fixity.Plan qualified as Plan+import Tilia.Fixity.Plan import Tilia.Parser import Tilia.Process (readProgramOutput)+import Tilia.TestUtils (parsed, routedResolver, unreadOnly, withEnvironment) import Tilia.Utils (inParallel) import Tilia.WithProjectPlan (withProjectPlan) @@ -73,6 +76,7 @@ hscModules generatedModuleSpec askedAtOnce+ givenInterfaces readBefore gitDependencies repositories@@ -524,11 +528,10 @@ -- | What a module written for @hsc2hs@ amounts to, by either route to one. ----- Both fixtures below are what @hsc2hs@ takes and no compiler does, and--- both write a fixity that the answer is expected to ignore. That is the--- point: a module nothing can read is answered out of--- 'Tilia.Fixity.ByHand.hscFixities' or not at all, and were either answer--- arrived at by reading the file there would be no answer to give.+-- The fixture is what @hsc2hs@ takes and no compiler does, and it writes a+-- fixity that the answer is expected to ignore: a module nothing can read+-- declares nothing, and an answer arrived at by reading the file would+-- hold the fixity. hscModules :: Spec hscModules = describe "a module written for hsc2hs" $ do it "declares nothing, where it is one of the project's own" $@@ -536,16 +539,9 @@ \rs -> do askModule rs "Cursed" `shouldReturn` mempty - it "declares what the table says, where it comes out of a tarball"- $ withFakeArchive- [ ("Cursed.hsc", cursed),- ("System/Posix/Signals.hsc", signals)- ]- $ \rs -> do- fixitiesOf rs "Cursed" `shouldReturn` Just Map.empty- signalled <- fixitiesOf rs "System.Posix.Signals"- fmap (Set.map snd . Map.keysSet) signalled- `shouldBe` Just (Set.fromList [OpName "addSignal", OpName "deleteSignal"])+ it "declares nothing, where it comes out of a tarball" $+ withFakeArchive [("Cursed.hsc", cursed)] $+ \rs -> fixitiesOf rs "Cursed" `shouldReturn` Just Map.empty -- | Where a package fetched from a repository is looked for. repositories :: Spec@@ -670,7 +666,7 @@ readGivenPlan project planFile >>= \case Left why -> expectationFailure (T.unpack why) Right plan -> do- resolver <- newResolverVia (Do #useCache) [FromInterface] plan+ resolver <- routedResolver [FromInterface] plan fixities <- fixitiesOf resolver "Ops" (Map.lookup (InTerms, OpName "<+>") =<< fixities) `shouldBe` Just (Fixity LeftAssoc 6)@@ -696,7 +692,7 @@ ( readGivenPlan project planFile >>= \case Left why -> expectationFailure (T.unpack why) Right plan -> do- resolver <- newResolverVia (Do #useCache) [FromInterface] plan+ resolver <- routedResolver [FromInterface] plan traverse_ (fixitiesOf resolver) ["Ops", "Nowhere.At.All"] ) `shouldReturn` []@@ -975,7 +971,7 @@ it "answers for every module of a ring that hands itself on" $ withFakePlan [("src/" <> T.unpack (ringModule i) <> ".hs", ringSource i) | i <- [0 .. ringSize - 1]] $ \plan -> forM_ [1 :: Int .. 50] $ \_ -> do- resolver <- newResolverVia (Do #useCache) [FromInterface] plan+ resolver <- routedResolver [FromInterface] plan answers <- timeout 30000000 $ inParallel (fixitiesOf resolver . ringModule) [0 .. ringSize - 1]@@ -986,6 +982,46 @@ (Map.lookup (InTerms, ringOperator i) =<< fixities) `shouldBe` Just (Fixity RightAssoc (i `mod` 10)) +-- | A resolver over interfaces handed to it, as an editor that has compiled+-- the project holds them.+givenInterfaces :: Spec+givenInterfaces = describe "a resolver over the interfaces it is given" $ do+ it "reads a module's own fixities" $ do+ resolver <- newResolverFrom given+ fixitiesOf resolver "Ops" `shouldReturn` Just plus++ it "reads a fixity a module passes on where it is declared" $ do+ resolver <- newResolverFrom given+ fixitiesOf resolver "Facade" `shouldReturn` Just plus++ it "says nothing about a module it is not given" $ do+ resolver <- newResolverFrom given+ fixitiesOf resolver "Nowhere" `shouldReturn` Nothing++ it "says nothing about a module given an interface of another" $ do+ resolver <- newResolverFrom (\_ -> given "Ops")+ fixitiesOf resolver "Facade" `shouldReturn` Nothing+ where+ plus = Map.singleton (InTerms, OpName "<+>") (Fixity RightAssoc 5)+ given m = pure (Map.lookup m interfaces)+ interfaces =+ Map.fromList+ [ ( "Ops",+ HiFile+ { hiModule = "Ops",+ hiExports = [Avail (HiName "Ops" InTerms (OpName "<+>"))],+ hiFixities = [(InTerms, OpName "<+>", Fixity RightAssoc 5)]+ }+ ),+ ( "Facade",+ HiFile+ { hiModule = "Facade",+ hiExports = [Avail (HiName "Ops" InTerms (OpName "<+>"))],+ hiFixities = []+ }+ )+ ]+ -- | A module of the project's own, which an earlier run has read. readBefore :: Spec readBefore = describe "a module of the project's own, read by an earlier run" $ do@@ -1070,18 +1106,6 @@ "interrupt = #const SIGINT" ] --- | What @unix@ writes, in miniature: a fixity declaration for a name used--- in backticks, in a file no reading of ours reaches.-signals :: Text-signals =- T.unlines- [ "#include <signal.h>",- "module System.Posix.Signals (addSignal, deleteSignal) where",- "infixr `addSignal`, `deleteSignal`",- "addSignal :: Int -> Int -> Int",- "addSignal s m = m + #const SIGINT"- ]- -- | What the chase makes of one module's @<+>@, against a world of modules -- that disagree about it, where it settles it. chased :: Text -> IO (Maybe Fixity)@@ -1097,11 +1121,8 @@ -- | What the chase makes of one module against that world. chasedModule :: Text -> IO Established chasedModule source =- withReexports reach "M" (summarize (Is #implicitPrelude) (pmModule parsed))+ withReexports reach "M" (summarize (Is #implicitPrelude) (pmModule (parsed source))) where- parsed = case parseModule defaultParserConfig "M.hs" source of- Left _ -> error "the test input did not parse"- Right m -> m reach m = pure $ case m of "Control.Arrow" -> bringing (Fixity RightAssoc 5)@@ -1409,7 +1430,7 @@ ("src/Inner.hs", "module Inner (T (..)) where\ninfixr 5 :|\ndata T = A | Int :| Int\n") ] $ \rs -> do- let m = parse "module M where\nimport Facade (T (..))\n"+ let m = parsed "module M where\nimport Facade (T (..))\n" scope <- scopeFor rs (Is #implicitPrelude) (pure (pmModule m)) lookupFixity scope InTerms Nothing (OpName ":|") `shouldBe` Resolved (Fixity RightAssoc 5) (DeclaredIn "Facade")@@ -1511,7 +1532,7 @@ it "settles an operator no unread module in scope could have declared" $ withFakeProject [("src/Opaque.hs", opaqueSource)] $ \rs -> do- let m = parse "module M where\nimport Opaque\n"+ let m = parsed "module M where\nimport Opaque\n" scope <- scopeFor rs (Is #implicitPrelude) (pure (pmModule m)) lookupFixity scope InTerms Nothing (OpName "<??>") `shouldBe` Resolved defaultFixity ReportDefault@@ -1519,7 +1540,7 @@ it "leaves one alone that the unread module's list does name" $ withFakeProject [("src/Opaque.hs", opaqueSource)] $ \rs -> do- let m = parse "module M where\nimport Opaque\n"+ let m = parsed "module M where\nimport Opaque\n" scope <- scopeFor rs (Is #implicitPrelude) (pure (pmModule m)) lookupFixity scope InTerms Nothing (OpName "<+>") `shouldBe` Unresolved (ModuleChain ("Opaque" :| ["No.Such.Module"]) :| [])@@ -1647,7 +1668,7 @@ ("src/B.hs", "module B (clampTo) where\nclampTo :: Int -> Int -> Int\nclampTo = min\n") ] $ \rs -> do- let m = parse "module C (w) where\nimport A\nimport B\nw :: Int\nw = 1 `clampTo` 2\n"+ let m = parsed "module C (w) where\nimport A\nimport B\nw :: Int\nw = 1 `clampTo` 2\n" scope <- scopeFor rs (Is #implicitPrelude) (pure (pmModule m)) lookupFixity scope InTerms Nothing (OpName "clampTo") `shouldBe` Resolved defaultFixity ReportDefault@@ -1658,7 +1679,7 @@ ("src/B.hs", "{-# LANGUAGE TypeOperators #-}\nmodule B ((:+:)) where\ndata a :+: b = a :+: b\n") ] $ \rs -> do- let m = parse "module C (w) where\nimport A\nimport B\nw = 1 :+: 2\n"+ let m = parsed "module C (w) where\nimport A\nimport B\nw = 1 :+: 2\n" scope <- scopeFor rs (Is #implicitPrelude) (pure (pmModule m)) lookupFixity scope InTerms Nothing (OpName ":+:") `shouldBe` Unresolved (unreadOnly "A")@@ -1670,7 +1691,7 @@ ("src/Tree.hs", "module Tree (Tree (..)) where\ndata Tree = Leaf | Node Tree Tree\n") ] $ \rs -> do- let m = parse "module C (w) where\nimport Core\nimport Tree (Tree (..))\nw = Leaf `Node` Leaf `Node` Leaf\n"+ let m = parsed "module C (w) where\nimport Core\nimport Tree (Tree (..))\nw = Leaf `Node` Leaf `Node` Leaf\n" scope <- scopeFor rs (Is #implicitPrelude) (pure (pmModule m)) lookupFixity scope InTerms Nothing (OpName "Node") `shouldBe` Resolved defaultFixity ReportDefault@@ -1682,7 +1703,7 @@ ("src/Tree.hs", "module Tree (Tree (..)) where\ndata Tree = Leaf | Node Tree Tree\n") ] $ \rs -> do- let m = parse "module C (w) where\nimport Core\nimport Tree (Tree)\nw = Leaf `Node` Leaf `Node` Leaf\n"+ let m = parsed "module C (w) where\nimport Core\nimport Tree (Tree)\nw = Leaf `Node` Leaf `Node` Leaf\n" scope <- scopeFor rs (Is #implicitPrelude) (pure (pmModule m)) lookupFixity scope InTerms Nothing (OpName "Node") `shouldBe` Unresolved (ModuleChain ("Core" :| ["A"]) :| [])@@ -1693,7 +1714,7 @@ ("src/Other.hs", "module Other ((<+>)) where\ninfixl 6 <+>\n(<+>) :: Int -> Int -> Int\na <+> b = a - b\n") ] $ \rs -> do- let m = parse "module C (w) where\nimport Ops (T (..))\nimport Other\nw = 1 <+> 2\n"+ let m = parsed "module C (w) where\nimport Ops (T (..))\nimport Other\nw = 1 <+> 2\n" scope <- scopeFor rs (Is #implicitPrelude) (pure (pmModule m)) lookupFixity scope InTerms Nothing (OpName "<+>") `shouldBe` Resolved (Fixity LeftAssoc 6) (DeclaredIn "Other")@@ -1957,11 +1978,8 @@ (Scope -> Expectation) -> Expectation endToEnd resolver source assertion =- case parseModule defaultParserConfig "test.hs" source of- Left _ -> expectationFailure "the test input did not parse"- Right pm -> do- scope <- scopeFor resolver (Is #implicitPrelude) (pure (pmModule pm))- assertion scope+ scopeFor resolver (Is #implicitPrelude) (pure (pmModule (parsed source)))+ >>= assertion -- | Check one expected fixity, returning a description of any mismatch. check ::@@ -2022,11 +2040,6 @@ "f = 1" ] -parse :: Text -> ParsedModule-parse source = case parseModule defaultParserConfig "M.hs" source of- Left _ -> error "the test input did not parse"- Right pm -> pm- -- | A project of made-up modules, with a build plan written by hand. -- -- The plan names one local package and nothing else, which is enough for a@@ -2126,20 +2139,3 @@ entry (inside, text) = case Tar.toTarPath False inside of Left why -> error why Right tarPath -> pure (Tar.fileEntry tarPath (BL.fromStrict (T.encodeUtf8 text)))---- | Run something with the given variables set, and the environment as it--- was afterwards however it turns out.-withEnvironment :: [(String, String)] -> IO a -> IO a-withEnvironment vars act = bracket set restore (const act)- where- set = traverse remember vars- remember (key, value) = do- was <- lookupEnv key- setEnv key value- pure (key, was)- restore = traverse_ (\(key, was) -> maybe (unsetEnv key) (setEnv key) was)---- | The one import blamed for an operator, unread on its own account and so--- with nothing below it.-unreadOnly :: Text -> NonEmpty ModuleChain-unreadOnly m = ModuleChain (m :| []) :| []
tests/Tilia/FixitySpec.hs view
@@ -20,6 +20,7 @@ import Tilia.Fixity import Tilia.Parser import Tilia.Span (spanStartColumn, spanStartLine)+import Tilia.TestUtils (parsed, unreadOnly) spec :: Spec spec = do@@ -827,11 +828,6 @@ where whichever = Just . inBothNamespaces . Map.fromList -parsed :: Text -> ParsedModule-parsed src = case parseModule defaultParserConfig "test.hs" src of- Left _ -> error "the test input did not parse"- Right pm -> pm- -- | The fixities a module declares, by name alone. -- -- One entry per operator: a fixity that governs both namespaces is one@@ -886,12 +882,6 @@ -- | What is known in a world made of 'exportsOf' alone. knowingExports :: Text -> Established knowingExports = maybe unreadable (\fixities -> mempty{establishedFixities = fixities}) . exportsOf---- | The one import blamed for an operator, unread on its own account and so--- with nothing below it. What every answer here was before a chain could be--- reported at all.-unreadOnly :: Text -> NonEmpty ModuleChain-unreadOnly m = ModuleChain (m :| []) :| [] -- | A module that uses an operator nothing in scope declares, alongside an -- import that could not be read.
tests/Tilia/PragmaSpec.hs view
@@ -34,30 +34,35 @@ ] (\source -> movesPositions source `shouldBe` False) describe "the extensions in force" $ do- it "starts from what is on by default" $- effectiveExtensions [] "module M where\n" `shouldBe` [ImplicitPrelude] it "keeps what the package puts in force" $ effectiveExtensions [ImplicitPrelude, GADTs] "module M where\n" `shouldBe` [ImplicitPrelude, GADTs]- it "leaves the Prelude off where the package's own set does" $- effectiveExtensions [GADTs] "module M where\n" `shouldBe` [GADTs] it "reads one extension" $ effectiveExtensions [] "{-# LANGUAGE BangPatterns #-}\nmodule M where\n"- `shouldBe` [ImplicitPrelude, BangPatterns]+ `shouldBe` [BangPatterns] it "reads several from one pragma" $- effectiveExtensions [] "{-# LANGUAGE GADTs, RankNTypes #-}\nmodule M where\n"- `shouldBe` [ImplicitPrelude, GADTs, RankNTypes]+ effectiveExtensions+ []+ "{-# LANGUAGE GADTs, RankNTypes #-}\nmodule M where\n"+ `shouldBe` [GADTs, RankNTypes] it "reads several pragmas" $- effectiveExtensions [] "{-# LANGUAGE GADTs #-}\n{-# LANGUAGE MagicHash #-}\n"- `shouldBe` [ImplicitPrelude, GADTs, MagicHash]+ effectiveExtensions+ []+ "{-# LANGUAGE GADTs #-}\n{-# LANGUAGE MagicHash #-}\n"+ `shouldBe` [GADTs, MagicHash] it "ignores other pragmas" $- effectiveExtensions [] "{-# OPTIONS_GHC -Wall #-}\n{-# LANGUAGE GADTs #-}\n"- `shouldBe` [ImplicitPrelude, GADTs]+ effectiveExtensions+ []+ "{-# OPTIONS_GHC -Wall #-}\n{-# LANGUAGE GADTs #-}\n"+ `shouldBe` [GADTs] it "ignores an unknown extension rather than failing" $ effectiveExtensions [] "{-# LANGUAGE GADTs, NotARealExtension #-}\n"- `shouldBe` [ImplicitPrelude, GADTs]+ `shouldBe` [GADTs] it "treats a No-prefix as turning one off" $- effectiveExtensions [] "{-# LANGUAGE NoImplicitPrelude #-}\n" `shouldBe` []+ effectiveExtensions+ [ImplicitPrelude]+ "{-# LANGUAGE NoImplicitPrelude #-}\n"+ `shouldBe` [] it "lets a module refuse what its package put in force" $ effectiveExtensions [ImplicitPrelude, GADTs] "{-# LANGUAGE NoGADTs #-}\n" `shouldBe` [ImplicitPrelude]@@ -65,8 +70,10 @@ effectiveExtensions [GADTs] "{-# LANGUAGE ImplicitPrelude #-}\n" `shouldBe` [GADTs, ImplicitPrelude] it "does not repeat an extension named twice" $- effectiveExtensions [] "{-# LANGUAGE GADTs #-}\n{-# LANGUAGE GADTs #-}\n"- `shouldBe` [ImplicitPrelude, GADTs]+ effectiveExtensions+ []+ "{-# LANGUAGE GADTs #-}\n{-# LANGUAGE GADTs #-}\n"+ `shouldBe` [GADTs] describe "lookupExtension" $ do it "knows an extension by the name one writes" $
tests/Tilia/RenderSpec.hs view
@@ -3,369 +3,41 @@ -- | Formatting whole modules. module Tilia.RenderSpec (spec) where -import Data.Map.Strict qualified as Map-import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T-import GHC.LanguageExtensions.Type (Extension (..)) import Test.Hspec-import Tilia.Doc (defaultRenderOptions, printDoc)-import Tilia.Fixity- ( Direction (..),- Fixity (..),- OpName (..),- Provenance (..),- Reach (..),- Scope (..),- )-import Tilia.Parser (defaultParserConfig, parseModule)+import Tilia.Doc (printDoc) import Tilia.Render+import Tilia.TestUtils (parsed) spec :: Spec-spec = do- describe "the module header" $ do- it "puts the pragmas above the module and sorts them" $- format- [ "{-# LANGUAGE OverloadedStrings, GADTs #-}",- "module M where"- ]- `shouldBe` [ "{-# LANGUAGE GADTs #-}",- "{-# LANGUAGE OverloadedStrings #-}",- "",- "module M where"- ]-- it "puts an extension pack before what it enables" $- format- [ "{-# LANGUAGE ApplicativeDo #-}",- "{-# LANGUAGE GHC2021 #-}",- "module M where"- ]- `shouldBe` [ "{-# LANGUAGE GHC2021 #-}",- "{-# LANGUAGE ApplicativeDo #-}",- "",- "module M where"- ]-- it "puts qualified first throughout when the extension is off" $- format- [ "module M where",- "import Data.Map qualified as M",- "import qualified Data.Set as S"- ]- `shouldBe` [ "module M where",- "",- "import qualified Data.Map as M",- "import qualified Data.Set as S"- ]-- it "puts qualified last throughout when the extension is on" $- formatUnder- [ImportQualifiedPost]- [ "module M where",- "import Data.Map qualified as M",- "import qualified Data.Set as S"- ]- `shouldBe` [ "module M where",- "",- "import Data.Map qualified as M",- "import Data.Set qualified as S"- ]-- it "parses a module that takes back an extension the edition puts in force" $- format- [ "{-# LANGUAGE NoStarIsType #-}",- "{-# LANGUAGE TypeOperators #-}",- "module M (type (*)) where",- "import GHC.TypeLits (type (*))"- ]- `shouldBe` [ "{-# LANGUAGE TypeOperators #-}",- "{-# LANGUAGE NoStarIsType #-}",- "",- "module M (type (*)) where",- "",- "import GHC.TypeLits (type (*))"- ]-- describe "comments" $ do- it "keeps one written between declarations" $- format- [ "module M where",- "",- "-- a note",- "f = 1"- ]- `shouldBe` [ "module M where",- "",- "-- a note",- "f = 1"- ]-- it "keeps one written inside a binding" $- format- [ "module M where",- "",- "f = g",- " where",- " -- about g",- " g = 1"- ]- `shouldBe` [ "module M where",- "",- "f = g",- " where",- " -- about g",- " g = 1"- ]-- it "keeps a self-delimiting one on the line it was written on" $- format ["module M where", "", "f =", " {- here -} 1"]- `shouldBe` ["module M where", "", "f =", " {- here -} 1"]-- it "will not put a construct holding one on a single line" $- format- [ "module M where",- "",- "f =",- " ( -- here",- " 1",- " )"- ]- `shouldBe` [ "module M where",- "",- "f =",- " ( -- here",- " 1",- " )"- ]-- describe "operator chains" $ do- -- With the fixities known, the chain is regrouped so that what binds- -- tightly stays together and the break falls where the reader expects.- it "breaks a chain at its loosest operator" $- formatWith (Just arithmetic) spreadChain- `shouldBe` [ "module M where",- "",- "f =",- " a * b",- " + c * d"- ]-- -- Without them nothing is asserted about how the chain associates, so- -- nothing is rearranged and every operator is treated alike.- it "leaves a chain alone when the fixities are unknown" $- formatWith Nothing spreadChain- `shouldBe` [ "module M where",- "",- "f =",- " a",- " * b",- " + c",- " * d"- ]-- it "takes an operator's fixity from the local binding that captures it" $- formatWith- (Just arithmetic)- [ "module M where",- "",- "f =",- " a * b",- " + c * d",- " where",- " infixl 8 +",- " x + y = x"- ]- `shouldBe` [ "module M where",- "",- "f =",- " a",- " * b",- " + c",- " * d",- " where",- " infixl 8 +",- " x + y = x"- ]-- it "lets a separator hand a block to what precedes it" $- formatWith- (Just arithmetic)- ["module M where", "", "f = g $ do", " h", " i"]- `shouldBe` [ "module M where",- "",- "f = g $ do",- " h",- " i"- ]-- describe "where a comment lands" $ do- -- A comment the author wrote after code ends a line here too, and the- -- printer has not finished with that line: the comma of a record field,- -- the arrow of an alternative and the closing bracket of a list all- -- still have to be emitted, and all of them belong before it.- it "keeps a comment at the end of the line it was written on" $- format- [ "module M where",- "",- "f = case x of",- " a -> -- why",- " b"- ]- `shouldBe` [ "module M where",- "",- "f = case x of",- " a -> -- why",- " b"- ]-- it "leaves a comment written on its own line on one" $- format- [ "module M where",- "",- "f = case x of",- " a ->",- " -- why",- " b"- ]- `shouldBe` [ "module M where",- "",- "f = case x of",- " a ->",- " -- why",- " b"- ]-- it "keeps one written before the closing bracket inside it" $- format- [ "module M where",- "",- "xs =",- " [ a,",- " b",- " -- and that is all",- " ]"- ]- `shouldBe` [ "module M where",- "",- "xs =",- " [ a,",- " b",- " -- and that is all",- " ]"- ]-- -- Formatting an already formatted file must change nothing. The property- -- is easy to lose the moment comments move, since where a comment goes is- -- read off the input and moving it changes what the next pass reads.- describe "settling"- $ it "reaches its answer in one pass"- $ let once = format awkward- in formatWith Nothing once `shouldBe` once-- describe "layout follows the input" $ do- it "keeps a declaration that was on one line on one line" $- format ["module M where", "", "f x = (x, x)"]- `shouldBe` ["module M where", "", "f x = (x, x)"]-- it "keeps one that was spread out spread out" $- format ["module M where", "", "f x =", " ( x,", " x", " )"]- `shouldBe` [ "module M where",- "",- "f x =",- " ( x,",- " x",- " )"- ]--------------------------------------------------------------------------------- Helpers+spec =+ -- Without fixities nothing is asserted about how a chain associates, so+ -- nothing is rearranged and every operator is treated alike.+ describe "operator chains"+ $ it "leaves a chain alone when the fixities are unknown"+ $ format+ [ "module M where",+ "",+ "f =",+ " a * b",+ " + c * d"+ ]+ `shouldBe` [ "module M where",+ "",+ "f =",+ " a",+ " * b",+ " + c",+ " * d"+ ] --- | Format the given lines and give the result back as lines.+-- | Format the given lines with no fixities known and give the result back+-- as lines. format :: [Text] -> [Text]-format = formatWith Nothing---- | Format with the given extensions in force, as a package would put them.-formatUnder :: [Extension] -> [Text] -> [Text]-formatUnder exts = withSettings defaultRenderConfig{rcExtensions = Set.fromList exts}--formatWith :: Maybe Scope -> [Text] -> [Text]-formatWith scope = withSettings defaultRenderConfig{rcScope = scope}--withSettings :: RenderConfig -> [Text] -> [Text]-withSettings settings input =- case parseModule defaultParserConfig "<test>" source of- Left _ -> error ("did not parse:\n" <> T.unpack source)- Right parsed ->- T.lines (printDoc defaultRenderOptions (renderModule settings parsed))- where- source = T.unlines input---- | A module with comments in all the places that are hard to put them--- back.-awkward :: [Text]-awkward =- [ "module M where",- "",- "-- | A heading",- "",- "-- and a note under it",- "data T = T",- " { a :: Int, -- the first",- " -- the second",- " b :: Int",- " }",- "",- "f x -- what to do with this?",- " | x > 0 = g x",- " | otherwise = h x",- " where",- " g = id",- "",- " -- about h",- " h = negate",- "",- "xs =",- " [ one,",- " {- inline -} two",- " -- last",- " ]"- ]---- | A chain the author already spread over more than one line, so that the--- question is where it breaks rather than whether it does.-spreadChain :: [Text]-spreadChain =- [ "module M where",- "",- "f =",- " a * b",- " + c * d"- ]---- | A scope that knows the operators the tests use.-arithmetic :: Scope-arithmetic =- Scope- { scopeInTypes = nothingReaches,- scopeInTerms =- nothingReaches- { reachUnqualified =- Map.fromList- [ (OpName "$", (Fixity RightAssoc 0, DeclaredHere)),- (OpName "+", (Fixity LeftAssoc 6, DeclaredHere)),- (OpName "*", (Fixity LeftAssoc 7, DeclaredHere))- ]- },- scopeUnsettled = []- }---- | A namespace with nothing in it.-nothingReaches :: Reach-nothingReaches =- Reach- { reachUnqualified = Map.empty,- reachQualified = Map.empty,- reachAmbiguous = Map.empty,- reachDecided = Set.empty- }+format =+ T.lines+ . printDoc+ . renderModule defaultRenderConfig+ . parsed+ . T.unlines
tests/Tilia/RunSpec.hs view
@@ -30,7 +30,7 @@ import Tilia.Format (FormatError (..), formatErrorExitCode, refused) import Tilia.Palette (Color (Bad), Palette (..)) import Tilia.Run-import Tilia.Utils (lineWidth, visibleLength, wrapTo)+import Tilia.Utils (inParallel, lineWidth, visibleLength, wrapTo) spec :: Spec spec = do@@ -60,10 +60,6 @@ it "counts a rewrite as a difference and nothing else" $ fmap differs [Changed "a" "b", Unchanged, decline, failure] `shouldBe` [True, False, False, False]-- it "keeps refusals and failures apart" $ do- fmap declined [decline, failure, Unchanged] `shouldBe` [True, False, False]- fmap failed [decline, failure, Unchanged] `shouldBe` [False, True, False] describe "the status a run leaves behind" $ do it "has none to give when nothing failed" $
tests/Tilia/TestConfig.hs view
@@ -3,9 +3,10 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} --- | The settings every corpus example is formatted with.+-- | How every corpus example is formatted. module Tilia.TestConfig- ( exampleRenderConfig,+ ( printExample,+ formatCppExample, ) where @@ -17,6 +18,8 @@ import Data.Text (Text) import GHC.LanguageExtensions.Type (Extension) import Tilia.BootFixities (bootFixities)+import Tilia.Cpp (CppError, everyBranch, formatWithCpp)+import Tilia.Doc (printDoc) import Tilia.Fixity ( Direction (..), Established (..),@@ -33,13 +36,13 @@ unreadable, ) import Tilia.Parser (ParsedModule (..))-import Tilia.Pragma (effectiveExtensions)-import Tilia.Render (RenderConfig (..), defaultRenderConfig)+import Tilia.Render (RenderConfig (..), defaultRenderConfig, renderModule)+import Tilia.TestUtils (effectiveExtensionsFor, parserConfigFor) -- | How to format one corpus example. exampleRenderConfig :: -- | What the package around it puts in force, if it is a module of one.- [Extension] ->+ Maybe [Extension] -> Text -> -- | Its configurations, parsed. NonEmpty ParsedModule ->@@ -47,7 +50,7 @@ exampleRenderConfig package source parsed = defaultRenderConfig { rcExtensions =- Set.fromList (effectiveExtensions package source),+ Set.fromList (effectiveExtensionsFor package source), rcScope = Just ( underEveryQualifier@@ -74,6 +77,35 @@ ] ) }++-- | Print a corpus example that does not use the preprocessor.+printExample ::+ -- | What the package around it puts in force, if it is a module of one.+ Maybe [Extension] ->+ Text ->+ -- | It, parsed.+ ParsedModule ->+ Text+printExample package source parsed =+ printDoc+ (renderModule (exampleRenderConfig package source (pure parsed)) parsed)++-- | Format a corpus example that uses the preprocessor.+formatCppExample ::+ -- | What the package around it puts in force, if it is a module of one.+ Maybe [Extension] ->+ FilePath ->+ Text ->+ Either CppError Text+formatCppExample package path source =+ formatWithCpp parser configured path source+ where+ parser = parserConfigFor package+ configured =+ maybe+ defaultRenderConfig+ (exampleRenderConfig package source)+ (everyBranch parser path source) -- | What a module in scope exports, as far as the corpus is concerned. exportsOf :: Text -> Maybe Fixities
+ tests/Tilia/TestUtils.hs view
@@ -0,0 +1,222 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedLabels #-}+{-# LANGUAGE PatternSynonyms #-}++-- | Helpers several specs share.+module Tilia.TestUtils+ ( withEnvironment,+ parserConfigFor,+ defaultParserConfig,+ effectiveExtensionsFor,+ parsed,+ commentsIn,+ unreadOnly,+ fingerprint,+ routedResolver,++ -- * Configurations, enumerated+ configurations,+ leaves,+ linearLeaves,+ countLeaves,+ answeredLeaves,+ answeredLinearLeaves,+ )+where++import Control.Exception (bracket)+import Data.Bits (xor)+import Data.Char (ord)+import Data.Choice (pattern Do)+import Data.Foldable (traverse_)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Map.Strict qualified as Map+import Data.Maybe (fromMaybe, isJust, listToMaybe)+import Data.Text (Text)+import Data.Text qualified as T+import Data.Word (Word64)+import GHC.Driver.Session qualified as GHC+import GHC.LanguageExtensions.Type (Extension (..))+import System.Environment (lookupEnv, setEnv, unsetEnv)+import Tilia.Cabal.Package (newPackageReader)+import Tilia.Cabal.Plan (BuildPlan, plannedCompiler)+import Tilia.Comments (Comment)+import Tilia.Cpp.Directives+ ( Assignment,+ Configurations (..),+ CppError,+ GroupSpec (..),+ Guard,+ configurationsOn,+ gsCount,+ implied,+ nestedIn,+ readConditionals,+ resolved,+ scanConditionals,+ unconditionalErrors,+ )+import Tilia.Fixity (ModuleChain (..))+import Tilia.Fixity.Plan (Resolver, Route, newResolverVia)+import Tilia.Parser+ ( ParsedModule (..),+ ParserConfig (..),+ describeParseError,+ parseModule,+ )+import Tilia.Pragma (effectiveExtensions)+import Tilia.Source (comments)++-- | Run something with the given variables set, and the environment as it+-- was afterwards however it turns out.+withEnvironment :: [(String, String)] -> IO a -> IO a+withEnvironment vars act = bracket set restore (const act)+ where+ set = traverse remember vars+ remember (key, value) = do+ was <- lookupEnv key+ setEnv key value+ pure (key, was)+ restore = traverse_ (\(key, was) -> maybe (unsetEnv key) (setEnv key) was)++-- | What to parse a module with, given what its package puts in force, or+-- 'Nothing' for a module of no package, which is read as GHC2021.+parserConfigFor :: Maybe [Extension] -> ParserConfig+parserConfigFor =+ ParserConfig . fromMaybe (GHC.languageExtensions (Just GHC.GHC2021))++-- | What to parse a module of no package with.+defaultParserConfig :: ParserConfig+defaultParserConfig = parserConfigFor Nothing++-- | The extensions in force in a module, given what its package puts in+-- force, or 'Nothing' for a module of no package, which starts from+-- @ImplicitPrelude@ alone.+--+-- That is less than 'parserConfigFor' reads such a module with, so that+-- what is printed, @import qualified@ for one, reads the same under every+-- edition.+effectiveExtensionsFor :: Maybe [Extension] -> Text -> [Extension]+effectiveExtensionsFor = effectiveExtensions . fromMaybe [ImplicitPrelude]++-- | Parse a module, or stop the test saying why it does not parse.+parsed :: Text -> ParsedModule+parsed source = case parseModule defaultParserConfig "M.hs" source of+ Left problem -> error (T.unpack (describeParseError problem))+ Right m -> m++-- | Every comment in a module.+commentsIn :: Text -> [Comment]+commentsIn = comments . pmSource . parsed++-- | The one import blamed for an operator, unread on its own account and so+-- with nothing below it.+unreadOnly :: Text -> NonEmpty ModuleChain+unreadOnly m = ModuleChain (m :| []) :| []++-- | FNV-1a, a hash of a string that is the same on every machine and every+-- run.+fingerprint :: String -> Word64+fingerprint = foldl' step 14695981039346656037+ where+ step h c = (h `xor` fromIntegral (ord c)) * 1099511628211++-- | A 'Resolver' that reads modules by the routes given and remembers what+-- it reads.+routedResolver :: [Route] -> BuildPlan -> IO Resolver+routedResolver routes plan = do+ packageReader <- newPackageReader (plannedCompiler plan)+ newResolverVia (Do #useCache) routes packageReader plan++-- | Split a module on its first outermost conditional, and on every other+-- one written behind the same directives, wherever in the module it sits.+configurations :: Text -> Maybe Configurations+configurations source = do+ forest <- scanConditionals source+ gs <- listToMaybe forest+ pure (configurationsOn gs forest source)++-- | Every configuration of a module, with every conditional resolved.+leaves :: Text -> Either CppError [Text]+leaves = fmap (fmap snd) . answeredLeaves++-- | The configurations reached by varying one conditional at a time.+linearLeaves :: Text -> Either CppError [Text]+linearLeaves = fmap (fmap snd) . answeredLinearLeaves++-- | How many configurations a module has, without building any of them,+-- and so counting those no definition of the macros gives.+countLeaves :: Text -> Either CppError Integer+countLeaves source = do+ forest <- readConditionals source+ pure (sum [across answers forest | answers <- combinations (afforded forest)])+ where+ across answers = product . fmap (one answers)+ one answers gs = case lookup (gsGuards gs) answers of+ Just i -> across answers (nestedIn gs i)+ Nothing -> sum [across answers (nestedIn gs i) | i <- [0 .. gsCount gs - 1]]+ combinations = traverse (\(g, k) -> [(g, i) | i <- [0 .. k - 1]])+ afforded forest = go 1 (repeated forest)+ where+ go _ [] = []+ go n ((g, k) : rest)+ | n * toInteger k <= guardsToTie = (g, k) : go (n * toInteger k) rest+ | otherwise = []++-- | The guards a module asks more than once, and how many answers each has.+repeated :: [GroupSpec] -> [([Guard], Int)]+repeated forest = distinct Map.empty [q | q@(g, _) <- asked forest, twice g]+ where+ asked gss = concat [(gsGuards gs, gsCount gs) : asked (concat (gsNested gs)) | gs <- gss]+ times = Map.fromListWith (+) [(g, 1 :: Int) | (g, _) <- asked forest]+ twice g = Map.findWithDefault 0 g times >= 2++ distinct _ [] = []+ distinct seen (q@(g, _) : rest)+ | Map.member g seen = distinct seen rest+ | otherwise = q : distinct (Map.insert g () seen) rest++-- | How many combinations of answers 'countLeaves' will enumerate.+guardsToTie :: Integer+guardsToTie = 4096++-- | Every configuration some definition of the macros gives, and the+-- answers that reach it.+answeredLeaves :: Text -> Either CppError [(Assignment, Text)]+answeredLeaves =+ fmap (filter (isJust . implied . Map.toList . fst)) . go Map.empty+ where+ go answers source = case configurations source of+ Nothing | not (null (unconditionalErrors source)) -> Right []+ Nothing -> (\t -> [(answers, t)]) <$> resolved source+ Just c ->+ concat+ <$> traverse+ (\(i, t) -> go (Map.insert (cfgGuards c) i answers) t)+ (zip [0 ..] (cfgTexts c))++-- | The same, labelled by the answers that reach each one, and for the same+-- reason as 'answeredLeaves'.+answeredLinearLeaves :: Text -> Either CppError [(Assignment, Text)]+answeredLinearLeaves =+ fmap (filter (isJust . implied . Map.toList . fst)) . go Map.empty+ where+ go answers source = case configurations source of+ Nothing -> (\t -> [(answers, t)]) <$> resolved source+ Just c -> case zip [0 ..] (cfgTexts c) of+ [] -> Right []+ (i, first) : rest ->+ (<>)+ <$> go (Map.insert (cfgGuards c) i answers) first+ <*> traverse (held answers (cfgGuards c)) rest+ where+ held before gs (i, t) = answeredBaseline (Map.insert gs i before) t++-- | The configuration in which every question still to be asked takes its+-- first branch, and the answers that gives.+answeredBaseline :: Assignment -> Text -> Either CppError (Assignment, Text)+answeredBaseline answers source = case configurations source of+ Nothing -> (answers,) <$> resolved source+ Just c -> case cfgTexts c of+ [] -> Right (answers, source)+ t : _ -> answeredBaseline (Map.insert (cfgGuards c) 0 answers) t
tests/Tilia/WithProjectPlan.hs view
@@ -24,6 +24,13 @@ import System.FilePath (takeDirectory, (</>)) import System.IO.Unsafe (unsafePerformIO) import Test.Hspec (Spec)+import Tilia.Cabal.Plan+ ( BuildPlan (..),+ PackageSource (..),+ PlanPackage (..),+ loadPlan,+ plannedTarballs,+ ) import Tilia.Cabal.Project (findProjectRoot) import Tilia.Cabal.Target ( Target (..),@@ -36,13 +43,6 @@ ( Installed (..), InstalledPackage (..), readInstalledPackages,- )-import Tilia.Fixity.Plan- ( BuildPlan (..),- PackageSource (..),- PlanPackage (..),- loadPlan,- plannedTarballs, ) import Tilia.Process (readProgramOutput)
tilia.cabal view
@@ -1,8 +1,9 @@ cabal-version: 2.4 name: tilia-version: 0.1.1.0+version: 0.1.2.0 license: BSD-3-Clause license-file: LICENSE.md+author: Mark Karpov <markkarpov92@gmail.com> maintainer: Mark Karpov <markkarpov92@gmail.com> tested-with: ghc ==9.10.3@@ -20,6 +21,7 @@ corpora/hackage/hackage.manifest corpora/hackage/hackage.report corpora/vendored/**/*.hs+ corpora/vendored/**/*.hsig extra-doc-files: CHANGELOG.md@@ -34,9 +36,25 @@ default: False manual: True +common common-ghc-options+ ghc-options:+ -O2+ -Wall++ if flag(dev)+ ghc-options:+ -Werror+ -Wredundant-constraints+ -Wpartial-fields+ -Wunused-packages+ -haddock+ -Winvalid-haddock+ library+ import: common-ghc-options exposed-modules: Tilia.Cabal.Package+ Tilia.Cabal.Plan Tilia.Cabal.Project Tilia.Cabal.Target Tilia.Comments@@ -56,7 +74,7 @@ Tilia.Editor Tilia.Equivalence Tilia.Fixity- Tilia.Fixity.ByHand+ Tilia.Fixity.Archive Tilia.Fixity.Cabal Tilia.Fixity.Cache Tilia.Fixity.Debug@@ -71,13 +89,13 @@ Tilia.Gathered Tilia.Ignore Tilia.Imports+ Tilia.Memo Tilia.Newline Tilia.Palette Tilia.Parser Tilia.Pragma Tilia.Process Tilia.Render- Tilia.Render.Body Tilia.Render.Class Tilia.Render.Context Tilia.Render.Data@@ -106,7 +124,7 @@ Cabal-syntax >=3.12 && <3.17, aeson >=2.1 && <3, array >=0.5 && <0.6,- base >=4.14 && <5,+ base >=4.20 && <5, base16-bytestring >=1 && <2, bytestring >=0.11 && <0.13, choice >=0.2 && <0.3,@@ -124,29 +142,15 @@ transformers >=0.5 && <0.7, zlib >=0.6 && <0.8, - if flag(dev)- ghc-options:- -O2- -Wall- -Werror- -Wredundant-constraints- -Wpartial-fields- -Wunused-packages- -haddock- -Winvalid-haddock- else- ghc-options:- -O2- -Wall- executable tilia+ import: common-ghc-options main-is: Main.hs hs-source-dirs: app other-modules: Paths_tilia autogen-modules: Paths_tilia default-language: GHC2021 build-depends:- base >=4.14 && <5,+ base >=4.20 && <5, bytestring >=0.11 && <0.13, choice >=0.2 && <0.3, directory ^>=1.3,@@ -156,37 +160,23 @@ -- Twelve capabilities rather than -N, because formatting a project stops -- getting faster well before a machine runs out of cores and every- -- capability past that point costs about 40 MiB and 0.15s of CPU while- -- formatting nothing. Over this project's 43 files: 1.88s at -N1, 575ms- -- at -N8, 518ms at -N12, 499ms at -N16, and back to 509ms at -N24. Asking- -- for -N here instead is the same wall time as -N12 give or take noise,- -- for 50% more CPU and 940 MiB against 520 MiB. Twelve is no worse on a- -- smaller machine either: pinned to four cores it lands within 3% of -N4.+ -- capability past that point costs about 35 MiB and 0.2s of CPU while+ -- formatting nothing. Over this project's 109 files: 2.42s at -N1, 690ms+ -- at -N8, 560ms at -N12, 590ms at -N16, and 600ms at -N24. Asking for -N+ -- here instead is the same wall time as -N12 give or take noise, for 50%+ -- more CPU and 1100 MiB against 680 MiB. Twelve is no worse on a smaller+ -- machine either: pinned to four cores it is 4% faster than -N4. -- -- The nursery is the flag that pays for itself. At the default size the- -- same run takes 828ms rather than 522ms, which is what the extra 320 MiB+ -- same run takes 810ms rather than 560ms, which is what the extra 270 MiB -- buys. ghc-options: -threaded -rtsopts "-with-rtsopts=-N12 -A32m" - if flag(dev)- ghc-options:- -O2- -Wall- -Werror- -Wredundant-constraints- -Wpartial-fields- -Wunused-packages- -haddock- -Winvalid-haddock- else- ghc-options:- -O2- -Wall- test-suite tests+ import: common-ghc-options type: exitcode-stdio-1.0 main-is: Main.hs build-tool-depends: hspec-discover:hspec-discover >=2 && <3@@ -230,13 +220,14 @@ Tilia.RenderSpec Tilia.RunSpec Tilia.TestConfig+ Tilia.TestUtils Tilia.WithProjectPlan default-language: GHC2021 build-depends: QuickCheck >=2.14 && <3, array >=0.5 && <0.6,- base >=4.14 && <5,+ base >=4.20 && <5, base16-bytestring >=1 && <2, bytestring >=0.11 && <0.13, choice >=0.2 && <0.3,@@ -260,22 +251,8 @@ -rtsopts "-with-rtsopts=-M8G -N" - if flag(dev)- ghc-options:- -O2- -Wall- -Werror- -Wredundant-constraints- -Wpartial-fields- -Wunused-packages- -haddock- -Winvalid-haddock- else- ghc-options:- -O2- -Wall- benchmark bench+ import: common-ghc-options type: exitcode-stdio-1.0 main-is: Main.hs hs-source-dirs:@@ -290,11 +267,12 @@ Tilia.BootFixities Tilia.Corpus Tilia.TestConfig+ Tilia.TestUtils c-sources: bench/cbits/counter.c default-language: GHC2021 build-depends:- base >=4.14 && <5,+ base >=4.20 && <5, base16-bytestring >=1 && <2, bytestring >=0.11 && <0.13, choice >=0.2 && <0.3,@@ -317,18 +295,3 @@ -threaded -rtsopts "-with-rtsopts=-N1 -A1g -O1g -C1000 -T"-- if flag(dev)- ghc-options:- -O2- -Wall- -Werror- -Wredundant-constraints- -Wpartial-fields- -Wunused-packages- -haddock- -Winvalid-haddock- else- ghc-options:- -O2- -Wall