Properly distinguish enclosing deriving-clause parentheses in the GHC AST
Consider this program:
data D1 = MkD1 deriving Eq
data D2 = MkD2 deriving (Eq)
These two deriving clauses, while very similar, have slightly different syntax. Surprisingly, this difference is not reflected in the AST, as compiling this program with -ddump-parsed-ast will reveal that both deriving clauses are represented like so:
[({ Foo.hs:3:16-26 }
(HsDerivingClause
(NoExtField)
(Nothing)
({ Foo.hs:3:25-26 }
[(HsIB
(NoExtField)
({ Foo.hs:3:25-26 }
(HsTyVar
(NoExtField)
(NotPromoted)
({ Foo.hs:3:25-26 }
(Unqual
{OccName: Eq})))))])))]
This is rather annoying for several reasons:
-
This makes pretty-printing more laborious than it needs to be. Consider this code from
GHC.Hs.Decls:instance OutputableBndrId p => Outputable (HsDerivingClause (GhcPass p)) where ppr (HsDerivingClause { deriv_clause_strategy = dcs , deriv_clause_tys = L _ dct }) = hsep [ text "deriving" , pp_strat_before , pp_dct dct , pp_strat_after ] where -- This complexity is to distinguish between -- deriving Show -- deriving (Show) pp_dct [HsIB { hsib_body = ty }] = ppr (parenthesizeHsType appPrec ty) pp_dct _ = parens (interpp'SP dct)As the comment indicates, we have to have a special case just for
derivingclauses with one class to get the parentheses right. This doesn't even preserve the user-written syntax, however, as this would pretty-print bothderiving Eqandderiving (Eq)asderiving Eq. Ugh.A similar problem arises in
GHC.ThToHs.cvtContext, which is used to convert aderivingclause in the TH AST to that of one in the GHC AST. Currently, this code strategically inserts parentheses aroundLHsContexts with only one class. (SeeGHC.Hs.Type.parenthesizeHsContext.) -
This omits a semantically important piece of information from the AST. GHC will parse
deriving (C a), but notderiving C a. However, when GHC parsesderiving (C a), it doesn't record the fact thatC awas surrounded by parentheses. -
This invites confusion between the enclosing parentheses in a
derivingclause andHsParTy, which are semantically different. When one writesderiving (Eq, Show), those enclosing parentheses aren't anHsParTy, but rather an entirely different syntactic form. Unfortunately, this distinction is lost in the AST. When one writesderiving ((Eq)), this gets turned into the following AST:[({ Foo.hs:3:16-30 } (HsDerivingClause (NoExtField) (Nothing) ({ Foo.hs:3:25-30 } [(HsIB (NoExtField) ({ Foo.hs:3:26-29 } (HsParTy (NoExtField) ({ Foo.hs:3:27-28 } (HsTyVar (NoExtField) (NotPromoted) ({ Foo.hs:3:27-28 } (Unqual {OccName: Eq})))))))])))]If you're not careful, it's easily to be misled into thinking that this represents
deriving (Eq)!
I propose to fix this infelicity by adding an extra field to HsDerivingClause of type DerivClauseEnclosingParens:
data DerivClauseEnclosingParens
= Parenthesized
| NotParenthesized
As the name suggests, this will be Parenthesized when enclosing parentheses are present and NotParenthesized otherwise. Consulting this field will avoid the need for awkward special cases when pretty-printing or converting deriving clauses and make it clearer exactly what the AST represents.