packages feed

intensional-datatys-0.2.0.0: src/Intensional/Constraints.hs

{-# 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')