packages feed

tilia-0.0.1.0: src/Tilia/Render/Data.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

-- | Data types and type synonyms.
--
-- One declaration form covers a great deal of ground here—@data@,
-- @newtype@, @type data@, ordinary constructors, record constructors, GADT
-- constructors, and instances of all of them—which is why this reads as a
-- series of decisions rather than as a single shape. The decisions are:
-- whether the constructors are written in GADT style, whether there is
-- exactly one and it is a record, and whether anything is documented with a
-- Haddock that takes whole lines.
module Tilia.Render.Data
  ( dataDecl,
    synDecl,
  )
where

import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Maybe (isJust, isNothing, mapMaybe, maybeToList)
import GHC.Hs
import GHC.Types.Fixity (LexicalFixity (..))
import GHC.Types.ForeignCall (CType (..), Header (..))
import GHC.Types.Name.Reader (RdrName)
import GHC.Types.SrcLoc (GenLocated (..), unLoc)
import Tilia.Doc.Combinators
import Tilia.Render.Context
import Tilia.Render.Haddock
import Tilia.Render.Layout
import Tilia.Render.Name
import Tilia.Render.Type
import Tilia.Span
import Tilia.Span.Ghc

-- | A @data@, @newtype@ or @type data@ declaration, or an instance of one.
--
-- The type variables are left abstract because a data instance is applied to
-- types rather than to variables, and the two are otherwise printed
-- identically.
dataDecl ::
  Ctx ->
  FamilyStyle ->
  -- | The type constructor
  LocatedN RdrName ->
  -- | What it is applied to
  [tyVar] ->
  -- | Where each of those was
  (tyVar -> Maybe Span) ->
  -- | How to print one
  (tyVar -> Doc) ->
  -- | Was the head written infix?
  LexicalFixity ->
  -- | The @forall@ a family instance may bind its variables with, which an
  -- ordinary declaration does not have and passes as 'mempty'.
  Doc ->
  HsDataDefn GhcPs ->
  Doc
