diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # dickinson
 
+## 1.3.0.0
+
+  * Linter now reports inexhaustive pattern matches
+  * Linter reports redundant patterns
+
 ## 1.2.0.0
 
   * Remove `dir` subcommand
diff --git a/bench/Bench.hs b/bench/Bench.hs
--- a/bench/Bench.hs
+++ b/bench/Bench.hs
@@ -1,14 +1,15 @@
 module Main (main) where
 
-import           Control.Exception                  (throw)
-import           Control.Exception.Value            (eitherThrow)
-import           Control.Monad                      (void)
+import           Control.Exception                   (throw)
+import           Control.Exception.Value             (eitherThrow)
+import           Control.Monad                       (void)
 import           Criterion.Main
-import           Data.Binary                        (decode, encode)
-import qualified Data.ByteString.Lazy               as BSL
-import qualified Data.Text                          as T
+import           Data.Binary                         (decode, encode)
+import qualified Data.ByteString.Lazy                as BSL
+import qualified Data.Text                           as T
 import           Language.Dickinson.Check
 import           Language.Dickinson.Check.Duplicate
+import           Language.Dickinson.Check.Exhaustive
 import           Language.Dickinson.Check.Internal
 import           Language.Dickinson.Check.Scope
 import           Language.Dickinson.Error
@@ -59,6 +60,10 @@
                     , bench "lib/adjectives.dck" $ nf checkMultiple p'
                     , bench "bench/data/multiple.dck" $ nf checkDuplicates p -- TODO: better example
                     ]
