inferno-core-0.1.0.0: src/Inferno/Infer/Exhaustiveness.hs
{-# LANGUAGE ExistentialQuantification #-}
-- this module is an implementation of the exhaustiveness checker from this paper:
-- MARANGET, L. (2007). Warnings for pattern matching.
-- Journal of Functional Programming, 17(3), 387-421.
-- doi:10.1017/S0956796807006223
module Inferno.Infer.Exhaustiveness
( mkEnumText,
Pattern (W),
exhaustive,
checkUsefullness,
cInf,
cEnum,
cOne,
cEmpty,
cTuple,
)
where
import Data.List (intercalate)
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as Text
import Inferno.Types.VersionControl (VCObjectHash)
import Prettyprinter (Pretty (pretty), align, tupled, (<+>))
data Con = COne | CEmpty | CTuple Int | forall a. (Show a, Pretty a, Enum a) => CInf a | CEnum VCObjectHash Text
instance Eq Con where
COne == COne = True
CEmpty == CEmpty = True
(CTuple i) == (CTuple j) = i == j
(CEnum e _) == (CEnum f _) = e == f
(CInf a) == (CInf b) = (show a) == (show b)
_ == _ = False
-- we don't really care about the ord instance here
instance Ord Con where
compare a b = compare (mkOrd a) (mkOrd b)
where
mkOrd = \case
COne -> "one"
CEmpty -> "empty"
CTuple n -> show n
CInf v -> show v
CEnum _ e -> show e
-- We define a more abstract type of a pattern here, which only deals with (C)onstructors and
-- holes/(W)ildcards, as we do not need to make a distinction between a variable and a wildcard
-- in the setting of exhaustiveness checking.
data Pattern = C Con [Pattern] | W deriving (Eq, Ord)
instance Show Pattern where
show = \case
W -> "_"
C COne [x] -> "one " <> show x
C CEmpty _ -> "empty"
C (CTuple _) xs -> "(" <> intercalate "," (map show xs) <> ")"
C (CInf x) _ -> show x
C (CEnum _ x) _ -> "#" <> show x
C _ _ -> "undefined"
instance Pretty Pattern where
pretty = \case
W -> "_"
C COne [x] -> "one" <+> align (pretty x)
C CEmpty _ -> "empty"
C (CTuple _) xs -> tupled (map pretty xs)
C (CInf x) _ -> pretty x
C (CEnum _ x) _ -> "#" <> pretty x
C _ _ -> "undefined"
type PMatrix = [[Pattern]]
cSize :: Con -> Int
cSize = \case
COne -> 1
CEmpty -> 0
CTuple s -> s
CInf _ -> 0
CEnum _ _ -> 0
specialize :: Con -> PMatrix -> PMatrix
specialize _ [] = []
specialize c ((pi1 : pis) : rest) = case pi1 of
C c' rs ->
if c == c'
then (rs ++ pis) : specialize c rest
else specialize c rest
W -> (replicate (cSize c) W ++ pis) : specialize c rest
specialize _ ([] : _) = error "malformed PMatrix"
specializeVec :: Con -> [Pattern] -> Maybe [Pattern]
specializeVec c p = case specialize c [p] of
[p'] -> Just p'
_ -> Nothing
col :: PMatrix -> [Pattern]
col [] = []
col ([] : _) = []
col ((x : _) : rest) = x : col rest
conNames :: [Pattern] -> Set Con
conNames [] = Set.empty
conNames (p : ps) = case p of
C c _ -> Set.insert c $ conNames ps
W -> conNames ps
data IsComplete = Complete | Incomplete Pattern
isComplete :: IsComplete -> Bool
isComplete Complete = True
isComplete _ = False
-- This function checks if a set Σ of patterns is a complete signature w.r.t the definition of the datatype, i.e.:
-- the set `{}` is always an incomplete signature (we disallow empty case exprs),
-- the set `{empty, one _}` is a complete signature for the optional type,
-- the set containing all constructors of some enum `E` is complete,
-- the set `{(_,...,_)}` is always complete for an n-tuple type,
-- the set containing any int/double/text/word patterns is always incomplete, because these sets are (theoretically) infinite
isCompleteSignature :: Map VCObjectHash (Set (VCObjectHash, Text)) -> Set Con -> IsComplete
isCompleteSignature enum_sigs s =
if Set.null s
then Incomplete W
else case Set.findMin s of
CEmpty -> if s == Set.fromList [COne, CEmpty] then Complete else Incomplete $ C COne [W]
COne -> if s == Set.fromList [COne, CEmpty] then Complete else Incomplete $ C CEmpty []
CTuple _ -> Complete
CEnum e _ ->
let e_sig = Set.map (uncurry CEnum) $ enum_sigs Map.! e
in if s == e_sig
then Complete
else Incomplete $ C (Set.findMin $ e_sig `Set.difference` s) []
CInf n -> Incomplete $ C (findSucc n) []
where
findSucc :: (Show a, Pretty a, Enum a) => a -> Con
findSucc n = let sn = (CInf $ succ n) in if sn `Set.member` s then findSucc (succ n) else sn
isUseful :: Map VCObjectHash (Set (VCObjectHash, Text)) -> PMatrix -> [Pattern] -> Bool
isUseful _ [] _ = True
isUseful _ ([] : _) _ = False
isUseful _ _ [] = False -- [Pattern] should never be empty
isUseful sigs p q@(q1 : qs) = case q1 of
C c _ ->
let sP = specialize c p
in case specializeVec c q of
Just sq -> isUseful sigs sP sq
Nothing -> error "unreachable specializeVec"
W ->
let sig = conNames $ col p
in if isComplete $ isCompleteSignature sigs sig
then
any
( \ck -> case specializeVec ck q of
Just sq ->
let sP = specialize ck p
in isUseful sigs sP sq
Nothing -> False
)
sig
else isUseful sigs (defaultMatrix p) qs
defaultMatrix :: PMatrix -> PMatrix
defaultMatrix [] = []
defaultMatrix ((C _ _ : _) : rest) = defaultMatrix rest
defaultMatrix ((W : pis) : rest) = pis : defaultMatrix rest
defaultMatrix _ = error "malformed PMatrix"
cTuple :: [Pattern] -> Pattern
cTuple xs = C (CTuple (length xs)) xs
cOne :: Pattern -> Pattern
cOne x = C COne [x]
cEmpty :: Pattern
cEmpty = C CEmpty []
cEnum :: VCObjectHash -> Text -> Pattern
cEnum h t = C (CEnum h t) []
cInf :: (Show a, Pretty a, Enum a) => a -> Pattern
cInf n = C (CInf n) []
exhaustive :: Map VCObjectHash (Set (VCObjectHash, Text)) -> PMatrix -> Maybe [Pattern]
exhaustive sigs pm = i sigs pm 1
where
-- i is a specialized version of isUseful, which returns a missing pattern list instead of just bool
i :: Map VCObjectHash (Set (VCObjectHash, Text)) -> PMatrix -> Int -> Maybe [Pattern]
i _ [] n = Just $ replicate n W
i _ ([] : _) 0 = Nothing
i enum_sigs p n =
let sig = conNames $ col p
in case isCompleteSignature enum_sigs sig of
Complete -> go $ Set.toList sig
Incomplete somePat -> (somePat :) <$> i enum_sigs (defaultMatrix p) (n -1)
where
go [] = Nothing
go (ck : rest) =
let sP = specialize ck p
in case i sigs sP (cSize ck + n - 1) of
Nothing -> go rest
Just pat ->
Just $
[C ck $ take (cSize ck) pat] ++ drop (cSize ck) pat
checkUsefullness :: Map VCObjectHash (Set (VCObjectHash, Text)) -> PMatrix -> [(Int, Int)]
checkUsefullness enum_sigs p = go 0 [] p
where
go _ _ [] = []
go n preceding (p_i : rest) =
if isUseful enum_sigs preceding p_i
then go (n + 1) (preceding ++ [p_i]) rest
else (n, findOverlap p_i 0 preceding) : go (n + 1) (preceding ++ [p_i]) rest
findOverlap :: [Pattern] -> Int -> PMatrix -> Int
findOverlap _ n [] = n
findOverlap p_i n (x : xs) =
if isUseful enum_sigs [x] p_i
then findOverlap p_i (n + 1) xs
else n
-- DO NOT export the constructor EnumText or use EnumText for anything else
-- It is used purely as a hack to give Text a half defined Enum instance,
-- specifically we abuse the `succ` function to be able to generate an
-- example of an incomplete match on text
newtype EnumText = EnumText Text
mkEnumText :: Text -> EnumText
mkEnumText = EnumText
instance Show EnumText where
show (EnumText t) = show t
instance Pretty EnumText where
pretty (EnumText t) = pretty $ show t
instance Enum EnumText where
toEnum = undefined
fromEnum = undefined
succ (EnumText t) =
EnumText $
if Text.null t
then "a"
else t <> t