packages feed

disco-0.1.6: 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!"