disco-0.2: src/Disco/Exhaustiveness/Constraint.hs
-- |
-- Module : Disco.Exhaustiveness.Constraint
-- Copyright : disco team and contributors
-- Maintainer : byorgey@gmail.com
--
-- SPDX-License-Identifier: BSD-3-Clause
--
-- The heart of the Lower Your Guards algorithm.
-- Functions for converting constraints detailing
-- pattern matches into a normalized description
-- of everything that is left uncovered.
module Disco.Exhaustiveness.Constraint where
import Control.Applicative (Alternative)
import Control.Monad (foldM, guard)
import Control.Monad.Trans (lift)
import Control.Monad.Trans.Maybe (MaybeT, runMaybeT)
import Data.Bifunctor (first)
import Data.List (partition)
import Data.Maybe (isJust, listToMaybe, mapMaybe)
import Disco.Effects.Fresh (Fresh)
import qualified Disco.Exhaustiveness.TypeInfo as TI
import qualified Disco.Types as Ty
import Polysemy
import Polysemy.Reader
data Constraint where
CMatch :: TI.DataCon -> [TI.TypedVar] -> Constraint
CNot :: TI.DataCon -> Constraint
CWasOriginally :: TI.TypedVar -> Constraint
deriving (Show, Eq, Ord)
posMatch :: [Constraint] -> Maybe (TI.DataCon, [TI.TypedVar])
posMatch constraints = listToMaybe $ mapMaybe (\case (CMatch k ys) -> Just (k, ys); _ -> Nothing) constraints
negMatches :: [Constraint] -> [TI.DataCon]
negMatches = mapMaybe (\case (CNot k) -> Just k; _ -> Nothing)
type ConstraintFor = (TI.TypedVar, Constraint)
-- Resolves term equalities, finding the leftmost id for a variable
-- I believe I3 of section 3.4 allows us to
-- do a linear scan from right to left
lookupVar :: TI.TypedVar -> [ConstraintFor] -> TI.TypedVar
lookupVar x = foldr getNextId x
where
getNextId (x', CWasOriginally y) | x' == x = const y
getNextId _ = id
alistLookup :: (Eq a) => a -> [(a, b)] -> [b]
alistLookup a = map snd . filter ((== a) . fst)
onVar :: TI.TypedVar -> [ConstraintFor] -> [Constraint]
onVar x cs = alistLookup (lookupVar x cs) cs
type NormRefType = [ConstraintFor]
addConstraints :: Members '[Fresh, Reader Ty.TyDefCtx] r => NormRefType -> [ConstraintFor] -> MaybeT (Sem r) NormRefType
addConstraints = foldM addConstraint
addConstraint :: Members '[Fresh, Reader Ty.TyDefCtx] r => NormRefType -> ConstraintFor -> MaybeT (Sem r) NormRefType
addConstraint cns (x, c) = do
breakIf $ any (conflictsWith c) (onVar x cns)
addConstraintHelper cns (lookupVar x cns, c)
addConstraintHelper :: Members '[Fresh, Reader Ty.TyDefCtx] r => NormRefType -> ConstraintFor -> MaybeT (Sem r) NormRefType
addConstraintHelper cns cf@(origX, c) = case c of
--- Equation (10)
CMatch k args -> do
case getConstructorArgs k (onVar origX cns) of
-- 10c
Just args' ->
addConstraints
cns
(zipWith (\a b -> (a, CWasOriginally b)) args args')
Nothing -> return added
--- Equation (11)
CNot _ -> do
inh <- lift (inhabited added origX)
guard inh -- ensure that origX is still inhabited, as per I2
return added
-- Equation (14)
CWasOriginally y -> do
let origY = lookupVar y cns
if origX == origY
then return cns
else do
let (noX', withX') = partition ((/= origX) . fst) cns
addConstraints (noX' ++ [cf]) (substituteVarIDs origY origX withX')
where
added = cns ++ [cf]
-----
----- Helper functions for adding constraints:
-----
breakIf :: (Alternative f) => Bool -> f ()
breakIf = guard . not
-- | Returns a predicate that returns true if another
-- constraint conflicts with the one given.
-- This alone is not sufficient to test
-- if a constraint can be added, but it
-- filters out the easy negatives early on
conflictsWith :: Constraint -> (Constraint -> Bool)
conflictsWith c = case c of
CMatch k _ -> \case
(CMatch k' _) | k /= k' -> True -- 10a
(CNot k') | k == k' -> True -- 10b
_ -> False
CNot k -> \case
(CMatch k' _) | k == k' -> True -- 11a
_ -> False
CWasOriginally _ -> const False
-- | Search for a MatchDataCon that is matching on k specifically
-- (there should be at most one, see I4 in section 3.4)
-- and if it exists, return the variable ids of its arguments
getConstructorArgs :: TI.DataCon -> [Constraint] -> Maybe [TI.TypedVar]
getConstructorArgs k cfs =
listToMaybe $
mapMaybe (\case (CMatch k' vs) | k' == k -> Just vs; _ -> Nothing) cfs
-- | substituting y *for* x
-- ie replace the second with the first, replace x with y
substituteVarIDs :: TI.TypedVar -> TI.TypedVar -> [ConstraintFor] -> [ConstraintFor]
substituteVarIDs y x = map (first subst)
where
subst var = if var == x then y else x
-- | Deals with I2 from section 3.4
-- if a variable in the context has a resolvable type, there must be at least one constructor
-- which can be instantiated without contradiction of the refinement type
-- This function tests if this is true
inhabited :: Members '[Fresh, Reader Ty.TyDefCtx] r => NormRefType -> TI.TypedVar -> Sem r Bool
inhabited n var = do
tyCtx <- ask @Ty.TyDefCtx
case TI.tyDataCons (TI.getType var) tyCtx of
TI.Infinite _ -> return True -- assume opaque types are inhabited
TI.Finite constructors -> do
or <$> mapM (instantiate n var) constructors
-- | Attempts to "instantiate" a match of the dataconstructor k on x
-- If we can add the MatchDataCon constraint to the normalized refinement
-- type without contradiction (a Nothing value),
-- then x is inhabited by k and we return true
instantiate :: Members '[Fresh, Reader Ty.TyDefCtx] r => NormRefType -> TI.TypedVar -> TI.DataCon -> Sem r Bool
instantiate cns var k = do
args <- TI.newVars $ TI.dcTypes k
let attempt = cns `addConstraint` (var, CMatch k args)
isJust <$> runMaybeT attempt