From 75c29aa1c2a38208a46fc2ab2dd9a6c9c1b6ffb9 Mon Sep 17 00:00:00 2001 From: sheaf <sam.derbyshire@gmail.com> Date: Thu, 13 Mar 2025 15:39:13 +0100 Subject: [PATCH] Reject instance with non-class head in renamer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit modifies rnClsInstDecl so that, when renaming, we reject a class instance declaration in which the head is not a class. Before this change, it would get rejected in the type-checker, but that meant that the renamer could emit unhelpful error messages, e.g.: data Foo m a instance Foo m where fmap _ x = case x of would rather unhelpfully say: ‘fmap’ is not a (visible) method of class ‘Foo’ when of course 'Foo' is not even a class. We now reject the above program with the following error message: Illegal instance for data type ‘Foo’. Instance heads must be of the form C ty_1 ... ty_n where ‘C’ is a class. Fixes #22688 --- compiler/GHC/Rename/Module.hs | 59 +++++++++++++------ compiler/GHC/Tc/Errors/Ppr.hs | 16 ++--- compiler/GHC/Tc/Errors/Types.hs | 15 +++-- compiler/GHC/Tc/Gen/HsType.hs | 6 +- compiler/GHC/Tc/Validity.hs | 7 ++- compiler/GHC/Types/Error/Codes.hs | 4 +- testsuite/tests/rename/should_fail/T22688.hs | 6 ++ .../tests/rename/should_fail/T22688.stderr | 7 +++ testsuite/tests/rename/should_fail/all.T | 1 + 9 files changed, 84 insertions(+), 37 deletions(-) create mode 100644 testsuite/tests/rename/should_fail/T22688.hs create mode 100644 testsuite/tests/rename/should_fail/T22688.stderr diff --git a/compiler/GHC/Rename/Module.hs b/compiler/GHC/Rename/Module.hs index 45d491fb7c6..b1598c9585d 100644 --- a/compiler/GHC/Rename/Module.hs +++ b/compiler/GHC/Rename/Module.hs @@ -1,6 +1,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RecursiveDo #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -9,6 +10,7 @@ {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + {- (c) The GRASP/AQUA Project, Glasgow University, 1992-1998 @@ -56,7 +58,7 @@ import GHC.Types.ForeignCall ( CCallTarget(..) ) import GHC.Types.Name import GHC.Types.Name.Set import GHC.Types.Name.Env -import GHC.Types.Basic ( VisArity, TypeOrKind(..), RuleName ) +import GHC.Types.Basic ( VisArity, TyConFlavour(..), TypeOrKind(..), RuleName ) import GHC.Types.GREInfo (ConLikeInfo (..), ConInfo, mkConInfo, conInfoFields) import GHC.Types.Hint (SigLike(..)) import GHC.Types.Unique.Set @@ -587,22 +589,45 @@ rnClsInstDecl (ClsInstDecl { cid_ext = (inst_warn_ps, _, _) ; (inst_ty', inst_fvs) <- rnHsSigType ctxt TypeLevel inst_ty ; let (ktv_names, _, head_ty') = splitLHsInstDeclTy inst_ty' } - ; let -- Check if there are any nested `forall`s or contexts, which are - -- illegal in the type of an instance declaration (see - -- Note [No nested foralls or contexts in instance types] in - -- GHC.Hs.Type)... - mb_nested_msg = noNestedForallsContextsErr NFC_InstanceHead head_ty' - -- ...then check that the instance head is actually headed by a - -- class type constructor... - eith_cls = case hsTyGetAppHead_maybe head_ty' of - Just (L _ cls) -> Right cls - Nothing -> - Left - ( getLocA head_ty' - , TcRnIllegalInstance $ - IllegalClassInstance (HsTypeRnThing $ unLoc head_ty') $ - IllegalInstanceHead $ InstHeadNonClass Nothing - ) + ; env <- getGlobalRdrEnv + ; let + -- Check if there are any nested `forall`s or contexts, which are + -- illegal in the type of an instance declaration (see + -- Note [No nested foralls or contexts in instance types] in + -- GHC.Hs.Type)... + mb_nested_msg = noNestedForallsContextsErr NFC_InstanceHead head_ty' + -- ...then check if the instance head is actually headed by a + -- class type constructor... + instance_head :: Either IllegalInstanceHeadReason Name + instance_head = + case hsTyGetAppHead_maybe head_ty' of + Just (L _ nm) -> + case lookupGRE_Name env nm of + Just (GRE { gre_info = IAmTyCon flav }) -> + if + | flav == ClassFlavour + -> Right nm + | flav == AbstractTypeFlavour + -> Left $ InstHeadAbstractClass nm + | otherwise + -> Left $ InstHeadNonClassHead $ InstNonClassTyCon nm flav + _ -> + -- The head of the instance head is out of scope; + -- we'll deal with that later. Continue for now. + Right nm + + Nothing -> + Left $ InstHeadNonClassHead InstNonTyCon + eith_cls = + case instance_head of + Right cls -> Right cls + Left illegal_head_reason -> + Left + ( getLocA head_ty' + , TcRnIllegalInstance $ + IllegalClassInstance (HsTypeRnThing $ unLoc head_ty') $ + IllegalInstanceHead illegal_head_reason + ) -- ...finally, attempt to retrieve the class type constructor, failing -- with an error message if there isn't one. To avoid excessive -- amounts of error messages, we will only report one of the errors diff --git a/compiler/GHC/Tc/Errors/Ppr.hs b/compiler/GHC/Tc/Errors/Ppr.hs index 6bb68b5516d..a5d4eefc0e9 100644 --- a/compiler/GHC/Tc/Errors/Ppr.hs +++ b/compiler/GHC/Tc/Errors/Ppr.hs @@ -6364,10 +6364,10 @@ pprIllegalInstanceHeadReason head_ty = \case text "and each type variable appears at most once in the instance head."] InstHeadMultiParam -> with_illegal_instance_header head_ty $ parens $ text "Only one type can be given in an instance head." - InstHeadAbstractClass clas -> + InstHeadAbstractClass cls -> text "Cannot define instance for abstract class" <+> - quotes (ppr (className clas)) - InstHeadNonClass bad_head -> + quotes (ppr cls) + InstHeadNonClassHead bad_head -> vcat [ text "Illegal" <+> what_illegal <> dot , text "Instance heads must be of the form" , nest 2 $ text "C ty_1 ... ty_n" @@ -6375,9 +6375,9 @@ pprIllegalInstanceHeadReason head_ty = \case ] where what_illegal = case bad_head of - Just tc -> - text "instance for" <+> ppr (tyConFlavour tc) <+> quotes (ppr $ tyConName tc) - Nothing -> + InstNonClassTyCon tc_nm flav -> + text "instance for" <+> ppr flav <+> quotes (ppr tc_nm) + InstNonTyCon -> text "head of an instance declaration:" <+> quotes (ppr head_ty) with_illegal_instance_header :: TypedThing -> SDoc -> SDoc @@ -6479,7 +6479,7 @@ illegalInstanceHeadHints = \case [suggestExtension LangExt.MultiParamTypeClasses] InstHeadAbstractClass {} -> noHints - InstHeadNonClass {} -> + InstHeadNonClassHead {} -> noHints illegalInstanceHeadReason :: IllegalInstanceHeadReason -> DiagnosticReason @@ -6487,7 +6487,7 @@ illegalInstanceHeadReason = \case -- These are serious InstHeadAbstractClass {} -> ErrorWithoutFlag - InstHeadNonClass {} -> + InstHeadNonClassHead {} -> ErrorWithoutFlag -- These are less serious (enable an extension) diff --git a/compiler/GHC/Tc/Errors/Types.hs b/compiler/GHC/Tc/Errors/Types.hs index 9bacf1fad41..d97090459ff 100644 --- a/compiler/GHC/Tc/Errors/Types.hs +++ b/compiler/GHC/Tc/Errors/Types.hs @@ -147,6 +147,7 @@ module GHC.Tc.Errors.Types ( , InvalidFamInstQTv(..), InvalidFamInstQTvReason(..) , InvalidAssoc(..), InvalidAssocInstance(..) , InvalidAssocDefault(..), AssocDefaultBadArgs(..) + , InstHeadNonClassHead(..) -- * Template Haskell errors , THError(..), THSyntaxError(..), THNameError(..) @@ -4809,7 +4810,7 @@ data IllegalInstanceHeadReason -- f :: a -- -- Test cases: typecheck/should_fail/T13068 - = InstHeadAbstractClass !Class + = InstHeadAbstractClass !Name -- ^ name of the abstract 'Class' -- | An instance whose head is not a class. -- -- Examples(s): @@ -4829,10 +4830,7 @@ data IllegalInstanceHeadReason -- rename/should_fail/T18240a -- polykinds/T13267 -- deriving/should_fail/T23522 - | InstHeadNonClass - !(Maybe TyCon) -- ^ the 'TyCon' at the head of the instance head, - -- or 'Nothing' if the instance head is not even headed - -- by a 'TyCon' + | InstHeadNonClassHead InstHeadNonClassHead -- | Instance head was headed by a type synonym. -- @@ -4863,6 +4861,13 @@ data IllegalInstanceHeadReason deriving Generic +-- | What was at the head of an instance head, when we expected a class? +data InstHeadNonClassHead + -- | A 'TyCon' that isn't a class was at the head + = InstNonClassTyCon Name (TyConFlavour Name) + -- | Something else than a 'TyCon' was at the head + | InstNonTyCon + -- | Why is a (type or data) family instance invalid? data IllegalFamilyInstanceReason {-| A top-level family instance for a 'TyCon' that isn't a family 'TyCon'. diff --git a/compiler/GHC/Tc/Gen/HsType.hs b/compiler/GHC/Tc/Gen/HsType.hs index 784defe5dbf..50cd75b26ce 100644 --- a/compiler/GHC/Tc/Gen/HsType.hs +++ b/compiler/GHC/Tc/Gen/HsType.hs @@ -675,8 +675,10 @@ tcHsDeriv hs_ty failWithTc $ TcRnIllegalInstance $ IllegalClassInstance (TypeThing ty) $ IllegalInstanceHead - $ InstHeadNonClass - $ Just tyCon + $ InstHeadNonClassHead + $ InstNonClassTyCon + (tyConName tyCon) + (fmap tyConName $ tyConFlavour tyCon) Right result -> return result } -- | Typecheck a deriving strategy. For most deriving strategies, this is a diff --git a/compiler/GHC/Tc/Validity.hs b/compiler/GHC/Tc/Validity.hs index 1d1434609fd..12a054a9715 100644 --- a/compiler/GHC/Tc/Validity.hs +++ b/compiler/GHC/Tc/Validity.hs @@ -1564,7 +1564,7 @@ check_special_inst_head dflags hs_src ctxt clas cls_args | isAbstractClass clas , hs_src == HsSrcFile = fail_with_inst_err $ IllegalInstanceHead - $ InstHeadAbstractClass clas + $ InstHeadAbstractClass (className clas) -- Complain about hand-written instances of built-in classes -- Typeable, KnownNat, KnownSymbol, Coercible, HasField. @@ -2111,9 +2111,10 @@ checkValidInstance ctxt hs_type ty = case tau of ; traceTc "End checkValidInstance }" empty } | otherwise -> failWithTc $ mk_err $ IllegalInstanceHead - $ InstHeadNonClass (Just tc) + $ InstHeadNonClassHead + $ InstNonClassTyCon (tyConName tc) (fmap tyConName $ tyConFlavour tc) _ -> failWithTc $ mk_err $ IllegalInstanceHead - $ InstHeadNonClass Nothing + $ InstHeadNonClassHead InstNonTyCon where (theta, tau) = splitInstTyForValidity ty diff --git a/compiler/GHC/Types/Error/Codes.hs b/compiler/GHC/Types/Error/Codes.hs index f75e90579ee..0f8b2075757 100644 --- a/compiler/GHC/Types/Error/Codes.hs +++ b/compiler/GHC/Types/Error/Codes.hs @@ -809,7 +809,7 @@ type family GhcDiagnosticCode c = n | n -> c where -- IllegalInstanceHead GhcDiagnosticCode "InstHeadAbstractClass" = 51758 - GhcDiagnosticCode "InstHeadNonClass" = 53946 + GhcDiagnosticCode "InstHeadNonClassHead" = 53946 GhcDiagnosticCode "InstHeadTySynArgs" = 93557 GhcDiagnosticCode "InstHeadNonTyVarArgs" = 48406 GhcDiagnosticCode "InstHeadMultiParam" = 91901 @@ -992,7 +992,7 @@ type family GhcDiagnosticCode c = n | n -> c where GhcDiagnosticCode "TcRnIllegalInstanceHeadDecl" = Outdated 12222 GhcDiagnosticCode "TcRnNoClassInstHead" = Outdated 56538 - -- The above two are subsumed by InstHeadNonClass [GHC-53946] + -- The above two are subsumed by InstHeadNonClassHead [GHC-53946] GhcDiagnosticCode "TcRnNameByTemplateHaskellQuote" = Outdated 40027 GhcDiagnosticCode "TcRnIllegalBindingOfBuiltIn" = Outdated 69639 diff --git a/testsuite/tests/rename/should_fail/T22688.hs b/testsuite/tests/rename/should_fail/T22688.hs new file mode 100644 index 00000000000..92273abbea5 --- /dev/null +++ b/testsuite/tests/rename/should_fail/T22688.hs @@ -0,0 +1,6 @@ +module T22688a where + +data Foo m a + +instance Foo m where + fmap _ x = case x of diff --git a/testsuite/tests/rename/should_fail/T22688.stderr b/testsuite/tests/rename/should_fail/T22688.stderr new file mode 100644 index 00000000000..94b25ba59ac --- /dev/null +++ b/testsuite/tests/rename/should_fail/T22688.stderr @@ -0,0 +1,7 @@ +T22688.hs:5:10: error: [GHC-53946] + • Illegal instance for data type ‘Foo’. + Instance heads must be of the form + C ty_1 ... ty_n + where ‘C’ is a class. + • In the instance declaration for ‘Foo m’ + diff --git a/testsuite/tests/rename/should_fail/all.T b/testsuite/tests/rename/should_fail/all.T index c13d4b87520..5e0d8c7c0db 100644 --- a/testsuite/tests/rename/should_fail/all.T +++ b/testsuite/tests/rename/should_fail/all.T @@ -212,6 +212,7 @@ test('T22478b', normal, compile_fail, ['']) test('T22478d', normal, compile_fail, ['']) test('T22478e', normal, compile_fail, ['']) test('T22478f', normal, compile_fail, ['']) +test('T22688', normal, compile_fail, ['']) test('T23740a', normal, compile_fail, ['']) test('T23740b', normal, compile_fail, ['']) test('T23740c', normal, compile_fail, ['']) -- GitLab