haskell-gi-0.20.1: lib/Data/GI/CodeGen/Code.hs
module Data.GI.CodeGen.Code
( Code(..)
, ModuleInfo(..)
, ModuleFlag(..)
, BaseCodeGen
, CodeGen
, ExcCodeGen
, CGError(..)
, genCode
, evalCodeGen
, writeModuleTree
, listModuleTree
, codeToText
, transitiveModuleDeps
, minBaseVersion
, BaseVersion(..)
, showBaseVersion
, registerNSDependency
, qualified
, getDeps
, recurseWithAPIs
, handleCGExc
, describeCGError
, notImplementedError
, badIntroError
, missingInfoError
, indent
, bline
, line
, blank
, group
, hsBoot
, submodule
, setLanguagePragmas
, setGHCOptions
, setModuleFlags
, setModuleMinBase
, exportToplevel
, exportModule
, exportDecl
, exportMethod
, exportProperty
, exportSignal
, findAPI
, getAPI
, findAPIByName
, getAPIs
, getC2HMap
, config
, currentModule
) where
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
import Data.Monoid (Monoid(..))
#endif
import Control.Monad.Reader
import Control.Monad.State.Strict
import Control.Monad.Except
import qualified Data.Foldable as F
import Data.Maybe (fromMaybe, catMaybes)
import Data.Monoid ((<>))
import Data.Sequence (Seq, ViewL ((:<)), (><), (|>), (<|))
import qualified Data.Map.Strict as M
import qualified Data.Sequence as S
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as T
import System.Directory (createDirectoryIfMissing)
import System.FilePath (joinPath, takeDirectory)
import Data.GI.CodeGen.API (API, Name(..))
import Data.GI.CodeGen.Config (Config(..))
import {-# SOURCE #-} Data.GI.CodeGen.CtoHaskellMap (cToHaskellMap,
Hyperlink)
import Data.GI.CodeGen.GtkDoc (CRef)
import Data.GI.CodeGen.ModulePath (ModulePath(..), dotModulePath, (/.))
import Data.GI.CodeGen.Type (Type(..))
import Data.GI.CodeGen.Util (tshow, terror, padTo, utf8WriteFile)
import Data.GI.CodeGen.ProjectInfo (authors, license, maintainers)
data Code
= NoCode -- ^ No code
| Line Text -- ^ A single line, indented to current indentation
| Indent Code -- ^ Indented region
| Sequence (Seq Code) -- ^ The basic sequence of code
| Group Code -- ^ A grouped set of lines
deriving (Eq, Show)
instance Monoid Code where
mempty = NoCode
NoCode `mappend` NoCode = NoCode
x `mappend` NoCode = x
NoCode `mappend` x = x
(Sequence a) `mappend` (Sequence b) = Sequence (a >< b)
(Sequence a) `mappend` b = Sequence (a |> b)
a `mappend` (Sequence b) = Sequence (a <| b)
a `mappend` b = Sequence (a <| b <| S.empty)
type Deps = Set.Set Text
-- | Subsection of the haddock documentation where the export should
-- be located.
type HaddockSection = Text
-- | Symbol to export.
type SymbolName = Text
-- | Possible exports for a given module. Every export type
-- constructor has two parameters: the section of the haddocks where
-- it should appear, and the symbol name to export in the export list
-- of the module.
data Export = Export {
exportType :: ExportType -- ^ Which kind of export.
, exportSymbol :: SymbolName -- ^ Actual symbol to export.
} deriving (Show, Eq, Ord)
-- | Possible types of exports.
data ExportType = ExportTypeDecl -- ^ A type declaration.
| ExportToplevel -- ^ An export in no specific section.
| ExportMethod HaddockSection -- ^ A method for a struct/union, etc.
| ExportProperty HaddockSection -- ^ A property for an object/interface.
| ExportSignal HaddockSection -- ^ A signal for an object/interface.
| ExportModule -- ^ Reexport of a whole module.
deriving (Show, Eq, Ord)
-- | Information on a generated module.
data ModuleInfo = ModuleInfo {
modulePath :: ModulePath -- ^ Full module name: ["Gtk", "Label"].
, moduleCode :: Code -- ^ Generated code for the module.
, bootCode :: Code -- ^ Interface going into the .hs-boot file.
, submodules :: M.Map Text ModuleInfo -- ^ Indexed by the relative
-- module name.
, moduleDeps :: Deps -- ^ Set of dependencies for this module.
, moduleExports :: Seq Export -- ^ Exports for the module.
, qualifiedImports :: Set.Set ModulePath -- ^ Qualified (source) imports
, modulePragmas :: Set.Set Text -- ^ Set of language pragmas for the module.
, moduleGHCOpts :: Set.Set Text -- ^ GHC options for compiling the module.
, moduleFlags :: Set.Set ModuleFlag -- ^ Flags for the module.
, moduleDoc :: Maybe Text -- ^ Documentation for the module.
, moduleMinBase :: BaseVersion -- ^ Minimal version of base the
-- module will work on.
}
-- | Flags for module code generation.
data ModuleFlag = ImplicitPrelude -- ^ Use the standard prelude,
-- instead of the haskell-gi-base short one.
deriving (Show, Eq, Ord)
-- | Minimal version of base supported by a given module.
data BaseVersion = Base47 -- ^ 4.7.0
| Base48 -- ^ 4.8.0
deriving (Show, Eq, Ord)
-- | A `Text` representation of the given base version bound.
showBaseVersion :: BaseVersion -> Text
showBaseVersion Base47 = "4.7"
showBaseVersion Base48 = "4.8"
-- | Generate the empty module.
emptyModule :: ModulePath -> ModuleInfo
emptyModule m = ModuleInfo { modulePath = m
, moduleCode = NoCode
, bootCode = NoCode
, submodules = M.empty
, moduleDeps = Set.empty
, moduleExports = S.empty
, qualifiedImports = Set.empty
, modulePragmas = Set.empty
, moduleGHCOpts = Set.empty
, moduleFlags = Set.empty
, moduleDoc = Nothing
, moduleMinBase = Base47
}
-- | Information for the code generator.
data CodeGenConfig = CodeGenConfig {
hConfig :: Config -- ^ Ambient config.
, loadedAPIs :: M.Map Name API -- ^ APIs available to the generator.
, c2hMap :: M.Map CRef Hyperlink -- ^ Map from C references
-- to Haskell symbols.
}
data CGError = CGErrorNotImplemented Text
| CGErrorBadIntrospectionInfo Text
| CGErrorMissingInfo Text
deriving (Show)
type BaseCodeGen excType a =
ReaderT CodeGenConfig (StateT ModuleInfo (ExceptT excType IO)) a
-- | The code generator monad, for generators that cannot throw
-- errors. The fact that they cannot throw errors is encoded in the
-- forall, which disallows any operation on the error, except
-- discarding it or passing it along without inspecting. This last
-- operation is useful in order to allow embedding `CodeGen`
-- computations inside `ExcCodeGen` computations, while disallowing
-- the opposite embedding without explicit error handling.
type CodeGen a = forall e. BaseCodeGen e a
-- | Code generators that can throw errors.
type ExcCodeGen a = BaseCodeGen CGError a
-- | Run a `CodeGen` with given `Config` and initial `ModuleInfo`,
-- returning either the resulting exception, or the result and final
-- state of the codegen.
runCodeGen :: BaseCodeGen e a -> CodeGenConfig -> ModuleInfo ->
IO (Either e (a, ModuleInfo))
runCodeGen cg cfg state = runExceptT (runStateT (runReaderT cg cfg) state)
-- | This is useful when we plan run a subgenerator, and `mconcat` the
-- result to the original structure later.
cleanInfo :: ModuleInfo -> ModuleInfo
cleanInfo info = info { moduleCode = NoCode, submodules = M.empty,
bootCode = NoCode, moduleExports = S.empty,
qualifiedImports = Set.empty,
moduleDoc = Nothing, moduleMinBase = Base47 }
-- | Run the given code generator using the state and config of an
-- ambient CodeGen, but without adding the generated code to
-- `moduleCode`, instead returning it explicitly.
recurseCG :: BaseCodeGen e a -> BaseCodeGen e (a, Code)
recurseCG cg = do
cfg <- ask
oldInfo <- get
-- Start the subgenerator with no code and no submodules.
let info = cleanInfo oldInfo
liftIO (runCodeGen cg cfg info) >>= \case
Left e -> throwError e
Right (r, new) -> put (mergeInfoState oldInfo new) >>
return (r, moduleCode new)
-- | Like `recurse`, giving explicitly the set of loaded APIs and C to
-- Haskell map for the subgenerator.
recurseWithAPIs :: M.Map Name API -> CodeGen () -> CodeGen ()
recurseWithAPIs apis cg = do
cfg <- ask
oldInfo <- get
-- Start the subgenerator with no code and no submodules.
let info = cleanInfo oldInfo
cfg' = cfg {loadedAPIs = apis,
c2hMap = cToHaskellMap (M.toList apis)}
liftIO (runCodeGen cg cfg' info) >>= \case
Left e -> throwError e
Right (_, new) -> put (mergeInfo oldInfo new)
-- | Merge everything but the generated code for the two given `ModuleInfo`.
mergeInfoState :: ModuleInfo -> ModuleInfo -> ModuleInfo
mergeInfoState oldState newState =
let newDeps = Set.union (moduleDeps oldState) (moduleDeps newState)
newSubmodules = M.unionWith mergeInfo (submodules oldState) (submodules newState)
newExports = moduleExports oldState <> moduleExports newState
newImports = qualifiedImports oldState <> qualifiedImports newState
newPragmas = Set.union (modulePragmas oldState) (modulePragmas newState)
newGHCOpts = Set.union (moduleGHCOpts oldState) (moduleGHCOpts newState)
newFlags = Set.union (moduleFlags oldState) (moduleFlags newState)
newBoot = bootCode oldState <> bootCode newState
newDoc = moduleDoc oldState <> moduleDoc newState
newMinBase = max (moduleMinBase oldState) (moduleMinBase newState)
in oldState {moduleDeps = newDeps, submodules = newSubmodules,
moduleExports = newExports, qualifiedImports = newImports,
modulePragmas = newPragmas,
moduleGHCOpts = newGHCOpts, moduleFlags = newFlags,
bootCode = newBoot, moduleDoc = newDoc,
moduleMinBase = newMinBase }
-- | Merge the infos, including code too.
mergeInfo :: ModuleInfo -> ModuleInfo -> ModuleInfo
mergeInfo oldInfo newInfo =
let info = mergeInfoState oldInfo newInfo
in info { moduleCode = moduleCode oldInfo <> moduleCode newInfo }
-- | Add the given submodule to the list of submodules of the current
-- module.
addSubmodule :: Text -> ModuleInfo -> ModuleInfo -> ModuleInfo
addSubmodule modName submodule current = current { submodules = M.insertWith mergeInfo modName submodule (submodules current)}
-- | Run the given CodeGen in order to generate a single submodule of the
-- current module. Note that we do not generate the submodule if the
-- code generator generated no code and the module does not have
-- submodules.
submodule' :: Text -> BaseCodeGen e () -> BaseCodeGen e ()
submodule' modName cg = do
cfg <- ask
oldInfo <- get
let info = emptyModule (modulePath oldInfo /. modName)
liftIO (runCodeGen cg cfg info) >>= \case
Left e -> throwError e
Right (_, smInfo) -> if moduleCode smInfo == NoCode &&
M.null (submodules smInfo)
then return ()
else modify' (addSubmodule modName smInfo)
-- | Run the given CodeGen in order to generate a submodule (specified
-- an an ordered list) of the current module.
submodule :: ModulePath -> BaseCodeGen e () -> BaseCodeGen e ()
submodule (ModulePath []) cg = cg
submodule (ModulePath (m:ms)) cg = submodule' m (submodule (ModulePath ms) cg)
-- | Try running the given `action`, and if it fails run `fallback`
-- instead.
handleCGExc :: (CGError -> CodeGen a) -> ExcCodeGen a -> CodeGen a
handleCGExc fallback
action = do
cfg <- ask
oldInfo <- get
let info = cleanInfo oldInfo
liftIO (runCodeGen action cfg info) >>= \case
Left e -> fallback e
Right (r, newInfo) -> do
put (mergeInfo oldInfo newInfo)
return r
-- | Return the currently loaded set of dependencies.
getDeps :: CodeGen Deps
getDeps = moduleDeps <$> get
-- | Return the ambient configuration for the code generator.
config :: CodeGen Config
config = hConfig <$> ask
-- | Return the name of the current module.
currentModule :: CodeGen Text
currentModule = do
s <- get
return (dotWithPrefix (modulePath s))
-- | Return the list of APIs available to the generator.
getAPIs :: CodeGen (M.Map Name API)
getAPIs = loadedAPIs <$> ask
-- | Return the C -> Haskell available to the generator.
getC2HMap :: CodeGen (M.Map CRef Hyperlink)
getC2HMap = c2hMap <$> ask
-- | Due to the `forall` in the definition of `CodeGen`, if we want to
-- run the monad transformer stack until we get an `IO` action, our
-- only option is ignoring the possible error code from
-- `runExceptT`. This is perfectly safe, since there is no way to
-- construct a computation in the `CodeGen` monad that throws an
-- exception, due to the higher rank type.
unwrapCodeGen :: CodeGen a -> CodeGenConfig -> ModuleInfo ->
IO (a, ModuleInfo)
unwrapCodeGen cg cfg info =
runCodeGen cg cfg info >>= \case
Left _ -> error "unwrapCodeGen:: The impossible happened!"
Right (r, newInfo) -> return (r, newInfo)
-- | Like `evalCodeGen`, but discard the resulting output value.
genCode :: Config -> M.Map Name API ->
ModulePath -> CodeGen () -> IO ModuleInfo
genCode cfg apis mPath cg = snd <$> evalCodeGen cfg apis mPath cg
-- | Run a code generator, and return the information for the
-- generated module together with the return value of the generator.
evalCodeGen :: Config -> M.Map Name API ->
ModulePath -> CodeGen a -> IO (a, ModuleInfo)
evalCodeGen cfg apis mPath cg = do
let initialInfo = emptyModule mPath
cfg' = CodeGenConfig {hConfig = cfg, loadedAPIs = apis,
c2hMap = cToHaskellMap (M.toList apis)}
unwrapCodeGen cg cfg' initialInfo
-- | Mark the given dependency as used by the module.
registerNSDependency :: Text -> CodeGen ()
registerNSDependency name = do
deps <- getDeps
unless (Set.member name deps) $ do
let newDeps = Set.insert name deps
modify' $ \s -> s {moduleDeps = newDeps}
-- | Return the transitive set of dependencies, i.e. the union of
-- those of the module and (transitively) its submodules.
transitiveModuleDeps :: ModuleInfo -> Deps
transitiveModuleDeps minfo =
Set.unions (moduleDeps minfo
: map transitiveModuleDeps (M.elems $ submodules minfo))
-- | Given a module name and a symbol in the module (including a
-- proper namespace), return a qualified name for the symbol.
qualified :: ModulePath -> Name -> CodeGen Text
qualified mp (Name ns s) = do
cfg <- config
-- Make sure the module is listed as a dependency.
when (modName cfg /= ns) $
registerNSDependency ns
minfo <- get
if mp == modulePath minfo
then return s
else do
qm <- qualifiedImport mp
return (qm <> "." <> s)
-- | Import the given module name qualified (as a source import if the
-- namespace is the same as the current one), and return the name
-- under which the module was imported.
qualifiedImport :: ModulePath -> CodeGen Text
qualifiedImport mp = do
modify' $ \s -> s {qualifiedImports = Set.insert mp (qualifiedImports s)}
return (qualifiedModuleName mp)
-- | Construct a simplified version of the module name, suitable for a
-- qualified import.
qualifiedModuleName :: ModulePath -> Text
qualifiedModuleName (ModulePath [ns, "Objects", o]) = ns <> "." <> o
qualifiedModuleName (ModulePath [ns, "Interfaces", i]) = ns <> "." <> i
qualifiedModuleName (ModulePath [ns, "Structs", s]) = ns <> "." <> s
qualifiedModuleName (ModulePath [ns, "Unions", u]) = ns <> "." <> u
qualifiedModuleName mp = dotModulePath mp
-- | Return the minimal base version supported by the module and all
-- its submodules.
minBaseVersion :: ModuleInfo -> BaseVersion
minBaseVersion minfo =
maximum (moduleMinBase minfo
: map minBaseVersion (M.elems $ submodules minfo))
-- | Give a friendly textual description of the error for presenting
-- to the user.
describeCGError :: CGError -> Text
describeCGError (CGErrorNotImplemented e) = "Not implemented: " <> tshow e
describeCGError (CGErrorBadIntrospectionInfo e) = "Bad introspection data: " <> tshow e
describeCGError (CGErrorMissingInfo e) = "Missing info: " <> tshow e
notImplementedError :: Text -> ExcCodeGen a
notImplementedError s = throwError $ CGErrorNotImplemented s
badIntroError :: Text -> ExcCodeGen a
badIntroError s = throwError $ CGErrorBadIntrospectionInfo s
missingInfoError :: Text -> ExcCodeGen a
missingInfoError s = throwError $ CGErrorMissingInfo s
findAPI :: Type -> CodeGen (Maybe API)
findAPI TError = Just <$> findAPIByName (Name "GLib" "Error")
findAPI (TInterface n) = Just <$> findAPIByName n
findAPI _ = return Nothing
-- | Find the API associated with a given type. If the API cannot be
-- found this raises an `error`.
getAPI :: Type -> CodeGen API
getAPI t = findAPI t >>= \case
Just a -> return a
Nothing -> terror ("Could not resolve type \"" <> tshow t <> "\".")
findAPIByName :: Name -> CodeGen API
findAPIByName n@(Name ns _) = do
apis <- getAPIs
case M.lookup n apis of
Just api -> return api
Nothing ->
terror $ "couldn't find API description for " <> ns <> "." <> name n
-- | Add some code to the current generator.
tellCode :: Code -> CodeGen ()
tellCode c = modify' (\s -> s {moduleCode = moduleCode s <> c})
-- | Print out a (newline-terminated) line.
line :: Text -> CodeGen ()
line = tellCode . Line
-- | Print out the given line both to the normal module, and to the
-- HsBoot file.
bline :: Text -> CodeGen ()
bline l = hsBoot (line l) >> line l
-- | A blank line
blank :: CodeGen ()
blank = line ""
-- | Increase the indent level for code generation.
indent :: BaseCodeGen e a -> BaseCodeGen e a
indent cg = do
(x, code) <- recurseCG cg
tellCode (Indent code)
return x
-- | Group a set of related code.
group :: BaseCodeGen e a -> BaseCodeGen e a
group cg = do
(x, code) <- recurseCG cg
tellCode (Group code)
blank
return x
-- | Write the given code into the .hs-boot file for the current module.
hsBoot :: BaseCodeGen e a -> BaseCodeGen e a
hsBoot cg = do
(x, code) <- recurseCG cg
modify' (\s -> s{bootCode = bootCode s <> code})
return x
-- | Add a export to the current module.
export :: Export -> CodeGen ()
export e =
modify' $ \s -> s{moduleExports = moduleExports s |> e}
-- | Reexport a whole module.
exportModule :: SymbolName -> CodeGen ()
exportModule m = export (Export ExportModule m)
-- | Export a toplevel (i.e. belonging to no section) symbol.
exportToplevel :: SymbolName -> CodeGen ()
exportToplevel t = export (Export ExportToplevel t)
-- | Add a type declaration-related export.
exportDecl :: SymbolName -> CodeGen ()
exportDecl d = export (Export ExportTypeDecl d)
-- | Add a method export under the given section.
exportMethod :: HaddockSection -> SymbolName -> CodeGen ()
exportMethod s n = export (Export (ExportMethod s) n)
-- | Add a property-related export under the given section.
exportProperty :: HaddockSection -> SymbolName -> CodeGen ()
exportProperty s n = export (Export (ExportProperty s) n)
-- | Add a signal-related export under the given section.
exportSignal :: HaddockSection -> SymbolName -> CodeGen ()
exportSignal s n = export (Export (ExportSignal s) n)
-- | Set the language pragmas for the current module.
setLanguagePragmas :: [Text] -> CodeGen ()
setLanguagePragmas ps =
modify' $ \s -> s{modulePragmas = Set.fromList ps}
-- | Set the GHC options for compiling this module (in a OPTIONS_GHC pragma).
setGHCOptions :: [Text] -> CodeGen ()
setGHCOptions opts =
modify' $ \s -> s{moduleGHCOpts = Set.fromList opts}
-- | Set the given flags for the module.
setModuleFlags :: [ModuleFlag] -> CodeGen ()
setModuleFlags flags =
modify' $ \s -> s{moduleFlags = Set.fromList flags}
-- | Set the minimum base version supported by the current module.
setModuleMinBase :: BaseVersion -> CodeGen ()
setModuleMinBase v =
modify' $ \s -> s{moduleMinBase = max v (moduleMinBase s)}
-- | Return a text representation of the `Code`.
codeToText :: Code -> Text
codeToText c = T.concat $ str 0 c []
where
str :: Int -> Code -> [Text] -> [Text]
str _ NoCode cont = cont
str n (Line s) cont = paddedLine n s : cont
str n (Indent c) cont = str (n + 1) c cont
str n (Sequence s) cont = deseq n (S.viewl s) cont
str n (Group c) cont = str n c cont
deseq _ S.EmptyL cont = cont
deseq n (c :< cs) cont = str n c (deseq n (S.viewl cs) cont)
-- | Pad a line to the given number of leading spaces, and add a
-- newline at the end.
paddedLine :: Int -> Text -> Text
paddedLine n s = T.replicate (n * 4) " " <> s <> "\n"
-- | Put a (padded) comma at the end of the text.
comma :: Text -> Text
comma s = padTo 40 s <> ","
-- | Format the list of exported modules.
formatExportedModules :: [Export] -> Maybe Text
formatExportedModules [] = Nothing
formatExportedModules exports =
Just . T.concat . map ( paddedLine 1
. comma
. ("module " <>)
. exportSymbol)
. filter ((== ExportModule) . exportType) $ exports
-- | Format the toplevel exported symbols.
formatToplevel :: [Export] -> Maybe Text
formatToplevel [] = Nothing
formatToplevel exports =
Just . T.concat . map (paddedLine 1 . comma . exportSymbol)
. filter ((== ExportToplevel) . exportType) $ exports
-- | Format the type declarations section.
formatTypeDecls :: [Export] -> Maybe Text
formatTypeDecls exports =
let exportedTypes = filter ((== ExportTypeDecl) . exportType) exports
in if exportedTypes == []
then Nothing
else Just . T.unlines $ [ "-- * Exported types"
, T.concat . map ( paddedLine 1
. comma
. exportSymbol )
$ exportedTypes ]
-- | A subsection name, with an optional anchor name.
data Subsection = Subsection { subsectionTitle :: Text
, subsectionAnchor :: Maybe Text
} deriving (Eq, Show, Ord)
-- | A subsection with an anchor given by the title and @prefix:title@ anchor.
subsecWithPrefix prefix title =
Subsection { subsectionTitle = title
, subsectionAnchor = Just (prefix <> ":" <> title) }
-- | Format a given section made of subsections.
formatSection :: Text -> (Export -> Maybe (Subsection, SymbolName)) ->
[Export] -> Maybe Text
formatSection section filter exports =
if M.null exportedSubsections
then Nothing
else Just . T.unlines $ [" -- * " <> section
, ( T.unlines
. map formatSubsection
. M.toList ) exportedSubsections]
where
filteredExports :: [(Subsection, SymbolName)]
filteredExports = catMaybes (map filter exports)
exportedSubsections :: M.Map Subsection (Set.Set SymbolName)
exportedSubsections = foldr extract M.empty filteredExports
extract :: (Subsection, SymbolName) -> M.Map Subsection (Set.Set Text)
-> M.Map Subsection (Set.Set Text)
extract (subsec, m) secs =
M.insertWith Set.union subsec (Set.singleton m) secs
formatSubsection :: (Subsection, Set.Set SymbolName) -> Text
formatSubsection (subsec, symbols) =
T.unlines [ "-- ** " <> case subsectionAnchor subsec of
Just anchor -> subsectionTitle subsec <>
" #" <> anchor <> "#"
Nothing -> subsectionTitle subsec
, ( T.concat
. map (paddedLine 1 . comma)
. Set.toList ) symbols]
-- | Format the list of methods.
formatMethods :: [Export] -> Maybe Text
formatMethods = formatSection "Methods" toMethod
where toMethod :: Export -> Maybe (Subsection, SymbolName)
toMethod (Export (ExportMethod s) m) =
Just (subsecWithPrefix "method" s, m)
toMethod _ = Nothing
-- | Format the list of properties.
formatProperties :: [Export] -> Maybe Text
formatProperties = formatSection "Properties" toProperty
where toProperty :: Export -> Maybe (Subsection, SymbolName)
toProperty (Export (ExportProperty s) m) =
Just (subsecWithPrefix "attr" s, m)
toProperty _ = Nothing
-- | Format the list of signals.
formatSignals :: [Export] -> Maybe Text
formatSignals = formatSection "Signals" toSignal
where toSignal :: Export -> Maybe (Subsection, SymbolName)
toSignal (Export (ExportSignal s) m) =
Just (subsecWithPrefix "signal" s, m)
toSignal _ = Nothing
-- | Format the given export list. This is just the inside of the
-- parenthesis.
formatExportList :: [Export] -> Text
formatExportList exports =
T.unlines . catMaybes $ [ formatExportedModules exports
, formatToplevel exports
, formatTypeDecls exports
, formatMethods exports
, formatProperties exports
, formatSignals exports ]
-- | Write down the list of language pragmas.
languagePragmas :: [Text] -> Text
languagePragmas [] = ""
languagePragmas ps = "{-# LANGUAGE " <> T.intercalate ", " ps <> " #-}\n"
-- | Write down the list of GHC options.
ghcOptions :: [Text] -> Text
ghcOptions [] = ""
ghcOptions opts = "{-# OPTIONS_GHC " <> T.intercalate ", " opts <> " #-}\n"
-- | Standard fields for every module.
standardFields :: Text
standardFields = T.unlines [ "Copyright : " <> authors
, "License : " <> license
, "Maintainer : " <> maintainers ]
-- | The haddock header for the module, including optionally a description.
moduleHaddock :: Maybe Text -> Text
moduleHaddock Nothing = T.unlines ["{- |", standardFields <> "-}"]
moduleHaddock (Just description) = T.unlines ["{- |", standardFields,
description, "-}"]
-- | Generic module prelude. We reexport all of the submodules.
modulePrelude :: Text -> [Export] -> [Text] -> Text
modulePrelude name [] [] = "module " <> name <> " () where\n"
modulePrelude name exports [] =
"module " <> name <> "\n ( "
<> formatExportList exports
<> " ) where\n"
modulePrelude name [] reexportedModules =
"module " <> name <> "\n ( "
<> formatExportList (map (Export ExportModule) reexportedModules)
<> " ) where\n\n"
<> T.unlines (map ("import " <>) reexportedModules)
modulePrelude name exports reexportedModules =
"module " <> name <> "\n ( "
<> formatExportList (map (Export ExportModule) reexportedModules)
<> "\n"
<> formatExportList exports
<> " ) where\n\n"
<> T.unlines (map ("import " <>) reexportedModules)
-- | Code for loading the needed dependencies. One needs to give the
-- prefix for the namespace being currently generated, modules with
-- this prefix will be imported as {-# SOURCE #-}, and otherwise will
-- be imported normally.
importDeps :: ModulePath -> [ModulePath] -> Text
importDeps _ [] = ""
importDeps (ModulePath prefix) deps = T.unlines . map toImport $ deps
where toImport :: ModulePath -> Text
toImport dep = let impSt = if importSource dep
then "import {-# SOURCE #-} qualified "
else "import qualified "
in impSt <> dotWithPrefix dep <>
" as " <> qualifiedModuleName dep
importSource :: ModulePath -> Bool
importSource (ModulePath [_, "Callbacks"]) = False
importSource (ModulePath mp) = take (length prefix) mp == prefix
-- | Standard imports.
moduleImports :: Text
moduleImports = T.unlines [
"import Data.GI.Base.ShortPrelude"
, "import qualified Data.GI.Base.ShortPrelude as SP"
, "import qualified Data.GI.Base.Overloading as O"
, "import qualified Prelude as P"
, ""
, "import qualified Data.GI.Base.Attributes as GI.Attributes"
, "import qualified Data.GI.Base.ManagedPtr as B.ManagedPtr"
, "import qualified Data.GI.Base.GError as B.GError"
, "import qualified Data.GI.Base.GVariant as B.GVariant"
, "import qualified Data.GI.Base.GParamSpec as B.GParamSpec"
, "import qualified Data.GI.Base.CallStack as B.CallStack"
, "import qualified Data.Text as T"
, "import qualified Data.ByteString.Char8 as B"
, "import qualified Data.Map as Map"
, "import qualified Foreign.Ptr as FP" ]
-- | Like `dotModulePath`, but add a "GI." prefix.
dotWithPrefix :: ModulePath -> Text
dotWithPrefix mp = dotModulePath ("GI" <> mp)
-- | Write to disk the code for a module, under the given base
-- directory. Does not write submodules recursively, for that use
-- `writeModuleTree`.
writeModuleInfo :: Bool -> Maybe FilePath -> ModuleInfo -> IO ()
writeModuleInfo verbose dirPrefix minfo = do
let submodulePaths = map (modulePath) (M.elems (submodules minfo))
-- We reexport any submodules.
submoduleExports = map dotWithPrefix submodulePaths
fname = modulePathToFilePath dirPrefix (modulePath minfo) ".hs"
dirname = takeDirectory fname
code = codeToText (moduleCode minfo)
pragmas = languagePragmas (Set.toList $ modulePragmas minfo)
optionsGHC = ghcOptions (Set.toList $ moduleGHCOpts minfo)
prelude = modulePrelude (dotWithPrefix $ modulePath minfo)
(F.toList (moduleExports minfo))
submoduleExports
imports = if ImplicitPrelude `Set.member` moduleFlags minfo
then ""
else moduleImports
pkgRoot = ModulePath (take 1 (modulePathToList $ modulePath minfo))
deps = importDeps pkgRoot (Set.toList $ qualifiedImports minfo)
haddock = moduleHaddock (moduleDoc minfo)
when verbose $ putStrLn ((T.unpack . dotWithPrefix . modulePath) minfo
++ " -> " ++ fname)
createDirectoryIfMissing True dirname
utf8WriteFile fname (T.unlines [pragmas, optionsGHC, haddock,
prelude, imports, deps, code])
when (bootCode minfo /= NoCode) $ do
let bootFName = modulePathToFilePath dirPrefix (modulePath minfo) ".hs-boot"
utf8WriteFile bootFName (genHsBoot minfo)
-- | Generate the .hs-boot file for the given module.
genHsBoot :: ModuleInfo -> Text
genHsBoot minfo =
"module " <> (dotWithPrefix . modulePath) minfo <> " where\n\n" <>
moduleImports <> "\n" <>
codeToText (bootCode minfo)
-- | Construct the filename corresponding to the given module.
modulePathToFilePath :: Maybe FilePath -> ModulePath -> FilePath -> FilePath
modulePathToFilePath dirPrefix (ModulePath mp) ext =
joinPath (fromMaybe "" dirPrefix : "GI" : map T.unpack mp) ++ ext
-- | Write down the code for a module and its submodules to disk under
-- the given base directory. It returns the list of written modules.
writeModuleTree :: Bool -> Maybe FilePath -> ModuleInfo -> IO [Text]
writeModuleTree verbose dirPrefix minfo = do
submodulePaths <- concat <$> forM (M.elems (submodules minfo))
(writeModuleTree verbose dirPrefix)
writeModuleInfo verbose dirPrefix minfo
return $ (dotWithPrefix (modulePath minfo) : submodulePaths)
-- | Return the list of modules `writeModuleTree` would write, without
-- actually writing anything to disk.
listModuleTree :: ModuleInfo -> [Text]
listModuleTree minfo =
let submodulePaths = concatMap listModuleTree (M.elems (submodules minfo))
in dotWithPrefix (modulePath minfo) : submodulePaths