Florian Ragwitz pushed to branch wip/rafl/hpc-instances at Glasgow Haskell Compiler / GHC

Commits:

4 changed files:

Changes:

  • changelog.d/hpc-classinsts
    1
    +section: compiler
    
    2
    +synopsis: Improve coverage reporting for class instances and their methods
    
    3
    +description:
    
    4
    +
    
    5
    +  Coverage reporting (:ghc-flag:`-fhpc`) now treats class instances more
    
    6
    +  naturally.
    
    7
    +
    
    8
    +  Compiler-generated instance methods, such as inherited default methods, no
    
    9
    +  longer receive separate top-level coverage boxes. Explicitly implemented
    
    10
    +  instance methods retain their own declaration coverage.
    
    11
    +
    
    12
    +  This means that it is no longer necessary to exercise all of a class' methods
    
    13
    +  in order for it to appear "covered".
    
    14
    +
    
    15
    +  Additionally, top-level coverage boxes are now created for all instances of
    
    16
    +  classes with methods, and ticked by the use of *any* of the class' methods.
    
    17
    +  This indicates that an instance was in fact used at run-time.
    
    18
    +
    
    19
    +  Note that classes without any instance methods are currently excluded from
    
    20
    +  this, and won't have any coverage boxes created.
    
    21
    +
    
    22
    +mrs: !16632
    
    23
    +issues: #17155 #15932

  • compiler/GHC/HsToCore.hs
    ... ... @@ -147,6 +147,7 @@ deSugar hsc_env
    147 147
                                 tcg_tcs          = tcs,
    
    148 148
                                 tcg_default_exports = defaults,
    
    149 149
                                 tcg_insts        = insts,
    
    150
    +                            tcg_inst_meths   = inst_meths,
    
    150 151
                                 tcg_fam_insts    = fam_insts,
    
    151 152
                                 tcg_complete_matches = complete_matches,
    
    152 153
                                 tcg_self_boot    = self_boot
    
    ... ... @@ -170,7 +171,8 @@ deSugar hsc_env
    170 171
                                            (hsc_logger hsc_env)
    
    171 172
                                            (initTicksConfig (hsc_dflags hsc_env))
    
    172 173
                                            mod mod_loc
    
    173
    -                                       export_set (typeEnvTyCons type_env) binds
    
    174
    +                                       export_set (typeEnvTyCons type_env)
    
    175
    +                                       insts inst_meths binds
    
    174 176
                                   else return (binds, Nothing)
    
    175 177
             ; let modBreaks
    
    176 178
                     | Just (_, _, breakpointSpecs) <- m_tickInfo
    

  • compiler/GHC/HsToCore/Ticks.hs
    ... ... @@ -5,7 +5,7 @@
    5 5
     {-
    
    6 6
     (c) Galois, 2006
    
    7 7
     (c) University of Glasgow, 2007
    
    8
    -(c) Florian Ragwitz, 2025
    
    8
    +(c) Florian Ragwitz, 2025-2026
    
    9 9
     -}
    
    10 10
     
    
    11 11
     module GHC.HsToCore.Ticks
    
    ... ... @@ -24,6 +24,8 @@ import GHC.Unit
    24 24
     
    
    25 25
     import GHC.Core.Type
    
    26 26
     import GHC.Core.TyCon
    
    27
    +import GHC.Core.InstEnv
    
    28
    +import GHC.Core.Class
    
    27 29
     
    
    28 30
     import GHC.Data.Maybe
    
    29 31
     import GHC.Data.FastString
    
    ... ... @@ -48,6 +50,7 @@ import GHC.Types.CostCentre.State
    48 50
     import GHC.Types.Tickish
    
    49 51
     import GHC.Types.ProfAuto
    
    50 52
     import GHC.Tc.Types.ErrCtxt
    
    53
    +import GHC.Tc.Utils.TcType
    
    51 54
     
    
    52 55
     import Control.Monad
    
    53 56
     import Data.List (isSuffixOf, intersperse)
    
    ... ... @@ -56,6 +59,7 @@ import Data.Foldable (toList)
    56 59
     import Trace.Hpc.Mix
    
    57 60
     
    
    58 61
     import Data.Bifunctor (second)
    
    62
    +import Data.Functor
    
    59 63
     import Data.List.NonEmpty (NonEmpty (..))
    
    60 64
     import Data.Set (Set)
    
    61 65
     import qualified Data.Set as Set
    
    ... ... @@ -100,11 +104,15 @@ addTicksToBinds
    100 104
                                     -- isExportedId doesn't work yet (the desugarer
    
    101 105
                                     -- hasn't set it), so we have to work from this set.
    
    102 106
             -> [TyCon]              -- ^ Type constructors in this module
    
    107
    +        -> [ClsInst]            -- ^ Class instances in this module
    
    108
    +        -> IdEnv DFunId         -- ^ Mapping from class method Ids to the DFunIds
    
    109
    +                                -- of the class instance they belong to. See Note
    
    110
    +                                -- [Instance Method Coverage].
    
    103 111
             -> LHsBinds GhcTc
    
    104 112
             -> IO (LHsBinds GhcTc, Maybe (FilePath, SizedSeq Tick, SizedSeq Tick))
    
    105 113
     
    
    106 114
     addTicksToBinds logger cfg
    
    107
    -                mod mod_loc exports tyCons binds
    
    115
    +                mod mod_loc exports tyCons insts inst_meths binds
    
    108 116
       | let passes = ticks_passes cfg
    
    109 117
       , not (null passes)
    
    110 118
       , Just orig_file <- ml_hs_file mod_loc = do
    
    ... ... @@ -129,8 +137,13 @@ addTicksToBinds logger cfg
    129 137
                           , this_mod     = mod
    
    130 138
                           , tickishType  = tickish
    
    131 139
                           , recSelBinds  = emptyVarEnv
    
    140
    +                      , instMeths    = inst_meths
    
    141
    +                      , instTicks    = emptyVarEnv
    
    132 142
                           }
    
    133
    -                (binds',_,st') = unTM (addTickLHsBinds binds) env st
    
    143
    +                addTick = do
    
    144
    +                  instTicks <- allocInstTicks insts
    
    145
    +                  withEnv (\e -> e{ instTicks }) $ addTickLHsBinds binds
    
    146
    +                (binds',_,st') = unTM addTick env st
    
    134 147
                 in (binds', st')
    
    135 148
     
    
    136 149
               (binds1,st) = foldr tickPass (binds, initTTState) passes
    
    ... ... @@ -231,7 +244,7 @@ addTickLHsBind :: LHsBind GhcTc -> TM (LHsBind GhcTc)
    231 244
     addTickLHsBind (L pos (XHsBindsLR bind@(AbsBinds { abs_binds = binds
    
    232 245
                                                      , abs_exports = abs_exports
    
    233 246
                                                      }))) =
    
    234
    -  withEnv (add_rec_sels . add_inlines . add_exports) $ do
    
    247
    +  withEnv (add_inst_meths . add_rec_sels . add_inlines . add_exports) $ do
    
    235 248
           binds' <- addTickLHsBinds binds
    
    236 249
           return $ L pos $ XHsBindsLR $ bind { abs_binds = binds' }
    
    237 250
       where
    
    ... ... @@ -259,19 +272,24 @@ addTickLHsBind (L pos (XHsBindsLR bind@(AbsBinds { abs_binds = binds
    259 272
                               | ABE{ abe_poly, abe_mono } <- abs_exports
    
    260 273
                               , RecSelId{} <- [idDetails abe_poly] ] }
    
    261 274
     
    
    275
    +   add_inst_meths env =
    
    276
    +     env{ instMeths = instMeths env `extendVarEnvList`
    
    277
    +                        [ (abe_mono, dfun)
    
    278
    +                        | ABE{ abe_poly, abe_mono } <- abs_exports
    
    279
    +                        , Just dfun <- [lookupVarEnv (instMeths env) abe_poly] ] }
    
    280
    +
    
    262 281
     addTickLHsBind (L pos (funBind@(FunBind { fun_id = L _ id, fun_matches = matches }))) = do
    
    263 282
       let name = getOccString id
    
    264 283
       decl_path <- getPathEntry
    
    265 284
       density <- getDensity
    
    285
    +  env <- getEnv
    
    266 286
     
    
    267
    -  inline_ids <- liftM inlines getEnv
    
    268 287
       -- See Note [inline sccs]
    
    269 288
       let inline   = isInlinePragma (idInlinePragma id)
    
    270
    -                 || id `elemVarSet` inline_ids
    
    289
    +                 || id `elemVarSet` inlines env
    
    271 290
     
    
    272 291
       -- See Note [inline sccs]
    
    273
    -  tickish <- tickishType `liftM` getEnv
    
    274
    -  case tickish of { ProfNotes | inline -> return (L pos funBind); _ -> do
    
    292
    +  case tickishType env of { ProfNotes | inline -> return (L pos funBind); _ -> do
    
    275 293
     
    
    276 294
       -- See Note [Record-selector ticks]
    
    277 295
       selTicks <- recSelTick id
    
    ... ... @@ -294,16 +312,26 @@ addTickLHsBind (L pos (funBind@(FunBind { fun_id = L _ id, fun_matches = matches
    294 312
           toplev = null decl_path
    
    295 313
           exported = idName id `elemNameSet` exported_names
    
    296 314
     
    
    297
    -  tick <- if not blackListed &&
    
    298
    -               shouldTickBind density toplev exported simple inline
    
    315
    +  -- See Note [Instance Method Coverage].
    
    316
    +  let generatedInstMeth = density == TickForCoverage
    
    317
    +        && id `elemVarEnv` instMeths env
    
    318
    +        && isGenerated (mg_origin (mg_ext matches))
    
    319
    +
    
    320
    +  tick <- if not generatedInstMeth
    
    321
    +               && not blackListed
    
    322
    +               && shouldTickBind density toplev exported simple inline
    
    299 323
                  then
    
    300 324
                     bindTick density name (locA pos) fvs
    
    301 325
                  else
    
    302 326
                     return Nothing
    
    303 327
     
    
    328
    +  -- See Note [Instance Method Coverage].
    
    329
    +  instTick <- instMethTick id
    
    330
    +
    
    304 331
       let mbCons = maybe Prelude.id (:)
    
    305 332
       return $ L pos $ funBind { fun_matches = mg
    
    306
    -                           , fun_ext = second (tick `mbCons`) (fun_ext funBind) }
    
    333
    +                           , fun_ext = second ((instTick `mbCons`) . (tick `mbCons`))
    
    334
    +                                              (fun_ext funBind) }
    
    307 335
       } }
    
    308 336
       where
    
    309 337
         -- See Note [Record-selector ticks]
    
    ... ... @@ -338,6 +366,32 @@ addTickLHsBind (L pos (funBind@(FunBind { fun_id = L _ id, fun_matches = matches
    338 366
     -- This scheme could be extended further in the future, making coverage for
    
    339 367
     -- positional constructor fields mean that the field was accessed at run-time.
    
    340 368
     
    
    369
    +-- Note [Instance Method Coverage]
    
    370
    +-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    371
    +-- Users expect source-level semantics for coverage reports (see #17155,
    
    372
    +-- #15932). That is, code they've written themselves should be marked as covered
    
    373
    +-- iff it is evaluated at run-time. Given that the implementation of class
    
    374
    +-- instances creates top-level bindings for all methods of an instance,
    
    375
    +-- inherited and hand-written, we need to go out of our way to create top-level
    
    376
    +-- tick boxes only for methods which correspond to user-written code.
    
    377
    +--
    
    378
    +-- To do so, 'addTicksToBinds' receives a mapping from class instance 'Id's to
    
    379
    +-- the 'DFunId's of the dictionary backing the associated instance. When
    
    380
    +-- creating tick boxes for top-level bindings, we can then decide if a given
    
    381
    +-- 'FunBind' corresponds to an inherited/generated instance method of a class
    
    382
    +-- and skip creating the tick box for it.
    
    383
    +--
    
    384
    +-- Additionally, we also want to inform the user whether or not their class
    
    385
    +-- instance as a whole has been used at run-time. To do so, we create a single
    
    386
    +-- tick box at the instance head and tick that box when any method of that
    
    387
    +-- instance is evaluated.
    
    388
    +--
    
    389
    +-- As marker/tag-like class instances without any methods would always appear
    
    390
    +-- as uncovered using the above mechanism, we exclude them when creating
    
    391
    +-- instance tick boxes. This could potentially be improved in the future by
    
    392
    +-- tracking if any functions requiring an instance of an empty class have been
    
    393
    +-- evaluated.
    
    394
    +
    
    341 395
     -- TODO: Revisit this
    
    342 396
     addTickLHsBind (L pos (pat@(PatBind { pat_lhs = lhs
    
    343 397
                                         , pat_rhs = rhs
    
    ... ... @@ -1098,6 +1152,8 @@ data TickTransEnv = TTE { fileName :: FastString
    1098 1152
                             , this_mod     :: Module
    
    1099 1153
                             , tickishType  :: TickishType
    
    1100 1154
                             , recSelBinds  :: IdEnv DVarSet
    
    1155
    +                        , instMeths    :: IdEnv DFunId
    
    1156
    +                        , instTicks    :: IdEnv CoreTickish
    
    1101 1157
                             }
    
    1102 1158
     
    
    1103 1159
     --      deriving Show
    
    ... ... @@ -1238,6 +1294,21 @@ isBlackListed (RealSrcSpan pos _) = TM $ \ env st -> (Set.member pos (blackList
    1238 1294
     isBlackListed GeneratedSrcSpan{} = return False
    
    1239 1295
     isBlackListed UnhelpfulSpan{} = return False
    
    1240 1296
     
    
    1297
    +-- See Note [Instance Method Coverage].
    
    1298
    +allocInstTicks :: [ClsInst] -> TM (IdEnv CoreTickish)
    
    1299
    +allocInstTicks insts =
    
    1300
    +    ifDensity TickForCoverage (mkVarEnv . catMaybes <$> mapM alloc insts) (pure emptyVarEnv)
    
    1301
    +  where
    
    1302
    +    alloc ClsInst{ is_dfun, is_cls }
    
    1303
    +      | null (classMethods is_cls) = pure Nothing
    
    1304
    +      | otherwise = fmap (is_dfun,) <$> allocATickBox (TopLevelBox [s])
    
    1305
    +                                                      False True (getSrcSpan is_dfun) noFVs
    
    1306
    +      where s = showSDocOneLine defaultSDocContext (pprSigmaType (idType is_dfun))
    
    1307
    +
    
    1308
    +-- See Note [Instance Method Coverage].
    
    1309
    +instMethTick :: Id -> TM (Maybe CoreTickish)
    
    1310
    +instMethTick id = getEnv <&> \e -> lookupVarEnv (instMeths e) id >>= lookupVarEnv (instTicks e)
    
    1311
    +
    
    1241 1312
     -- the tick application inherits the source position of its
    
    1242 1313
     -- expression argument to support nested box allocations
    
    1243 1314
     allocTickBox :: BoxLabel -> Bool -> Bool -> SrcSpan -> TM (HsExpr GhcTc)
    

  • compiler/GHC/Tc/Types.hs
    ... ... @@ -533,6 +533,7 @@ data TcGblEnv
    533 533
               -- NB. BangPattern is to fix a leak, see #15111
    
    534 534
             tcg_inst_meths   :: IdEnv DFunId,
    
    535 535
               -- ^ Maps instance method Ids to the DFunId of the instance they belong to.
    
    536
    +          -- See Note [Instance Method Coverage] in GHC.HsToCore.Ticks.
    
    536 537
             tcg_ann_env      :: AnnEnv,     -- ^ And for annotations
    
    537 538
             tcg_complete_match_env :: CompleteMatches,
    
    538 539
             -- ^ The complete matches for all /home-package/ modules;