From ae88ed9a73d52ab103df491ef2e43c483f1d548d Mon Sep 17 00:00:00 2001 From: sheaf <sam.derbyshire@gmail.com> Date: Tue, 11 Jul 2023 14:00:53 +0200 Subject: [PATCH] Skip PMC for boring patterns Some patterns introduce no new information to the pattern-match checker (such as plain variable or wildcard patterns). We can thus skip doing any pattern-match checking on them when the sole purpose for doing so was introducing new long-distance information. See Note [Boring patterns] in GHC.Hs.Pat. Doing this avoids regressing in performance now that we do additional pattern-match checking inside do notation. (cherry picked from commit bea0e323c09e9e4b841a37aacd6b67e87a85e7cb) --- compiler/GHC/Hs/Pat.hs | 103 +++++++++++++++++- compiler/GHC/HsToCore/Match.hs | 7 +- compiler/GHC/HsToCore/Pmc.hs | 2 +- compiler/GHC/HsToCore/Pmc/Utils.hs | 25 ++++- .../pmcheck/should_compile/LongDistanceDo.hs | 19 ++++ .../should_compile/LongDistanceGRHS.hs | 22 ++++ testsuite/tests/pmcheck/should_compile/all.T | 2 + 7 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 testsuite/tests/pmcheck/should_compile/LongDistanceDo.hs create mode 100644 testsuite/tests/pmcheck/should_compile/LongDistanceGRHS.hs diff --git a/compiler/GHC/Hs/Pat.hs b/compiler/GHC/Hs/Pat.hs index a6a7fbd3e66..7aa9e593bc0 100644 --- a/compiler/GHC/Hs/Pat.hs +++ b/compiler/GHC/Hs/Pat.hs @@ -43,7 +43,7 @@ module GHC.Hs.Pat ( looksLazyPatBind, isBangedLPat, gParPat, patNeedsParens, parenthesizePat, - isIrrefutableHsPat, + isIrrefutableHsPat, isBoringHsPat, collectEvVarsPat, collectEvVarsPats, @@ -609,6 +609,55 @@ isSimplePat p = case unLoc p of VarPat _ x -> Just (unLoc x) _ -> Nothing +-- | Is this pattern boring from the perspective of pattern-match checking, +-- i.e. introduces no new pieces of long-dinstance information +-- which could influence pattern-match checking? +-- +-- See Note [Boring patterns]. +isBoringHsPat :: forall p. OutputableBndrId p => LPat (GhcPass p) -> Bool +-- NB: it's always safe to return 'False' in this function; that just means +-- performing potentially-redundant pattern-match checking. +isBoringHsPat = goL + where + goL :: forall p. OutputableBndrId p => LPat (GhcPass p) -> Bool + goL = go . unLoc + + go :: forall p. OutputableBndrId p => Pat (GhcPass p) -> Bool + go = \case + WildPat {} -> True + VarPat {} -> True + LazyPat {} -> True + BangPat _ pat -> goL pat + ParPat _ _ pat _ -> goL pat + AsPat {} -> False -- the pattern x@y links x and y together, + -- which is a nontrivial piece of information + ViewPat _ _ pat -> goL pat + SigPat _ pat _ -> goL pat + TuplePat _ pats _ -> all goL pats + SumPat _ pat _ _ -> goL pat + ListPat _ pats -> all goL pats + ConPat { pat_con = con, pat_args = details } + -> case ghcPass @p of + GhcPs -> False -- conservative + GhcRn -> False -- conservative + GhcTc + | isVanillaConLike (unLoc con) + -> all goL (hsConPatArgs details) + | otherwise + -- A pattern match on a GADT constructor can introduce + -- type-level information (for example, T18572). + -> False + LitPat {} -> True + NPat {} -> True + NPlusKPat {} -> True + SplicePat {} -> False + XPat ext -> + case ghcPass @p of + GhcRn -> case ext of + HsPatExpanded _ pat -> go pat + GhcTc -> case ext of + CoPat _ pat _ -> go pat + ExpansionPat _ pat -> go pat {- Note [Unboxed sum patterns aren't irrefutable] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -630,6 +679,58 @@ minimum unboxed sum arity is 2. Failing to mark unboxed sum patterns as non-irrefutable would cause the Just' case in foo to be unreachable, as GHC would mistakenly believe that Nothing' is the only thing that could possibly be matched! + +Note [Boring patterns] +~~~~~~~~~~~~~~~~~~~~~~ +A pattern is called boring when no new information is gained upon successfully +matching on the pattern. + +Some examples of boring patterns: + + - x, for a variable x. We learn nothing about x upon matching this pattern. + - Just y. This pattern can fail, but if it matches, we don't learn anything + about y. + +Some examples of non-boring patterns: + + - x@(Just y). A match on this pattern introduces the fact that x is headed + by the constructor Just, which means that a subsequent pattern match such as + + case x of { Just z -> ... } + + should not be marked as incomplete. + - a@b. Matching on this pattern introduces a relation between 'a' and 'b', + which means that we shouldn't emit any warnings in code of the form + + case a of + True -> case b of { True -> .. } -- no warning here! + False -> ... + - GADT patterns. For example, with the GADT + + data G i where { MkGInt :: G Int } + + a match on the pattern 'MkGInt' introduces type-level information: + + foo :: G i -> i + foo MkGInt = 3 + + Here we learn that i ~ Int after matching on 'MkGInt', so this pattern + is not boring. + +When a pattern is boring, and we are only interested in additional long-distance +information (not whether the pattern itself is fallible), we can skip pattern-match +checking entirely. Doing this saves about 10% allocations in test T11195. + +This happens when we are checking pattern-matches in do-notation, for example: + + do { x@(Just y) <- z + ; ... + ; return $ case x of { Just w -> ... } } + +Here we *do not* want to emit a pattern-match warning on the first line for the +incomplete pattern-match, as incompleteness inside do-notation is handled +using MonadFail. However, we still want to propagate the fact that x is headed +by the 'Just' constructor, to avoid a pattern-match warning on the last line. -} -- | @'patNeedsParens' p pat@ returns 'True' if the pattern @pat@ needs diff --git a/compiler/GHC/HsToCore/Match.hs b/compiler/GHC/HsToCore/Match.hs index 49dc24ae074..54f9741f01b 100644 --- a/compiler/GHC/HsToCore/Match.hs +++ b/compiler/GHC/HsToCore/Match.hs @@ -953,10 +953,9 @@ matchSinglePatVar var mb_scrut ctx pat ty match_result -- See Note [Long-distance information in matchWrapper] and -- Note [Long-distance information in do notation] in GHC.HsToCore.Expr. ; ldi_nablas <- - if isMatchContextPmChecked dflags FromSource ctx - then - addCoreScrutTmCs (maybeToList mb_scrut) [var] $ - pmcPatBind (DsMatchContext ctx locn) var (unLoc pat) + if isMatchContextPmChecked_SinglePat dflags FromSource ctx pat + then addCoreScrutTmCs (maybeToList mb_scrut) [var] $ + pmcPatBind (DsMatchContext ctx locn) var (unLoc pat) else getLdiNablas ; let eqn_info = EqnInfo { eqn_pats = [unLoc (decideBangHood dflags pat)] diff --git a/compiler/GHC/HsToCore/Pmc.hs b/compiler/GHC/HsToCore/Pmc.hs index f588c842c8b..9fde238fe97 100644 --- a/compiler/GHC/HsToCore/Pmc.hs +++ b/compiler/GHC/HsToCore/Pmc.hs @@ -36,7 +36,7 @@ module GHC.HsToCore.Pmc ( -- Checking and printing pmcPatBind, pmcMatches, pmcGRHSs, - isMatchContextPmChecked, + isMatchContextPmChecked, isMatchContextPmChecked_SinglePat, -- See Note [Long-distance information] addTyCs, addCoreScrutTmCs, addHsScrutTmCs, getLdiNablas diff --git a/compiler/GHC/HsToCore/Pmc/Utils.hs b/compiler/GHC/HsToCore/Pmc/Utils.hs index b39862821a5..b208723e7b4 100644 --- a/compiler/GHC/HsToCore/Pmc/Utils.hs +++ b/compiler/GHC/HsToCore/Pmc/Utils.hs @@ -8,7 +8,8 @@ module GHC.HsToCore.Pmc.Utils ( tracePm, traceWhenFailPm, mkPmId, allPmCheckWarnings, overlapping, exhaustive, redundantBang, exhaustiveWarningFlag, - isMatchContextPmChecked, needToRunPmCheck + isMatchContextPmChecked, isMatchContextPmChecked_SinglePat, + needToRunPmCheck ) where @@ -25,7 +26,6 @@ import GHC.Types.Id import GHC.Types.Name import GHC.Types.Unique.Supply import GHC.Types.SrcLoc -import GHC.Utils.Misc import GHC.Utils.Outputable import GHC.Utils.Logger import GHC.HsToCore.Monad @@ -108,16 +108,31 @@ arrowMatchContextExhaustiveWarningFlag = \ case -- 'HsMatchContext' (does not matter whether it is the redundancy check or the -- exhaustiveness check). isMatchContextPmChecked :: DynFlags -> Origin -> HsMatchContext id -> Bool -isMatchContextPmChecked dflags origin kind +isMatchContextPmChecked dflags origin ctxt = requiresPMC origin - && (overlapping dflags kind || exhaustive dflags kind) + && (overlapping dflags ctxt || exhaustive dflags ctxt) + +-- | Check whether exhaustivity checks are enabled for this 'HsMatchContext', +-- when dealing with a single pattern (using the 'matchSinglePatVar' function). +isMatchContextPmChecked_SinglePat :: DynFlags -> Origin -> HsMatchContext id -> LPat GhcTc -> Bool +isMatchContextPmChecked_SinglePat dflags origin ctxt pat + | not (needToRunPmCheck dflags origin) + = False + | StmtCtxt {} <- ctxt + -- For @StmtCtxt@, we are interested in propagating pattern-match information + -- but not in the actual outcome of pattern-match checking, so we skip + -- if the pattern is "boring" (gives rise to no long-distance information). + -- (This is done purely for runtime performance.) + = not (isBoringHsPat pat) -- See Note [Boring patterns] in GHC.Hs.Pat. + | otherwise + = overlapping dflags ctxt || exhaustive dflags ctxt -- | Return True when any of the pattern match warnings ('allPmCheckWarnings') -- are enabled, in which case we need to run the pattern match checker. needToRunPmCheck :: DynFlags -> Origin -> Bool needToRunPmCheck dflags origin = requiresPMC origin - && notNull (filter (`wopt` dflags) allPmCheckWarnings) + && any (`wopt` dflags) allPmCheckWarnings {- Note [Inaccessible warnings for record updates] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/testsuite/tests/pmcheck/should_compile/LongDistanceDo.hs b/testsuite/tests/pmcheck/should_compile/LongDistanceDo.hs new file mode 100644 index 00000000000..8bbbd2c9dc2 --- /dev/null +++ b/testsuite/tests/pmcheck/should_compile/LongDistanceDo.hs @@ -0,0 +1,19 @@ +module LongDistanceDo where + +data D = T1 | T2 + +foo1 :: IO D -> IO () +foo1 d = do + x@y <- d + let z = case x of + T1 -> () + T2 -> case y of { T2 -> () } + return z + +foo2 :: IO D -> IO () +foo2 d = do + x@y <- d + let z = case y of + T1 -> () + T2 -> case x of { T2 -> () } + return z diff --git a/testsuite/tests/pmcheck/should_compile/LongDistanceGRHS.hs b/testsuite/tests/pmcheck/should_compile/LongDistanceGRHS.hs new file mode 100644 index 00000000000..7bf7d5e13a7 --- /dev/null +++ b/testsuite/tests/pmcheck/should_compile/LongDistanceGRHS.hs @@ -0,0 +1,22 @@ +module LongDistanceGRHS where + +data D = T1 | T2 + +foo1 y + | x <- y + = case x of + T1 -> case y of { T1 -> False } + T2 -> True + +foo2 y + | x <- y + = case y of + T1 -> case x of { T1 -> False } + T2 -> True + +foo3 y + | x@T1 <- y + = case y of + T1 -> False + | otherwise + = True diff --git a/testsuite/tests/pmcheck/should_compile/all.T b/testsuite/tests/pmcheck/should_compile/all.T index 141aa0433b3..0030a9f2d74 100644 --- a/testsuite/tests/pmcheck/should_compile/all.T +++ b/testsuite/tests/pmcheck/should_compile/all.T @@ -113,6 +113,8 @@ test('CyclicSubst', [], compile, [overlapping_incomplete]) test('CaseOfKnownCon', [], compile, [overlapping_incomplete]) test('TooManyDeltas', [], compile, [overlapping_incomplete+'-fmax-pmcheck-models=0']) test('LongDistanceInfo', [], compile, [overlapping_incomplete]) +test('LongDistanceDo', [], compile, [overlapping_incomplete]) +test('LongDistanceGRHS', [], compile, [overlapping_incomplete]) test('T21662', [], compile, [overlapping_incomplete]) test('T19271', [], compile, [overlapping_incomplete]) test('T21761', [], compile, [overlapping_incomplete]) -- GitLab