packages feed

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