diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,16 @@
 and is generated by [Changie](https://github.com/miniscruff/changie).
 
 
+## 2.7.0 - 2023-08-17
+### Added
+* Weeder now supports type class instances. Type class instances can be marked as roots with the `root-instances` configuration option. (#126, #133, #136)
+* Weeder now optionally detects uses of types, excluding type family instances. This can be enabled with the `unused-types` configuration option. (#132)
+* Weeder's analysis now runs in parallel. This can almost halve execution time when given enough cores. Enabled by calling Weeder with `-j X` or `-N`. (#137)
+* `--write-default-config` flag to write and read a default configuration, if no configuration file is found. (#133)
+### Changed
+* All configuration options now have default values. This can be disabled with the `--no-default-fields` flag. (#133)
+* Weeder will now use distinct exit codes for certain failures. See `README.md` for more details. (#134)
+
 ## 2.6.0 - 2023-07-07
 ### Added
 * Weeder now supports GHC 9.6.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -64,10 +64,8 @@
 add all exported functions as roots automatically but in many cases `main` from a
 test suite could be a good workaround for that
 
-`type-class-roots` configures whether or not Weeder should consider anything in
-a type class instance as a root. Weeder is currently unable to add dependency
-edges into type class instances, and without this flag may produce false
-positives. It's recommended to initially set this to `True`:
+`type-class-roots` configures whether or not Weeder should consider all instances
+of type classes as roots. Defaults to `false`.
 
 ``` toml
 roots = [ "^Main.main$" ]
@@ -87,25 +85,76 @@
 (Please note these warnings are just for demonstration and not necessarily weeds
 in the Dhall project).
 
+## Configuration options
+
+| Name             | Default value                        | Description |
+| ---------------- | ------------------------------------ | --- |
+| roots            | `[ "Main.main", "^Paths_weeder.*" ]` | Any declarations matching these regular expressions will be considered as alive. |
+| type-class-roots | `false`                              | Consider all instances of type classes as roots. Overrides `root-instances`. |
+| root-instances   | `[ {class = '\.IsString$'}, {class = '\.IsList$'} ]` | Type class instances that match on all specified fields will be considered as roots. Accepts the fields `instance` matching on the pretty-printed type of the instance (visible in the output), `class` matching on its parent class declaration, and `module` matching on the module the instance is in. |
+| unused-types     | `false`                              | Enable analysis of unused types. |
+
+`root-instances` can also accept string literals as a shorthand for writing a table
+containing only the `instance` field. See the following example from the test suite:
+
+``` toml
+root-instances = [ { module = "Spec.ConfigInstanceModules.Module1", instance = "Bounded T" }
+                 , "Read T" 
+                 , { module = "Spec.ConfigInstanceModules.Module3" }
+                 , { class = '\.Enum$' }
+                 , { module = "Spec.ConfigInstanceModules.Module2", class = '\.Show$' }
+                 ]
+```
+
+## Exit codes
+
+Weeder emits the following exit codes:
+
+| Exit code | Cause |
+| --- | --- |
+|  0  | No weeds were found |
+| 228 | One or more weeds found |
+|  1  | Generic failing exit code |
+|  2  | Failure to read HIE file due to GHC version mismatch |
+|  3  | Failure to parse config file |
+|  4  | No HIE files found |
+
 # Tips
 
 - You may want to add `^Paths_.*` to the roots in `weeder.toml` to ignore the
   `Paths_packageName` module automatically generated by Cabal.
 
+- You can automatically write and use a default configuration file by calling 
+  Weeder with the `--write-default-config` flag, if no configuration file is
+  found.
+
+- You can mandate explicitly specifying every option in the configuration by 
+  calling Weeder with the `--no-default-fields` flag. This can prevent being
+  caught off guard by new configuration options or changes to default values.
+
+- To mark all instances in a module `M` as roots, add `{ module = "^M$" }`
+  to `root-instances`.
+
 # Limitations
 
 Weeder currently has a few limitations:
 
-## Type Class Instances
+## Overloaded syntax
 
-Weeder is not currently able to analyse whether a type class instance is used.
-For this reason, Weeder adds all symbols referenced to from a type class
-instance to the root set, keeping this code alive. In short, this means Weeder
-might not detect dead code if it's used from a type class instance which is
-never actually needed.
+On some versions of GHC, Weeder might report various type classes that are used
+for syntax extensions as weeds. For example, `Num` and `IsString` classes might be
+flagged as weeds if they are only used for overloaded literal syntax (that is,
+the `fromInteger` and `fromString` methods).
 
-You can toggle whether Weeder consider type class instances as roots with the
-`type-class-roots` configuration option.
+You can add instances of specific type classes as roots with the `root-instances` 
+field, or toggle whether Weeder considers all type class instances as roots with 
+the `type-class-roots` configuration option.
+
+## Type families
+
+Weeder cannot yet analyse uses of type family instances. For this reason type
+family instances will be marked as implicit roots if analysis of types is
+enabled via `unused-types`.
 
 ## Template Haskell
 
diff --git a/src/Weeder.hs b/src/Weeder.hs
--- a/src/Weeder.hs
+++ b/src/Weeder.hs
@@ -1,19 +1,22 @@
 {-# language ApplicativeDo #-}
 {-# language BlockArguments #-}
 {-# language DeriveGeneric #-}
+{-# language DeriveAnyClass #-}
 {-# language FlexibleContexts #-}
 {-# language LambdaCase #-}
 {-# language NamedFieldPuns #-}
 {-# language NoImplicitPrelude #-}
 {-# language OverloadedLabels #-}
 {-# language OverloadedStrings #-}
+{-# language TupleSections #-}
 
 module Weeder
   ( -- * Analysis
     Analysis(..)
+  , analyseEvidenceUses
   , analyseHieFile
   , emptyAnalysis
-  , allDeclarations
+  , outputableDeclarations
 
     -- ** Reachability
   , Root(..)
@@ -25,15 +28,18 @@
    where
 
 -- algebraic-graphs
-import Algebra.Graph ( Graph, edge, empty, overlay, vertex, vertexList )
+import Algebra.Graph ( Graph, edge, empty, overlay, vertex, stars, star, overlays )
 import Algebra.Graph.ToGraph ( dfs )
 
 -- base
 import Control.Applicative ( Alternative )
-import Control.Monad ( guard, msum, when )
-import Data.Foldable ( for_, traverse_ )
+import Control.Monad ( guard, msum, when, unless, mzero )
+import Data.Traversable ( for )
+import Data.Maybe ( mapMaybe )
+import Data.Foldable ( for_, traverse_, toList )
+import Data.Function ( (&) )
 import Data.List ( intercalate )
-import Data.Monoid ( First( First ) )
+import Data.Monoid ( First( First ), getFirst )
 import GHC.Generics ( Generic )
 import Prelude hiding ( span )
 
@@ -43,6 +49,8 @@
 import Data.Sequence ( Seq )
 import Data.Set ( Set )
 import qualified Data.Set as Set
+import Data.Tree (Tree)
+import qualified Data.Tree as Tree
 
 -- generic-lens
 import Data.Generics.Labels ()
@@ -56,18 +64,38 @@
 import GHC.Types.FieldLabel ( FieldLabel( FieldLabel, flSelector ) )
 import GHC.Iface.Ext.Types
   ( BindType( RegularBind )
-  , ContextInfo( Decl, ValBind, PatternBind, Use, TyDecl, ClassTyDecl )
-  , DeclType( DataDec, ClassDec, ConDec )
+  , ContextInfo( Decl, ValBind, PatternBind, Use, TyDecl, ClassTyDecl, EvidenceVarBind, RecField )
+  , DeclType( DataDec, ClassDec, ConDec, SynDec, FamDec )
+  , EvVarSource ( EvInstBind, cls )
   , HieAST( Node, nodeChildren, nodeSpan, sourcedNodeInfo )
   , HieASTs( HieASTs )
-  , HieFile( HieFile, hie_asts, hie_exports, hie_module, hie_hs_file )
-  , IdentifierDetails( IdentifierDetails, identInfo )
+  , HieFile( HieFile, hie_asts, hie_exports, hie_module, hie_hs_file, hie_types )
+  , HieType( HTyVarTy, HAppTy, HTyConApp, HForAllTy, HFunTy, HQualTy, HLitTy, HCastTy, HCoercionTy )
+  , HieArgs( HieArgs )
+  , HieTypeFix( Roll )
+  , IdentifierDetails( IdentifierDetails, identInfo, identType )
   , NodeAnnotation( NodeAnnotation, nodeAnnotType )
   , NodeInfo( nodeIdentifiers, nodeAnnotations )
   , Scope( ModuleScope )
+  , RecFieldContext ( RecFieldOcc )
+  , TypeIndex
   , getSourcedNodeInfo
   )
+import GHC.Iface.Ext.Utils
+  ( EvidenceInfo( EvidenceInfo, evidenceVar )
+  , RefMap
+  , findEvidenceUse
+  , getEvidenceTree
+  , hieTypeToIface
+  , recoverFullType
+  )
 import GHC.Unit.Module ( Module, moduleStableString )
+import GHC.Utils.Outputable ( defaultSDocContext, showSDocOneLine )
+import GHC.Iface.Type
+  ( ShowForAllFlag (ShowForAllWhen)
+  , pprIfaceSigmaType
+  , IfaceTyCon (IfaceTyCon, ifaceTyConName)
+  )
 import GHC.Types.Name
   ( Name, nameModule_maybe, nameOccName
   , OccName
@@ -78,18 +106,26 @@
   , isVarOcc
   , occNameString
   )
-import GHC.Types.SrcLoc ( RealSrcSpan, realSrcSpanEnd, realSrcSpanStart )
+import GHC.Types.SrcLoc ( RealSrcSpan, realSrcSpanEnd, realSrcSpanStart, srcLocLine )
 
 -- lens
 import Control.Lens ( (%=) )
 
 -- mtl
 import Control.Monad.State.Class ( MonadState )
+import Control.Monad.Reader.Class ( MonadReader, asks )
 
+-- parallel
+import Control.Parallel.Strategies ( NFData )
+
 -- transformers
 import Control.Monad.Trans.Maybe ( runMaybeT )
+import Control.Monad.Trans.Reader ( runReaderT )
 
+-- weeder
+import Weeder.Config ( Config, ConfigType( Config, typeClassRoots, unusedTypes ) )
 
+
 data Declaration =
   Declaration
     { declModule :: Module
@@ -98,7 +134,7 @@
       -- ^ The symbol name of a declaration.
     }
   deriving
-    ( Eq, Ord )
+    ( Eq, Ord, Generic, NFData )
 
 
 instance Show Declaration where
@@ -126,39 +162,70 @@
   Analysis
     { dependencyGraph :: Graph Declaration
       -- ^ A graph between declarations, capturing dependencies.
-    , declarationSites :: Map Declaration ( Set RealSrcSpan )
-      -- ^ A partial mapping between declarations and their definition site.
+    , declarationSites :: Map Declaration (Set Int)
+      -- ^ A partial mapping between declarations and their line numbers.
       -- This Map is partial as we don't always know where a Declaration was
       -- defined (e.g., it may come from a package without source code).
-      -- We capture a set of spans, because a declaration may be defined in
+      -- We capture a set of sites, because a declaration may be defined in
       -- multiple locations, e.g., a type signature for a function separate
       -- from its definition.
-    , implicitRoots :: Set Declaration
-      -- ^ The Set of all Declarations that are always reachable. This is used
-      -- to capture knowledge not yet modelled in weeder, such as instance
-      -- declarations depending on top-level functions.
+    , implicitRoots :: Set Root
+      -- ^ Stores information on Declarations that may be automatically marked
+      -- as always reachable. This is used, for example, to capture knowledge 
+      -- not yet modelled in weeder, or to mark all instances of a class as 
+      -- roots.
     , exports :: Map Module ( Set Declaration )
       -- ^ All exports for a given module.
     , modulePaths :: Map Module FilePath
       -- ^ A map from modules to the file path to the .hs file defining them.
+    , prettyPrintedType :: Map Declaration String
+      -- ^ Used to match against the types of instances and to replace the
+      -- appearance of declarations in the output
+    , requestedEvidence :: Map Declaration (Set Name)
+      -- ^ Map from declarations to the names containing evidence uses that
+      -- should be followed and treated as dependencies of the declaration.
+      -- We use this to be able to delay analysing evidence uses until later,
+      -- allowing us to begin the rest of the analysis before we have read all
+      -- hie files.
     }
   deriving
-    ( Generic )
+    ( Generic, NFData )
 
 
+instance Semigroup Analysis where
+  (<>) (Analysis a1 b1 c1 d1 e1 f1 g1) (Analysis a2 b2 c2 d2 e2 f2 g2)= 
+    Analysis (a1 `overlay` a2) (Map.unionWith (<>) b1 b2) (c1 <> c2) (Map.unionWith (<>) d1 d2) (e1 <> e2) (f1 <> f2) (Map.unionWith (<>) g1 g2)
+
+
+instance Monoid Analysis where
+  mempty = emptyAnalysis
+
+
+data AnalysisInfo =
+  AnalysisInfo
+    { currentHieFile :: HieFile
+    , weederConfig :: Config
+    }
+
+
 -- | The empty analysis - the result of analysing zero @.hie@ files.
 emptyAnalysis :: Analysis
-emptyAnalysis = Analysis empty mempty mempty mempty mempty
+emptyAnalysis = Analysis empty mempty mempty mempty mempty mempty mempty
 
 
 -- | A root for reachability analysis.
 data Root
   = -- | A given declaration is a root.
     DeclarationRoot Declaration
+  | -- | We store extra information for instances in order to be able
+    -- to specify e.g. all instances of a class as roots.
+    InstanceRoot 
+      Declaration -- ^ Declaration of the instance
+      Declaration -- ^ Declaration of the parent class
   | -- | All exported declarations in a module are roots.
     ModuleRoot Module
   deriving
-    ( Eq, Ord )
+    ( Eq, Ord, Generic, NFData )
 
 
 -- | Determine the set of all declaration reachable from a set of roots.
@@ -170,27 +237,100 @@
 
     rootDeclarations = \case
       DeclarationRoot d -> [ d ]
+      InstanceRoot d _ -> [ d ] -- filter InstanceRoots in `Main.hs`
       ModuleRoot m -> foldMap Set.toList ( Map.lookup m exports )
 
 
--- | The set of all known declarations, including usages.
-allDeclarations :: Analysis -> Set Declaration
-allDeclarations Analysis{ dependencyGraph } =
-  Set.fromList ( vertexList dependencyGraph )
+-- | The set of all declarations that could possibly
+-- appear in the output.
+outputableDeclarations :: Analysis -> Set Declaration
+outputableDeclarations Analysis{ declarationSites } =
+  Map.keysSet declarationSites
 
 
+-- Generate an initial graph of the current HieFile.
+initialGraph :: AnalysisInfo -> Graph Declaration
+initialGraph info =
+  let hf@HieFile{ hie_asts = HieASTs hieAsts } = currentHieFile info
+      Config{ unusedTypes } = weederConfig info
+      asts = Map.elems hieAsts
+      decls = concatMap (toList . findIdentifiers' (const True)) asts
+  in if unusedTypes
+    then stars do 
+      (d, IdentifierDetails{identType}, _) <- decls
+      t <- maybe mzero pure identType
+      let ns = Set.toList $ typeToNames (lookupType hf t)
+          ds = mapMaybe nameToDeclaration ns
+      guard $ not (null ds)
+      pure (d, ds)
+    else mempty
+
+
 -- | Incrementally update 'Analysis' with information in a 'HieFile'.
-analyseHieFile :: MonadState Analysis m => HieFile -> m ()
-analyseHieFile HieFile{ hie_asts = HieASTs hieASTs, hie_exports, hie_module, hie_hs_file } = do
+analyseHieFile :: (MonadState Analysis m) => Config -> HieFile -> m ()
+analyseHieFile weederConfig hieFile =
+  let info = AnalysisInfo hieFile weederConfig
+   in runReaderT analyseHieFile' info
+
+
+analyseHieFile' :: ( MonadState Analysis m, MonadReader AnalysisInfo m ) => m ()
+analyseHieFile' = do
+  HieFile{ hie_asts = HieASTs hieASTs, hie_exports, hie_module, hie_hs_file } <- asks currentHieFile
   #modulePaths %= Map.insert hie_module hie_hs_file
+  
+  g <- asks initialGraph
+  #dependencyGraph %= overlay g
 
-  for_ hieASTs \ast -> do
-    addAllDeclarations ast
-    topLevelAnalysis ast
+  for_ hieASTs topLevelAnalysis
 
   for_ hie_exports ( analyseExport hie_module )
 
 
+lookupType :: HieFile -> TypeIndex -> HieTypeFix
+lookupType hf t = recoverFullType t $ hie_types hf
+
+
+lookupPprType :: MonadReader AnalysisInfo m => TypeIndex -> m String
+lookupPprType t = do
+  hf <- asks currentHieFile
+  pure . renderType $ lookupType hf t
+
+  where
+
+    renderType = showSDocOneLine defaultSDocContext . pprIfaceSigmaType ShowForAllWhen . hieTypeToIface
+
+
+-- | Names mentioned within the type.
+typeToNames :: HieTypeFix -> Set Name
+typeToNames (Roll t) = case t of
+  HTyVarTy n -> Set.singleton n
+
+  HAppTy a (HieArgs args) ->
+    typeToNames a <> hieArgsTypes args
+
+  HTyConApp (IfaceTyCon{ifaceTyConName}) (HieArgs args) ->
+    Set.singleton ifaceTyConName <> hieArgsTypes args
+
+  HForAllTy _ a -> typeToNames a
+
+  HFunTy _mult b c ->
+    typeToNames b <> typeToNames c
+
+  HQualTy a b ->
+    typeToNames a <> typeToNames b
+
+  HLitTy _ -> mempty
+
+  HCastTy a -> typeToNames a
+
+  HCoercionTy -> mempty
+
+  where
+
+    hieArgsTypes :: [(Bool, HieTypeFix)] -> Set Name
+    hieArgsTypes = foldMap (typeToNames . snd) . filter fst
+
+
 analyseExport :: MonadState Analysis m => Module -> AvailInfo -> m ()
 analyseExport m = \case
   Avail (NormalGreName name) ->
@@ -203,8 +343,8 @@
     for_ ( nameToDeclaration name ) addExport
 
     for_ pieces \case
-      NormalGreName name ->
-        traverse_ addExport $ nameToDeclaration name
+      NormalGreName name' ->
+        traverse_ addExport $ nameToDeclaration name'
 
       FieldGreName (FieldLabel{ flSelector }) ->
         traverse_ addExport $ nameToDeclaration flSelector
@@ -223,43 +363,48 @@
 
 addImplicitRoot :: MonadState Analysis m => Declaration -> m ()
 addImplicitRoot x =
-  #implicitRoots %= Set.insert x
+  #implicitRoots %= Set.insert (DeclarationRoot x)
 
 
+addInstanceRoot :: ( MonadState Analysis m, MonadReader AnalysisInfo m ) => Declaration -> TypeIndex -> Name -> m ()
+addInstanceRoot x t cls = do
+  for_ (nameToDeclaration cls) \cls' ->
+    #implicitRoots %= Set.insert (InstanceRoot x cls')
+
+  -- since instances will not appear in the output if typeClassRoots is True
+  Config{ typeClassRoots } <- asks weederConfig
+  unless typeClassRoots $ do
+    str <- lookupPprType t
+    #prettyPrintedType %= Map.insert x str
+
+
 define :: MonadState Analysis m => Declaration -> RealSrcSpan -> m ()
 define decl span =
   when ( realSrcSpanStart span /= realSrcSpanEnd span ) do
-    #declarationSites %= Map.insertWith Set.union decl ( Set.singleton span )
+    #declarationSites %= Map.insertWith Set.union decl ( Set.singleton . srcLocLine $ realSrcSpanStart span )
     #dependencyGraph %= overlay ( vertex decl )
 
 
-addDeclaration :: MonadState Analysis m => Declaration -> m ()
-addDeclaration decl =
-  #dependencyGraph %= overlay ( vertex decl )
-
-
--- | Try and add vertices for all declarations in an AST - both
--- those declared here, and those referred to from here.
-addAllDeclarations :: ( MonadState Analysis m ) => HieAST a -> m ()
-addAllDeclarations n = do
-  for_ ( findIdentifiers ( const True ) n ) addDeclaration
-
-
-topLevelAnalysis :: MonadState Analysis m => HieAST a -> m ()
+topLevelAnalysis :: ( MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST TypeIndex -> m ()
 topLevelAnalysis n@Node{ nodeChildren } = do
+  Config{ unusedTypes } <- asks weederConfig
   analysed <-
     runMaybeT
-      ( msum
+      ( msum $
           [
-          --   analyseStandaloneDeriving n
-          -- ,
-            analyseInstanceDeclaration n
+            analyseStandaloneDeriving n
+          , analyseInstanceDeclaration n
           , analyseBinding n
           , analyseRewriteRule n
           , analyseClassDeclaration n
           , analyseDataDeclaration n
           , analysePatternSynonyms n
-          ]
+          ] ++ if unusedTypes then
+          [ analyseTypeSynonym n
+          , analyseFamilyDeclaration n
+          , analyseFamilyInstance n
+          , analyseTypeSignature n
+          ] else []
       )
 
   case analysed of
@@ -273,38 +418,60 @@
       return ()
 
 
-analyseBinding :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()
-analyseBinding n@Node{ nodeSpan, sourcedNodeInfo } = do
+annsContain :: HieAST a -> (String, String) -> Bool
+annsContain Node{ sourcedNodeInfo } ann =
+  any (Set.member ann . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo
+
+
+analyseBinding :: ( Alternative m, MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST a -> m ()
+analyseBinding n@Node{ nodeSpan } = do
   let bindAnns = Set.fromList [("FunBind", "HsBindLR"), ("PatBind", "HsBindLR")]
-  guard $ any (not . Set.disjoint bindAnns . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo
+  guard $ any (annsContain n) bindAnns
 
   for_ ( findDeclarations n ) \d -> do
     define d nodeSpan
 
+    requestEvidence n d
+
     for_ ( uses n ) $ addDependency d
 
 
 analyseRewriteRule :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()
-analyseRewriteRule n@Node{ sourcedNodeInfo } = do
-  guard $ any (Set.member ("HsRule", "RuleDecl") . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo
+analyseRewriteRule n = do
+  guard $ annsContain n ("HsRule", "RuleDecl")
 
   for_ ( uses n ) addImplicitRoot
 
 
-analyseInstanceDeclaration :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()
-analyseInstanceDeclaration n@Node{ sourcedNodeInfo } = do
-  guard $ any (Set.member ("ClsInstD", "InstDecl") . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo
+analyseInstanceDeclaration :: ( Alternative m, MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST TypeIndex -> m ()
+analyseInstanceDeclaration n@Node{ nodeSpan } = do
+  guard $ annsContain n ("ClsInstD", "InstDecl")
 
-  traverse_ addImplicitRoot ( uses n )
+  for_ ( findEvInstBinds n ) \(d, cs, ids, _) -> do
+    -- This makes instance declarations show up in 
+    -- the output if type-class-roots is set to False.
+    define d nodeSpan
 
+    requestEvidence n d
 
-analyseClassDeclaration :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()
-analyseClassDeclaration n@Node{ sourcedNodeInfo } = do
-  guard $ any (Set.member ("ClassDecl", "TyClDecl") . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo
+    for_ ( uses n ) $ addDependency d
 
-  for_ ( findIdentifiers isClassDeclaration n ) $
-    for_ ( findIdentifiers ( const True ) n ) . addDependency
+    case identType ids of
+      Just t -> for_ cs (addInstanceRoot d t)
+      Nothing -> pure ()
 
+
+analyseClassDeclaration :: ( Alternative m, MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST a -> m ()
+analyseClassDeclaration n@Node{ nodeSpan } = do
+  guard $ annsContain n ("ClassDecl", "TyClDecl")
+
+  for_ ( findIdentifiers isClassDeclaration n ) $ \d -> do
+    define d nodeSpan
+
+    requestEvidence n d
+
+    (for_ ( findIdentifiers ( const True ) n ) . addDependency) d
+
   where
 
     isClassDeclaration =
@@ -316,22 +483,44 @@
           False
 
 
-analyseDataDeclaration :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()
-analyseDataDeclaration n@Node{ sourcedNodeInfo } = do
-  guard $ any (Set.member ("DataDecl", "TyClDecl") . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo
+analyseDataDeclaration :: ( Alternative m, MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST TypeIndex -> m ()
+analyseDataDeclaration n = do
+  guard $ annsContain n ("DataDecl", "TyClDecl")
 
+  Config{ unusedTypes } <- asks weederConfig
+
   for_
     ( foldMap
         ( First . Just )
         ( findIdentifiers ( any isDataDec ) n )
     )
-    \dataTypeName ->
-      for_ ( constructors n ) \constructor ->
-        for_ ( foldMap ( First . Just ) ( findIdentifiers ( any isConDec ) constructor ) ) \conDec -> do
+    \dataTypeName -> do
+      when unusedTypes $
+        define dataTypeName (nodeSpan n)
+
+      -- Without connecting constructors to the data declaration TypeAliasGADT.hs 
+      -- fails with a false positive for A
+      conDecs <- for ( constructors n ) \constructor ->
+        for ( foldMap ( First . Just ) ( findIdentifiers ( any isConDec ) constructor ) ) \conDec -> do
           addDependency conDec dataTypeName
+          pure conDec
 
-          for_ ( uses constructor ) ( addDependency conDec )
+      -- To keep acyclicity in record declarations
+      let isDependent d = Just d `elem` fmap getFirst conDecs
 
+      for_ ( uses n ) (\d -> unless (isDependent d) $ addDependency dataTypeName d)
+
+  for_ ( derivedInstances n ) \(d, cs, ids, ast) -> do
+    define d (nodeSpan ast)
+
+    requestEvidence ast d
+
+    for_ ( uses ast ) $ addDependency d
+
+    case identType ids of
+      Just t -> for_ cs (addInstanceRoot d t)
+      Nothing -> pure ()
+
   where
 
     isDataDec = \case
@@ -344,19 +533,124 @@
 
 
 constructors :: HieAST a -> Seq ( HieAST a )
-constructors n@Node{ nodeChildren, sourcedNodeInfo } =
-  if any (any ( ("ConDecl" ==) . unpackFS . nodeAnnotType) . nodeAnnotations) (getSourcedNodeInfo sourcedNodeInfo) then
+constructors = findNodeTypes "ConDecl"
+
+
+derivedInstances :: HieAST a -> Seq (Declaration, Set Name, IdentifierDetails a, HieAST a)
+derivedInstances n = findNodeTypes "HsDerivingClause" n >>= findEvInstBinds
+
+
+findNodeTypes :: String -> HieAST a -> Seq ( HieAST a )
+findNodeTypes t n@Node{ nodeChildren, sourcedNodeInfo } =
+  if any (any ( (t ==) . unpackFS . nodeAnnotType) . nodeAnnotations) (getSourcedNodeInfo sourcedNodeInfo) then
     pure n
 
   else
-    foldMap constructors nodeChildren
+    foldMap (findNodeTypes t) nodeChildren
 
+
+analyseStandaloneDeriving :: ( Alternative m, MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST TypeIndex -> m ()
+analyseStandaloneDeriving n@Node{ nodeSpan } = do
+  guard $ annsContain n ("DerivDecl", "DerivDecl")
+
+  for_ (findEvInstBinds n) \(d, cs, ids, _) -> do
+    define d nodeSpan
+
+    requestEvidence n d
+
+    for_ (uses n) $ addDependency d
+
+    case identType ids of
+      Just t -> for_ cs (addInstanceRoot d t)
+      Nothing -> pure ()
+
+
+analyseTypeSynonym :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()
+analyseTypeSynonym n@Node{ nodeSpan } = do
+  guard $ annsContain n ("SynDecl", "TyClDecl")
+
+  for_ ( findIdentifiers isTypeSynonym n ) $ \d -> do
+    define d nodeSpan
+
+    for_ (uses n) (addDependency d)
+
+  where
+
+    isTypeSynonym =
+      any \case
+        Decl SynDec _ -> True
+        _             -> False
+
+
+analyseFamilyDeclaration :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()
+analyseFamilyDeclaration n@Node{ nodeSpan } = do
+  guard $ annsContain n ("FamDecl", "TyClDecl")
+
+  for_ ( findIdentifiers isFamDec n ) $ \d -> do
+    define d nodeSpan
+
+    for_ (uses n) (addDependency d)
+
+  where
+
+    isFamDec =
+      any \case
+        Decl FamDec _ -> True
+        _             -> False
+
+
+analyseFamilyInstance :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()
+analyseFamilyInstance n = do
+  guard $ annsContain n ("TyFamInstD", "InstDecl")
+
+  for_ ( uses n ) addImplicitRoot
+
+
+analyseTypeSignature :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()
+analyseTypeSignature n = do
+  guard $ annsContain n ("TypeSig", "Sig")
+
+  for_ (findIdentifiers isTypeSigDecl n) $
+    for_ ( uses n ) . addDependency
+
+  where
+
+    isTypeSigDecl =
+      any \case
+        TyDecl -> True
+        _      -> False
+
+
 analysePatternSynonyms :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()
-analysePatternSynonyms n@Node{ sourcedNodeInfo } = do
-  guard $ any (Set.member ("PatSynBind", "HsBindLR") . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo
+analysePatternSynonyms n = do
+  guard $ annsContain n ("PatSynBind", "HsBindLR")
 
   for_ ( findDeclarations n ) $ for_ ( uses n ) . addDependency
 
+
+findEvInstBinds :: HieAST a -> Seq (Declaration, Set Name, IdentifierDetails a, HieAST a)
+findEvInstBinds n = (\(d, ids, ast) -> (d, getClassNames ids, ids, ast)) <$>
+  findIdentifiers'
+    (   not
+      . Set.null
+      . getEvVarSources
+    ) n
+
+  where
+
+    getEvVarSources :: Set ContextInfo -> Set EvVarSource
+    getEvVarSources = foldMap (maybe mempty Set.singleton) .
+      Set.map \case
+        EvidenceVarBind a@EvInstBind{} ModuleScope _ -> Just a
+        _ -> Nothing
+
+    getClassNames :: IdentifierDetails a -> Set Name
+    getClassNames =
+      Set.map cls
+      . getEvVarSources
+      . identInfo
+
+
 findDeclarations :: HieAST a -> Seq Declaration
 findDeclarations =
   findIdentifiers
@@ -379,29 +673,49 @@
   :: ( Set ContextInfo -> Bool )
   -> HieAST a
   -> Seq Declaration
-findIdentifiers f Node{ sourcedNodeInfo, nodeChildren } =
+findIdentifiers f = fmap (\(d, _, _) -> d) . findIdentifiers' f
+
+
+-- | Version of findIdentifiers containing more information,
+-- namely the IdentifierDetails of the declaration and the
+-- node it was found in.
+findIdentifiers'
+  :: ( Set ContextInfo -> Bool )
+  -> HieAST a
+  -> Seq (Declaration, IdentifierDetails a, HieAST a)
+findIdentifiers' f n@Node{ sourcedNodeInfo, nodeChildren } =
      foldMap
-       ( \case
+       (\case
            ( Left _, _ ) ->
              mempty
 
-           ( Right name, IdentifierDetails{ identInfo } ) ->
+           ( Right name, ids@IdentifierDetails{ identInfo } ) ->
              if f identInfo then
-               foldMap pure ( nameToDeclaration name )
+               (, ids, n) <$> foldMap pure (nameToDeclaration name)
 
              else
                mempty
            )
        (foldMap (Map.toList . nodeIdentifiers) (getSourcedNodeInfo sourcedNodeInfo))
-  <> foldMap ( findIdentifiers f ) nodeChildren
+  <> foldMap ( findIdentifiers' f ) nodeChildren
 
 
 uses :: HieAST a -> Set Declaration
 uses =
     foldMap Set.singleton
-  . findIdentifiers \identInfo -> Use `Set.member` identInfo
+  . findIdentifiers (any isUse)
 
+isUse :: ContextInfo -> Bool
+isUse = \case
+  Use -> True
+  -- not RecFieldMatch and RecFieldDecl because they occur under
+  -- data declarations, which we do not want to add as dependencies
+  -- because that would make the graph no longer acyclic
+  -- RecFieldAssign will be most likely accompanied by the constructor
+  RecField RecFieldOcc _ -> True
+  _ -> False
 
+
 nameToDeclaration :: Name -> Maybe Declaration
 nameToDeclaration name = do
   m <- nameModule_maybe name
@@ -410,3 +724,45 @@
 
 unNodeAnnotation :: NodeAnnotation -> (String, String)
 unNodeAnnotation (NodeAnnotation x y) = (unpackFS x, unpackFS y)
+
+
+-- | Add evidence uses found under the given node to 'requestedEvidence'.
+requestEvidence :: ( MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST a -> Declaration -> m ()
+requestEvidence n d = do
+  Config{ typeClassRoots } <- asks weederConfig
+
+  -- If type-class-roots flag is set then we don't need to follow
+  -- evidence uses as the binding sites will be roots anyway
+  unless typeClassRoots $
+    #requestedEvidence %= Map.insertWith (<>) d (Set.fromList names)
+
+  where
+
+    names = concat . Tree.flatten $ evidenceUseTree n
+
+    evidenceUseTree :: HieAST a -> Tree [Name]
+    evidenceUseTree Node{ sourcedNodeInfo, nodeChildren } = Tree.Node
+      { Tree.rootLabel = concatMap (findEvidenceUse . nodeIdentifiers) (getSourcedNodeInfo sourcedNodeInfo)
+      , Tree.subForest = map evidenceUseTree nodeChildren
+      }
+
+
+-- | Follow the given evidence uses back to their instance bindings,
+-- and connect the declaration to those bindings.
+followEvidenceUses :: RefMap TypeIndex -> Declaration -> Set Name -> Graph Declaration
+followEvidenceUses refMap d names =
+  let getEvidenceTrees = mapMaybe (getEvidenceTree refMap) . Set.toList
+      evidenceInfos = concatMap Tree.flatten (getEvidenceTrees names)
+      instanceEvidenceInfos = evidenceInfos & filter \case
+        EvidenceInfo _ _ _ (Just (EvInstBind _ _, ModuleScope, _)) -> True
+        _ -> False
+      evBindSiteDecls = mapMaybe (nameToDeclaration . evidenceVar) instanceEvidenceInfos
+   in star d evBindSiteDecls
+
+
+-- | Follow evidence uses listed under 'requestedEvidence' back to their 
+-- instance bindings, and connect their corresponding declaration to those bindings.
+analyseEvidenceUses :: RefMap TypeIndex -> Analysis -> Analysis
+analyseEvidenceUses rf a@Analysis{ requestedEvidence, dependencyGraph } =
+  let graphs = map (uncurry (followEvidenceUses rf)) $ Map.toList requestedEvidence
+   in a { dependencyGraph = overlays (dependencyGraph : graphs) }
diff --git a/src/Weeder/Config.hs b/src/Weeder/Config.hs
--- a/src/Weeder/Config.hs
+++ b/src/Weeder/Config.hs
@@ -2,30 +2,198 @@
 {-# language BlockArguments #-}
 {-# language OverloadedStrings #-}
 {-# language RecordWildCards #-}
+{-# language LambdaCase #-}
+{-# language PatternSynonyms #-}
+{-# language FlexibleInstances #-}
+{-# language DeriveTraversable #-}
+{-# language NamedFieldPuns #-}
 
-module Weeder.Config ( Config(..) ) where
+module Weeder.Config
+  ( -- * Config
+    Config
+  , ConfigParsed
+  , ConfigType(..)
+  , compileConfig
+  , configToToml
+  , decodeNoDefaults
+  , defaultConfig
+    -- * Marking instances as roots
+  , InstancePattern
+  , modulePattern
+  , instancePattern
+  , classPattern
+  , pattern InstanceOnly
+  , pattern ClassOnly
+  , pattern ModuleOnly
+  )
+   where
 
+-- base
+import Control.Applicative ((<|>), empty)
+import Data.Bifunctor (bimap)
+import Data.Char (toLower)
+import Data.List (intersperse, intercalate)
+
 -- containers
-import Data.Set ( Set )
+import Data.Containers.ListUtils (nubOrd)
 
+-- regex-tdfa
+import Text.Regex.TDFA ( Regex, RegexOptions ( defaultExecOpt, defaultCompOpt ) )
+import Text.Regex.TDFA.TDFA ( patternToRegex )
+import Text.Regex.TDFA.ReadRegex ( parseRegex )
+
 -- toml-reader
 import qualified TOML
 
 
 -- | Configuration for Weeder analysis.
-data Config = Config
-  { rootPatterns :: Set String
+type Config = ConfigType Regex
+
+
+-- | Configuration that has been parsed from TOML (and can still be 
+-- converted back), but not yet compiled to a 'Config'.
+type ConfigParsed = ConfigType String
+
+
+-- | Underlying type for 'Config' and 'ConfigParsed'.
+data ConfigType a = Config
+  { rootPatterns :: [a]
     -- ^ Any declarations matching these regular expressions will be added to
     -- the root set.
   , typeClassRoots :: Bool
     -- ^ If True, consider all declarations in a type class as part of the root
-    -- set. Weeder is currently unable to identify whether or not a type class
-    -- instance is used - enabling this option can prevent false positives.
+    -- set. Overrides root-instances.
+  , rootInstances :: [InstancePattern a]
+    -- ^ All matching instances will be added to the root set. An absent field
+    -- will always match.
+  , unusedTypes :: Bool
+    -- ^ Toggle to look for and output unused types. Type family instances will
+    -- be marked as implicit roots.
+  } deriving (Eq, Show)
+
+
+-- | Construct via InstanceOnly, ClassOnly or ModuleOnly, 
+-- and combine with the Semigroup instance. The Semigroup
+-- instance ignores duplicate fields, prioritising the 
+-- left argument.
+data InstancePattern a = InstancePattern
+  { instancePattern :: Maybe a
+  , classPattern :: Maybe a
+  , modulePattern :: Maybe a
+  } deriving (Eq, Show, Ord, Functor, Foldable, Traversable)
+
+
+instance Semigroup (InstancePattern a) where
+  InstancePattern i c m <> InstancePattern i' c' m' =
+    InstancePattern (i <|> i') (c <|> c') (m <|> m')
+
+
+pattern InstanceOnly, ClassOnly, ModuleOnly :: a -> InstancePattern a
+pattern InstanceOnly t = InstancePattern (Just t) Nothing Nothing
+pattern ClassOnly c = InstancePattern Nothing (Just c) Nothing
+pattern ModuleOnly m = InstancePattern Nothing Nothing (Just m)
+
+
+defaultConfig :: ConfigParsed
+defaultConfig = Config
+  { rootPatterns = [ "Main.main", "^Paths_.*"]
+  , typeClassRoots = False
+  , rootInstances = [ ClassOnly "\\.IsString$", ClassOnly "\\.IsList$" ]
+  , unusedTypes = False
   }
 
+
 instance TOML.DecodeTOML Config where
   tomlDecoder = do
-    rootPatterns <- TOML.getField "roots"
-    typeClassRoots <- TOML.getField "type-class-roots"
+    conf <- TOML.tomlDecoder
+    either fail pure $ compileConfig conf
 
-    return Config{..}
+
+instance TOML.DecodeTOML ConfigParsed where
+  tomlDecoder = do
+    rootPatterns <- TOML.getFieldOr (rootPatterns defaultConfig) "roots"
+    typeClassRoots <- TOML.getFieldOr (typeClassRoots defaultConfig) "type-class-roots"
+    rootInstances <- TOML.getFieldOr (rootInstances defaultConfig) "root-instances" 
+    unusedTypes <- TOML.getFieldOr (unusedTypes defaultConfig) "unused-types"
+
+    pure Config{..}
+
+
+decodeNoDefaults :: TOML.Decoder Config
+decodeNoDefaults = do
+  rootPatterns <- TOML.getField "roots"
+  typeClassRoots <- TOML.getField "type-class-roots"
+  rootInstances <- TOML.getField "root-instances"
+  unusedTypes <- TOML.getField "unused-types"
+
+  either fail pure $ compileConfig Config{..}
+
+
+instance TOML.DecodeTOML (InstancePattern String) where
+  tomlDecoder = decodeInstancePattern
+
+
+-- | Decoder for a value of any of the forms:
+--
+-- @{instance = t, class = c, module = m} -> InstanceClassAndModule t c m@
+--
+-- @a -> InstanceOnly a@
+--
+-- @{instance = t} -> InstanceOnly t@
+--
+-- @{class = m} -> ClassOnly c@
+--
+-- etc.
+decodeInstancePattern :: TOML.Decoder (InstancePattern String)
+decodeInstancePattern = decodeTable <|> decodeStringLiteral <|> decodeInstanceError
+
+  where
+
+    decodeStringLiteral = InstanceOnly <$> TOML.tomlDecoder
+
+    decodeTable = do
+      t <- fmap InstanceOnly <$> TOML.getFieldOpt "instance"
+      c <- fmap ClassOnly <$> TOML.getFieldOpt "class"
+      m <- fmap ModuleOnly <$> TOML.getFieldOpt "module"
+      maybe empty pure (t <> c <> m)
+
+    decodeInstanceError = TOML.makeDecoder $
+      TOML.invalidValue "Need to specify at least one of 'instance', 'class', or 'module'"
+
+
+showInstancePattern :: Show a => InstancePattern a -> String
+showInstancePattern = \case
+  InstanceOnly a -> show a
+  p -> "{ " ++ table ++ " }"
+    where
+      table = intercalate ", " . filter (not . null) $
+          [ maybe mempty typeField (instancePattern p)
+          , maybe mempty classField (classPattern p)
+          , maybe mempty moduleField (modulePattern p)
+          ]
+      typeField t = "instance = " ++ show t
+      classField c = "class = " ++ show c
+      moduleField m = "module = " ++ show m
+
+
+compileRegex :: String -> Either String Regex
+compileRegex = bimap show (\p -> patternToRegex p defaultCompOpt defaultExecOpt) . parseRegex
+
+
+compileConfig :: ConfigParsed -> Either String Config
+compileConfig conf@Config{ rootInstances, rootPatterns } = do
+  rootInstances' <- traverse (traverse compileRegex) . nubOrd $ rootInstances
+  rootPatterns' <- traverse compileRegex $ nubOrd rootPatterns
+  pure conf{ rootInstances = rootInstances', rootPatterns = rootPatterns' }
+
+
+configToToml :: ConfigParsed -> String
+configToToml Config{..}
+  = unlines . intersperse mempty $
+      [ "roots = " ++ show rootPatterns
+      , "type-class-roots = " ++ map toLower (show typeClassRoots)
+      , "root-instances = " ++ "[" ++ intercalate "," (map showInstancePattern rootInstances') ++ "]"
+      , "unused-types = " ++ map toLower (show unusedTypes)
+      ]
+  where
+    rootInstances' = rootInstances
diff --git a/src/Weeder/Main.hs b/src/Weeder/Main.hs
--- a/src/Weeder/Main.hs
+++ b/src/Weeder/Main.hs
@@ -3,27 +3,26 @@
 {-# language FlexibleContexts #-}
 {-# language NamedFieldPuns #-}
 {-# language OverloadedStrings #-}
+{-# language LambdaCase #-}
+{-# language RecordWildCards #-}
 
 -- | This module provides an entry point to the Weeder executable.
 
-module Weeder.Main ( main, mainWithConfig ) where
+module Weeder.Main ( main, mainWithConfig, getHieFiles ) where
 
+-- async
+import Control.Concurrent.Async ( async, link, ExceptionInLinkedThread ( ExceptionInLinkedThread ) )
+
 -- base
-import Control.Exception ( throwIO )
-import Control.Monad ( guard, when )
-import Control.Monad.IO.Class ( liftIO )
-import Data.Bool
+import Control.Exception ( Exception, throwIO, displayException, catches, Handler ( Handler ), SomeException ( SomeException ) )
+import Control.Concurrent ( getChanContents, newChan, writeChan, setNumCapabilities )
+import Control.Monad ( unless, when )
 import Data.Foldable
-import Data.List ( isSuffixOf, sortOn )
+import Data.List ( isSuffixOf )
+import Data.Maybe ( isJust, catMaybes )
 import Data.Version ( showVersion )
-import System.Exit ( exitFailure, ExitCode(..), exitWith )
-
--- containers
-import qualified Data.Map.Strict as Map
-import qualified Data.Set as Set
-
--- text
-import qualified Data.Text as T
+import System.Exit ( ExitCode(..), exitWith )
+import System.IO ( stderr, hPutStrLn )
 
 -- toml-reader
 import qualified TOML
@@ -37,65 +36,168 @@
 -- ghc
 import GHC.Iface.Ext.Binary ( HieFileResult( HieFileResult, hie_file_result ), readHieFileWithVersion )
 import GHC.Iface.Ext.Types ( HieFile( hie_hs_file ), hieVersion )
-import GHC.Unit.Module ( moduleName, moduleNameString )
 import GHC.Types.Name.Cache ( initNameCache, NameCache )
-import GHC.Types.Name ( occNameString )
-import GHC.Types.SrcLoc ( RealSrcLoc, realSrcSpanStart, srcLocLine )
 
--- regex-tdfa
-import Text.Regex.TDFA ( (=~) )
-
 -- optparse-applicative
 import Options.Applicative
 
--- transformers
-import Control.Monad.Trans.State.Strict ( execStateT )
+-- text
+import qualified Data.Text.IO as T
 
 -- weeder
-import Weeder
+import Weeder.Run
 import Weeder.Config
 import Paths_weeder (version)
 
 
+-- | Each exception corresponds to an exit code.
+data WeederException 
+  = ExitNoHieFilesFailure
+  | ExitHieVersionFailure 
+      FilePath -- ^ Path to HIE file
+      Integer -- ^ HIE file's header version
+  | ExitConfigFailure
+      String -- ^ Error message
+  | ExitWeedsFound
+  deriving Show
+
+
+weederExitCode :: WeederException -> ExitCode
+weederExitCode = \case
+  ExitWeedsFound -> ExitFailure 228
+  ExitHieVersionFailure _ _ -> ExitFailure 2
+  ExitConfigFailure _ -> ExitFailure 3
+  ExitNoHieFilesFailure -> ExitFailure 4
+
+
+instance Exception WeederException where
+  displayException = \case
+    ExitNoHieFilesFailure -> noHieFilesFoundMessage
+    ExitHieVersionFailure path v -> hieVersionMismatchMessage path v
+    ExitConfigFailure s -> s
+    ExitWeedsFound -> mempty
+    where
+
+      noHieFilesFoundMessage =  
+        "No HIE files found: check that the directory is correct "
+        <> "and that the -fwrite-ide-info compilation flag is set."
+
+      hieVersionMismatchMessage path v = unlines
+        [ "incompatible hie file: " <> path
+        , "    this version of weeder was compiled with GHC version "
+          <> show hieVersion
+        , "    the hie files in this project were generated with GHC version "
+          <> show v
+        , "    weeder must be built with the same GHC version"
+          <> " as the project it is used on"
+        ]
+
+
+-- | Convert 'WeederException' to the corresponding 'ExitCode' and emit an error 
+-- message to stderr.
+--
+-- Additionally, unwrap 'ExceptionInLinkedThread' exceptions: this is for
+-- 'getHieFiles'.
+handleWeederException :: IO a -> IO a
+handleWeederException a = catches a handlers 
+  where
+    handlers = [ Handler rethrowExits
+               , Handler unwrapLinks
+               ]
+    rethrowExits w = do
+      hPutStrLn stderr (displayException w)
+      exitWith (weederExitCode w)
+    unwrapLinks (ExceptionInLinkedThread _ (SomeException w)) =
+      throwIO w
+
+
+data CLIArguments = CLIArguments
+  { configPath :: FilePath
+  , hieExt :: String
+  , hieDirectories :: [FilePath]
+  , requireHsFiles :: Bool
+  , writeDefaultConfig :: Bool
+  , noDefaultFields :: Bool
+  , capabilities :: Maybe Int
+  }
+
+
+parseCLIArguments :: Parser CLIArguments
+parseCLIArguments = do
+    configPath <- strOption
+        ( long "config"
+            <> help "A file path for Weeder's configuration."
+            <> value "./weeder.toml"
+            <> metavar "<weeder.toml>"
+        )
+    hieExt <- strOption
+        ( long "hie-extension"
+            <> value ".hie"
+            <> help "Extension of HIE files"
+            <> showDefault
+        )
+    hieDirectories <- many (
+        strOption
+            ( long "hie-directory"
+                <> help "A directory to look for .hie files in. Maybe specified multiple times. Default ./."
+            )
+        )
+    requireHsFiles <- switch
+          ( long "require-hs-files"
+              <> help "Skip stale .hie files with no matching .hs modules"
+          )
+    writeDefaultConfig <- switch
+          ( long "write-default-config"
+              <> help "Write a default configuration file if the one specified by --config does not exist"
+          )
+    noDefaultFields <- switch
+          ( long "no-default-fields"
+              <> help "Do not use default field values for missing fields in the configuration."
+          )
+    capabilities <- nParser <|> jParser
+    pure CLIArguments{..}
+    where
+      jParser = Just <$> option auto
+          ( short 'j'
+              <> value 1
+              <> help "Number of cores to use."
+              <> showDefault)
+      nParser = flag' Nothing
+          ( short 'N'
+              <> help "Use all available cores."
+          )
+
+
 -- | Parse command line arguments and into a 'Config' and run 'mainWithConfig'.
+--
+-- Exits with one of the listed Weeder exit codes on failure.
 main :: IO ()
-main = do
-  (configExpr, hieExt, hieDirectories, requireHsFiles) <-
+main = handleWeederException do
+  CLIArguments{..} <-
     execParser $
-      info (optsP <**> helper <**> versionP) mempty
+      info (parseCLIArguments <**> helper <**> versionP) mempty
 
-  (exitCode, _) <- 
-    TOML.decodeFile (T.unpack configExpr)
-      >>= either throwIO pure
-      >>= mainWithConfig hieExt hieDirectories requireHsFiles
-  
-  exitWith exitCode
+  traverse_ setNumCapabilities capabilities
+
+  configExists <-
+    doesFileExist configPath
+
+  unless (writeDefaultConfig ==> configExists) do
+    hPutStrLn stderr $ "Did not find config: wrote default config to " ++ configPath
+    writeFile configPath (configToToml defaultConfig)
+
+  decodeConfig noDefaultFields configPath
+    >>= either throwConfigError pure
+    >>= mainWithConfig hieExt hieDirectories requireHsFiles
   where
-    optsP = (,,,)
-        <$> strOption
-            ( long "config"
-                <> help "A file path for Weeder's configuration."
-                <> value "./weeder.toml"
-                <> metavar "<weeder.toml>"
-                <> showDefaultWith T.unpack
-            )
-        <*> strOption
-            ( long "hie-extension"
-                <> value ".hie"
-                <> help "Extension of HIE files"
-                <> showDefault
-            )
-        <*> many (
-            strOption
-                ( long "hie-directory"
-                    <> help "A directory to look for .hie files in. Maybe specified multiple times. Default ./."
-                )
-            )
-        <*> switch
-              ( long "require-hs-files"
-                  <> help "Skip stale .hie files with no matching .hs modules"
-              )
+    throwConfigError e =
+      throwIO $ ExitConfigFailure (show e)
 
+    decodeConfig noDefaultFields =
+      if noDefaultFields
+        then fmap (TOML.decodeWith decodeNoDefaults) . T.readFile
+        else TOML.decodeFile
+
     versionP = infoOption ( "weeder version "
                             <> showVersion version
                             <> "\nhie version "
@@ -107,8 +209,31 @@
 --
 -- This will recursively find all files with the given extension in the given directories, perform
 -- analysis, and report all unused definitions according to the 'Config'.
-mainWithConfig :: String -> [FilePath] -> Bool -> Config -> IO (ExitCode, Analysis)
-mainWithConfig hieExt hieDirectories requireHsFiles Config{ rootPatterns, typeClassRoots } = do
+--
+-- Exits with one of the listed Weeder exit codes on failure.
+mainWithConfig :: String -> [FilePath] -> Bool -> Config -> IO ()
+mainWithConfig hieExt hieDirectories requireHsFiles weederConfig = handleWeederException do
+  hieFiles <-
+    getHieFiles hieExt hieDirectories requireHsFiles
+
+  when (null hieFiles) $ throwIO ExitNoHieFilesFailure
+
+  let
+    (weeds, _) =
+      runWeeder weederConfig hieFiles
+
+  mapM_ (putStrLn . formatWeed) weeds
+
+  unless (null weeds) $ throwIO ExitWeedsFound
+
+
+-- | Find and read all .hie files in the given directories according to the given parameters,
+-- exiting if any are incompatible with the current version of GHC.
+-- The .hie files are returned as a lazy stream in the form of a list.
+--
+-- Will rethrow exceptions as 'ExceptionInLinkedThread' to the calling thread.
+getHieFiles :: String -> [FilePath] -> Bool -> IO [HieFile]
+getHieFiles hieExt hieDirectories requireHsFiles = do
   hieFilePaths <-
     concat <$>
       traverse ( getFilesIn hieExt )
@@ -122,60 +247,28 @@
       then getFilesIn ".hs" "./."
       else pure []
 
+  hieFileResultsChan <- newChan
+
   nameCache <-
     initNameCache 'z' []
 
-  analysis <-
-    flip execStateT emptyAnalysis do
-      for_ hieFilePaths \hieFilePath -> do
-        hieFileResult <- liftIO ( readCompatibleHieFileOrExit nameCache hieFilePath )
-        let hsFileExists = any ( hie_hs_file hieFileResult `isSuffixOf` ) hsFilePaths
-        when (requireHsFiles ==> hsFileExists) do
-          analyseHieFile hieFileResult
-
-  let
-    roots =
-      Set.filter
-        ( \d ->
-            any
-              ( ( moduleNameString ( moduleName ( declModule d ) ) <> "." <> occNameString ( declOccName d ) ) =~ )
-              rootPatterns
-        )
-        ( allDeclarations analysis )
-
-    reachableSet =
-      reachable
-        analysis
-        ( Set.map DeclarationRoot roots <> bool mempty ( Set.map DeclarationRoot ( implicitRoots analysis ) ) typeClassRoots )
-
-    dead =
-      allDeclarations analysis Set.\\ reachableSet
-
-    warnings =
-      Map.unionsWith (++) $
-      foldMap
-        ( \d ->
-            fold $ do
-              moduleFilePath <- Map.lookup ( declModule d ) ( modulePaths analysis )
-              spans <- Map.lookup d ( declarationSites analysis )
-              guard $ not $ null spans
-              let starts = map realSrcSpanStart $ Set.toList spans
-              return [ Map.singleton moduleFilePath ( liftA2 (,) starts (pure d) ) ]
-        )
-        dead
-
-  for_ ( Map.toList warnings ) \( path, declarations ) ->
-    for_ (sortOn (srcLocLine . fst) declarations) \( start, d ) ->
-      putStrLn $ showWeed path start d
+  a <- async $ handleWeederException do
+    readHieFiles nameCache hieFilePaths hieFileResultsChan hsFilePaths
+    writeChan hieFileResultsChan Nothing
+ 
+  link a
 
-  let exitCode = if null warnings then ExitSuccess else ExitFailure 1
+  catMaybes . takeWhile isJust <$> getChanContents hieFileResultsChan
 
-  pure (exitCode, analysis)
+  where
 
-showWeed :: FilePath -> RealSrcLoc -> Declaration -> String
-showWeed path start d =
-  path <> ":" <> show ( srcLocLine start ) <> ": "
-    <> occNameString ( declOccName d)
+    readHieFiles nameCache hieFilePaths hieFileResultsChan hsFilePaths =
+      for_ hieFilePaths \hieFilePath -> do
+        hieFileResult <-
+          readCompatibleHieFileOrExit nameCache hieFilePath
+        let hsFileExists = any ( hie_hs_file hieFileResult `isSuffixOf` ) hsFilePaths
+        when (requireHsFiles ==> hsFileExists) $
+          writeChan hieFileResultsChan (Just hieFileResult)
 
 
 -- | Recursively search for files with the given extension in given directory
@@ -226,15 +319,8 @@
   case res of
     Right HieFileResult{ hie_file_result } ->
       return hie_file_result
-    Left ( v, _ghcVersion ) -> do
-      putStrLn $ "incompatible hie file: " <> path
-      putStrLn $ "    this version of weeder was compiled with GHC version "
-               <> show hieVersion
-      putStrLn $ "    the hie files in this project were generated with GHC version "
-               <> show v
-      putStrLn $ "    weeder must be built with the same GHC version"
-               <> " as the project it is used on"
-      exitFailure
+    Left ( v, _ghcVersion ) ->
+      throwIO $ ExitHieVersionFailure path v
 
 
 infixr 5 ==>
diff --git a/src/Weeder/Run.hs b/src/Weeder/Run.hs
new file mode 100644
--- /dev/null
+++ b/src/Weeder/Run.hs
@@ -0,0 +1,159 @@
+{-# language RecordWildCards #-}
+{-# language BlockArguments #-}
+{-# language LambdaCase #-}
+{-# language NamedFieldPuns #-}
+{-# LANGUAGE FlexibleContexts #-}
+
+module Weeder.Run ( runWeeder, Weed(..), formatWeed ) where
+
+-- base
+import Control.Applicative ( liftA2 )
+import Control.Monad ( guard )
+import Data.List ( sortOn )
+import Data.Foldable ( fold, foldl' )
+import Data.Function ( (&) )
+
+-- containers
+import Data.Set ( Set )
+import qualified Data.Set as Set
+import qualified Data.Map.Strict as Map
+
+-- ghc
+import GHC.Plugins 
+  ( occNameString
+  , moduleName
+  , moduleNameString 
+  )
+import GHC.Iface.Ext.Types ( HieFile( hie_asts ), getAsts )
+import GHC.Iface.Ext.Utils (generateReferencesMap)
+
+-- parallel
+import Control.Parallel (pseq)
+import Control.Parallel.Strategies (parMap, rdeepseq)
+
+-- regex-tdfa
+import Text.Regex.TDFA ( matchTest )
+
+-- transformers
+import Control.Monad.State.Strict ( execState )
+
+-- weeder
+import Weeder
+import Weeder.Config
+
+
+data Weed = Weed
+  { weedPath :: FilePath
+  , weedLoc :: Int
+  , weedDeclaration :: Declaration
+  , weedPrettyPrintedType :: Maybe String
+  }
+
+
+formatWeed :: Weed -> String
+formatWeed Weed{..} =
+  weedPath <> ":" <> show weedLoc <> ": "
+    <> case weedPrettyPrintedType of
+      Nothing -> occNameString ( declOccName weedDeclaration )
+      Just t -> "(Instance) :: " <> t
+
+-- | Run Weeder on the given .hie files with the given 'Config'.
+--
+-- Returns a list of 'Weed's that can be displayed using
+-- 'formatWeed', and the final 'Analysis'.
+runWeeder :: Config -> [HieFile] -> ([Weed], Analysis)
+runWeeder weederConfig@Config{ rootPatterns, typeClassRoots, rootInstances } hieFiles =
+  let 
+    asts = concatMap (Map.elems . getAsts . hie_asts) hieFiles
+
+    rf = generateReferencesMap asts
+
+    analyses =
+      parMap rdeepseq (\hf -> execState (analyseHieFile weederConfig hf) emptyAnalysis) hieFiles
+
+    analyseEvidenceUses' = 
+      if typeClassRoots
+        then id
+        else analyseEvidenceUses rf
+
+    analysis1 = 
+      foldl' mappend mempty analyses
+
+    -- Evaluating 'analysis1' first allows us to begin analysis 
+    -- while hieFiles is still being read (since rf depends on all hie files)
+    analysis = analysis1 `pseq`
+      analyseEvidenceUses' analysis1
+
+    -- We limit ourselves to outputable declarations only rather than all
+    -- declarations in the graph. This has a slight performance benefit,
+    -- at the cost of having to assume that a non-outputable declaration
+    -- will always either be an implicit root or irrelevant.
+    roots =
+      Set.filter
+        ( \d ->
+            any
+              (`matchTest` displayDeclaration d)
+              rootPatterns
+        )
+        ( outputableDeclarations analysis )
+
+    reachableSet =
+      reachable
+        analysis
+        ( Set.map DeclarationRoot roots <> filterImplicitRoots analysis ( implicitRoots analysis ) )
+
+    -- We only care about dead declarations if they have a span assigned,
+    -- since they don't show up in the output otherwise
+    dead =
+      outputableDeclarations analysis Set.\\ reachableSet
+
+    warnings =
+      Map.unionsWith (++) $
+      foldMap
+        ( \d ->
+            fold $ do
+              moduleFilePath <- Map.lookup ( declModule d ) ( modulePaths analysis )
+              starts <- Map.lookup d ( declarationSites analysis )
+              guard $ not $ null starts
+              return [ Map.singleton moduleFilePath ( liftA2 (,) (Set.toList starts) (pure d) ) ]
+        )
+        dead
+
+    weeds =
+      Map.toList warnings & concatMap \( weedPath, declarations ) ->
+        sortOn fst declarations & map \( weedLoc, weedDeclaration ) ->
+          Weed { weedPrettyPrintedType = Map.lookup weedDeclaration (prettyPrintedType analysis)
+               , weedPath
+               , weedLoc
+               , weedDeclaration
+               }
+
+  in (weeds, analysis)
+
+  where
+
+    filterImplicitRoots :: Analysis -> Set Root -> Set Root
+    filterImplicitRoots Analysis{ prettyPrintedType, modulePaths } = Set.filter $ \case
+      DeclarationRoot _ -> True -- keep implicit roots for rewrite rules etc
+
+      ModuleRoot _ -> True
+
+      InstanceRoot d c -> typeClassRoots || matchingType
+        where
+          matchingType = 
+            let mt = Map.lookup d prettyPrintedType
+                matches = maybe (const False) (flip matchTest) mt
+            in any (maybe True matches) filteredInstances
+
+          filteredInstances = 
+            map instancePattern 
+            . filter (maybe True (`matchTest` displayDeclaration c) . classPattern) 
+            . filter (maybe True modulePathMatches . modulePattern) 
+            $ rootInstances
+
+          modulePathMatches p = maybe False (p `matchTest`) (Map.lookup ( declModule d ) modulePaths)
+
+
+displayDeclaration :: Declaration -> String
+displayDeclaration d = 
+  moduleNameString ( moduleName ( declModule d ) ) <> "." <> occNameString ( declOccName d )
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,6 +1,8 @@
 import qualified Weeder.Main
+import qualified Weeder.Run
 import qualified Weeder
 import qualified TOML
+import qualified UnitTests
 
 import Algebra.Graph.Export.Dot
 import GHC.Types.Name.Occurrence (occNameString)
@@ -8,57 +10,79 @@
 import System.Environment (getArgs, withArgs)
 import System.FilePath
 import System.Process
-import System.IO.Silently (hCapture_)
-import System.IO (stdout, stderr, hPrint)
+import System.IO (stderr, hPrint)
 import Test.Hspec
 import Control.Monad (zipWithM_, when)
 import Control.Exception ( throwIO, IOException, handle )
+import Data.Maybe (isJust)
+import Data.List (find, sortOn)
 
 main :: IO ()
 main = do
   args <- getArgs
-  stdoutFiles <- discoverIntegrationTests
-  let hieDirectories = map dropExtension stdoutFiles
+  testOutputFiles <- fmap sortTests discoverIntegrationTests
+  let hieDirectories = map (dropExtension . snd) testOutputFiles
       drawDots = mapM_ (drawDot . (<.> ".dot")) hieDirectories
       graphviz = "--graphviz" `elem` args
   withArgs (filter (/="--graphviz") args) $
     hspec $ afterAll_ (when graphviz drawDots) $ do
-      describe "Weeder.Main" $
-        describe "mainWithConfig" $
-          zipWithM_ integrationTestSpec stdoutFiles hieDirectories
+      describe "Weeder.Run" $
+        describe "runWeeder" $
+          zipWithM_ (uncurry integrationTestSpec) testOutputFiles hieDirectories
+      UnitTests.spec
   where
     -- Draw a dotfile via graphviz
     drawDot f = callCommand $ "dot -Tpng " ++ f ++ " -o " ++ (f -<.> ".png")
+    -- Sort the output files such that the failing ones go last
+    sortTests = sortOn (isJust . fst)
 
--- | Run weeder on hieDirectory, comparing the output to stdoutFile
--- The directory containing hieDirectory must also have a .toml file
--- with the same name as hieDirectory
-integrationTestSpec :: FilePath -> FilePath -> Spec
-integrationTestSpec stdoutFile hieDirectory = do
-  it ("produces the expected output for " ++ hieDirectory) $ do
+-- | Run weeder on @hieDirectory@, comparing the output to @stdoutFile@.
+--
+-- The directory containing @hieDirectory@ must also have a @.toml@ file
+-- with the same name as @hieDirectory@.
+--
+-- If @failingFile@ is @Just@, it is used as the expected output instead of
+-- @stdoutFile@, and a different failure message is printed if the output
+-- matches @stdoutFile@.
+integrationTestSpec :: Maybe FilePath -> FilePath -> FilePath -> Spec
+integrationTestSpec failingFile stdoutFile hieDirectory = do
+  it (integrationTestText ++ hieDirectory) $ do
     expectedOutput <- readFile stdoutFile
     actualOutput <- integrationTestOutput hieDirectory
-    actualOutput `shouldBe` expectedOutput
+    case failingFile of
+      Just f -> do
+        failingOutput <- readFile f
+        actualOutput `shouldNotBe` expectedOutput
+        actualOutput `shouldBe` failingOutput
+      Nothing ->
+        actualOutput `shouldBe` expectedOutput
+  where
+    integrationTestText = case failingFile of
+      Nothing -> "produces the expected output for "
+      Just _ -> "produces the expected (wrong) output for "
 
--- | Returns detected .stdout files in ./test/Spec
-discoverIntegrationTests :: IO [FilePath]
+-- | Returns detected .failing and .stdout files in ./test/Spec
+discoverIntegrationTests :: IO [(Maybe FilePath, FilePath)]
 discoverIntegrationTests = do
-  contents <- listDirectory "./test/Spec"
-  pure . map ("./test/Spec" </>) $ filter (".stdout" `isExtensionOf`) contents
+  contents <- listDirectory testPath
+  let stdoutFiles = map (testPath </>) $
+        filter (".stdout" `isExtensionOf`) contents
+  pure . map (\s -> (findFailing s contents, s)) $ stdoutFiles
+    where
+      findFailing s = fmap (testPath </>) . find (takeBaseName s <.> ".failing" ==)
+      testPath = "./test/Spec"
 
 -- | Run weeder on the given directory for .hie files, returning stdout
 -- Also creates a dotfile containing the dependency graph as seen by Weeder
 integrationTestOutput :: FilePath -> IO String
-integrationTestOutput hieDirectory = hCapture_ [stdout] $ do
-  isEmpty <- not . any (".hie" `isExtensionOf`) <$> listDirectory hieDirectory
-  when isEmpty $ fail "No .hie files found in directory, this is probably unintended"
-  (_, analysis) <-
-    TOML.decodeFile configExpr
-      >>= either throwIO pure
-      >>= Weeder.Main.mainWithConfig ".hie" [hieDirectory] True
-  let graph = Weeder.dependencyGraph analysis
+integrationTestOutput hieDirectory = do
+  hieFiles <- Weeder.Main.getHieFiles ".hie" [hieDirectory] True
+  weederConfig <- TOML.decodeFile configExpr >>= either throwIO pure
+  let (weeds, analysis) = Weeder.Run.runWeeder weederConfig hieFiles
+      graph = Weeder.dependencyGraph analysis
       graph' = export (defaultStyle (occNameString . Weeder.declOccName)) graph
   handle (\e -> hPrint stderr (e :: IOException)) $
     writeFile (hieDirectory <.> ".dot") graph'
+  pure (unlines $ map Weeder.Run.formatWeed weeds)
   where
     configExpr = hieDirectory <.> ".toml"
diff --git a/test/Spec/ApplicativeDo.failing b/test/Spec/ApplicativeDo.failing
new file mode 100644
--- /dev/null
+++ b/test/Spec/ApplicativeDo.failing
@@ -0,0 +1,2 @@
+test/Spec/ApplicativeDo/ApplicativeDo.hs:6: (Instance) :: Functor Foo
+test/Spec/ApplicativeDo/ApplicativeDo.hs:9: (Instance) :: Applicative Foo
diff --git a/test/Spec/ApplicativeDo.stdout b/test/Spec/ApplicativeDo.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/ApplicativeDo.stdout
diff --git a/test/Spec/ApplicativeDo.toml b/test/Spec/ApplicativeDo.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/ApplicativeDo.toml
@@ -0,0 +1,3 @@
+roots = [ "Spec.ApplicativeDo.ApplicativeDo.root" ]
+
+type-class-roots = false
diff --git a/test/Spec/ApplicativeDo/ApplicativeDo.hs b/test/Spec/ApplicativeDo/ApplicativeDo.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/ApplicativeDo/ApplicativeDo.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE ApplicativeDo #-}
+module Spec.ApplicativeDo.ApplicativeDo where
+
+newtype Foo a = Foo a
+
+instance Functor Foo where
+  fmap f (Foo a) = Foo (f a)
+
+instance Applicative Foo where
+  pure = Foo
+  Foo f <*> Foo a = Foo (f a)
+
+root :: Foo Int
+root = do
+  a <- Foo 1
+  b <- Foo 2
+  pure (a + b)
diff --git a/test/Spec/ConfigInstanceModules.stdout b/test/Spec/ConfigInstanceModules.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/ConfigInstanceModules.stdout
@@ -0,0 +1,2 @@
+test/Spec/ConfigInstanceModules/Module1.hs:3: (Instance) :: Show T
+test/Spec/ConfigInstanceModules/Module2.hs:3: (Instance) :: Bounded T
diff --git a/test/Spec/ConfigInstanceModules.toml b/test/Spec/ConfigInstanceModules.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/ConfigInstanceModules.toml
@@ -0,0 +1,6 @@
+root-instances = [ { module = "Spec.ConfigInstanceModules.Module1", instance = "Bounded T" }
+                 , "Read T" 
+                 , { module = "Spec.ConfigInstanceModules.Module3" }
+                 , { class = '\.Enum$' }
+                 , { module = "Spec.ConfigInstanceModules.Module2", class = '\.Show$' } 
+                 ]
diff --git a/test/Spec/ConfigInstanceModules/Module1.hs b/test/Spec/ConfigInstanceModules/Module1.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/ConfigInstanceModules/Module1.hs
@@ -0,0 +1,3 @@
+module Spec.ConfigInstanceModules.Module1 where
+
+data T = MkT deriving (Show, Bounded, Enum, Read)
diff --git a/test/Spec/ConfigInstanceModules/Module2.hs b/test/Spec/ConfigInstanceModules/Module2.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/ConfigInstanceModules/Module2.hs
@@ -0,0 +1,3 @@
+module Spec.ConfigInstanceModules.Module2 where
+
+data T = MkT deriving (Show, Bounded, Enum, Read)
diff --git a/test/Spec/ConfigInstanceModules/Module3.hs b/test/Spec/ConfigInstanceModules/Module3.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/ConfigInstanceModules/Module3.hs
@@ -0,0 +1,3 @@
+module Spec.ConfigInstanceModules.Module3 where
+
+data A = MkA deriving Bounded
diff --git a/test/Spec/DeriveGeneric.stdout b/test/Spec/DeriveGeneric.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/DeriveGeneric.stdout
@@ -0,0 +1,1 @@
+test/Spec/DeriveGeneric/DeriveGeneric.hs:12: (Instance) :: FromJSON T
diff --git a/test/Spec/DeriveGeneric.toml b/test/Spec/DeriveGeneric.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/DeriveGeneric.toml
@@ -0,0 +1,3 @@
+roots = [ "Spec.DeriveGeneric.DeriveGeneric.t" ]
+
+type-class-roots = false
diff --git a/test/Spec/DeriveGeneric/DeriveGeneric.hs b/test/Spec/DeriveGeneric/DeriveGeneric.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/DeriveGeneric/DeriveGeneric.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+module Spec.DeriveGeneric.DeriveGeneric where
+
+import GHC.Generics
+import Data.Aeson
+
+newtype T = MkT Bool
+  -- Generic and ToJSON must not be detected as unused
+  -- but FromJSON should be detected as unused
+  deriving ( Generic, ToJSON
+    , FromJSON )
+
+t :: Value
+t = toJSON $ MkT True
diff --git a/test/Spec/InstanceRootConstraint.stdout b/test/Spec/InstanceRootConstraint.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/InstanceRootConstraint.stdout
diff --git a/test/Spec/InstanceRootConstraint.toml b/test/Spec/InstanceRootConstraint.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/InstanceRootConstraint.toml
@@ -0,0 +1,5 @@
+roots = []
+
+type-class-roots = false
+
+root-instances = [ 'Foo a => Foo \[a\]' ]
diff --git a/test/Spec/InstanceRootConstraint/InstanceRootConstraint.hs b/test/Spec/InstanceRootConstraint/InstanceRootConstraint.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/InstanceRootConstraint/InstanceRootConstraint.hs
@@ -0,0 +1,13 @@
+module Spec.InstanceRootConstraint.InstanceRootConstraint where
+
+class Foo a where
+  foo :: a -> Char
+
+instance Foo Char where
+  foo = id
+
+instance Foo a => Foo [a] where
+  foo = const a
+
+a :: Char
+a = foo 'a'
diff --git a/test/Spec/InstanceTypeclass.stdout b/test/Spec/InstanceTypeclass.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/InstanceTypeclass.stdout
@@ -0,0 +1,2 @@
+test/Spec/InstanceTypeclass/InstanceTypeclass.hs:4: Foo
+test/Spec/InstanceTypeclass/InstanceTypeclass.hs:10: (Instance) :: Foo Char
diff --git a/test/Spec/InstanceTypeclass.toml b/test/Spec/InstanceTypeclass.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/InstanceTypeclass.toml
@@ -0,0 +1,5 @@
+roots = []
+
+type-class-roots = false
+
+root-instances = [ "RootClass Char" ]
diff --git a/test/Spec/InstanceTypeclass/InstanceTypeclass.hs b/test/Spec/InstanceTypeclass/InstanceTypeclass.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/InstanceTypeclass/InstanceTypeclass.hs
@@ -0,0 +1,20 @@
+-- | Test for correct output of unreachable classes and instances
+module Spec.InstanceTypeclass.InstanceTypeclass where
+
+class Foo a where
+  foo :: a -> Char
+
+-- this instance is not marked as root,
+-- therefore class Foo will show up in the output
+-- as well
+instance Foo Char where
+  foo = id
+
+class RootClass a where
+  rootClass :: a -> Char
+
+-- this instance is explicitly marked as root,
+-- hence RootClass will not show up in the output
+-- (note the way it is written in InstanceTypeclass.toml)
+instance RootClass Char where
+  rootClass = id
diff --git a/test/Spec/Monads.failing b/test/Spec/Monads.failing
new file mode 100644
--- /dev/null
+++ b/test/Spec/Monads.failing
@@ -0,0 +1,3 @@
+test/Spec/Monads/Monads.hs:20: (Instance) :: Functor Identity'
+test/Spec/Monads/Monads.hs:23: (Instance) :: Applicative Identity'
+test/Spec/Monads/Monads.hs:27: (Instance) :: Monad Identity'
diff --git a/test/Spec/Monads.stdout b/test/Spec/Monads.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/Monads.stdout
diff --git a/test/Spec/Monads.toml b/test/Spec/Monads.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/Monads.toml
@@ -0,0 +1,3 @@
+roots = [ "Spec.Monads.Monads.foo", "Spec.Monads.Monads.bar" ]
+
+type-class-roots = false
diff --git a/test/Spec/Monads/Monads.hs b/test/Spec/Monads/Monads.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/Monads/Monads.hs
@@ -0,0 +1,38 @@
+{-# OPTIONS_GHC -Wno-missing-signatures #-}
+{-# OPTIONS_GHC -Wno-type-defaults #-}
+module Spec.Monads.Monads where
+
+newtype Identity a = Identity { runIdentity :: a }
+
+instance Functor Identity where
+  fmap f (Identity x) = Identity (f x)
+
+instance Applicative Identity where
+  pure = Identity
+  Identity f <*> Identity x = Identity (f x)
+
+instance Monad Identity where
+  return = pure
+  Identity x >>= f = f x
+
+newtype Identity' a = Identity' { runIdentity' :: a}
+
+instance Functor Identity' where
+  fmap f (Identity' x) = Identity' (f x)
+
+instance Applicative Identity' where
+  pure = Identity'
+  Identity' f <*> Identity' x = Identity' (f x)
+
+instance Monad Identity' where
+  return = pure
+  Identity' x >>= f = f x
+
+foo = do
+  _x <- Identity 3
+  Identity 4
+
+bar :: Identity' Integer -- oh no (the type signature breaks the evidence variables)
+bar = do
+  _x <- Identity' 3
+  Identity' 4
diff --git a/test/Spec/NumInstance.stdout b/test/Spec/NumInstance.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/NumInstance.stdout
diff --git a/test/Spec/NumInstance.toml b/test/Spec/NumInstance.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/NumInstance.toml
@@ -0,0 +1,3 @@
+roots = [ "Spec.NumInstance.NumInstance.two" ]
+
+type-class-roots = false
diff --git a/test/Spec/NumInstance/NumInstance.hs b/test/Spec/NumInstance/NumInstance.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/NumInstance/NumInstance.hs
@@ -0,0 +1,17 @@
+{-# OPTIONS_GHC -Wno-missing-methods #-}
+module Spec.NumInstance.NumInstance where
+
+data Modulo2 = Zero | One
+
+instance Num Modulo2 where
+  (+) = add
+  -- leave the rest undefined
+
+-- add should not be detected as unused
+add :: Modulo2 -> Modulo2 -> Modulo2
+add One One = Zero
+add Zero n = n
+add n Zero = n
+
+two :: Modulo2
+two = One + One
diff --git a/test/Spec/NumInstanceLiteral.failing b/test/Spec/NumInstanceLiteral.failing
new file mode 100644
--- /dev/null
+++ b/test/Spec/NumInstanceLiteral.failing
@@ -0,0 +1,1 @@
+test/Spec/NumInstanceLiteral/NumInstanceLiteral.hs:7: (Instance) :: Num Modulo1
diff --git a/test/Spec/NumInstanceLiteral.stdout b/test/Spec/NumInstanceLiteral.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/NumInstanceLiteral.stdout
diff --git a/test/Spec/NumInstanceLiteral.toml b/test/Spec/NumInstanceLiteral.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/NumInstanceLiteral.toml
@@ -0,0 +1,5 @@
+roots = [ "Spec.NumInstanceLiteral.NumInstanceLiteral.zero"
+        , "Spec.NumInstanceLiteral.NumInstanceLiteral.Modulo1"
+        ]
+
+type-class-roots = false
diff --git a/test/Spec/NumInstanceLiteral/NumInstanceLiteral.hs b/test/Spec/NumInstanceLiteral/NumInstanceLiteral.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/NumInstanceLiteral/NumInstanceLiteral.hs
@@ -0,0 +1,12 @@
+{-# OPTIONS_GHC -Wno-missing-methods #-}
+module Spec.NumInstanceLiteral.NumInstanceLiteral where
+
+data Modulo1 = Zero
+
+-- $fNumModulo1 should not be detected as unused
+instance Num Modulo1 where
+  fromInteger _ = Zero
+  -- leave the rest undefined
+
+zero :: Modulo1
+zero = 0 -- no evidence usage here at all in the HieAST (9.4.4 and 9.6.1)
diff --git a/test/Spec/OverloadedLabels.stdout b/test/Spec/OverloadedLabels.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/OverloadedLabels.stdout
@@ -0,0 +1,1 @@
+test/Spec/OverloadedLabels/OverloadedLabels.hs:17: (Instance) :: Has Point "y" Int
diff --git a/test/Spec/OverloadedLabels.toml b/test/Spec/OverloadedLabels.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/OverloadedLabels.toml
@@ -0,0 +1,3 @@
+roots = [ "Spec.OverloadedLabels.OverloadedLabels.root" ]
+
+type-class-roots = false
diff --git a/test/Spec/OverloadedLabels/OverloadedLabels.hs b/test/Spec/OverloadedLabels/OverloadedLabels.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/OverloadedLabels/OverloadedLabels.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE DataKinds, KindSignatures, 
+            FunctionalDependencies, FlexibleInstances,
+            OverloadedLabels, ScopedTypeVariables #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+module Spec.OverloadedLabels.OverloadedLabels where
+import GHC.OverloadedLabels (IsLabel(..))
+import GHC.TypeLits (Symbol)
+
+data Label (l :: Symbol) = Get
+
+class Has a l b | a l -> b where
+  from :: a -> Label l -> b
+
+data Point = Point Int Int -- odd behaviour with dependencies between Point and Int
+
+instance Has Point "x" Int where from (Point x _) _ = x
+instance Has Point "y" Int where from (Point _ y) _ = y
+
+instance Has a l b => IsLabel l (a -> b) where
+  fromLabel x = from x (Get :: Label l)
+
+root :: Int
+root = #x (Point 1 2) 
+  -- surprisingly OverloadedLabels works perfectly out of the box
diff --git a/test/Spec/OverloadedLists.failing b/test/Spec/OverloadedLists.failing
new file mode 100644
--- /dev/null
+++ b/test/Spec/OverloadedLists.failing
@@ -0,0 +1,1 @@
+test/Spec/OverloadedLists/OverloadedLists.hs:9: (Instance) :: IsList (BetterList x)
diff --git a/test/Spec/OverloadedLists.stdout b/test/Spec/OverloadedLists.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/OverloadedLists.stdout
diff --git a/test/Spec/OverloadedLists.toml b/test/Spec/OverloadedLists.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/OverloadedLists.toml
@@ -0,0 +1,6 @@
+roots = [ "Spec.OverloadedLists.OverloadedLists.root"
+        , "Spec.OverloadedLists.OverloadedLists.BetterList" ]
+
+root-instances = []
+
+type-class-roots = false
diff --git a/test/Spec/OverloadedLists/OverloadedLists.hs b/test/Spec/OverloadedLists/OverloadedLists.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/OverloadedLists/OverloadedLists.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE OverloadedLists #-}
+{-# LANGUAGE TypeFamilies #-}
+module Spec.OverloadedLists.OverloadedLists where
+
+import GHC.IsList ( IsList(..) )
+
+data BetterList x = Nil | Cons x (BetterList x)
+
+instance IsList (BetterList x) where
+  type Item (BetterList x) = x
+  fromList = foldr Cons Nil
+  toList Nil = []
+  toList (Cons x xs) = x : toList xs
+
+root :: BetterList Int
+root = [1, 2, 3]
diff --git a/test/Spec/OverloadedStrings.failing b/test/Spec/OverloadedStrings.failing
new file mode 100644
--- /dev/null
+++ b/test/Spec/OverloadedStrings.failing
@@ -0,0 +1,1 @@
+test/Spec/OverloadedStrings/OverloadedStrings.hs:10: (Instance) :: IsString BetterString
diff --git a/test/Spec/OverloadedStrings.stdout b/test/Spec/OverloadedStrings.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/OverloadedStrings.stdout
diff --git a/test/Spec/OverloadedStrings.toml b/test/Spec/OverloadedStrings.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/OverloadedStrings.toml
@@ -0,0 +1,8 @@
+roots = [ "Spec.OverloadedStrings.OverloadedStrings.root"
+        , "Spec.OverloadedStrings.OverloadedStrings.root'" 
+        , "Spec.OverloadedStrings.OverloadedStrings.BetterString"
+        ]
+
+root-instances = []
+
+type-class-roots = false
diff --git a/test/Spec/OverloadedStrings/OverloadedStrings.hs b/test/Spec/OverloadedStrings/OverloadedStrings.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/OverloadedStrings/OverloadedStrings.hs
@@ -0,0 +1,22 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -Wno-missing-signatures #-}
+module Spec.OverloadedStrings.OverloadedStrings where
+
+import Data.String ( IsString(fromString) )
+
+newtype BetterString = BetterString String
+
+-- $fIsStringBetterString should not be detected as unused
+instance IsString BetterString where
+  fromString = BetterString
+
+newtype BetterString' = BetterString' String
+
+instance IsString BetterString' where
+  fromString = BetterString'
+
+-- Thought: this problem might be similar to RebindableSyntax, QualifiedDo, etc
+root :: BetterString
+root = "Hello World" -- no evidence variable usage here
+
+root' = "Hello World" :: BetterString' -- evidence usage present
diff --git a/test/Spec/RangeEnum.failing b/test/Spec/RangeEnum.failing
new file mode 100644
--- /dev/null
+++ b/test/Spec/RangeEnum.failing
@@ -0,0 +1,1 @@
+test/Spec/RangeEnum/RangeEnum.hs:14: (Instance) :: Enum Colour
diff --git a/test/Spec/RangeEnum.stdout b/test/Spec/RangeEnum.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/RangeEnum.stdout
diff --git a/test/Spec/RangeEnum.toml b/test/Spec/RangeEnum.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/RangeEnum.toml
@@ -0,0 +1,7 @@
+roots = [ "Spec.RangeEnum.RangeEnum.planets"
+        , "Spec.RangeEnum.RangeEnum.letters" 
+        , "Spec.RangeEnum.RangeEnum.shapes"
+        , "Spec.RangeEnum.RangeEnum.colours"
+        ]
+
+type-class-roots = false 
diff --git a/test/Spec/RangeEnum/RangeEnum.hs b/test/Spec/RangeEnum/RangeEnum.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/RangeEnum/RangeEnum.hs
@@ -0,0 +1,26 @@
+{-# OPTIONS_GHC -Wno-missing-signatures #-}
+module Spec.RangeEnum.RangeEnum where
+
+data Planet = Mercury | Venus | Earth
+  deriving (Enum, Bounded)
+
+data Letter = A | B | C
+  deriving (Enum, Bounded, Show)
+
+data Shape = Circle | Square | Triangle
+  deriving (Enum, Bounded)
+
+data Colour = Red | Green | Blue
+  deriving (Enum, Bounded)
+
+planets = [minBound .. (maxBound :: Planet)]
+
+letters = map f [minBound .. maxBound]
+  where
+    f :: Letter -> String
+    f = show
+
+shapes = [minBound .. maxBound] :: [Shape]
+
+colours :: [Colour]
+colours = [minBound .. maxBound] :: [Colour] -- breaks
diff --git a/test/Spec/RootClasses.stdout b/test/Spec/RootClasses.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/RootClasses.stdout
@@ -0,0 +1,1 @@
+test/Spec/RootClasses/RootClasses.hs:5: (Instance) :: Enum T
diff --git a/test/Spec/RootClasses.toml b/test/Spec/RootClasses.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/RootClasses.toml
@@ -0,0 +1,5 @@
+roots = []
+
+type-class-roots = false
+
+root-instances = [ {class = '\.Show$'}, {class = '\.Ord$'}, {class = '\.Bar$'} ]
diff --git a/test/Spec/RootClasses/RootClasses.hs b/test/Spec/RootClasses/RootClasses.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/RootClasses/RootClasses.hs
@@ -0,0 +1,15 @@
+-- | Test for marking classes as roots
+{-# LANGUAGE StandaloneDeriving #-}
+module Spec.RootClasses.RootClasses where
+
+data T = MkT deriving (Eq, Show, Enum)
+
+deriving instance Ord T
+
+data V = MkV
+
+class Bar a where
+  bar :: a -> Char
+
+instance Bar V where
+  bar = const 'b'
diff --git a/test/Spec/StandaloneDeriving.stdout b/test/Spec/StandaloneDeriving.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/StandaloneDeriving.stdout
@@ -0,0 +1,1 @@
+test/Spec/StandaloneDeriving/StandaloneDeriving.hs:6: (Instance) :: Show A
diff --git a/test/Spec/StandaloneDeriving.toml b/test/Spec/StandaloneDeriving.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/StandaloneDeriving.toml
@@ -0,0 +1,5 @@
+roots = [ "Spec.StandaloneDeriving.StandaloneDeriving.root"
+        , "Spec.StandaloneDeriving.StandaloneDeriving.A"
+        ]
+
+type-class-roots = false
diff --git a/test/Spec/StandaloneDeriving/StandaloneDeriving.hs b/test/Spec/StandaloneDeriving/StandaloneDeriving.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/StandaloneDeriving/StandaloneDeriving.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE StandaloneDeriving #-}
+module Spec.StandaloneDeriving.StandaloneDeriving where
+
+data A = A
+
+deriving instance Show A
+
+data T = T
+
+deriving instance Show T
+
+root :: String
+root = show T
diff --git a/test/Spec/TypeAliasGADT.stdout b/test/Spec/TypeAliasGADT.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypeAliasGADT.stdout
diff --git a/test/Spec/TypeAliasGADT.toml b/test/Spec/TypeAliasGADT.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypeAliasGADT.toml
@@ -0,0 +1,7 @@
+roots = [ "Spec.TypeAliasGADT.TypeAliasGADT.root" ]
+
+type-class-roots = false
+
+unused-types = true
+
+root-instances = []
diff --git a/test/Spec/TypeAliasGADT/TypeAliasGADT.hs b/test/Spec/TypeAliasGADT/TypeAliasGADT.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypeAliasGADT/TypeAliasGADT.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE GADTs #-}
+
+module Spec.TypeAliasGADT.TypeAliasGADT where
+
+-- This is a false positive when the GADT language extension 
+-- is enabled and we do not consider type signatures
+type Secret = String
+
+data A = MkA String Int
+
+root :: Secret -> Int -> Secret
+root secret a =
+  let _params = MkA mempty a
+   in secret
diff --git a/test/Spec/TypeDataDecl.stdout b/test/Spec/TypeDataDecl.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypeDataDecl.stdout
diff --git a/test/Spec/TypeDataDecl.toml b/test/Spec/TypeDataDecl.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypeDataDecl.toml
@@ -0,0 +1,7 @@
+roots = [ "Spec.TypeDataDecl.TypeDataDecl.Root" ]
+
+type-class-roots = false
+
+unused-types = true
+
+root-instances = []
diff --git a/test/Spec/TypeDataDecl/TypeDataDecl.hs b/test/Spec/TypeDataDecl/TypeDataDecl.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypeDataDecl/TypeDataDecl.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE KindSignatures #-}
+
+module Spec.TypeDataDecl.TypeDataDecl where
+
+data Kind = MkKind
+
+type Number = Int
+
+data Root (l :: Kind) = MkRecord
+  { recordField :: Number
+  }
diff --git a/test/Spec/TypeFamilies.failing b/test/Spec/TypeFamilies.failing
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypeFamilies.failing
diff --git a/test/Spec/TypeFamilies.stdout b/test/Spec/TypeFamilies.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypeFamilies.stdout
@@ -0,0 +1,1 @@
+test/Spec/TypeFamilies/TypeFamilies.hs:9: (Type Family Instance) :: Family Bool
diff --git a/test/Spec/TypeFamilies.toml b/test/Spec/TypeFamilies.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypeFamilies.toml
@@ -0,0 +1,5 @@
+roots = [ "Spec.TypeFamilies.TypeFamilyUsage.root" ]
+
+type-class-roots = false
+
+unused-types = true
diff --git a/test/Spec/TypeFamilies/TypeFamilies.hs b/test/Spec/TypeFamilies/TypeFamilies.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypeFamilies/TypeFamilies.hs
@@ -0,0 +1,9 @@
+{-# LANGUAGE TypeFamilies #-}
+-- | NOTE: the .stdout file for this test is a placeholder
+module Spec.TypeFamilies.TypeFamilies where
+
+type family Family a
+
+type instance Family Int = Bool
+
+type instance Family Bool = Int
diff --git a/test/Spec/TypeFamilies/TypeFamilyUsage.hs b/test/Spec/TypeFamilies/TypeFamilyUsage.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypeFamilies/TypeFamilyUsage.hs
@@ -0,0 +1,6 @@
+module Spec.TypeFamilies.TypeFamilyUsage where
+
+import Spec.TypeFamilies.TypeFamilies
+
+root :: Family Int
+root = True
diff --git a/test/Spec/Types.stdout b/test/Spec/Types.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/Types.stdout
@@ -0,0 +1,1 @@
+test/Spec/Types/Usages.hs:16: notRoot
diff --git a/test/Spec/Types.toml b/test/Spec/Types.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/Types.toml
@@ -0,0 +1,9 @@
+roots = [ "Spec.Types.Usages.numberUsage"
+        , "Spec.Types.Usages.modulo1Usage"
+        , "Spec.Types.Usages.recordUsage"
+        , "Spec.Types.Usages.vectorUsage"
+        ]
+
+type-class-roots = false
+
+unused-types = true
diff --git a/test/Spec/Types/Types.hs b/test/Spec/Types/Types.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/Types/Types.hs
@@ -0,0 +1,19 @@
+{-# OPTIONS_GHC -Wno-missing-methods #-}
+{-# LANGUAGE DeriveFunctor #-}
+
+module Spec.Types.Types where
+
+-- this Bounded will not depend on Modulo1 as intended
+data Modulo1 = Zero deriving Bounded 
+
+-- should be reachable via Number
+data Modulo2 = Zero' | One'
+
+type Number = Modulo2
+
+newtype Vector a = MkVector (a, a, a) deriving (Functor)
+
+data Record = MkRecord
+  { recordField1 :: Int
+  , recordField2 :: Double
+  }
diff --git a/test/Spec/Types/Usages.hs b/test/Spec/Types/Usages.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/Types/Usages.hs
@@ -0,0 +1,23 @@
+{-# OPTIONS_GHC -Wno-missing-signatures #-}
+{-# OPTIONS_GHC -Wno-unused-foralls #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+module Spec.Types.Usages where
+
+import Spec.Types.Types
+
+numberUsage :: forall a. Number -> Number
+numberUsage = id
+
+-- should depend on Modulo1
+modulo1Usage = minBound
+
+-- exists to force type inference of modulo1Root to Modulo1
+notRoot :: Modulo1
+notRoot = modulo1Usage
+
+recordUsage = recordField1
+
+vectorUsage = mapVector (const (0 :: Integer))
+  where
+    mapVector :: (b -> c) -> Vector b -> Vector c
+    mapVector = fmap
diff --git a/test/Spec/TypesUnused.stdout b/test/Spec/TypesUnused.stdout
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypesUnused.stdout
@@ -0,0 +1,4 @@
+test/Spec/TypesUnused/TypesUnused.hs:6: Modulo1
+test/Spec/TypesUnused/TypesUnused.hs:8: Number
+test/Spec/TypesUnused/TypesUnused.hs:10: Vector
+test/Spec/TypesUnused/TypesUnused.hs:12: Family
diff --git a/test/Spec/TypesUnused.toml b/test/Spec/TypesUnused.toml
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypesUnused.toml
@@ -0,0 +1,5 @@
+roots = []
+
+type-class-roots = false
+
+unused-types = true
diff --git a/test/Spec/TypesUnused/TypesUnused.hs b/test/Spec/TypesUnused/TypesUnused.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec/TypesUnused/TypesUnused.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE TypeFamilies #-}
+-- | Control version of Types that is not imported
+-- All these declarations should show up in the output
+module Spec.TypesUnused.TypesUnused where
+
+data Modulo1 = Zero
+
+type Number = Double
+
+newtype Vector = MkVector (Double, Double, Double)
+
+type family Family a
diff --git a/test/UnitTests.hs b/test/UnitTests.hs
new file mode 100644
--- /dev/null
+++ b/test/UnitTests.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=UnitTests #-}
diff --git a/test/UnitTests/Weeder/ConfigSpec.hs b/test/UnitTests/Weeder/ConfigSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/UnitTests/Weeder/ConfigSpec.hs
@@ -0,0 +1,25 @@
+module UnitTests.Weeder.ConfigSpec (spec) where
+
+import Weeder.Config
+import qualified TOML
+import qualified Data.Text as T
+import Test.Hspec (Spec, describe, it)
+
+spec :: Spec
+spec = 
+  describe "Weeder.Config" $
+    describe "configToToml" $
+      it "passes prop_configToToml" prop_configToToml
+
+-- >>> prop_configToToml
+-- True
+prop_configToToml :: Bool
+prop_configToToml =
+  let cf = Config
+        { rootPatterns = mempty
+        , typeClassRoots = True
+        , rootInstances = [InstanceOnly "Quux\\\\[\\]", ClassOnly "[\\[\\\\[baz" <> ModuleOnly "[Quuux]", InstanceOnly "[\\[\\\\[baz" <> ClassOnly "[Quuux]" <> ModuleOnly "[Quuuux]"]
+        , unusedTypes = True
+        }
+      cf' = T.pack $ configToToml cf
+   in TOML.decode cf' == Right cf
diff --git a/weeder.cabal b/weeder.cabal
--- a/weeder.cabal
+++ b/weeder.cabal
@@ -5,7 +5,7 @@
 author:        Ollie Charles <ollie@ocharles.org.uk>
 maintainer:    Ollie Charles <ollie@ocharles.org.uk>
 build-type:    Simple
-version:       2.6.0
+version:       2.7.0
 copyright:     Neil Mitchell 2017-2020, Oliver Charles 2020-2023
 synopsis:      Detect dead code
 description:   Find declarations.
@@ -18,10 +18,12 @@
 extra-source-files:
     test/Spec/*.toml
     test/Spec/*.stdout
+    test/Spec/*.failing
 
 library
   build-depends:
     , algebraic-graphs     ^>= 0.7
+    , async                ^>= 2.2.0
     , base                 ^>= 4.17.0.0 || ^>= 4.18.0.0
     , bytestring           ^>= 0.10.9.0 || ^>= 0.11.0.0
     , containers           ^>= 0.6.2.1
@@ -32,6 +34,7 @@
     , lens                 ^>= 5.1 || ^>= 5.2
     , mtl                  ^>= 2.2.2 || ^>= 2.3
     , optparse-applicative ^>= 0.14.3.0 || ^>= 0.15.1.0 || ^>= 0.16.0.0 || ^>=  0.17
+    , parallel             ^>= 3.2.0.0
     , regex-tdfa           ^>= 1.2.0.0 || ^>= 1.3.1.0
     , text                 ^>= 2.0.1
     , toml-reader          ^>= 0.2.0.0
@@ -40,12 +43,13 @@
   exposed-modules:
     Weeder
     Weeder.Config
+    Weeder.Run
     Weeder.Main
   autogen-modules:
     Paths_weeder
   other-modules:
     Paths_weeder
-  ghc-options: -Wall -fwarn-incomplete-uni-patterns
+  ghc-options: -Wall -fwarn-incomplete-uni-patterns -threaded
   default-language: Haskell2010
 
 
@@ -62,7 +66,7 @@
     , weeder
   main-is: Main.hs
   hs-source-dirs: exe-weeder
-  ghc-options: -Wall -fwarn-incomplete-uni-patterns
+  ghc-options: -Wall -fwarn-incomplete-uni-patterns -threaded -no-rtsopts-suggestions -with-rtsopts=-N
   default-language: Haskell2010
 
 test-suite weeder-test
@@ -70,12 +74,12 @@
     , aeson
     , algebraic-graphs
     , base
+    , containers
     , directory
     , filepath
     , ghc
     , hspec
     , process
-    , silently
     , text
     , toml-reader
     , weeder
@@ -86,7 +90,32 @@
     Paths_weeder
   other-modules:
     Paths_weeder
+    UnitTests
     -- Tests
+    Spec.ApplicativeDo.ApplicativeDo
     Spec.BasicExample.BasicExample
+    Spec.ConfigInstanceModules.Module1
+    Spec.ConfigInstanceModules.Module2
+    Spec.ConfigInstanceModules.Module3
+    Spec.DeriveGeneric.DeriveGeneric
+    Spec.InstanceRootConstraint.InstanceRootConstraint
+    Spec.InstanceTypeclass.InstanceTypeclass
+    Spec.Monads.Monads
+    Spec.NumInstance.NumInstance
+    Spec.NumInstanceLiteral.NumInstanceLiteral
+    Spec.OverloadedLabels.OverloadedLabels
+    Spec.OverloadedLists.OverloadedLists
+    Spec.OverloadedStrings.OverloadedStrings
+    Spec.RangeEnum.RangeEnum
+    Spec.RootClasses.RootClasses
+    Spec.StandaloneDeriving.StandaloneDeriving
+    Spec.TypeAliasGADT.TypeAliasGADT
+    Spec.TypeDataDecl.TypeDataDecl
+    Spec.Types.Types
+    Spec.Types.Usages
+    Spec.TypeFamilies.TypeFamilies
+    Spec.TypeFamilies.TypeFamilyUsage
+    Spec.TypesUnused.TypesUnused
+    UnitTests.Weeder.ConfigSpec
   ghc-options: -Wall -fwarn-incomplete-uni-patterns -fwrite-ide-info -hiedir ./test
   default-language: Haskell2010
