packages feed

disco-0.2: src/Disco/Exhaustiveness.hs

{-# LANGUAGE PatternSynonyms #-}

-- |
-- Module      :  Disco.Exhaustiveness
-- Copyright   :  disco team and contributors
-- Maintainer  :  byorgey@gmail.com
--
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Entry point into the exhaustiveness checker.
-- Converts information into a format the checker understands,
-- then pretty prints the results of running it.
module Disco.Exhaustiveness where

import Control.Monad (forM, unless, zipWithM)
import Control.Monad.Trans.Maybe (MaybeT, runMaybeT)
import Data.List (nub)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Maybe (catMaybes)
import Disco.AST.Generic (Side (..))
import Disco.AST.Surface (
  Pattern,
  prettyPatternP,
  pattern PBool,
  pattern PChar,
  pattern PInj,
  pattern PList,
  pattern PNat,
  pattern PNeg,
  pattern PString,
  pattern PTup,
  pattern PUnit,
  pattern PWild,
 )
import Disco.AST.Typed (
  APattern,
  ATerm,
  pattern APBool,
  pattern APChar,
  pattern APCons,
  pattern APInj,
  pattern APList,
  pattern APNat,
  pattern APNeg,
  pattern APString,
  pattern APTup,
  pattern APUnit,
  pattern APVar,
  pattern APWild,
 )
import Disco.Effects.Fresh (Fresh)
import Disco.Effects.LFresh (LFresh, runLFresh)
import qualified Disco.Exhaustiveness.Constraint as C
import qualified Disco.Exhaustiveness.Possibilities as Poss
import qualified Disco.Exhaustiveness.TypeInfo as TI
import Disco.Messages
import Disco.Pretty (Doc, initPA, withPA)
import qualified Disco.Pretty.DSL as DSL
import Disco.Pretty.Prec (PA, funPA)
import qualified Disco.Types as Ty
import Polysemy
import Polysemy.Output
import Polysemy.Reader
import Unbound.Generics.LocallyNameless (Name)

-- | This exhaustiveness checking algorithm is based on the paper
--   "Lower Your Guards: A Compositional Pattern-Match Coverage Checker":
--   https://www.microsoft.com/en-us/research/uploads/prod/2020/03/lyg.pdf
--
--   Some simplifications were made to adapt the algorithm to suit Disco.
--   The most notable change is that here we always generate (at most) 3
--   concrete examples of uncovered patterns, instead of finding the most
--   general complete description of every uncovered input.
checkClauses :: (Members '[Fresh, Reader Ty.TyDefCtx, Output (Message ann), Embed IO] r) => Name ATerm -> [Ty.Type] -> NonEmpty [APattern] -> Sem r ()
checkClauses name types pats = do
  args <- TI.newVars types
  cl <- zipWithM (desugarClause args) [1 ..] (NonEmpty.toList pats)
  let gdt = foldr1 Branch cl

  let argsNref = []
  (normalizedNrefs, _) <- ua [argsNref] gdt

  examples <- findPosExamples normalizedNrefs args

  unless (null examples) $ do
    let prettyExampleArgs exArgs =
          DSL.hcat $ map prettyPrintExample exArgs

    let prettyExampleLine prettyArgs =
          DSL.text (show name) DSL.<> prettyArgs DSL.<+> DSL.text "= ..."

    let prettyExamples =
          DSL.vcat $ map (prettyExampleLine . prettyExampleArgs) examples

    warn $
      DSL.text "Warning: the function"
        DSL.<+> DSL.text (show name)
        DSL.<+> DSL.text "is undefined for some inputs. For example:"
        DSL.$+$ prettyExamples

prettyPrintExample :: ExamplePat -> Sem r (Doc ann)
prettyPrintExample = runLFresh . runReader initPA . prettyPrintPattern . exampleToDiscoPattern

prettyPrintPattern :: (Members '[Reader PA, LFresh] r) => Pattern -> Sem r (Doc ann)
prettyPrintPattern = withPA funPA . prettyPatternP

exampleToDiscoPattern :: ExamplePat -> Pattern
exampleToDiscoPattern e@(ExamplePat TI.DataCon {TI.dcIdent = ident, TI.dcTypes = types} args) = case (ident, args) of
  (TI.KUnknown, _) -> PWild
  (TI.KUnit, _) -> PUnit
  (TI.KBool b, _) -> PBool b
  (TI.KNat n, _) -> PNat n
  (TI.KInt z, _) ->
    if z >= 0
      then PNat z
      else PNeg (PNat (abs z))
  (TI.KPair, _) -> PTup $ map exampleToDiscoPattern $ resugarPair e
  (TI.KCons, _) ->
    if take 1 types == [Ty.TyC]
      then PString $ resugarString e
      else PList $ map exampleToDiscoPattern $ resugarList e
  (TI.KNil, _) -> PList []
  (TI.KChar c, _) -> PChar c
  (TI.KLeft, [l]) -> PInj L $ exampleToDiscoPattern l
  (TI.KRight, [r]) -> PInj R $ exampleToDiscoPattern r
  (TI.KLeft, _) -> error "Found KLeft data constructor with 0 or multiple arguments"
  (TI.KRight, _) -> error "Found KRight data constructor with 0 or multiple arguments"

resugarPair :: ExamplePat -> [ExamplePat]
resugarPair e@(ExamplePat TI.DataCon {TI.dcIdent = ident} args) = case (ident, args) of
  (TI.KPair, [efst, esnd]) -> efst : resugarPair esnd
  (_, _) -> [e]

resugarList :: ExamplePat -> [ExamplePat]
resugarList (ExamplePat TI.DataCon {TI.dcIdent = ident} args) = case (ident, args) of
  (TI.KCons, [ehead, etail]) -> ehead : resugarList etail
  (_, _) -> []

resugarString :: ExamplePat -> String
resugarString (ExamplePat TI.DataCon {TI.dcIdent = ident} args) = case (ident, args) of
  (TI.KCons, [ehead, etail]) -> assumeExampleChar ehead : resugarString etail
  (_, _) -> []

assumeExampleChar :: ExamplePat -> Char
assumeExampleChar (ExamplePat TI.DataCon {TI.dcIdent = TI.KChar c} _) = c
assumeExampleChar _ = error "Wrongly assumed that an ExamplePat was a Char"

desugarClause :: (Members '[Fresh, Embed IO] r) => [TI.TypedVar] -> Int -> [APattern] -> Sem r Gdt
desugarClause args clauseIdx argsPats = do
  guards <- zipWithM desugarMatch args argsPats
  return $ foldr Guarded (Grhs clauseIdx) $ concat guards

-- To work with the LYG algorithm, we need to desugar n-tuples to nested pairs
-- Just having a Tuple type with a variable number of arguments breaks.
-- Imagine we have
-- foo (1,2,3) = ...
-- foo (1,(2,n)) = ...
-- if we keep things in our nice "sugared" form, the solver will get confused,
-- and not realize that the last element of the tuple is fully covered by n,
-- because there will be two notions of last element: the last in the triple and
-- the last in the nested pair
desugarTuplePats :: [APattern] -> APattern
desugarTuplePats [] = error "Found empty tuple, what happened?"
desugarTuplePats [p] = p
desugarTuplePats (pfst : rest) = APTup (Ty.getType pfst Ty.:*: Ty.getType psnd) [pfst, psnd]
 where
  psnd = desugarTuplePats rest

-- | Convert a Disco APattern into a list of Guards which cover that pattern
--
--   These patterns are currently not handled:
--     , APNeg     --still need to handle rational case
--     , APFrac    --required for rationals?
--     algebraic (probably will be eventually replaced anyway):
--     , APAdd
--     , APMul
--     , APSub
--   These (or some updated version of them) may be handled eventually,
--   once updated arithmetic patterns are merged.
--
--   We treat unhandled patterns as if they are exhaustively matched against
--   (aka, they are seen as wildcards by the checker).
--   This necessarily results in some false negatives, but no false positives.
desugarMatch :: (Members '[Fresh, Embed IO] r) => TI.TypedVar -> APattern -> Sem r [Guard]
desugarMatch var pat = case pat of
  (APTup (ta Ty.:*: tb) [pfst, psnd]) -> do
    varFst <- TI.newVar ta
    varSnd <- TI.newVar tb
    guardsFst <- desugarMatch varFst pfst
    guardsSnd <- desugarMatch varSnd psnd
    let guardPair = (var, GMatch (TI.pair ta tb) [varFst, varSnd])
    return $ [guardPair] ++ guardsFst ++ guardsSnd
  (APTup _ sugary) -> desugarMatch var (desugarTuplePats sugary)
  (APCons _ subHead subTail) -> do
    let typeHead = Ty.getType subHead
    let typeTail = Ty.getType subTail
    varHead <- TI.newVar typeHead
    varTail <- TI.newVar typeTail
    guardsHead <- desugarMatch varHead subHead
    guardsTail <- desugarMatch varTail subTail
    let guardCons = (var, GMatch (TI.cons typeHead typeTail) [varHead, varTail])
    return $ [guardCons] ++ guardsHead ++ guardsTail
  -- We have to desugar Lists into Cons and Nils
  (APList _ []) -> return [(var, GMatch TI.nil [])]
  (APList ty (phead : ptail)) -> do
    -- APCons have the type of the list they are part of
    desugarMatch var (APCons ty phead (APList ty ptail))
  -- we have to desugar to a list, becuse we can match strings with cons
  (APString str) -> do
    let strType = Ty.TyList Ty.TyC
    desugarMatch var (APList strType (map APChar str))
  -- A bit of strangeness is required here because of how patterns work
  (APNat Ty.TyN nat) -> return [(var, GMatch (TI.natural nat) [])]
  (APNat Ty.TyZ z) -> return [(var, GMatch (TI.integer z) [])]
  (APNeg Ty.TyZ (APNat Ty.TyN z)) -> return [(var, GMatch (TI.integer (-z)) [])]
  -- These are more straightforward:
  (APWild _) -> return []
  (APVar ty name) -> do
    let newAlias = TI.TypedVar (name, ty)
    return [(newAlias, GWasOriginally var)]
  APUnit -> return [(var, GMatch TI.unit [])]
  (APBool b) -> return [(var, GMatch (TI.bool b) [])]
  (APChar c) -> return [(var, GMatch (TI.char c) [])]
  (APInj (tl Ty.:+: tr) side subPat) -> do
    let dc = case side of
          L -> TI.left tl
          R -> TI.right tr
    newVar <- case side of
      L -> TI.newVar tl
      R -> TI.newVar tr
    guards <- desugarMatch newVar subPat
    return $ (var, GMatch dc [newVar]) : guards
  _ -> return []

data Gdt where
  Grhs :: Int -> Gdt
  Branch :: Gdt -> Gdt -> Gdt
  Guarded :: Guard -> Gdt -> Gdt
  deriving (Show, Eq)

type Guard = (TI.TypedVar, GuardConstraint)

data GuardConstraint where
  GMatch :: TI.DataCon -> [TI.TypedVar] -> GuardConstraint
  GWasOriginally :: TI.TypedVar -> GuardConstraint
  deriving (Show, Eq)

newtype Literal = Literal (TI.TypedVar, LitCond)

data LitCond where
  LitMatch :: TI.DataCon -> [TI.TypedVar] -> LitCond
  LitNot :: TI.DataCon -> LitCond
  LitWasOriginally :: TI.TypedVar -> LitCond
  deriving (Show, Eq, Ord)

data Ant where
  AGrhs :: [C.NormRefType] -> Int -> Ant
  ABranch :: Ant -> Ant -> Ant
  deriving (Show)

ua :: (Members '[Fresh, Reader Ty.TyDefCtx] r) => [C.NormRefType] -> Gdt -> Sem r ([C.NormRefType], Ant)
ua nrefs gdt = case gdt of
  Grhs k -> return ([], AGrhs nrefs k)
  Branch t1 t2 -> do
    (n1, u1) <- ua nrefs t1
    (n2, u2) <- ua n1 t2
    return (n2, ABranch u1 u2)
  Guarded (y, GWasOriginally x) t -> do
    n <- addLitMulti nrefs $ Literal (y, LitWasOriginally x)
    ua n t
  Guarded (x, GMatch dc args) t -> do
    n <- addLitMulti nrefs $ Literal (x, LitMatch dc args)
    (n', u) <- ua n t
    n'' <- addLitMulti nrefs $ Literal (x, LitNot dc)
    return (n'' ++ n', u)

addLitMulti :: (Members '[Fresh, Reader Ty.TyDefCtx] r) => [C.NormRefType] -> Literal -> Sem r [C.NormRefType]
addLitMulti [] _ = return []
addLitMulti (n : ns) lit = do
  r <- runMaybeT $ addLiteral n lit
  case r of
    Nothing -> addLitMulti ns lit
    Just cfs -> do
      ns' <- addLitMulti ns lit
      return $ cfs : ns'

addLiteral :: (Members '[Fresh, Reader Ty.TyDefCtx] r) => C.NormRefType -> Literal -> MaybeT (Sem r) C.NormRefType
addLiteral constraints (Literal (x, c)) = case c of
  LitWasOriginally z ->
    constraints `C.addConstraint` (x, C.CWasOriginally z)
  LitMatch dc args ->
    constraints `C.addConstraint` (x, C.CMatch dc args)
  LitNot dc ->
    constraints `C.addConstraint` (x, C.CNot dc)

data InhabPat where
  IPIs :: TI.DataCon -> [InhabPat] -> InhabPat
  IPNot :: [TI.DataCon] -> InhabPat
  deriving (Show, Eq, Ord)

-- Sanity check: are we giving the dataconstructor the
-- correct number of arguments?
mkIPMatch :: TI.DataCon -> [InhabPat] -> InhabPat
mkIPMatch k pats =
  if length (TI.dcTypes k) /= length pats
    then error $ "Wrong number of DataCon args" ++ show (k, pats)
    else IPIs k pats

findInhabitants :: (Members '[Fresh, Reader Ty.TyDefCtx] r) => [C.NormRefType] -> [TI.TypedVar] -> Sem r (Poss.Possibilities [InhabPat])
findInhabitants nrefs args = do
  a <- forM nrefs (`findAllForNref` args)
  return $ Poss.anyOf a

findAllForNref :: (Members '[Fresh, Reader Ty.TyDefCtx] r) => C.NormRefType -> [TI.TypedVar] -> Sem r (Poss.Possibilities [InhabPat])
findAllForNref nref args = do
  argPats <- forM args (`findVarInhabitants` nref)
  return $ Poss.allCombinations argPats

findVarInhabitants :: (Members '[Fresh, Reader Ty.TyDefCtx] r) => TI.TypedVar -> C.NormRefType -> Sem r (Poss.Possibilities InhabPat)
findVarInhabitants var cns =
  case posMatch of
    Just (k, args) -> do
      argPossibilities <- findAllForNref cns args
      return (mkIPMatch k <$> argPossibilities)
    Nothing -> case nub negMatches of
      [] -> Poss.retSingle $ IPNot []
      neg -> do
        tyCtx <- ask @Ty.TyDefCtx
        case TI.tyDataCons (TI.getType var) tyCtx of
          TI.Infinite _ -> Poss.retSingle $ IPNot neg
          TI.Finite dcs ->
            do
              let tryAddDc dc = do
                    vars <- TI.newVars (TI.dcTypes dc)
                    runMaybeT (cns `C.addConstraint` (var, C.CMatch dc vars))

              -- Try to add a positive constraint for each data constructor
              -- to the current nref
              -- If any of these additions succeed, save that nref,
              -- it now has positive information
              posNrefs <- catMaybes <$> forM dcs tryAddDc

              if null posNrefs
                then Poss.retSingle $ IPNot []
                else Poss.anyOf <$> forM posNrefs (findVarInhabitants var)
 where
  constraintsOnX = C.onVar var cns
  posMatch = C.posMatch constraintsOnX
  negMatches = C.negMatches constraintsOnX

findRedundant :: (Members '[Fresh, Reader Ty.TyDefCtx] r) => Ant -> [TI.TypedVar] -> Sem r [Int]
findRedundant ant args = case ant of
  AGrhs ref i -> do
    uninhabited <- Poss.none <$> findInhabitants ref args
    return [i | uninhabited]
  ABranch a1 a2 -> mappend <$> findRedundant a1 args <*> findRedundant a2 args

data ExamplePat where
  ExamplePat :: TI.DataCon -> [ExamplePat] -> ExamplePat
  deriving (Show)

-- | Less general version of the above inhabitant finding function
--   returns a maximum of 3 possible args lists that haven't been matched against,
--   as to not overwhelm new users of the language.
--   This is essentially a DFS, and it has a bad habit of
--   trying to build infinite lists whenever it can, so we give it a max depth of 32
--   If we reach 32 levels of nested dataconstructors in this language,
--   it is pretty safe to assume we were chasing after an infinite structure
findPosExamples :: (Members '[Fresh, Reader Ty.TyDefCtx] r) => [C.NormRefType] -> [TI.TypedVar] -> Sem r [[ExamplePat]]
findPosExamples nrefs args = do
  a <- forM nrefs (\nref -> findAllPosForNref 32 nref args)
  return $ take 3 $ Poss.getPossibilities $ Poss.anyOf a

findAllPosForNref :: (Members '[Fresh, Reader Ty.TyDefCtx] r) => Int -> C.NormRefType -> [TI.TypedVar] -> Sem r (Poss.Possibilities [ExamplePat])
findAllPosForNref fuel nref args = do
  argPats <- forM args (\arg -> findVarPosExamples (fuel - 1) arg nref)
  return $ Poss.allCombinations argPats

findVarPosExamples :: (Members '[Fresh, Reader Ty.TyDefCtx] r) => Int -> TI.TypedVar -> C.NormRefType -> Sem r (Poss.Possibilities ExamplePat)
findVarPosExamples fuel var cns =
  if fuel < 0
    then return mempty
    else case posMatch of
      Just (k, args) -> do
        argPossibilities <- findAllPosForNref (fuel - 1) cns args
        return (mkExampleMatch k <$> argPossibilities)
      Nothing -> do
        tyCtx <- ask @Ty.TyDefCtx
        let dcs = getPosFrom (TI.getType var) tyCtx negMatches
        let tryAddDc dc = do
              vars <- TI.newVars (TI.dcTypes dc)
              runMaybeT (cns `C.addConstraint` (var, C.CMatch dc vars))
        -- Try to add a positive constraint for each data constructor
        -- to the current nref
        -- If any of these additions succeed, save that nref,
        -- it now has positive information
        posNrefs <- catMaybes <$> forM dcs tryAddDc

        Poss.anyOf <$> forM posNrefs (findVarPosExamples (fuel - 1) var)
 where
  constraintsOnX = C.onVar var cns
  posMatch = C.posMatch constraintsOnX
  negMatches = C.negMatches constraintsOnX

getPosFrom :: Ty.Type -> Ty.TyDefCtx -> [TI.DataCon] -> [TI.DataCon]
getPosFrom ty ctx neg = take 3 $ filter (`notElem` neg) dcs
 where
  dcs = case TI.tyDataCons ty ctx of
    TI.Finite dcs' -> dcs'
    TI.Infinite dcs' -> dcs'

mkExampleMatch :: TI.DataCon -> [ExamplePat] -> ExamplePat
mkExampleMatch k pats =
  if length (TI.dcTypes k) /= length pats
    then error $ "Wrong number of DataCon args" ++ show (k, pats)
    else ExamplePat k pats