+                , env patternEnv $ \ ~(Dickinson _ p) ->
+                  bgroup "pattern match exhaustiveness"
+                    [ bench "test/examples/declension.dck" $ nf checkExhaustive p
+                    ]
                 , bgroup "pipeline"
                     [ benchPipeline "examples/shakespeare.dck"
                     , benchPipeline "examples/fortune.dck"
@@ -112,6 +117,7 @@
           checkEnv = (,)
             <$> multiParsed
             <*> libText
+          patternEnv = either throw id . parse <$> BSL.readFile "test/examples/declension.dck"
 
 plainExpr :: (UniqueCtx, Dickinson a) -> Dickinson a
 plainExpr = fst . uncurry renameDickinson
diff --git a/language-dickinson.cabal b/language-dickinson.cabal
--- a/language-dickinson.cabal
+++ b/language-dickinson.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.0
 name:               language-dickinson
-version:            1.2.0.0
+version:            1.3.0.0
 license:            BSD3
 license-file:       LICENSE
 copyright:          Copyright: (c) 2020 Vanessa McHale
@@ -97,11 +97,13 @@
         Language.Dickinson.Check.Internal
         Language.Dickinson.Check.Pattern
         Language.Dickinson.Check.Duplicate
+        Language.Dickinson.Check.Exhaustive
         Language.Dickinson.Unique
         Language.Dickinson.File
         Language.Dickinson.Pipeline
         Language.Dickinson.Import
         Language.Dickinson.Lib
+        Language.Dickinson.Pattern.Useless
         Data.Tuple.Ext
         Control.Exception.Value
         Data.Text.Prettyprint.Doc.Ext
@@ -223,6 +225,7 @@
     other-modules:
         Eval
         Golden
+        Pattern
         Roundtrip.Compare
         TypeCheck
         TH
diff --git a/src/Data/List/Ext.hs b/src/Data/List/Ext.hs
--- a/src/Data/List/Ext.hs
+++ b/src/Data/List/Ext.hs
@@ -1,5 +1,6 @@
 module Data.List.Ext ( anyA
                      , allA
+                     , forAnyA
                      ) where
 
 -- TODO: does this short-circuit appropriately with state monad?
@@ -9,3 +10,6 @@
 -- TODO: does this short-circuit appropriately with state monad?
 allA :: (Traversable t, Applicative f) => (a -> f Bool) -> t a -> f Bool
 allA p = fmap and . traverse p
+
+forAnyA :: (Traversable t, Applicative f) => t a -> (a -> f Bool) -> f Bool
+forAnyA = flip anyA
diff --git a/src/Data/Text/Prettyprint/Doc/Ext.hs b/src/Data/Text/Prettyprint/Doc/Ext.hs
--- a/src/Data/Text/Prettyprint/Doc/Ext.hs
+++ b/src/Data/Text/Prettyprint/Doc/Ext.hs
@@ -22,9 +22,8 @@
 import           Data.Semigroup            ((<>))
 import qualified Data.Text                 as T
 import qualified Data.Text.Lazy            as TL
-import           Prettyprinter             (Doc, LayoutOptions (LayoutOptions), PageWidth (AvailablePerLine),
-                                            Pretty (pretty), SimpleDocStream, concatWith, flatAlt, hardline, indent,
-                                            layoutSmart, softline, vsep, (<+>))
+import           Prettyprinter             (Doc, LayoutOptions (LayoutOptions), PageWidth (AvailablePerLine), Pretty (pretty), SimpleDocStream, concatWith,
+                                            flatAlt, hardline, indent, layoutSmart, softline, vsep, (<+>))
 import           Prettyprinter.Render.Text (renderLazy, renderStrict)
 
 infixr 6 <#>
diff --git a/src/Language/Dickinson/Check/Exhaustive.hs b/src/Language/Dickinson/Check/Exhaustive.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Dickinson/Check/Exhaustive.hs
@@ -0,0 +1,71 @@
+module Language.Dickinson.Check.Exhaustive ( checkExhaustive
+                                           , foliate
+                                           ) where
+
+import           Control.Applicative                ((<|>))
+import           Data.Foldable                      (toList)
+import           Data.List                          (inits)
+import           Data.Maybe                         (mapMaybe)
+import           Language.Dickinson.Check.Common
+import           Language.Dickinson.Error
+import           Language.Dickinson.Pattern.Useless
+import           Language.Dickinson.Type
+
+checkExhaustive :: [Declaration a] -> Maybe (DickinsonWarning a)
+checkExhaustive ds = runPatternM (checkDeclsM ds)
+
+checkDeclsM :: [Declaration a] -> PatternM (Maybe (DickinsonWarning a))
+checkDeclsM ds =
+    patternEnvDecls ds *>
+    mapSumM checkDeclM ds
+
+checkDeclM :: Declaration a -> PatternM (Maybe (DickinsonWarning a))
+checkDeclM TyDecl{}       = pure Nothing
+checkDeclM (Define _ _ e) = checkExprM e
+
+isExhaustiveM :: [Pattern a] -> a -> PatternM (Maybe (DickinsonWarning a))
+isExhaustiveM ps loc = do
+    e <- isExhaustive ps
+    pure $ if e
+        then Nothing
+        else Just $ InexhaustiveMatch loc
+
+uselessErr :: [Pattern a] -> Pattern a -> PatternM (Maybe (DickinsonWarning a))
+uselessErr ps p = {-# SCC "uselessErr" #-} do
+    e <- useful ps p
+    pure $ if e
+        then Nothing
+        else Just $ UselessPattern (patAnn p) p
+
+foliate :: [a] -> [([a], a)]
+foliate = mapMaybe split . inits
+    where split []  = Nothing
+          split [_] = Nothing
+          split xs  = Just (init xs, last xs)
+
+checkMatch :: [Pattern a] -> a -> PatternM (Maybe (DickinsonWarning a))
+checkMatch ps loc = {-# SCC "checkMatch" #-}
+    (<|>)
+        <$> mapSumM (uncurry uselessErr) ({-# SCC "foliate" #-} foliate ps)
+        <*> isExhaustiveM ps loc
+
+checkExprM :: Expression a -> PatternM (Maybe (DickinsonWarning a))
+checkExprM Var{}              = pure Nothing
+checkExprM Literal{}          = pure Nothing
+checkExprM StrChunk{}         = pure Nothing
+checkExprM Constructor{}      = pure Nothing
+checkExprM BuiltinFn{}        = pure Nothing
+checkExprM (Flatten _ e)      = checkExprM e
+checkExprM (Annot _ e _)      = checkExprM e
+checkExprM (Lambda _ _ _ e)   = checkExprM e
+checkExprM (Choice _ brs)     = mapSumM checkExprM (snd <$> brs)
+checkExprM (Let _ brs e)      = (<|>) <$> mapSumM checkExprM (snd <$> brs) <*> checkExprM e
+checkExprM (Interp _ es)      = mapSumM checkExprM es
+checkExprM (MultiInterp _ es) = mapSumM checkExprM es
+checkExprM (Apply _ e e')     = (<|>) <$> checkExprM e <*> checkExprM e'
+checkExprM (Concat _ es)      = mapSumM checkExprM es
+checkExprM (Tuple _ es)       = mapSumM checkExprM es
+checkExprM (Match l e brs)    =
+    (<|>)
+        <$> checkExprM e
+        <*> ((<|>) <$> checkMatch (toList (fst <$> brs)) l <*> mapSumM checkExprM (snd <$> brs))
diff --git a/src/Language/Dickinson/Error.hs b/src/Language/Dickinson/Error.hs
--- a/src/Language/Dickinson/Error.hs
+++ b/src/Language/Dickinson/Error.hs
@@ -36,6 +36,8 @@
 
 data DickinsonWarning a = MultipleNames a (Name a) -- TODO: throw both?
                         | DuplicateStr a T.Text
+                        | InexhaustiveMatch a
+                        | UselessPattern a (Pattern a)
                         deriving (Generic, NFData)
 
 maybeThrow :: MonadError e m => Maybe e -> m ()
@@ -66,7 +68,9 @@
     show = show . pretty
 
 instance (Pretty a) => Pretty (DickinsonWarning a) where
-    pretty (MultipleNames l n) = pretty n <+> "at" <+> pretty l <+> "has already been defined"
-    pretty (DuplicateStr l t)  = pretty l <+> "duplicate string" <+> dquotes (pretty t)
+    pretty (MultipleNames l n)   = pretty n <+> "at" <+> pretty l <+> "has already been defined"
+    pretty (DuplicateStr l t)    = pretty l <+> "duplicate string" <+> dquotes (pretty t)
+    pretty (InexhaustiveMatch l) = pretty l <+> "Inexhaustive match in expression"
+    pretty (UselessPattern l p)  = pretty l <+> "Pattern" <+> pretty p <+> "is redundant"
 
 instance (Pretty a, Typeable a) => Exception (DickinsonWarning a)
diff --git a/src/Language/Dickinson/File.hs b/src/Language/Dickinson/File.hs
--- a/src/Language/Dickinson/File.hs
+++ b/src/Language/Dickinson/File.hs
@@ -26,6 +26,7 @@
 import           Data.Text                            as T
 import           Language.Dickinson.Check
 import           Language.Dickinson.Check.Duplicate
+import           Language.Dickinson.Check.Exhaustive
 import           Language.Dickinson.Check.Scope
 import           Language.Dickinson.Error
 import           Language.Dickinson.Eval
@@ -88,7 +89,7 @@
 
 -- | Run some lints
 warnFile :: FilePath -> IO ()
-warnFile = maybeThrowIO . (\x -> checkDuplicates x <|> checkMultiple x) . modDefs
+warnFile = maybeThrowIO . (\x -> checkDuplicates x <|> checkMultiple x <|> checkExhaustive x) . modDefs
     <=< eitherThrowIO . parse
     <=< BSL.readFile
 
diff --git a/src/Language/Dickinson/Pattern/Useless.hs b/src/Language/Dickinson/Pattern/Useless.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Dickinson/Pattern/Useless.hs
@@ -0,0 +1,164 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | This module is loosely based off _Warnings for pattern matching_ by Luc
+-- Maranget
+module Language.Dickinson.Pattern.Useless ( PatternM
+                                          , runPatternM
+                                          , isExhaustive
+                                          , patternEnvDecls
+                                          , useful
+                                          -- * Exported for testing
+                                          , specializeTuple
+                                          , specializeTag
+                                          ) where
+
+import           Control.Monad             (forM, forM_)
+import           Control.Monad.State       (State, evalState, get)
+import           Data.Coerce               (coerce)
+import           Data.Foldable             (toList, traverse_)
+import           Data.Functor              (void)
+import           Data.IntMap               (findWithDefault)
+import qualified Data.IntMap               as IM
+import qualified Data.IntSet               as IS
+import           Data.List.Ext
+import           Language.Dickinson.Name
+import           Language.Dickinson.Type
+import           Language.Dickinson.Unique
+import           Lens.Micro                (Lens')
+import           Lens.Micro.Mtl            (modifying)
+
+-- all constructors of a
+data PatternEnv = PatternEnv { allCons :: IM.IntMap IS.IntSet -- ^ all constructors indexed by type
+                             , types   :: IM.IntMap Int -- ^ all types indexed by constructor
+                             }
+
+allConsLens :: Lens' PatternEnv (IM.IntMap IS.IntSet)
+allConsLens f s = fmap (\x -> s { allCons = x }) (f (allCons s))
+
+typesLens :: Lens' PatternEnv (IM.IntMap Int)
+typesLens f s = fmap (\x -> s { types = x }) (f (types s))
+
+declAdd :: Declaration a -> PatternM ()
+declAdd Define{}         = pure ()
+declAdd (TyDecl _ (Name _ (Unique i) _) cs) = do
+    forM_ cs $ \(Name _ (Unique j) _) ->
+        modifying typesLens (IM.insert j i)
+    let cons = IS.fromList $ toList (unUnique . unique <$> cs)
+    modifying allConsLens (IM.insert i cons)
+
+patternEnvDecls :: [Declaration a] -> PatternM ()
+patternEnvDecls = traverse_ declAdd
+
+type PatternM = State PatternEnv
+
+runPatternM :: PatternM a -> a
+runPatternM = flip evalState (PatternEnv mempty mempty)
+
+-- given a constructor name, get the IntSet of all constructors of that type
+assocUniques :: Name a -> PatternM IS.IntSet
+assocUniques (Name _ (Unique i) _) = {-# SCC "assocUniques" #-} do
+    st <- get
+    let ty = findWithDefault undefined i (types st)
+    pure $ findWithDefault undefined ty (allCons st)
+
+isExhaustive :: [Pattern a] -> PatternM Bool
+isExhaustive ps = {-# SCC "isExhaustive" #-} not <$> useful ps (Wildcard undefined)
+
+isCompleteSet :: [Name a] -> PatternM (Maybe [Name ()])
+isCompleteSet []       = pure Nothing
+isCompleteSet ns@(n:_) = do
+    allU <- assocUniques n
+    let ty = coerce (unique <$> ns)
+    pure $
+        if IS.null (allU IS.\\ IS.fromList ty)
+            then Just ((\u -> (Name undefined (Unique u) ())) <$> IS.toList allU)
+            else Nothing
+
+useful :: [Pattern a] -> Pattern a -> PatternM Bool
+useful ps p = usefulMaranget [[p'] | p' <- ps] [p]
+
+sanityFailed :: a
+sanityFailed = error "Sanity check failed! Perhaps you ran the pattern match exhaustiveness checker on an ill-typed program?"
+
+specializeTag :: Name a -> [[Pattern a]] -> [[Pattern a]]
+specializeTag c = {-# SCC "specializeTag" #-} concatMap withRow
+    where withRow (PatternCons _ c':ps) | c' == c = [ps]
+                                        | otherwise = []
+          withRow (PatternTuple{}:_)    = sanityFailed
+          withRow (Wildcard{}:ps)       = [ps]
+          withRow (PatternVar{}:ps)     = [ps]
+          withRow (OrPattern _ rs:ps)   = specializeTag c [r:ps | r <- toList rs] -- TODO: unit test case for this
+          withRow []                    = emptySpecialize
+
+specializeTuple :: Int -> [[Pattern a]] -> [[Pattern a]]
+specializeTuple n = {-# SCC "specializeTuple" #-} concatMap withRow
+    where withRow (PatternTuple _ ps:ps') = [toList ps ++ ps']
+          withRow (p@Wildcard{}:ps')      = [replicate n p ++ ps']
+          withRow (p@PatternVar{}:ps')    = [replicate n p ++ ps']
+          withRow (OrPattern _ rs:ps)     = specializeTuple n [r:ps | r <- toList rs]
+          withRow (PatternCons{}:_)       = sanityFailed
+          withRow []                      = emptySpecialize
+
+emptySpecialize :: a
+emptySpecialize = error "Internal error: tried to take specialized matrix of an empty row"
+
+-- | \\( \matcal(D) \\) in the Maranget paper
+defaultMatrix :: [[Pattern a]] -> [[Pattern a]]
+defaultMatrix = {-# SCC "defaultMatrix" #-} concatMap withRow where
+    withRow []                  = error "Internal error: tried to take default matrix of an empty row"
+    withRow (PatternTuple{}:_)  = error "Sanity check failed!" -- because a tuple would be complete by itself
+    withRow (PatternCons{}:_)   = []
+    withRow (Wildcard{}:ps)     = [ps]
+    withRow (PatternVar{}:ps)   = [ps]
+    withRow (OrPattern _ rs:ps) = defaultMatrix [r:ps | r <- toList rs]
+
+data Complete a = NotComplete
+                | CompleteTuple Int
+                | CompleteTags [Name a]
+
+extrCons :: Pattern a -> [Name a]
+extrCons (PatternCons _ c) = [c]
+extrCons (OrPattern _ ps)  = concatMap extrCons (toList ps)
+extrCons _                 = []
+
+-- Is the first column of the pattern matrix complete?
+fstComplete :: [[Pattern a]] -> PatternM (Complete ())
+fstComplete ps = {-# SCC "fstComplete" #-}
+    if maxTupleLength > 0
+        then pure $ CompleteTuple maxTupleLength
+        else do
+            res <- isCompleteSet (concatMap extrCons fstColumn)
+            pure $ case res of
+                Just ns -> CompleteTags ns
+                Nothing -> NotComplete
+    where fstColumn = fmap head ps
+          tuple (PatternTuple _ ps') = length ps'
+          tuple (OrPattern _ ps')    = maximum (tuple <$> ps')
+          tuple _                    = 0
+          maxTupleLength = maximum (tuple <$> fstColumn)
+
+-- follows maranget paper
+usefulMaranget :: [[Pattern a]] -> [Pattern a] -> PatternM Bool
+usefulMaranget [] _                       = pure True
+usefulMaranget _ []                       = pure False
+usefulMaranget ps (PatternCons _ c:qs)    = usefulMaranget (specializeTag c ps) qs
+usefulMaranget ps (PatternTuple _ ps':qs) = usefulMaranget (specializeTuple (length ps') ps) (toList ps' ++ qs)
+usefulMaranget ps (OrPattern _ ps':qs)    = forAnyA ps' $ \p -> usefulMaranget ps (p:qs)
+usefulMaranget ps (q@Wildcard{}:qs)       = do
+    cont <- fstComplete ps
+    case cont of
+        NotComplete     -> usefulMaranget (defaultMatrix ps) qs
+        CompleteTuple n -> usefulMaranget (specializeTuple n ps) (specializeTupleVector n q qs)
+        CompleteTags ns -> or <$> (forM ns $ \n -> usefulMaranget (specializeTag n (forget ps)) (fmap void qs))
+usefulMaranget ps (q@PatternVar{}:qs)     = do
+    cont <- fstComplete ps
+    case cont of
+        NotComplete     -> usefulMaranget (defaultMatrix ps) qs
+        CompleteTuple n -> usefulMaranget (specializeTuple n ps) (specializeTupleVector n q qs)
+        CompleteTags ns -> or <$> (forM ns $ \n -> usefulMaranget (specializeTag n (forget ps)) (fmap void qs))
+
+specializeTupleVector :: Int -> Pattern a -> [Pattern a] -> [Pattern a]
+specializeTupleVector n p ps = {-# SCC "specializeTupleVector" #-} replicate n p ++ ps
+
+forget :: [[Pattern a]] -> [[Pattern ()]]
+forget = fmap (fmap void)
diff --git a/src/Language/Dickinson/Type.hs b/src/Language/Dickinson/Type.hs
--- a/src/Language/Dickinson/Type.hs
+++ b/src/Language/Dickinson/Type.hs
@@ -54,7 +54,7 @@
                | PatternCons { patAnn :: a, patCons :: TyName a }
                | Wildcard { patAnn :: a }
                | OrPattern { patAnn :: a, patOr :: NonEmpty (Pattern a) }
-               deriving (Generic, NFData, Binary, Functor, Show, Data)
+               deriving (Generic, NFData, Binary, Functor, Eq, Show, Data)
 
 data Expression a = Literal { exprAnn :: a, litText :: T.Text }
                   | StrChunk { exprAnn :: a, chunkText :: T.Text }
diff --git a/test/Pattern.hs b/test/Pattern.hs
new file mode 100644
--- /dev/null
+++ b/test/Pattern.hs
@@ -0,0 +1,36 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Pattern ( patternTests
+               ) where
+
+import           Data.List.NonEmpty                 (NonEmpty (..))
+import           Language.Dickinson.Name
+import           Language.Dickinson.Pattern.Useless
+import           Language.Dickinson.Type
+import           Language.Dickinson.Unique
+import           Test.Tasty
+import           Test.Tasty.HUnit
+
+patternTests :: TestTree
+patternTests = testGroup "Pattern match unit tests"
+    [ testSpecializeTuple
+    , testSpecializeTag
+    ]
+
+-- just tests that they make sense as I understand them
+testSpecializeTuple :: TestTree
+testSpecializeTuple = testCase "specializeTuple" $ do
+    let patternMatrix = [[Wildcard ()]]
+        specializedMatrix = specializeTuple 2 patternMatrix
+    specializedMatrix @?= [[Wildcard (), Wildcard ()]]
+
+testSpecializeTag :: TestTree
+testSpecializeTag = testCase "specializeTag" $ do
+    let n0 = Name ("a0" :| []) (Unique 0) ()
+        n1 = Name ("a1" :| []) (Unique 1) ()
+        patternMatrix = [ [PatternCons () n0, Wildcard ()]
+                        , [PatternCons () n1, Wildcard ()]
+                        ]
+        specializedMatrix = [ [Wildcard ()]
+                            ]
+    specializeTag n0 patternMatrix @?= specializedMatrix
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -2,15 +2,16 @@
 
 module Main (main) where
 
-import           Control.Exception.Value            (eitherThrow)
-import qualified Data.ByteString.Lazy               as BSL
-import           Data.Either                        (isRight)
-import           Data.List.NonEmpty                 (NonEmpty (..))
-import           Data.Maybe                         (isJust, isNothing)
+import           Control.Exception.Value             (eitherThrow)
+import qualified Data.ByteString.Lazy                as BSL
+import           Data.Either                         (isRight)
+import           Data.List.NonEmpty                  (NonEmpty (..))
+import           Data.Maybe                          (isJust, isNothing)
 import           Eval
 import           Golden
 import           Language.Dickinson.Check
 import           Language.Dickinson.Check.Duplicate
+import           Language.Dickinson.Check.Exhaustive
 import           Language.Dickinson.Check.Internal
 import           Language.Dickinson.Check.Pattern
 import           Language.Dickinson.Check.Scope
@@ -22,6 +23,7 @@
 import           Language.Dickinson.Rename
 import           Language.Dickinson.Type
 import           Language.Dickinson.Unique
+import           Pattern
 import           Test.Tasty
 import           Test.Tasty.HUnit
 import           TH
@@ -35,6 +37,7 @@
             , parserTests
             , evalTests
             , tcTests
+            , patternTests
             ]
 
 parserTests :: TestTree
@@ -68,6 +71,10 @@
         , findPath
         , sanityCheckTest "test/data/adt.dck"
         , detectSuspiciousPattern "test/error/badMatch.dck"
+        , detectBadPattern "test/demo/inexhaustive.dck"
+        , noInexhaustive "test/demo/exhaustive.dck"
+        , detectBadPattern "test/error/inexhaustivePatternMatch.dck"
+        , noInexhaustive "prelude/curry.dck"
         , thTests
         ]
 
@@ -123,3 +130,17 @@
     fmap eitherThrow $ evalIO $ do
         ds <- amalgamateRenameM [] fp
         sanityCheck ds
+
+noInexhaustive :: FilePath -> TestTree
+noInexhaustive fp = testCase "Reports non-sketchy pattern matches" $ do
+    (Dickinson _ ds) <- parseNoRename fp
+    assertBool fp $ isNothing (checkExhaustive ds)
+
+detectBadPattern :: FilePath -> TestTree
+detectBadPattern fp = testCase "Detects sketchy pattern match" $ do
+    (Dickinson _ ds) <- parseNoRename fp
+    assertBool fp $ isJust (checkExhaustive ds)
+
+
+parseNoRename :: FilePath -> IO (Dickinson AlexPosn)
+parseNoRename = fmap (eitherThrow . parse) . BSL.readFile
diff --git a/test/demo/exhaustive.dck b/test/demo/exhaustive.dck
new file mode 100644
--- /dev/null
+++ b/test/demo/exhaustive.dck
@@ -0,0 +1,18 @@
+%-
+
+tydecl octit = Zero
+             | One
+             | Two
+             | Three
+             | Four
+             | Five
+             | Six
+             | Seven
+
+(:def match
+  (:lambda x octit
+    (:match x
+      [Zero Zero]
+      [One One]
+      [a x])))
+
diff --git a/test/demo/inexhaustive.dck b/test/demo/inexhaustive.dck
new file mode 100644
--- /dev/null
+++ b/test/demo/inexhaustive.dck
@@ -0,0 +1,17 @@
+%-
+
+tydecl octit = Zero
+             | One
+             | Two
+             | Three
+             | Four
+             | Five
+             | Six
+             | Seven
+
+(:def match
+  (:lambda x octit
+    (:match x
+      [Zero "0"]
+      [One "1"]
+      [Two "2"])))
diff --git a/test/demo/redundant.dck b/test/demo/redundant.dck
new file mode 100644
--- /dev/null
+++ b/test/demo/redundant.dck
@@ -0,0 +1,18 @@
+%-
+
+tydecl octit = Zero
+             | One
+             | Two
+             | Three
+             | Four
+             | Five
+             | Six
+             | Seven
+
+(:def match
+  (:lambda x octit
+    (:match x
+      [Zero Zero]
+      [One One]
+      [One One]
+      [a x])))
diff --git a/test/error/inexhaustivePatternMatch.dck b/test/error/inexhaustivePatternMatch.dck
new file mode 100644
--- /dev/null
+++ b/test/error/inexhaustivePatternMatch.dck
@@ -0,0 +1,26 @@
+%-
+
+tydecl case = Nominative | Accusative | Dative | Genitive | Instrumental
+
+tydecl gender = Masculine | Feminine | Neuter
+
+tydecl number = Singular | Plural
+
+; demonstrative pronouns
+; "this" or "these"
+(:def decline
+  (:lambda x (case, gender, number)
+    (:match x
+      [(Nominative, Masculine, Singular) "þes"]
+      [(Accusative, Masculine, Singular) "þisne"]
+      [(Genitive, (Masculine|Neuter), Singular) "þisses"]
+      [(Dative, (Masculine|Neuter), Singular) "þissum"]
+      [(Instrumental, (Masculine|Neuter), Singular) "þys"]
+      [((Nominative|Accusative), Neuter, Singular) "þis"]
+      [(Nominative, Feminine, Singular) "þeos"]
+      [(Accusative, Feminine, Singular) "þas"]
+      [((Genitive|Dative|Instrumental), Feminine, Singular) "þisse"]
+      [(Nominative, _, Plural) "þas"]
+      [(Genitive, _, Plural) "þissa"]
+      [(Dative, _, Plural) "þissum"]
+      )))
