Sasha Bogicevic pushed to branch wip/21101 at Glasgow Haskell Compiler / GHC Commits: c2f6dcd4 by Sasha Bogicevic at 2026-07-20T10:31:56+02:00 Improve error messages for invalid record wildcards Record wildcard hints are now shown in more contexts and include constructor arity; matching with `..` on a fieldless constructor now produces a dedicated error message. Fixes #21101 - - - - - 17 changed files: - + changelog.d/21101 - compiler/GHC/Hs/Utils.hs - compiler/GHC/Rename/Env.hs - compiler/GHC/Rename/Names.hs - compiler/GHC/Rename/Pat.hs - compiler/GHC/Tc/Errors/Ppr.hs - compiler/GHC/Tc/Errors/Types.hs - compiler/GHC/Types/GREInfo.hs - compiler/GHC/Types/Hint.hs - compiler/GHC/Types/Hint/Ppr.hs - + testsuite/tests/rename/should_fail/T21101.hs - + testsuite/tests/rename/should_fail/T21101.stderr - testsuite/tests/rename/should_fail/T9815.stderr - testsuite/tests/rename/should_fail/T9815b.stderr - testsuite/tests/rename/should_fail/T9815bghci.stderr - testsuite/tests/rename/should_fail/T9815ghci.stderr - testsuite/tests/rename/should_fail/all.T Changes: ===================================== changelog.d/21101 ===================================== @@ -0,0 +1,8 @@ +section: compiler +synopsis: Improve error messages and hints for invalid record wildcards +description: + Record wildcard hints are now shown in more contexts and include + constructor arity; matching with ``..`` on a fieldless constructor + now produces a dedicated error message. +mrs: !8673 +issues: #21101 ===================================== compiler/GHC/Hs/Utils.hs ===================================== @@ -1613,8 +1613,8 @@ hsConDeclsBinders in the following format: with its record fields, in the form of a list of Int indices into... - IntMap FieldOcc, an IntMap of record fields. -(In actual fact, we use [(ConRdrName, Maybe [Located Int])], with Nothing indicating -that the constructor has unlabelled fields: see Note [Local constructor info in the renamer] +(In actual fact, we use [(ConRdrName, Either VisArity [Located Int])], with Left n indicating +that the constructor has n unlabelled arguments: see Note [Local constructor info in the renamer] in GHC.Types.GREInfo.) This allows us to do the following (see GHC.Rename.Names.getLocalNonValBinders.new_tc): @@ -1635,7 +1635,7 @@ Other relevant test cases: rnfail015. -- See Note [Collecting record fields in data declarations]. data LConsWithFields p = LConsWithFields - { consWithFieldIndices :: [(LocatedA (IdP (GhcPass p)), Maybe [Located Int])] + { consWithFieldIndices :: [(LocatedA (IdP (GhcPass p)), Either VisArity [Located Int])] , consFields :: IntMap (LFieldOcc (GhcPass p)) } @@ -1675,16 +1675,15 @@ hsConDeclsBinders cons = go emptyFieldIndices cons LConsWithFields ns fs = go seen' rs get_flds_h98 :: FieldIndices p -> HsConDeclH98Details (GhcPass p) - -> (Maybe [Located Int], FieldIndices p) - get_flds_h98 seen (RecCon _ flds) = first Just $ get_flds seen flds - get_flds_h98 seen (PrefixCon _ []) = (Just [], seen) - get_flds_h98 seen _ = (Nothing, seen) + -> (Either VisArity [Located Int], FieldIndices p) + get_flds_h98 seen (RecCon _ flds) = first Right $ get_flds seen flds + get_flds_h98 seen (PrefixCon _ args) = (Left (length args), seen) + get_flds_h98 seen (InfixCon {}) = (Left 2, seen) get_flds_gadt :: FieldIndices p -> HsConDeclGADTDetails (GhcPass p) - -> (Maybe [Located Int], FieldIndices p) - get_flds_gadt seen (RecConGADT _ flds) = first Just $ get_flds seen flds - get_flds_gadt seen (PrefixConGADT _ []) = (Just [], seen) - get_flds_gadt seen _ = (Nothing, seen) + -> (Either VisArity [Located Int], FieldIndices p) + get_flds_gadt seen (RecConGADT _ flds) = first Right $ get_flds seen flds + get_flds_gadt seen (PrefixConGADT _ args) = (Left (length args), seen) get_flds :: FieldIndices p -> LocatedA [LHsConDeclRecField (GhcPass p)] -> ([Located Int], FieldIndices p) ===================================== compiler/GHC/Rename/Env.hs ===================================== @@ -423,7 +423,12 @@ lookupConstructorInfo qcon@(WithUserRdr _ con_name) = do { info <- lookupGREInfo_GRE con_name ; case info of IAmConLike con_info -> return con_info - UnboundGRE -> return $ ConInfo (ConIsData []) ConHasPositionalArgs + UnboundGRE -> return $ ConInfo (ConIsData []) (ConHasPositionalArgs 0) + -- NB: it's OK to use the dummy value of '0' for the constructor arity: + -- we only use this information for 'TcRnIllegalWildcardsInConstructor', + -- which is an error we don't emit when the constructor is unbound. + -- See GHC.Rename.Pat.rnHsRecFields.rn_dotdot. + IAmTyCon {} -> failIllegalTyCon WL_ConLike qcon _ -> pprPanic "lookupConstructorInfo: not a ConLike" $ vcat [ text "name:" <+> ppr con_name ] ===================================== compiler/GHC/Rename/Names.hs ===================================== @@ -71,7 +71,7 @@ import GHC.Types.FieldLabel import GHC.Types.Hint import GHC.Types.SourceFile import GHC.Types.SrcLoc as SrcLoc -import GHC.Types.Basic ( TyConFlavour (..), convImportLevel ) +import GHC.Types.Basic (TyConFlavour (..), convImportLevel, VisArity) import GHC.Types.Id import GHC.Types.PkgQual import GHC.Types.GREInfo (ConInfo(..), ConFieldInfo (..), ConLikeInfo (ConIsData)) @@ -875,15 +875,16 @@ getLocalNonValBinders fixity_env -- -- The information we needed was all set up for us: -- see Note [Collecting record fields in data declarations] in GHC.Hs.Utils. - mk_fld_env :: [(Name, Maybe [Located Int])] -> IntMap FieldLabel + mk_fld_env :: [(Name, Either VisArity [Located Int])] -> IntMap FieldLabel -> [(ConLikeName, ConInfo)] mk_fld_env names flds = [ (DataConName con, ConInfo (ConIsData (map fst names)) fld_info) - | (con, mb_fl_indxs) <- names - , let fld_info = case fmap (map ((flds IntMap.!) . unLoc)) mb_fl_indxs of - Nothing -> ConHasPositionalArgs - Just [] -> ConIsNullary - Just (fld:flds) -> ConHasRecordFields $ fld NE.:| flds ] + | (con, con_fl_indxs) <- names + , let fld_info = case fmap (map ((flds IntMap.!) . unLoc)) con_fl_indxs of + Left 0 -> ConIsNullary + Left arity -> ConHasPositionalArgs arity + Right [] -> ConIsNullary + Right (fld:flds) -> ConHasRecordFields $ fld NE.:| flds ] new_assoc :: DuplicateRecordFields -> FieldSelectors -> LInstDecl GhcPs -> RnM [GlobalRdrElt] @@ -939,10 +940,10 @@ getLocalNonValBinders fixity_env -- Add errors if a constructor has a duplicate record field. add_dup_fld_errs :: IntMap FieldLabel - -> (Name, Maybe [Located Int]) + -> (Name, Either VisArity [Located Int]) -> IOEnv (Env TcGblEnv TcLclEnv) () - add_dup_fld_errs all_flds (con, mb_con_flds) - | Just con_flds <- mb_con_flds + add_dup_fld_errs all_flds (con, con_flds_or_arity) + | Right con_flds <- con_flds_or_arity , let (_, dups) = removeDups (comparing unLoc) con_flds = for_ dups $ \ dup_flds -> -- Report the error at the location of the second occurrence ===================================== compiler/GHC/Rename/Pat.hs ===================================== @@ -874,7 +874,10 @@ rnHsRecFields ctxt mk_arg (HsRecFields { rec_flds = flds, rec_dotdot = dotdot }) ; checkErr dd_flag (needFlagDotDot ctxt) ; (rdr_env, lcl_env) <- getRdrEnvs ; conInfo <- lookupConstructorInfo qcon - ; when (conFieldInfo conInfo == ConHasPositionalArgs) (addErr (TcRnIllegalWildcardsInConstructor con)) + ; case conFieldInfo conInfo of + ConHasPositionalArgs nbArgs -> + addErr $ TcRnIllegalWildcardsInConstructor (toRecordFieldPart ctxt) con nbArgs + _ -> return () ; let present_flds = mkOccSet $ map rdrNameOcc (getFieldRdrs flds) -- For constructor uses (but not patterns) ===================================== compiler/GHC/Tc/Errors/Ppr.hs ===================================== @@ -357,11 +357,11 @@ instance Diagnostic TcRnMessage where -> mkSimpleDecorated $ vcat [text "Illegal view pattern: " <+> ppr pat] TcRnCharLiteralOutOfRange c -> mkSimpleDecorated $ text "character literal out of range: '\\" <> char c <> char '\'' - TcRnIllegalWildcardsInConstructor con + TcRnIllegalWildcardsInConstructor ctx con _ -> mkSimpleDecorated $ - vcat [ text "Illegal `{..}' notation for constructor" <+> quotes (ppr con) - , nest 2 (text "Record wildcards may not be used for constructors with unlabelled fields.") - , nest 2 (text "Possible fix: Remove the `{..}' and add a match for each field of the constructor.") + vcat [ text "Invalid record" <+> pprRecordFieldPart ctx <+> quotes (ppr con <> text "{..}") <> dot + , text "The data constructor" <+> quotes (ppr con) + <+> text "does not have named record fields." ] TcRnIgnoringAnnotations anns -> mkSimpleDecorated $ @@ -2791,8 +2791,12 @@ instance Diagnostic TcRnMessage where -> [suggestExtension LangExt.ViewPatterns] TcRnCharLiteralOutOfRange{} -> noHints - TcRnIllegalWildcardsInConstructor{} - -> noHints + TcRnIllegalWildcardsInConstructor ctx con arity + -> case ctx of + RecordFieldPattern{} -> [ SuggestEmptyRecordBraces con + , SuggestExplicitConstructorArguments con arity + ] + _ -> [SuggestExplicitConstructorArguments con arity] TcRnIgnoringAnnotations{} -> noHints TcRnAnnotationInSafeHaskell ===================================== compiler/GHC/Tc/Errors/Types.hs ===================================== @@ -818,17 +818,35 @@ data TcRnMessage where TcRnNegativeNumTypeLiteral :: IntegralLit GhcRn -> TcRnMessage {-| TcRnIllegalWildcardsInConstructor is an error that occurs whenever - the record wildcards '..' are used inside a constructor without labeled fields. + the record wildcards '..' are used with a constructor whose fields are + positional (unlabelled). The 'RecordFieldPart' field records whether + the wildcards occurred in a record construction (an expression) or in + a record pattern, so that the message and its suggested fixes can be + worded accordingly. Constructors with no fields at all do not trigger + this error: since GHC proposal 496 ("Nullary record wildcards"), + @C {..}@ is legal for nullary constructors. + Example(s): - Examples(s): None + data D = D Int Bool + + f :: D -> () + f D{..} = () -- record pattern + + g :: D + g = D{..} -- record construction Test cases: rename/should_fail/T9815.hs rename/should_fail/T9815b.hs rename/should_fail/T9815ghci.hs rename/should_fail/T9815bghci.hs + rename/should_fail/T21101.hs -} - TcRnIllegalWildcardsInConstructor :: !Name -> TcRnMessage + TcRnIllegalWildcardsInConstructor + :: !RecordFieldPart -- ^ context in which the constructor application occurs + -> !Name -- ^ name of the constructor + -> !VisArity -- ^ arity of the constructor + -> TcRnMessage {-| TcRnIgnoringAnnotations is a warning that occurs when the source code contains annotation pragmas but the platform in use does not support an ===================================== compiler/GHC/Types/GREInfo.hs ===================================== @@ -244,14 +244,14 @@ instance NFData ConLikeInfo where -- See Note [Local constructor info in the renamer] data ConFieldInfo = ConHasRecordFields (NonEmpty FieldLabel) - | ConHasPositionalArgs + | ConHasPositionalArgs !VisArity | ConIsNullary deriving stock Eq deriving Data instance NFData ConFieldInfo where rnf ConIsNullary = () - rnf ConHasPositionalArgs = () + rnf (ConHasPositionalArgs arity) = rnf arity rnf (ConHasRecordFields flds) = rnf flds mkConInfo :: ConLikeInfo -> VisArity -> [FieldLabel] -> ConInfo @@ -259,9 +259,9 @@ mkConInfo con_ty n flds = ConInfo { conLikeInfo = con_ty , conFieldInfo = mkConFieldInfo n flds } -mkConFieldInfo :: Arity -> [FieldLabel] -> ConFieldInfo +mkConFieldInfo :: VisArity -> [FieldLabel] -> ConFieldInfo mkConFieldInfo 0 _ = ConIsNullary -mkConFieldInfo _ fields = maybe ConHasPositionalArgs ConHasRecordFields +mkConFieldInfo arity fields = maybe (ConHasPositionalArgs arity) ConHasRecordFields $ NonEmpty.nonEmpty fields conInfoFields :: ConInfo -> [FieldLabel] @@ -269,7 +269,7 @@ conInfoFields = conFieldInfoFields . conFieldInfo conFieldInfoFields :: ConFieldInfo -> [FieldLabel] conFieldInfoFields (ConHasRecordFields fields) = NonEmpty.toList fields -conFieldInfoFields ConHasPositionalArgs = [] +conFieldInfoFields (ConHasPositionalArgs _) = [] conFieldInfoFields ConIsNullary = [] instance Outputable ConInfo where @@ -284,7 +284,7 @@ instance Outputable ConLikeInfo where instance Outputable ConFieldInfo where ppr ConIsNullary = text "ConIsNullary" - ppr ConHasPositionalArgs = text "ConHasPositionalArgs" + ppr (ConHasPositionalArgs arity) = text "ConHasPositionalArgs" <+> braces (ppr arity) ppr (ConHasRecordFields fieldLabels) = text "ConHasRecordFields" <+> braces (ppr fieldLabels) ===================================== compiler/GHC/Types/Hint.hs ===================================== @@ -45,7 +45,7 @@ import GHC.Types.InlinePragma (ActivationGhc) import GHC.Types.Name (Name, NameSpace, OccName (occNameFS), isSymOcc, nameOccName) import GHC.Types.Name.Reader (RdrName (Unqual), ImpDeclSpec, GlobalRdrElt) import GHC.Types.SrcLoc (SrcSpan) -import GHC.Types.Basic (RuleName) +import GHC.Types.Basic (RuleName, VisArity) import GHC.Parser.Errors.Basic import GHC.Utils.Outputable import GHC.Data.FastString (fsLit) @@ -548,6 +548,23 @@ data GhcHint | SuggestUpgradeForSemaphoreVersionMismatch !SemaphoreUpgradeTarget !Int -- ^ The 'Int' is the required protocol version. + {-| Suggest replacing a record wildcard pattern @C {..}@ with @C {}@, + which matches a constructor without binding its fields. + + Triggered by 'GHC.Tc.Errors.Types.TcRnIllegalWildcardsInConstructor' + in a record pattern. + -} + | SuggestEmptyRecordBraces !Name + + {-| Suggest applying a constructor directly to its arguments instead + of record syntax, for constructors without labelled fields. + + Triggered by 'GHC.Tc.Errors.Types.TcRnIllegalWildcardsInConstructor' + in a record construction and record patterns. + The 'VisArity' is the number of positional arguments of the constructor. + -} + | SuggestExplicitConstructorArguments !Name !VisArity + -- | What the user should upgrade to resolve an @-jsem@ semaphore -- protocol version mismatch. data SemaphoreUpgradeTarget ===================================== compiler/GHC/Types/Hint/Ppr.hs ===================================== @@ -345,6 +345,12 @@ instance Outputable GhcHint where text "The jobserver uses a newer semaphore protocol than this GHC." $$ (text "Upgrade GHC to a version that supports semaphore protocol v" <> int required <> text " to resolve this.") + SuggestEmptyRecordBraces con + -> text "Use" <+> quotes (ppr con <> text "{}") <+> text "instead," + <+> text "which matches" <+> quotes (ppr con) <+> text "regardless of its fields" + SuggestExplicitConstructorArguments con nbArgs + -> text "Apply" <+> quotes (ppr con) <+> text "to its" + <+> speakNOf nbArgs (text "argument") perhapsAsPat :: SDoc perhapsAsPat = text "Perhaps you meant an as-pattern, which must not be surrounded by whitespace" ===================================== testsuite/tests/rename/should_fail/T21101.hs ===================================== @@ -0,0 +1,7 @@ +{-# LANGUAGE RecordWildCards #-} +module T21101 where + +data D = D Int Bool + +f :: D -> () +f D{..} = () ===================================== testsuite/tests/rename/should_fail/T21101.stderr ===================================== @@ -0,0 +1,7 @@ +T21101.hs:7:3: error: [GHC-47217] + Invalid record pattern ‘D{..}’. + The data constructor ‘D’ does not have named record fields. + Suggested fixes: + • Use ‘D{}’ instead, which matches ‘D’ regardless of its fields + • Apply ‘D’ to its two arguments + ===================================== testsuite/tests/rename/should_fail/T9815.stderr ===================================== @@ -1,5 +1,5 @@ - T9815.hs:6:13: error: [GHC-47217] - Illegal `{..}' notation for constructor ‘N’ - Record wildcards may not be used for constructors with unlabelled fields. - Possible fix: Remove the `{..}' and add a match for each field of the constructor. + Invalid record construction ‘N{..}’. + The data constructor ‘N’ does not have named record fields. + Suggested fix: Apply ‘N’ to its one argument + ===================================== testsuite/tests/rename/should_fail/T9815b.stderr ===================================== @@ -1,5 +1,5 @@ - T9815.hs:6:13: error: [GHC-47217] - Illegal `{..}' notation for constructor ‘N’ - Record wildcards may not be used for constructors with unlabelled fields. - Possible fix: Remove the `{..}' and add a match for each field of the constructor. + Invalid record construction ‘N{..}’. + The data constructor ‘N’ does not have named record fields. + Suggested fix: Apply ‘N’ to its one argument + ===================================== testsuite/tests/rename/should_fail/T9815bghci.stderr ===================================== @@ -1,5 +1,5 @@ +<interactive>:5:7: error: [GHC-47217] + Invalid record construction ‘Arg{..}’. + The data constructor ‘Arg’ does not have named record fields. + Suggested fix: Apply ‘Arg’ to its two arguments -<interactive>:5:7: [GHC-47217] - Illegal `{..}' notation for constructor ‘Arg’ - Record wildcards may not be used for constructors with unlabelled fields. - Possible fix: Remove the `{..}' and add a match for each field of the constructor. ===================================== testsuite/tests/rename/should_fail/T9815ghci.stderr ===================================== @@ -1,5 +1,5 @@ +<interactive>:3:7: error: [GHC-47217] + Invalid record construction ‘Data.Semigroup.Arg{..}’. + The data constructor ‘Data.Semigroup.Arg’ does not have named record fields. + Suggested fix: Apply ‘Data.Semigroup.Arg’ to its two arguments -<interactive>:3:7: [GHC-47217] - Illegal `{..}' notation for constructor ‘Data.Semigroup.Arg’ - Record wildcards may not be used for constructors with unlabelled fields. - Possible fix: Remove the `{..}' and add a match for each field of the constructor. ===================================== testsuite/tests/rename/should_fail/all.T ===================================== @@ -186,6 +186,7 @@ test('T18138', normal, compile_fail, ['']) test('T20147', normal, compile_fail, ['']) test('RnEmptyStatementGroup1', normal, compile_fail, ['']) test('RnImplicitBindInMdoNotation', normal, compile_fail, ['']) +test('T21101', normal, compile_fail, ['']) test('T21605a', normal, compile_fail, ['']) test('T21605b', normal, compile_fail, ['']) test('T21605c', normal, compile_fail, ['']) View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/c2f6dcd4118d09356e9812a507288644... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/c2f6dcd4118d09356e9812a507288644... You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
participants (1)
-
Sasha Bogicevic (@Bogicevic)