dataDecl ctx style tyCon tyVars tyVarSpan renderTyVar fixity outerBinders HsDataDefn {..} =
  txt keyword <> txt instanceWord <> 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
        <> foldMap spanOf dd_kindSig
        <> foldMap spanOf dd_ctxt
        <> foldMap spanOf dd_cType

    header =
      layoutFrom ctx wholeHeadSpan . indent $
        foreignType
          <> breakOrSpace
          <> outerBinders
          <> foldMap (leftContext ctx) dd_ctxt
          <> layoutFrom
            ctx
            headSpan
            (defHead (fixity == Infix) True (name ctx tyCon) (map renderTyVar tyVars))
          <> foldMap kindSignature dd_kindSig

    kindSignature k =
      joinedBy "::" <> indent (hsType ctx k)

    -- The @{-# CTYPE … #-}@ pragma of a foreign data type.
    foreignType = case unLoc <$> dd_cType of
      Nothing -> mempty
      Just (CType prag header' (type_, _)) ->
        breakOrSpace
          <> sourceText prag
          <> foldMap (\(Header h _) -> space <> sourceText h) header'
          <> space
          <> sourceText type_
          <> txt " #-}"

    cons = case dd_cons of
      NewTypeCon c -> [c]
      DataTypeCons _ cs -> cs

    -- A kind signature on the head, or any constructor written with a
    -- signature of its own, means the whole declaration is in GADT style.
    isGadt = isJust dd_kindSig || any (isGadtCon . unLoc) cons

    constructors = case cons of
      [] -> mempty
      (firstCon : _)
        | isGadt ->
            indent $
              layoutFrom ctx wholeHeadSpan (breakOrSpace <> txt "where")
                <> breakOrSpace
                -- Braces once there is a semicolon to protect: written flat
                -- the @where@ block has no column to end at, so anything
                -- after the declaration would be read as another
                -- constructor. One constructor needs no separator and so no
                -- braces.
                <> items
                  (if null (drop 1 cons) then NoBrace else MayBrace)
                  (map (at_ ctx (conDecl ctx False)) cons)
        | otherwise ->
            layoutFrom ctx (spanOf tyCon <> spansOf cons) . indent $
              beforeEquals <> txt "=" <> space <> alternatives
        where
          -- A single record constructor is laid out as one thing with the
          -- @=@, since there is no choice of constructor to present.
          singleRecCon = case cons of
            [L _ ConDeclH98 {con_args = RecCon {}}] -> True
            _ -> False
          compactAroundEquals =
            sameLine (spanOf tyCon) (conNamesSpan (unLoc firstCon))
          conNamesSpan = \case
            ConDeclGADT {..} -> spansOf (NE.toList con_names)
            ConDeclH98 {..} -> spanOf con_name

          -- Documentation written as @--@ lines owns the rest of the line
          -- it starts, so nothing can follow it and the constructors go one
          -- to a line. Written as @{- | … -}@ it closes itself and asks
          -- nothing of the layout.
          lineHaddocks = any (printsWholeLineDocs ctx . visibleDocs . unLoc) cons

          beforeEquals
            | lineHaddocks = hardBreak
            | singleRecCon && compactAroundEquals = space
            | otherwise = breakOrSpace

          separator
            | lineHaddocks = hardBreak <> txt "|" <> space
            | otherwise = breakOrSpace <> txt "|" <> space

          keepTogether
            | lineHaddocks || not singleRecCon = align
            | otherwise = id

          alternatives =
            sepBy separator (map (keepTogether . at_ ctx (conDecl ctx singleRecCon)) cons)

    derivings =
      includeUnless (null dd_derivs) beforeDerivings
        <> indent (vsep (map (at_ ctx (derivingClause ctx)) dd_derivs))
    beforeDerivings
      | length dd_derivs > 1 = hardBreak
      | otherwise = breakOrSpace

-- | The documentation a constructor's own layout has to make room for.
--
-- Which is its Haddock and the ones on its prefix arguments, and nothing
-- deeper. A field of a record gets a line of its own wherever the @=@ ends
-- up, so a Haddock on one of those settles nothing about the constructor
-- around it and is left out of the question.
visibleDocs :: ConDecl GhcPs -> [LHsDoc GhcPs]
visibleDocs = \case
  ConDeclH98 {..} ->
    maybeToList con_doc <> case con_args of
      PrefixCon xs -> mapMaybe cdf_doc xs
      _ -> []
  ConDeclGADT {} -> []

isGadtCon :: ConDecl GhcPs -> Bool
isGadtCon = \case
  ConDeclGADT {} -> True
  ConDeclH98 {} -> False

----------------------------------------------------------------------------
-- Constructors

-- | One constructor.
conDecl :: Ctx -> Bool -> ConDecl GhcPs -> Doc
conDecl ctx _ ConDeclGADT {..} =
  foldMap (haddock ctx Pipe Closed) con_doc
    <> layoutFrom ctx declSpan (brokenIfDocumented ctx documented body)
  where
    -- Every part of the signature shares one layout decision, so a Haddock
    -- anywhere in it puts the whole of it on several lines.
    documented = (con_g_args, con_res_ty)

    c :| cs = con_names
    body =
      name ctx c
        <> includeUnless
          (null cs)
          (indent (comma <> breakOrSpace <> commaSep (map (name ctx) cs)))
        <> joinedBy "::"
        <> 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
           )
        <> foldMap (\tele -> forallTelescope ctx tele <> breakOrSpace) con_inner_bndrs
        <> foldMap
          (\qs -> context ctx qs <> joinedBy "=>")
          con_mb_cxt
        <> layoutFrom ctx argResSpan (brokenIfDocumented ctx documented argsAndResult)

    argsAndResult = arguments <> resultType

    -- GHC keeps a GADT's result type without the brackets it was written
    -- with, and there is one shape that does not survive losing them. A
    -- kind signature needs them back: @MkT :: Int -> T :: Star@ reads as a
    -- second signature on the constructor rather than as a kind on its
    -- result, and does not parse at all.
    resultType = case unLoc con_res_ty of
      HsKindSig {} -> parens (hsType ctx con_res_ty)
      HsForAllTy {} | standsAlone -> parens (hsType ctx con_res_ty)
      HsQualTy {} | standsAlone -> parens (hsType ctx con_res_ty)
      _ -> hsType ctx con_res_ty
    standsAlone = case (unLoc con_outer_bndrs, con_g_args) of
      (HsOuterImplicit {}, PrefixConGADT _ []) ->
        null con_inner_bndrs && null con_mb_cxt
      _ -> False
    arguments = case con_g_args of
      PrefixConGADT NoExtField xs -> foldMap argument xs
      RecConGADT _ x ->
        recordFieldsAt ctx x <> joinedBy "->"
    argument x =
      documentedConDeclField ctx x
        <> space
        <> multiplicity (hsType ctx) (cdf_multiplicity x)
        <> joinedBy "->"

    declSpan = spansOf (NE.toList con_names) <> sigSpan
    sigSpan = spanOf con_outer_bndrs <> foldMap spanOf con_mb_cxt <> argResSpan
    argResSpan =
      spanOf con_res_ty <> case con_g_args of
        PrefixConGADT NoExtField xs -> spansOf (map cdf_type xs)
        RecConGADT _ x -> spanOf x
conDecl ctx singleRecCon ConDeclH98 {..} = case con_args of
  PrefixCon xs ->
    ownDoc
      <> existentials
      <> layoutFrom
        ctx
        declSpan
        ( brokenIfDocumented ctx xs $
            name ctx con_name
              <> includeUnless (null xs) breakOrSpace
              <> indent (align (sepBy breakOrSpace (map (align . documentedConDeclField ctx) xs)))
        )
  RecCon l ->
    ownDoc
      <> existentials
      <> layoutFrom
        ctx
        declSpan
        ( name ctx con_name
            <> breakOrSpace
            <> nest (if singleRecCon then 0 else 1) (recordFieldsAt ctx l)
        )
  InfixCon l r ->
    -- The constructor's own Haddock can only go above the whole constructor
    -- when neither argument has one of its own; otherwise it goes between
    -- them, next to the name.
    includeWhen docOnTop ownDoc
      <> existentials
      <> layoutFrom
        ctx
        declSpan
        ( leftArgument l
            <> indent
              ( includeUnless docOnTop ownDoc
                  <> name ctx con_name
                  <> rightDoc r
                  <> conDeclField ctx r
              )
        )
    where
      docOnTop = isNothing (cdf_doc l) && isNothing (cdf_doc r)
      -- The left argument's Haddock may use pipe style only when the
      -- constructor itself is documented, since otherwise there is nothing
      -- above it for the pipe to point at.
      leftArgument x
        | isJust con_doc =
            foldMap (haddock ctx Pipe Closed) (cdf_doc x)
              <> conDeclField ctx x
              <> breakOrSpace
        | otherwise =
            conDeclField ctx x
              <> case cdf_doc x of
                Just d -> space <> haddock ctx Caret Closed d
                Nothing -> breakOrSpace
      rightDoc x = case cdf_doc x of
        Just d -> hardBreak <> haddock ctx Pipe Closed d
        Nothing -> breakOrSpace
  where
    ownDoc = foldMap (haddock ctx Pipe Closed) con_doc

    existentials =
      layoutFrom ctx contextSpan $
        includeWhen
          con_forall
          (forallBndrs ctx Invisible (tyVarBndr ctx) con_ex_tvs <> breakOrSpace)
          <> foldMap (leftContext ctx) con_mb_cxt

    contextSpan =
      spanOfSrcSpan (getHasLoc (acdh_forall con_ext))
        <> spansOf con_ex_tvs
        <> foldMap spanOf con_mb_cxt
        <> spanOf con_name

    declSpan = spanOf con_name <> argSpans
    argSpans = case con_args of
      PrefixCon xs -> spansOf (map cdf_type xs)
      RecCon l -> spanOf l
      InfixCon x y -> spansOf (map 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 "=>"

----------------------------------------------------------------------------
-- Deriving clauses

derivingClause :: Ctx -> HsDerivingClause GhcPs -> Doc
derivingClause ctx HsDerivingClause {..} =
  brokenIfDocumented ctx deriv_clause_tys $
    txt "deriving" <> space <> strategy
  where
    what =
      at ctx deriv_clause_tys $ \tys ->
        brokenIfDocumented ctx tys $ case tys of
          DctSingle NoExtField sigTy -> parens (hsSigType ctx sigTy)
          DctMulti NoExtField sigTys ->
            parens (commaSep (map (align . hsSigType ctx) sigTys))

    strategy = case deriv_clause_strategy of
      Nothing -> breakOrSpace <> indent what
      Just (L _ s) -> case s of
        StockStrategy _ -> named "stock"
        AnyclassStrategy _ -> named "anyclass"
        NewtypeStrategy _ -> named "newtype"
        ViaStrategy (XViaStrategyPs _ sigTy) ->
          breakOrSpace
            <> indent
              ( what
                  <> breakOrSpace
                  <> txt "via"
                  <> space
                  <> hsSigType ctx sigTy
              )
      where
        named kw = txt kw <> breakOrSpace <> indent what

----------------------------------------------------------------------------
-- Type synonyms

-- | @type T a = …@.
synDecl ::
  Ctx ->
  LocatedN RdrName ->
  LexicalFixity ->
  LHsQTyVars GhcPs ->
  LHsType GhcPs ->
  Doc
synDecl ctx tyCon fixity HsQTvs {..} rhs =
  txt "type"
    <> space
    <> layoutFrom
      ctx
      (spanOf tyCon <> spansOf hsq_explicit)
      (defHead (fixity == Infix) True (name ctx tyCon) (map (at_ ctx (tyVarBndr ctx)) hsq_explicit))
    <> indent (space <> txt "=" <> separator <> hsType ctx rhs)
  where
    separator
      | typeIsDocumented (unLoc rhs) = hardBreak
      | otherwise = breakOrSpace