{-# OPTIONS_HADDOCK ignore-exports #-} {-# OPTIONS_GHC -Wno-orphans #-} -- the Foldable instance for GHC.UniqFM is an orphan {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveFoldable #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-| Atomic constraints and sets of atomic constraints, represented as antichains. Saturation and restriction. -} module Intensional.Constraints ( CInfo(..), modInfo, spanInfo, Atomic, Constraint (..), ConstraintSet, insert, guardWith, toList, fromList, isTriviallyUnsat, unsats, saturate, cexs, size, ) where import Binary import Data.Foldable import Control.Monad.State (State) import Data.Set (Set) import qualified Data.Set as Set import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.IntMap (IntMap) import Data.HashMap.Strict (HashMap) import Data.IntSet (IntSet) import qualified Control.Monad.State as State import qualified Data.HashMap.Strict as HashMap import qualified Data.IntMap as IntMap import qualified Data.IntSet as IntSet import qualified Data.Maybe as Maybe import qualified GhcPlugins as GHC import qualified Control.Monad as Monad import Intensional.Ubiq import Intensional.Types import Intensional.Constructors import Intensional.Guard (Guard) import qualified Intensional.Guard as Guard import Debug.Trace {-| Type type of auxilliary information attached to constraints @c@: * @prov c@ is the module in which the constraint was created * @span c@ is the originating location of the rhs of the constraint INV: CInfo does not determine equality of constraints. -} data CInfo = CInfo { prov :: GHC.Module, sspn :: GHC.SrcSpan } deriving (Eq, Ord) instance GHC.Outputable CInfo where ppr (CInfo _ sp) = GHC.ppr sp instance Binary CInfo where put_ bh (CInfo m sp) = put_ bh m >> put_ bh sp get bh = Monad.liftM2 CInfo (get bh) (get bh) {-| The type of constraints @c@ of shape @guard c@ ? @left c@ ⊆ @right c@ that originated from the source at @srcSpan c@. -} type Atomic = Constraint 'L 'R data Constraint l r = Constraint { left :: K l, right :: K r, guard :: Guard, cinfo :: CInfo } instance Eq (Constraint l r) where -- Disregard info in comparison Constraint l r g _ == Constraint l' r' g' _ = l == l' && r == r' && g == g' instance GHC.Outputable (Constraint l r) where ppr = prpr GHC.ppr instance (Binary (K l), Binary (K r)) => Binary (Constraint l r) where put_ bh (Constraint l r g ss) = put_ bh l >> put_ bh r >> put_ bh g >> put_ bh ss get bh = Constraint <$> Binary.get bh <*> Binary.get bh <*> Binary.get bh <*> Binary.get bh instance Refined (Constraint l r) where domain c = domain (left c) <> domain (right c) <> domain (guard c) rename x y c = c { left = rename x y (left c), right = rename x y (right c), guard = rename x y (guard c) } prpr m a = prpr m (guard a) GHC.<+> GHC.char '?' GHC.<+> prpr m (left a) GHC.<+> GHC.text "<=" GHC.<+> prpr m (right a) modInfo :: Atomic -> GHC.Module modInfo = prov . cinfo spanInfo :: Atomic -> GHC.SrcSpan spanInfo = sspn . cinfo {-| Given an atomic constraint @a@, @isTriviallyUnsat a@ just if @a@ is of the form G ? k in {}. -} isTriviallyUnsat :: Atomic -> Bool isTriviallyUnsat a = case (Guard.isEmpty (guard a), left a, right a) of (True, Con _ _, Set ks _) | GHC.isEmptyUniqSet ks -> True (_, _, _ ) -> False headVar :: Atomic -> Maybe (Int, GHC.Name) headVar a = case right a of Dom (Inj x d) -> Just (x, d) Set _ _ -> Nothing _ -> error "Not possible!" bodyVars :: Atomic -> Set (Int, GHC.Name) bodyVars a = gVars <> lVars where gVars = Guard.typedVars (guard a) lVars = case left a of Dom (Inj x d) -> Set.singleton (x, d) Con _ _ -> Set.empty _ -> error "Not possible!" {-| Given two atomic constraints @a@ and @b@, @a `impliedBy` b@ just if @a@ and @b@ have the same body and the guard of @a@ is implied by the guard of @b@. -} impliedBy :: Atomic -> Atomic -> Bool impliedBy a a' = left a == left a' && right a == right a' && guard a `Guard.impliedBy` guard a' {-| Given an atomic constraint @a@, @tautology a@ just if @a@ is satisfied in every assignment. -} tautology :: Atomic -> Bool tautology a = case left a of Dom d -> case right a of Dom d' -> d == d' _ -> False Con k _ -> case right a of Dom (Inj x d) -> case Guard.lookup x d (guard a) of Just ks -> GHC.elementOfUniqSet k ks Nothing -> False Set ks _ -> GHC.elementOfUniqSet k ks _ -> error "[ERROR] Unexpected right atom" {-| When @m@ is the current module: Given atomic constraints @a@ and @b@, @resolve m a b@ is: * @Nothing@ if the resolvant of @a@ and @b@ is a tautology * @Just r@ otherwise, where @r@ is the resolvant as an atomic constraint -} resolve :: CInfo -> Atomic -> Atomic -> Maybe Atomic resolve ci l r = -- simplify the the new constraint if it's body contains two literals case (left newR, right newR) of (Con k _, Set ks _) | GHC.elementOfUniqSet k ks -> Nothing (Con _ _, Set _ s) | otherwise -> Just (newR { right = Set mempty s}) (_, _ ) -> Just newR where weaken x d y g = -- weaken x(d) in g to y(d) case Guard.lookup x d g of Just ms | ks <- GHC.nonDetEltsUniqSet ms -> Just (Guard.singleton ks y d <> Guard.deleteAll ks x d g) Nothing -> Nothing satisfy x d k g = -- satisfy x(d) in g by k case Guard.lookup x d g of Just ks | k `GHC.elementOfUniqSet` ks -> Just (Guard.delete k x d g) _ -> Nothing mbNewGuard = -- create a new guard by modifying the old one according to -- saturation and weakening (ignoring l's guard, for now) case (left l, right l) of (Dom (Inj y _), Dom (Inj x d)) -> weaken x d y (guard r) (Con k _, Dom (Inj x d)) -> satisfy x d k (guard r) (_, _ ) -> Nothing mbNewLeft = -- apply transitivity, or not (ignoring l's guard, for now) case (right l, left r) of (Dom (Inj x d), Dom (Inj z d')) | z == x && d == d' -> Just (left l) (_, _ ) -> Nothing newR = -- determine whether or not to attach a single copy of l's guards case (mbNewGuard, mbNewLeft) of (Just grd, Just lft) -> r { left = lft, guard = guard l <> grd, cinfo = ci } (Just grd, Nothing ) -> r { guard = guard l <> grd, cinfo = ci } (Nothing, Just lft) -> r { left = lft, guard = guard l <> guard r, cinfo = ci } (Nothing, Nothing ) -> r -- We only use ConstraintSetF with Atomic so far, but it is worth making -- this structure clear to derive Foldable etc -- TODO: Remove these hashmaps in favour of IntMaps data ConstraintSetF a = ConstraintSetF { -- constraints of the form GS ? Y(d) <= Y(d) -- represented as X -> (d -> (Y -> GS)) definiteVV :: IntMap (GHC.NameEnv (HashMap Int [a])), -- constraints of the form GS ? k in X(d) -- represented as X -> (d -> (k -> GS)) definiteKV :: IntMap (GHC.NameEnv (HashMap GHC.Name [a])), -- constraints of the form G ? S1 <= {k1,...,km} -- represented as a list goal :: [a], -- a reverse lookup to find all those constraints -- that have a given sorted variable X(d) in front of -- the head (i.e. eligible for saturation on the right) revMap :: Map (Int, GHC.Name) [Atomic] } deriving (Eq, Functor, Foldable) {-| The type of sets of constraints. We say that a set of constraints @cs@ is /reduced/ just if, for all X, d, Y, k: @definiteVV cs X d Y@, @definiteKV cs X d k@ and @goal cs@ are non-increasing lists wrt @impliedBy@. We say that a set of constraints @cs@ is an /antichain/ just if each of these lists are also non-decreasing. Most sets of constraints we process are reduced, but we allow non-reduced constraint sets to occur as a consequence of renaming of variables. Many sets of constraints are also antichains, for example, when filtering a reduced constraint set one can guarantee the new constraint set will be an antichain by using @insert@ and constructing the new constraint set in the reverse order. Constraint sets returned from saturation will always be antichains. -} type ConstraintSet = ConstraintSetF Atomic instance Foldable GHC.UniqFM where foldr = GHC.foldUFM {-| Given a list of atomic constraints @cs@, @fromList cs@ is largest antichain contained in @cs@ -} fromList :: [Atomic] -> ConstraintSet fromList = foldr insert empty instance Semigroup ConstraintSet where cs <> ds = foldr insert ds cs instance Monoid ConstraintSet where mempty = empty instance GHC.Outputable ConstraintSet where ppr = prpr GHC.ppr instance Binary ConstraintSet where put_ bh = put_ bh . toList get bh = fromList <$> get bh instance Refined ConstraintSet where domain = foldMap domain rename x y = -- this may create non-reduced constraint sets foldl' (\ds a -> unsafeInsert (rename x y a) ds) empty prpr m = foldr ((GHC.$$) . prpr m) GHC.empty {-| @empty@ is the empty constraint set -} empty :: ConstraintSet empty = ConstraintSetF { definiteVV = mempty, definiteKV = mempty, goal = [], revMap = mempty } {-| Given a constraint set @cs@, @unsats cs@ is the constraint set consisting of just those trivially unsatisfiable constraints in @cs@. -} unsats :: ConstraintSet -> ConstraintSet unsats cs = mempty { goal = filter isTriviallyUnsat (goal cs) } {-| When @cs@ is a constraint set, @size cs@ is its cardinality. -} size :: ConstraintSet -> Int size = foldl' (\sz _ -> 1 + sz) 0 {-| When @a@ is an atomic constraint and @cs@ a constraint set, @unsafeInsert a cs@ is the constraint set obtained by inserting @a@ into @cs@. Note: @unsafeInsert a cs@ may not be reduced even if @cs@ is. -} unsafeInsert :: Atomic -> ConstraintSet -> ConstraintSet unsafeInsert a cs = fwd { revMap = Set.foldl' (\m (x,d) -> Map.insertWith (++) (x,d) [a] m) (revMap fwd) (bodyVars a) } where fwd = case (left a, right a) of (Dom (Inj x _), Dom (Inj y yd)) -> let ne = IntMap.findWithDefault mempty y (definiteVV cs) hm = GHC.lookupWithDefaultUFM ne mempty yd as = HashMap.findWithDefault [] x hm in cs {definiteVV = IntMap.insert y (GHC.addToUFM ne yd (HashMap.insert x (a : as) hm)) (definiteVV cs)} (Con k _, Dom (Inj y yd)) -> let ne = IntMap.findWithDefault mempty y (definiteKV cs) hm = GHC.lookupWithDefaultUFM ne mempty yd as = HashMap.findWithDefault [] k hm in cs {definiteKV = IntMap.insert y (GHC.addToUFM ne yd (HashMap.insert k (a : as) hm)) (definiteKV cs)} (_, Set _ _) -> cs {goal = a : goal cs} (_, _) -> cs {-| When @a@ is an atomic constraint and @cs@ is a constraint antichain @insert a cs@ is: * @Nothing@ just if @a@ is already implied by some constraint in @cs@. * @Just ds@ otherwise, where @ds@ is the constraint antichain obtained by inserting @a@ into @cs@. Note: @ds@ may be smaller than @cs@. -} insert' :: Atomic -> ConstraintSet -> Maybe ConstraintSet insert' a cs | not (tautology a) = case mbFwd of Just fwd -> Just $ fwd { revMap = Set.foldl' (\m (x,d) -> Map.insertWith (++) (x,d) [a] m) (revMap fwd) (bodyVars a) } Nothing -> Nothing where mbFwd = case (left a, right a) of (Dom (Inj x _), Dom (Inj y yd)) -> let ne = IntMap.findWithDefault mempty y (definiteVV cs) hm = GHC.lookupWithDefaultUFM ne mempty yd as = HashMap.findWithDefault [] x hm in fmap (\as' -> cs {definiteVV = IntMap.insert y (GHC.addToUFM ne yd (HashMap.insert x as' hm)) (definiteVV cs)}) (maintain as) (Con k _, Dom (Inj y yd)) -> let ne = IntMap.findWithDefault mempty y (definiteKV cs) hm = GHC.lookupWithDefaultUFM ne mempty yd as = HashMap.findWithDefault [] k hm in fmap (\as' -> cs {definiteKV = IntMap.insert y (GHC.addToUFM ne yd (HashMap.insert k as' hm)) (definiteKV cs)}) (maintain as) (_, Set _ _) -> fmap (\as' -> cs {goal = as'}) (maintain (goal cs)) (_, _) -> Nothing -- Ignore constraints concerning base types maintain ds = if any (a `impliedBy`) ds then Nothing else Just (a : ds) insert' _ _ = Nothing {-| When @a@ is an atomic constraint and @cs@ is a constraint set @insert a cs@ is the constraint set obtained by inserting @a@ into @cs@. If @cs@ is reduced then so is @insert a cs@. However, @insert a cs@ may not be an antichain even when @cs@ is. -} insert :: Atomic -> ConstraintSet -> ConstraintSet insert a cs = Maybe.fromMaybe cs (insert' a cs) {-| When @g@ is a guard and @cs@ a set of constraints, @guardWith g cs@ is the set of constraints obtained from @cs@ by appending @g@ to every guard of every constraint. -} guardWith :: Guard -> ConstraintSet -> ConstraintSet guardWith g = foldl' (\ds a -> insert (a { guard = g <> guard a }) ds) mempty {-| When @iface@ is a set of interface variables and @ci@ is the current ctx and @cs@ is a constraint set, @saturate ci iface cs@ is the constraint set obtained from @cs@ by saturation and restriction to @iface@. -} saturate :: CInfo -> IntSet -> ConstraintSet -> ConstraintSet saturate ci iface cs = es where ls = foldl' (\ms c -> if eligible iface c then unsafeInsert c ms else ms) mempty cs ds = snd $ State.execState (fix ci iface) (ls, cs) -- doing foldl with insert here will guarantee the result is an antichain es = foldl' (\fs a -> if domain a `IntSet.isSubsetOf` iface then insert a fs else fs) mempty ds {-| Given a set of interface variables @exts@ and a constraint @c@, @eligible exts c@ just if there are no interface variables in the head of @c@ and only interface variables elsewhere in @c@. (This means it is eligible to be used as the left-constraint in a resolution step.) -} eligible :: IntSet -> Atomic -> Bool eligible exts c = domain (guard c) `IntSet.isSubsetOf` exts && domain (left c) `IntSet.isSubsetOf` exts && not (domain (right c) `IntSet.isSubsetOf` exts) {-| Given some context @ci@ and constraints @cs@ attempt to build a model of @cs@. @cexs ci cs@ is the set of trivially unsatisfiable constrants obtained from the candidate model. -} cexs :: CInfo -> ConstraintSet -> ConstraintSet cexs ci = saturate ci mempty {-| When @exts@ is the set of interface variables and @ci@ is the current ctx @fix ci exts@ is the stateful computation that transforms an initial state @(ls, rs)@ where @ls@ are all unit constraints modulo @exts@ and @ls@ is contained in @rs@, into a final state @(ls', rs')@ in which @ls@ is empty and @rs'@ is the saturation of @rs@. -} fix :: CInfo -> IntSet -> State (ConstraintSet, ConstraintSet) () fix ci exts = do (ls, rs) <- State.get Monad.when debugging $ traceM ("#ls = " ++ show (size ls) ++ ", #rs = " ++ show (size rs)) Monad.unless (null ls) $ saturate1 ci exts >> fix ci exts {-| When @exts@ is a set of interface variables and @ci@ is the current ctx @saturate1 ci exts@ is the stateful computation that takes an initial state @(ls, rs)@ consisting of a pair of constraint sets with @ls@ being unit clauses modulo `exts` and @ls@ being contained in @rs@ to a final state @(ls', rs')@ in which: * @ls'@ is those constraints in @rs'@ that are not in @rs@ and which are unit modulo @exts@. * @rs'@ contains all the constraints obtained from @rs@ by resolving on the left with each clauses from @ls@ once. -} saturate1 :: CInfo -> IntSet -> State (ConstraintSet, ConstraintSet) () saturate1 ci exts = do (ls, rs) <- State.get State.put (mempty, rs) Monad.forM_ ls $ \l -> do -- We immediately get the current state to allow -- for two left constraints to be applied to the -- same right constraint rs' <- State.gets snd -- This is guaranteed by varsAllExts case headVar l of Nothing -> error "impossible" Just (x,d) -> Monad.forM_ (Map.findWithDefault [] (x,d) $ revMap rs') $ \r -> addResolvant (resolve ci l r) where addResolvant Nothing = return () addResolvant (Just r) = do (ls, rs) <- State.get case insert' r rs of Nothing -> return () Just rs' -> -- Insert the new constraint into ls only if it is a -- unit clause modulo externals. State.put (if eligible exts r then insert r ls else ls, rs')