tilia-0.0.1.0: src/Tilia/Render/Type.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
-- | Types.
module Tilia.Render.Type
( -- * Types
hsType,
hsTypeBody,
hsSigType,
hsSigTypeBody,
typeAscription,
-- * Contexts
context,
contextOf,
-- * Binders
TyVarBndrFlag (..),
tyVarBndr,
Visibility (..),
forallBndrs,
forallTelescope,
outerBndrs,
-- * Record fields
recordFieldsAt,
conDeclField,
documentedConDeclField,
strictness,
-- * Arguments
typeArgument,
typeArgSpan,
-- * Asking about a type
typeIsDocumented,
-- * Conversions
asSigType,
)
where
import Data.List.NonEmpty (NonEmpty (..))
import Data.Text qualified as T
import GHC.Hs
import GHC.Types.Name.Occurrence (isTvOcc)
import GHC.Types.Name.Reader (RdrName, rdrNameOcc)
import GHC.Types.SourceText
import GHC.Types.SrcLoc (GenLocated (..), getLoc, unLoc)
import GHC.Types.Var (Specificity (..))
import Tilia.Doc.Combinators
import Tilia.Fixity (Namespace (..))
import Tilia.Render.Context
import Tilia.Render.Haddock
import Tilia.Render.Literal (stringLiteral)
import Tilia.Render.Name
import Tilia.Render.Operator
import Tilia.Span
import Tilia.Span.Ghc
----------------------------------------------------------------------------
-- Types
-- | A type.
hsType :: Ctx -> LHsType GhcPs -> Doc
hsType ctx l = at ctx l (hsTypeBody ctx (spanOf l))
-- | A type whose location the caller has already entered.
hsTypeBody :: Ctx -> Maybe Span -> HsType GhcPs -> Doc
hsTypeBody ctx here t = typeBody ctx (typeIsDocumented t) here t
-- | The body of a type, with the decision about its arguments handed down.
--
-- A type one of whose arguments carries documentation cannot keep its arrows
-- on one line: the Haddock takes the rest of the line with it. So the
-- question is settled once, at the outermost type, and passed inwards—a
-- nested arrow has to know what the whole signature decided rather than what
-- its own subtree would have decided on its own.
typeBody :: Ctx -> Bool -> Maybe Span -> HsType GhcPs -> Doc
typeBody ctx documented here = \case
HsForAllTy _ tele t ->
forallTelescope ctx tele <> betweenArgs <> hsType ctx t
HsQualTy _ qs t ->
context ctx qs
<> space
<> txt "=>"
<> betweenArgs
<> case unLoc t of
-- A nested context or arrow inherits the outer type's decision
-- rather than making a fresh one, so a signature breaks all of its
-- arrows or none of them.
HsQualTy {} -> recur (unLoc t)
HsFunTy {} -> hsType ctx t
_ -> at ctx t recur
HsTyVar _ promoted n -> promotion promoted n <> name ctx n
HsAppTy _ f x ->
let (func, args) = gatherAppArgs f [x]
in layoutFrom ctx (spanOf f <> spansOf args) . align $
hsType ctx func
<> breakOrSpace
<> indent (sepBy breakOrSpace (map (hsType ctx) args))
HsAppKindTy _ ty kd ->
align (hsType ctx ty <> breakOrSpace <> indent (txt "@" <> hsType ctx kd))
HsFunTy _ multAnn x y ->
hsType ctx x
<> space
<> multiplicity (at_ ctx recur) multAnn
<> space
<> txt "->"
<> betweenArgs
<> case unLoc y of
HsFunTy {} -> recur (unLoc y)
_ -> at ctx y recur
HsListTy _ t ->
layoutWithin ctx here (spanOf t) $
brackets (insideBrackets here (hsType ctx t))
HsTupleTy _ sort xs ->
layoutWithin ctx here (spansOf xs) $
tupleBrackets sort (insideBrackets here (commaSep (map (align . hsType ctx) xs)))
HsSumTy _ xs ->
unboxed (sepBy (joinedBy "|") (map (align . hsType ctx) xs))
HsOpTy _ _ x op y -> typeChain ctx x op y
HsParTy _ t ->
layoutWithin ctx here (spanOf t) (parens (insideBrackets here (hsType ctx t)))
HsIParamTy _ n t ->
align (at ctx n outputable <> joinedBy "::" <> indent (hsType ctx t))
HsStarTy _ _ -> txt "*"
HsKindSig _ t k ->
align (hsType ctx t <> joinedBy "::" <> indent (hsType ctx k))
HsSpliceTy _ splice -> knotSplice (ctxKnot ctx) ctx DollarSplice splice
HsDocTy _ t str -> haddockInline ctx Pipe str <> hsType ctx t
HsExplicitListTy _ promoted xs ->
tick promoted
<> brackets (insideBrackets here (quoteGap promoted xs <> commaSep (map (align . hsType ctx) xs)))
HsExplicitTupleTy _ promoted xs ->
tick promoted
<> parens (insideBrackets here (quoteGap promoted xs <> commaSep (map (hsType ctx) xs)))
HsTyLit _ t -> case t of
HsStrTy (SourceText s) _ -> stringLiteral s
other -> outputable other
HsWildCardTy _ -> txt "_"
XHsType ext -> case ext of
HsCoreTy t -> outputable t
HsBangTy _ (HsSrcBang _ unpacked strict) t ->
unpackPragma unpacked <> strictness strict <> hsType ctx t
-- A bare record type has no wrapper of its own, so there is no span to
-- anchor a comment written inside empty braces to.
HsRecTy _ fields -> recordFields ctx Nothing fields
where
recur = typeBody ctx documented Nothing
betweenArgs = if documented then hardBreak else breakOrSpace
----------------------------------------------------------------------------
-- Operator chains
-- | A chain of type operators, regrouped by precedence.
typeChain :: Ctx -> LHsType GhcPs -> LocatedN RdrName -> LHsType GhcPs -> Doc
typeChain ctx x op y =
renderChain ctx (uncurry (associate fixity) (flattenAround split x op y))
where
split t = case unLoc t of
HsOpTy _ _ l o r -> Just (l, o, r)
_ -> Nothing
fixity o = operatorFixity ctx InTypes (unLoc o)
renderChain :: Ctx -> OpChain (LHsType GhcPs) (LocatedN RdrName) -> Doc
renderChain ctx = \case
Operand t -> hsType ctx t
chain@(Chain (firstOne :| rest) operators) ->
layoutFrom ctx (chainSpan spanOf chain) $
renderChain ctx firstOne <> mconcat (zipWith piece operators rest)
where
-- Type operators have no hanging form: no type absorbs a line break the
-- way a @do@ block does, so a broken chain always indents.
piece op operand =
attach Normal (name ctx op <> space <> renderChain ctx operand)
----------------------------------------------------------------------------
-- Pieces of a type
-- | Gather a nest of applications into a head and its arguments.
--
-- The tree is built one argument at a time, which would lay @F a b c@ out as
-- though each application were a separate decision. Collecting them first
-- lets the whole application break as one.
gatherAppArgs :: LHsType GhcPs -> [LHsType GhcPs] -> (LHsType GhcPs, [LHsType GhcPs])
gatherAppArgs f known = case unLoc f of
HsAppTy _ l r -> gatherAppArgs l (r : known)
_ -> (f, known)
tupleBrackets :: HsTupleSort -> Doc -> Doc
tupleBrackets = \case
HsUnboxedTuple -> unboxed
HsBoxedOrConstraintTuple -> parens
tick :: PromotionFlag -> Doc
tick = \case
IsPromoted -> txt "'"
NotPromoted -> mempty
-- | The tick on a promoted name, held off it when the name itself begins
-- with one, since @''@ is the spelling of a type-level quote.
promotion :: PromotionFlag -> LocatedN RdrName -> Doc
promotion NotPromoted _ = mempty
promotion IsPromoted n = txt "'" <> includeWhen (beginsWithTick (showGhc (unLoc n))) space
where
beginsWithTick shown = case T.uncons (T.drop 1 shown) of
Just ('\'', _) -> True
_ -> False
-- | A promoted list or tuple whose first element is itself promoted needs a
-- space, or @'['a]@ would begin with a character literal.
quoteGap :: PromotionFlag -> [LHsType GhcPs] -> Doc
quoteGap IsPromoted (t : _) | startsWithTick (unLoc t) = space
quoteGap _ _ = mempty
startsWithTick :: HsType GhcPs -> Bool
startsWithTick = \case
HsAppTy _ (L _ f) _ -> startsWithTick f
HsTyVar _ IsPromoted _ -> True
HsExplicitTupleTy {} -> True
HsExplicitListTy {} -> True
HsTyLit _ HsCharTy {} -> True
_ -> False
-- | The pragma asking for a field to be unpacked, or not to be.
unpackPragma :: SrcUnpackedness -> Doc
unpackPragma = \case
SrcUnpack -> txt "{-# UNPACK #-}" <> space
SrcNoUnpack -> txt "{-# NOUNPACK #-}" <> space
NoSrcUnpack -> mempty
-- | The @!@ or @~@ in front of a field.
strictness :: SrcStrictness -> Doc
strictness = \case
SrcLazy -> txt "~"
SrcStrict -> txt "!"
NoSrcStrict -> mempty
-- | Does any argument of this type carry documentation?
typeIsDocumented :: HsType GhcPs -> Bool
typeIsDocumented = any documented . spine
where
documented = \case
HsDocTy {} -> True
_ -> False
-- | The pieces of a type that a signature would put on lines of their own:
-- the argument and result types, and whatever a @forall@ or a context is
-- wrapped around.
--
-- Not the types nested inside those. A Haddock written on an element of a
-- list argument documents the element and says nothing about how the
-- signature it sits in should be laid out.
spine :: HsType GhcPs -> [HsType GhcPs]
spine t =
t : case t of
HsFunTy _ _ a b -> spine (unLoc a) <> spine (unLoc b)
HsForAllTy _ _ b -> spine (unLoc b)
HsQualTy _ _ b -> spine (unLoc b)
_ -> []
----------------------------------------------------------------------------
-- Contexts
-- | A class context, as it appears before a @=>@.
context :: Ctx -> LHsContext GhcPs -> Doc
context ctx = at_ ctx (contextOf loneVariable (hsType ctx) . map unbracket)
-- | Is this constraint nothing but a type variable?
loneVariable :: LHsType GhcPs -> Bool
loneVariable t = case unLoc t of
HsTyVar _ _ (L _ n) -> isTvOcc (rdrNameOcc n)
_ -> False
-- | A constraint without the brackets a context puts around it anyway.
--
-- Stripped before the context writes its own, or formatting would add a
-- layer every time it ran.
unbracket :: LHsType GhcPs -> LHsType GhcPs
unbracket t = case unLoc t of
HsParTy _ inner -> unbracket inner
_ -> t
-- | A context over anything that can stand as a constraint.
contextOf ::
-- | Is this constraint nothing but a variable?
(a -> Bool) ->
(a -> Doc) ->
[a] ->
Doc
contextOf lone render = \case
[] -> txt "()"
[x] | lone x -> render x
xs -> parens (commaSep (map (align . render) xs))
----------------------------------------------------------------------------
-- Binders
-- | The flags a type variable binder may carry.
--
-- Three kinds of binder exist with three different flag types, and each
-- decides both whether the binder is inferred—which is what braces around it
-- mean—and whether anything is printed in front of it.
class TyVarBndrFlag flag where
flagIsInferred :: flag -> Bool
flagPrefix :: flag -> Doc
flagPrefix _ = mempty
instance TyVarBndrFlag () where
flagIsInferred () = False
instance TyVarBndrFlag Specificity where
flagIsInferred = \case
InferredSpec -> True
SpecifiedSpec -> False
instance TyVarBndrFlag (HsBndrVis GhcPs) where
flagIsInferred _ = False
flagPrefix = \case
HsBndrRequired NoExtField -> mempty
HsBndrInvisible _ -> txt "@"
-- | One type variable binder.
tyVarBndr :: (TyVarBndrFlag flag) => Ctx -> HsTyVarBndr flag GhcPs -> Doc
tyVarBndr ctx HsTvb {..} = flagPrefix tvb_flag <> enclosed (binder <> kind)
where
binder = case tvb_var of
HsBndrVar _ x -> name ctx x
HsBndrWildCard _ -> txt "_"
-- Whether a kind is written and whether brackets are needed are the same
-- question, so they are answered together.
(kind, kinded) = case tvb_kind of
HsBndrNoKind _ -> (mempty, False)
HsBndrKind _ k ->
(joinedBy "::" <> indent (hsType ctx k), True)
enclosed
| flagIsInferred tvb_flag = braces
| kinded = parens
| otherwise = id
-- | Whether a @forall@ binds visibly.
data Visibility
= -- | @forall a.@
Invisible
| -- | @forall a ->@
Visible
deriving (Eq, Show)
-- | The variables of a @forall@, with the punctuation that closes it.
forallBndrs ::
(HasLoc l) =>
Ctx ->
Visibility ->
(a -> Doc) ->
[GenLocated l a] ->
Doc
forallBndrs _ Invisible _ [] = txt "forall."
forallBndrs _ Visible _ [] = txt "forall ->"
forallBndrs ctx visibility render bndrs =
layoutAcross ctx bndrs $
txt "forall"
<> breakOrSpace
<> indent (align (sepBy breakOrSpace (map (align . at_ ctx render) bndrs)) <> close)
where
close = case visibility of
Invisible -> txt "."
Visible -> space <> txt "->"
-- | The @forall@ that opens a type.
forallTelescope :: Ctx -> HsForAllTelescope GhcPs -> Doc
forallTelescope ctx = \case
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
HsOuterImplicit _ -> mempty
HsOuterExplicit _ bndrs -> forallTelescope ctx (mkHsForAllInvisTele noAnn bndrs)
----------------------------------------------------------------------------
-- Signatures
-- | A type together with whatever it quantifies over.
hsSigType :: Ctx -> LHsSigType GhcPs -> Doc
hsSigType ctx = at_ ctx (hsSigTypeBody ctx)
-- | 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
where
afterBinders
| typeIsDocumented (unLoc sig_body) = hardBreak
| otherwise = breakOrSpace
-- | The @:: t@ that follows a name.
--
-- A signature with documentation in it breaks unconditionally, since a
-- Haddock on the first argument would otherwise take the @::@ with it.
typeAscription :: Ctx -> LHsSigType GhcPs -> Doc
typeAscription ctx sigType =
indent (space <> txt "::" <> separator <> hsSigType ctx sigType)
where
separator
| typeIsDocumented (unLoc (sig_body (unLoc sigType))) = hardBreak
| otherwise = breakOrSpace
-- | Give a plain type the shape of a signature type.
asSigType :: LHsType GhcPs -> LHsSigType GhcPs
asSigType ty = L (getLoc ty) (HsSig NoExtField (HsOuterImplicit NoExtField) ty)
----------------------------------------------------------------------------
-- Record fields
-- | The braces of a record, and the fields inside them.
recordFieldsAt :: Ctx -> XRec GhcPs [LHsConDeclRecField GhcPs] -> Doc
recordFieldsAt ctx l = at ctx l (recordFields ctx (spanOf l))
-- | The fields of a record.
--
-- A record with no fields still needs something between its braces for a
-- comment written there to attach to, or the comment would be pushed outside
-- them and end up documenting the constructor.
recordFields :: Ctx -> Maybe Span -> [LHsConDeclRecField GhcPs] -> Doc
recordFields ctx enclosing xs =
brokenIfDocumented ctx xs . braces . insideBrackets enclosing $
commaSep (map (align . at_ ctx (recordField ctx)) xs)
recordField :: Ctx -> HsConDeclRecField GhcPs -> Doc
recordField ctx HsConDeclRecField {..} =
foldMap (haddockInline ctx Pipe) (cdf_doc cdrf_spec)
<> align (commaSep (map (at_ ctx (name ctx . foLabel)) cdrf_names))
<> space
<> multiplicity (hsType ctx) (cdf_multiplicity cdrf_spec)
<> joinedBy "::"
<> align (indent (conDeclField ctx cdrf_spec))
-- | A constructor field, without its documentation or its multiplicity.
--
-- Those two are left to the caller because there is no one place they
-- belong: a record field puts the multiplicity before the @::@ and a GADT
-- argument puts it before the arrow.
conDeclField :: Ctx -> HsConDeclField GhcPs -> Doc
conDeclField ctx CDF {..} =
unpackPragma cdf_unpack
<> at ctx cdf_type (\ty -> strictness cdf_bang <> hsTypeBody ctx (spanOf cdf_type) ty)
-- | A constructor field with its documentation in front of it.
documentedConDeclField :: Ctx -> HsConDeclField GhcPs -> Doc
documentedConDeclField ctx cdf =
foldMap (haddockInline ctx Pipe) (cdf_doc cdf) <> conDeclField ctx cdf
----------------------------------------------------------------------------
-- Arguments
-- | One argument on the left of a family or data instance.
typeArgument :: Ctx -> LHsTypeArg GhcPs -> Doc
typeArgument ctx = \case
HsValArg NoExtField ty -> hsType ctx ty
-- The annotation holds the span of the @\@@, which is always immediately
-- in front of the type, so nothing is lost by not entering it.
HsTypeArg _ ty -> txt "@" <> hsType ctx ty
HsArgPar _ -> error "Tilia: HsArgPar is not expected in parsed source"
-- | Where an argument was.
typeArgSpan :: LHsTypeArg GhcPs -> Maybe Span
typeArgSpan = spanOfSrcSpan . lhsTypeArgSrcSpan