packages feed

disco-0.1.0.0: src/Disco/Typecheck/Unify.hs

-----------------------------------------------------------------------------
-- |
-- Module      :  Disco.Typecheck.Unify
-- Copyright   :  disco team and contributors
-- Maintainer  :  byorgey@gmail.com
--
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Unification.
--
-----------------------------------------------------------------------------

module Disco.Typecheck.Unify where

import           Unbound.Generics.LocallyNameless (Name, fv)

import           Control.Lens                     (anyOf)
import           Control.Monad.State
import qualified Data.Map                         as M
import           Data.Set                         (Set)
import qualified Data.Set                         as S

import           Disco.Subst
import           Disco.Types

-- XXX todo: might be better if unification took sorts into account
-- directly.  As it is, however, I think it works properly;
-- e.g. suppose we have a with sort {sub} and we unify it with Bool.
-- unify will just return a substitution [a |-> Bool].  But then when
-- we call extendSubst, and in particular substSortMap, the sort {sub}
-- will be applied to Bool and decomposed which will throw an error.

-- | Given a list of equations between types, return a substitution
--   which makes all the equations satisfied (or fail if it is not
--   possible).
--
--   This is not the most efficient way to implement unification but
--   it is simple.
unify :: TyDefCtx -> [(Type, Type)] -> Maybe S
unify = unify' (==)

-- | Given a list of equations between types, return a substitution
--   which makes all the equations equal *up to* identifying all base
--   types.  So, for example, Int = Nat weakly unifies but Int = (Int
--   -> Int) does not.  This is used to check whether subtyping
--   constraints are structurally sound before doing constraint
--   simplification/solving, to ensure termination.
weakUnify :: TyDefCtx -> [(Type, Type)] -> Maybe S
weakUnify = unify' (\_ _ -> True)

-- | Given a list of equations between types, return a substitution
--   which makes all the equations satisfied (or fail if it is not
--   possible), up to the given comparison on base types.
unify' :: (BaseTy -> BaseTy -> Bool) -> TyDefCtx
       -> [(Type, Type)] -> Maybe S
unify' baseEq tyDefns eqs = evalStateT (go eqs) S.empty
  where
    go :: [(Type, Type)] -> StateT (Set (Type,Type)) Maybe S
    go [] = return idS
    go (e:es) = do
      u <- unifyOne e
      case u of
        Left sub    -> (@@ sub) <$> go (applySubst sub es)
        Right newEs -> go (newEs ++ es)

    unifyOne :: (Type, Type) -> StateT (Set (Type,Type)) Maybe (Either S [(Type, Type)])
    unifyOne pair = do
      seen <- get
      case pair `S.member` seen of
        True  -> return $ Left idS
        False -> unifyOne' pair

    unifyOne' :: (Type, Type) -> StateT (Set (Type,Type)) Maybe (Either S [(Type, Type)])

    unifyOne' (ty1, ty2)
      | ty1 == ty2 = return $ Left idS

    unifyOne' (TyVar x, ty2)
      | occurs x ty2 = mzero
      | otherwise    = return $ Left (x |-> ty2)
    unifyOne' (ty1, x@(TyVar _))
      = unifyOne (x, ty1)

    -- At this point we know ty2 isn't the same skolem nor a unification variable.
    -- Skolems don't unify with anything.
    unifyOne' (TySkolem _, _) = mzero
    unifyOne' (_, TySkolem _) = mzero

    -- Unify two container types: unify the container descriptors as
    -- well as the type arguments
    unifyOne' p@(TyCon (CContainer ctr1) tys1, TyCon (CContainer ctr2) tys2) = do
      modify (S.insert p)
      return $ Right ((TyAtom ctr1, TyAtom ctr2) : zip tys1 tys2)

    -- If one of the types to be unified is a user-defined type,
    -- unfold its definition before continuing the matching
    unifyOne' p@(TyCon (CUser t) tys1, ty2) = do
      modify (S.insert p)
      case M.lookup t tyDefns of
        Nothing                 -> mzero
        Just (TyDefBody _ body) -> return $ Right [(body tys1, ty2)]

    unifyOne' p@(ty1, TyCon (CUser t) tys2) = do
      modify (S.insert p)
      case M.lookup t tyDefns of
        Nothing                 -> mzero
        Just (TyDefBody _ body) -> return $ Right [(ty1, body tys2)]

    -- Unify any other pair of type constructor applications: the type
    -- constructors must match exactly
    unifyOne' p@(TyCon c1 tys1, TyCon c2 tys2)
      | c1 == c2  = do
          modify (S.insert p)
          return $ Right (zip tys1 tys2)
      | otherwise = mzero
    unifyOne' (TyAtom (ABase b1), TyAtom (ABase b2))
      | baseEq b1 b2 = return $ Left idS
      | otherwise    = mzero
    unifyOne' _ = mzero  -- Atom = Cons


equate :: TyDefCtx -> [Type] -> Maybe S
equate tyDefns tys = unify tyDefns eqns
  where
    eqns = zip tys (tail tys)

occurs :: Name Type -> Type -> Bool
occurs x = anyOf fv (==x)


unifyAtoms :: TyDefCtx -> [Atom] -> Maybe (Substitution Atom)
unifyAtoms tyDefns = fmap (fmap fromTyAtom) . equate tyDefns . map TyAtom
  where
    fromTyAtom (TyAtom a) = a
    fromTyAtom _          = error "fromTyAtom on non-TyAtom!"

unifyUAtoms :: TyDefCtx -> [UAtom] -> Maybe (Substitution UAtom)
unifyUAtoms tyDefns = fmap (fmap fromTyAtom) . equate tyDefns . map (TyAtom . uatomToAtom)
  where
    fromTyAtom (TyAtom (ABase b))    = UB b
    fromTyAtom (TyAtom (AVar (U v))) = UV v
    fromTyAtom _                     = error "fromTyAtom on wrong thing!"