liquidhaskell-boot-0.9.10.1.2: src/Language/Haskell/Liquid/Constraint/RewriteCase.hs
module Language.Haskell.Liquid.Constraint.RewriteCase
(getCaseRewrites)
where
import Language.Fixpoint.Types
import qualified Language.Fixpoint.Misc as M
import Language.Haskell.Liquid.Constraint.Types
import Language.Haskell.Liquid.Types.Types
import Language.Haskell.Liquid.Types.RType
import Data.Maybe
import Data.Tuple
import qualified Data.HashMap.Strict as M
import qualified Data.HashSet as S
getCaseRewrites :: CGEnv -> SpecType -> LocalRewrites
getCaseRewrites γ spec =
let Reft (_, refinement) = ur_reft $ rt_reft spec
-- Names of constustors both type constructors and data constructors
ctors = toSet $ seBinds $ constEnv γ
-- All the global names (top level functions, types, etc...)
globals = toSet $ reGlobal $ renv γ
in unloop
$ concatMap (uncurry $ unify ctors globals)
$ groupUnifiableEqualities
$ getEqualities refinement
where toSet = S.fromList . M.keys
-- | Generates substitutions for non-global variables that make @e1@ and @e2@
-- equal.
--
-- If @v@ is not global, and @C@ is a data constructor
--
-- * @v@ and @e2@ produces @(v, e2)@
-- * @e1@ and @v@ produces @(e1, v)@
-- * @C a₁ ... aₙ@ and @C b₁ ... bₙ@ produces the substitutions from unifying
-- @(a₁, b₁), ..., (aₙ, bₙ)@
--
-- If any unification fails, the substitutions from the unifications that
-- succeed are still produced.
--
unify :: S.HashSet Symbol -> S.HashSet Symbol -> Expr -> Expr -> [(Symbol, Expr)]
unify ctors globals = go
where
go e1 e2 | e1 == e2 = []
-- NOTE: We don't need to check for ECst because the expressions arent
-- elaborated
go (EVar s1) e2 | isLocal s1 = [(s1, e2)]
go e1 (EVar s2) | isLocal s2 = [(s2, e1)]
-- TODO: Tecnically we could also unify under lambdas but you have to be
-- carefull about alpha equivalence idk if the effort is worth it.
go e1 e2
-- Performing the unification under constructor is safe because
-- C a₁ ... aₙ = C b₁ ... bₙ ⟺ ∀ n . a₁ = bₙ
| (EVar name1 , args1) <- splitEApp e2
, (EVar name2 , args2) <- splitEApp e1
, name1 == name2
, isCtor name1
, length args1 == length args2
= concat $ zipWith go args1 args2
go _ _ = []
isCtor name = name `S.member` ctors
isLocal name = not (name `S.member` globals
|| name `S.member` ctors
|| isPrefixOfSym anfPrefix name)
-- | Given a list of equalities this function produces the equalities that
-- result from applying transitivity exactly once. For instance, if we have
-- @[e1=e2, e2=e3, e1=e4]@ this function will produce @[e1=e3, e2=e4]@.
groupUnifiableEqualities :: [(Expr, Expr)] -> [(Expr, Expr)]
groupUnifiableEqualities = concat . concatMap mkEqs . grouping
where
mkEqs (e1 : es) = [ (e1, e) | e <- es ] : mkEqs es
mkEqs _ = []
grouping eqs = fmap snd $ M.groupList $ eqs ++ fmap swap eqs
getEqualities :: Expr -> [(Expr, Expr)]
getEqualities (PAtom Eq e1 e2) = [(e1, e2)]
getEqualities (PAnd eqs) = concatMap getEqualities eqs
getEqualities _ = []
-- +-----------------------------------------------------+
-- | AcyclicRewrites: collection of rewrites that cannot |
-- | cause an infinite loop |
-- +-----------------------------------------------------+
-- This could be extracted as a separate module
-- | A collection of rewrites that cannot cause an infinite loop
newtype AcyclicRewrites = AR (M.HashMap Symbol Expr)
-- We can think of the map as a directed graph where every symbol is a vertex and
-- there is an edge v₁ → v₂ if v₂ is free in the expression that v₁ is rewritten to.
-- To guarantee that the set of rewrite rules is terminating, we ensure that there
-- aren't any cycles in the graph.
-- | Drops rewrites that would cause an infinite loop. The procedure is order
-- biased as rewrites earlier in the list take precedence.
unloop :: [(Symbol, Expr)] -> LocalRewrites
unloop = LocalRewrites . toRewrites . foldl' doInsert empty
where doInsert ar (s, e) = ar `fromMaybe` insert ar s e
-- | Get the "raw" list of rewrites
toRewrites :: AcyclicRewrites -> M.HashMap Symbol Expr
toRewrites (AR m) = m
-- | @existsPth g s1 s2@ yields @True@ checks if there is a path from @s1@ to @s2@
-- in @g@. Time is @O(Σ(e ∈ m) |exprSymbolSet e|)@, or said otherwise, it is @O(m)@
-- where @m@ is the number of edges.
existsPath :: AcyclicRewrites -> Symbol -> Symbol -> Bool
existsPath (AR m) s1' s2 = go s1'
where
-- Since m is a DAG, we can use DFS to check if there is a path from s1 to
-- s2 without worrying about infinite loops
go s1 | s1 == s2 = True
go s1 | Just e <- M.lookup s1 m
= any go $ S.toList $ exprSymbolsSet e
go _ = False
-- | Constructs an empty set of rewrites
empty :: AcyclicRewrites
empty = AR M.empty
-- | Inserts the rewrite if it wont't cause an infinite loop
insert :: AcyclicRewrites -> Symbol -> Expr -> Maybe AcyclicRewrites
insert ar s e | not $ s `S.member` exprSymbolsSet e
, not $ any (\s2 -> existsPath ar s2 s) $ S.toList $ exprSymbolsSet e
-- There are two ways to break the DAG invariant
-- 1. If the rewrite is closing a loop
-- 2. If the rewrite by itself is a cycle
= Just $ insertUnsafe ar s e
| otherwise
= Nothing
where insertUnsafe (AR m) s' e' = AR $ M.insert s' e' m