packages feed

rzk-0.10.0: src/Rzk/TypeCheck/Unify.hs

{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE PatternSynonyms     #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Unification, which in rzk is really subtyping: an extension type's faces, a
-- shape's tope and the variance of the position all take part.
module Rzk.TypeCheck.Unify where

import           Control.Monad            (forM_, unless, when)
import           Control.Monad.Except     (catchError, throwError)
import           Control.Monad.Reader     (asks)
import           Data.Maybe               (fromMaybe)
import           Data.Tuple               (swap)

import           Control.Monad.Foil       (DExt, Distinct, NameBinder)
import qualified Control.Monad.Foil       as Foil
import           Control.Monad.Free.Foil  (AST (Var))

import           Language.Rzk.Foil.Syntax
import           Language.Rzk.Foil.Names (Binder, TModality (..),
                                           TypeInfo (..))
import           Rzk.TypeCheck.Context
import           Rzk.TypeCheck.Display (panicImpossible)
import           Rzk.TypeCheck.Error
import           Rzk.TypeCheck.Eval
import           Rzk.TypeCheck.Monad
import           Rzk.TypeCheck.NbE (nbeConvertible)

-- | Open two scoped terms under /one/ binder, so that the two sides of a
-- comparison are compared as functions of the same variable.
inScope2
  :: Distinct n
  => Binder -> TModality -> TermT n
  -> ScopedTermT n -> ScopedTermT n
  -> (forall l. (DExt n l, Distinct l)
        => NameBinder n l -> TermT l -> TermT l -> TypeCheck l a)
  -> TypeCheck n a
inScope2 orig md ty s1 s2 k = do
  scope <- asks ctxScope
  withScopedT2 scope s1 s2 $ \binder body1 body2 ->
    underBinder binder orig md ty Nothing (k binder body1 body2)

-- | α-equivalence in the ambient scope.
alphaEq :: Distinct n => TermT n -> TermT n -> TypeCheck n Bool
alphaEq l r = do
  scope <- asks ctxScope
  pure (alphaEqT scope l r)

unifyTopes :: Distinct n => TermT n -> TermT n -> TypeCheck n ()
unifyTopes l r = do
  equiv <- (&&)
    <$> [plainTope l] `entailM` r
    <*> [plainTope r] `entailM` l
  unless equiv $
    issueTypeError (TypeErrorTopesNotEquivalent l r)

unify
  :: Distinct n
  => Maybe (TermT n) -> TermT n -> TermT n -> TypeCheck n ()
unify mterm expected actual = performUnification `catchError` \typeError -> do
  inAllSubContexts (throwError typeError) performUnification
  where
    performUnification = unifyInCurrentContext mterm expected actual

-- | The syntactic fast path.
--
-- α-equivalence, not the structural equality the old representation used: two
-- terms that differ only in a binder's name are the same term, and saying so here
-- saves the whole unification below.
unifyViaDecompose :: Distinct n => TermT n -> TermT n -> TypeCheck n ()
unifyViaDecompose expected actual = do
  same <- alphaEq expected actual
  if same
    then return ()
    else do
      -- The NbE fast path: a shared-evaluation βδη-conversion check over the
      -- context-insensitive fragment. 'True' is definite (see the module's
      -- soundness note); 'False' only means "do not know", and unification
      -- proceeds unchanged. It must run /before/ the application decomposition
      -- below: decomposing @f x@ against @g y@ compares the arguments pairwise,
      -- which for βδ-equal but structurally different applications creates
      -- false subgoals (e.g. @16 =? 128@ from @16 · 16 =? 128 + 128@) that the
      -- old path then grinds through only to fail and unwind.
      fastPath <- nbeConvertible expected actual
      if fastPath
        then return ()
        else case (expected, actual) of
          (AppT _ f x, AppT _ g y) -> do
            unify Nothing f g
            setVariance Invariant $ unify Nothing x y
          _ -> issueTypeError (TypeErrorOther "cannot decompose")

unifyTypes :: Distinct n => TermT n -> TermT n -> TermT n -> TypeCheck n ()
unifyTypes = unify . Just

unifyTerms :: Distinct n => TermT n -> TermT n -> TypeCheck n ()
unifyTerms = unify Nothing

checkCoherence
  :: Distinct n
  => (TermT n, TermT n) -> (TermT n, TermT n) -> TypeCheck n ()
checkCoherence (ltope, lterm) (rtope, rterm) =
  performing (ActionCheckCoherence (ltope, lterm) (rtope, rterm)) $
    localTope (topeAndT ltope rtope) $ do
      ltype <- stripTypeRestrictions <$> typeOf lterm   -- FIXME: why strip?
      rtype <- stripTypeRestrictions <$> typeOf rterm   -- FIXME: why strip?
      -- FIXME: do we need to unify types here or is it included in unification of terms?
      unifyTerms ltype rtype
      unifyTerms lterm rterm

unifyInCurrentContext
  :: forall n. Distinct n
  => Maybe (TermT n) -> TermT n -> TermT n -> TypeCheck n ()
unifyInCurrentContext mterm expected actual = performing action $ do
  inBottom <- contextEntailsBottom
  unless inBottom $
    -- NOTE: the decomposition gives a small, but noticeable speedup
    unifyViaDecompose expected actual `catchError` \_ -> do
      expectedVal <- whnfT expected
      actualVal <- whnfT actual
      mea <- asks ctxCovariance >>= \case
        Covariant     -> Just <$> etaMatch mterm expectedVal actualVal
        Contravariant -> Just . swap <$> etaMatch mterm actualVal expectedVal
        Invariant     -> traceTypeCheck Debug "invariant" $ do
          -- FIXME: inefficient
          traceTypeCheck Debug "invariant->covariant" $
            setVariance Covariant     $ unifyInCurrentContext mterm expectedVal actualVal
          traceTypeCheck Debug "invariant->contravariant" $
            setVariance Contravariant $ unifyInCurrentContext mterm expectedVal actualVal
          return Nothing
      case mea of
        Nothing -> return ()
        -- A hole (in lenient mode) stands for a term of the expected type, so it
        -- unifies with anything; accept it rather than falling through to the
        -- dispatch below (which would panic on an unexpected term).
        Just (expected', actual') | isHoleT expected' || isHoleT actual' -> return ()
        Just (expected', actual') -> do
          same <- alphaEq expected' actual'
          unless same $ dispatch expected' actual'
  where
    action = case mterm of
               Nothing   -> ActionUnifyTerms expected actual
               Just term -> ActionUnify term expected actual

    dispatch :: TermT n -> TermT n -> TypeCheck n ()
    dispatch expected' actual' =
      case actual' of
        RecBottomT{} -> return ()
        RecOrT _ty rs' ->
          case expected' of
            RecOrT _ty rs -> sequence_ (checkCoherence <$> rs <*> rs')
            _ ->
              forM_ rs' $ \(tope, term) ->
                localTope tope $
                  unifyTerms expected' term
        _ -> typeOf expected' >>= typeOf >>= \case
          UniverseCubeT{} -> contextEntails (topeEQT expected' actual')
          _ -> unifyStructurally expected' actual'

    unifyStructurally :: TermT n -> TermT n -> TypeCheck n ()
    unifyStructurally expected' actual' = do
      -- A hole stands for a term of the expected type, so a unification that would
      -- otherwise fail is deferred when either side still contains an (unfilled)
      -- hole — including one nested in a larger term, e.g. @f ?@ checked against an
      -- extension-type boundary. The hole may also sit in the tope context rather
      -- than the terms: a hole standing for a whole shape point makes the enclosing
      -- 'recOR' split over hole-dependent faces, and a branch reduction can drop the
      -- hole from the terms while the assumed face (@π₁ ? ≤ π₂ ?@, say) still
      -- mentions it. Such a branch is only entered because the hole is unfilled, so
      -- a mismatch under it is deferred too. 'structuralHoleUnify' turns this off,
      -- keeping a structural mismatch around a hole an error.
      defer <- asks ctxDeferHoleMismatches
      topeContextHasHole <- asks (any (containsHole . tTope) . ctxTopes)
      let holePresent = defer &&
            (containsHole expected' || containsHole actual' || topeContextHasHole)

          err :: TypeCheck n ()
          err
            | holePresent = return ()
            | otherwise =
                case mterm of
                  Nothing   -> issueTypeError (TypeErrorUnifyTerms expected' actual')
                  Just term -> issueTypeError (TypeErrorUnify term expected' actual')

          -- The same error, raised from inside a binder: the terms are sunk into
          -- the inner scope, which is a coercion. (The old representation had to
          -- shift each of them with @S <$>@.)
          errIn :: DExt n l => TypeCheck l ()
          errIn
            | holePresent = return ()
            | otherwise =
                case mterm of
                  Nothing -> issueTypeError
                    (TypeErrorUnifyTerms (Foil.sink expected') (Foil.sink actual'))
                  Just term -> issueTypeError
                    (TypeErrorUnify (Foil.sink term) (Foil.sink expected') (Foil.sink actual'))

          def = do
            same <- alphaEq expected' actual'
            unless same err

      case expected' of
        Var{} -> def

        UniverseT{} -> def
        UniverseCubeT{} -> def
        UniverseTopeT{} -> def

        TypeUnitT{} -> def
        UnitT{} -> return ()  -- Unit always unifies!

        CubeUnitT{} -> def
        CubeUnitStarT{} -> def
        Cube2T{} -> def
        Cube2_0T{} -> def
        Cube2_1T{} -> def
        CubeIT{} -> def
        CubeI_0T{} -> def
        CubeI_1T{} -> def
        CubeProductT _ l r ->
          case actual' of
            CubeProductT _ l' r' -> do
              unifyTerms l l'
              unifyTerms r r'
            _ -> err

        PairT _ty l r ->
          case actual' of
            PairT _ty' l' r' -> do
              unifyTerms l l'
              unifyTerms r r'
            -- one part of eta-expansion for pairs
            -- FIXME: add symmetric version!
            _ -> err

        FirstT _ty t ->
          case actual' of
            FirstT _ty' t' -> unifyTerms t t'
            _              -> err

        SecondT _ty t ->
          case actual' of
            SecondT _ty' t' -> unifyTerms t t'
            _               -> err

        TopeTopT{}    -> unifyTopes expected' actual'
        TopeBottomT{} -> unifyTopes expected' actual'
        TopeEQT{}     -> unifyTopes expected' actual'
        TopeLEQT{}    -> unifyTopes expected' actual'
        TopeAndT{}    -> unifyTopes expected' actual'
        TopeOrT{}     -> unifyTopes expected' actual'
        TopeInvT{}    -> unifyTopes expected' actual'
        TopeUninvT{}  -> unifyTopes expected' actual'

        RecBottomT{} -> return () -- unifies with anything
        RecOrT _ty rs ->
          -- IMPORTANT: matching on actual' here would be redundant, but that is
          -- not obvious; take care when refactoring.
          forM_ rs $ \(tope, term) ->
            localTope tope $
              unifyTerms term actual'

        TypeFunT _ty _orig md cube mtope ret ->
          case actual' of
            TypeFunT _ty' orig' md' cube' mtope' ret' -> do
              when (md /= md') $
                issueTypeError (TypeErrorOther $ "modality mismatch in function type: expected " <> show md <> " but got " <> show md')
              switchVariance $  -- unifying in the negative position!
                unifyTerms cube cube' -- FIXME: unifyCubes
              inScope2 orig' md cube' ret ret' $ \binder retBody retBody' -> do
                -- The tope checks below are subtyping checks with a fixed direction
                -- relative to (subtype, supertype). Which side is the subtype
                -- depends on the ambient variance: under Covariant the actual type
                -- must be a subtype of the expected one; under Contravariant (inside
                -- a domain) the roles are reversed. Invariant is normally handled
                -- upstream by running both directions; it is handled here as well
                -- for safety.
                variance <- asks ctxCovariance
                scope <- asks ctxScope
                let openTope = fmap (openWith scope (Foil.nameOf binder))
                    mtopeIn = openTope mtope
                    mtopeIn' = openTope mtope'
                case retBody' of
                  UniverseTopeT{} -> do
                    -- This is the case for tope families (shapes).
                    --
                    -- (Λ → TOPE) <: (Δ → TOPE) since if φ : Λ → TOPE then φ ⊢ Δ.
                    -- We DO NOT take the tope context Φ into account!
                    expectedTopeNF <- fromMaybe topeTopT <$> traverse nfT mtopeIn
                    actualTopeNF   <- fromMaybe topeTopT <$> traverse nfT mtopeIn'
                    let subEntailsSuper subNF superNF = do
                          entails <- [plainTope subNF] `entailM` superNF
                          unless (entails || containsHole subNF || containsHole superNF) $
                            issueTypeError (TypeErrorTopeNotSatisfied [subNF] superNF)
                    case variance of
                      Covariant     -> subEntailsSuper actualTopeNF expectedTopeNF
                      Contravariant -> subEntailsSuper expectedTopeNF actualTopeNF
                      Invariant     -> do
                        subEntailsSuper actualTopeNF expectedTopeNF
                        subEntailsSuper expectedTopeNF actualTopeNF
                  _ -> do
                    -- this is the case for Π-types and extension types
                    --
                    -- Ξ | Φ | Γ ⊢ {t : I | φ} → A t <: {s : J | ψ} → B s
                    -- when Ξ | Φ, ψ ⊢ φ
                    expectedTopeNF <- fromMaybe topeTopT <$> traverse nfT mtopeIn
                    actualTopeNF   <- fromMaybe topeTopT <$> traverse nfT mtopeIn'
                    let superEntailsSub superNF subNF =
                          localTope superNF $ contextEntails subNF
                    case variance of
                      Covariant     -> superEntailsSub expectedTopeNF actualTopeNF
                      Contravariant -> superEntailsSub actualTopeNF expectedTopeNF
                      Invariant     -> do
                        superEntailsSub expectedTopeNF actualTopeNF
                        superEntailsSub actualTopeNF expectedTopeNF
                case mterm of
                  Nothing -> unifyTerms retBody retBody'
                  Just term ->
                    unifyTypes
                      (appT retBody' (Foil.sink term) (Var (Foil.nameOf binder)))
                      retBody retBody'
            _ -> err

        TypeSigmaT _ty _orig md a b ->
          case actual' of
            TypeSigmaT _ty' orig' md' a' b' -> do
              when (md /= md') $
                issueTypeError (TypeErrorOther $ "modality mismatch in sigma type: expected " <> show md <> " but got " <> show md')
              unify Nothing a a'
              inScope2 orig' md a' b b' $ \_binder bBody bBody' ->
                unify Nothing bBody bBody'
            _ -> err

        TypeIdT _ty x tA y ->
          case actual' of
            TypeIdT _ty' x' tA' y' -> do
              -- The underlying types must be compared: without this check the
              -- routine equates identity types over different types whenever the
              -- endpoints unify, accepting a free homotopy (a path in the type of
              -- functions) where an endpoint-fixing one (a path in a hom-type) is
              -- expected. Compared invariantly: subtyping between the underlying
              -- types must not leak into equality of identity types over them.
              mapM_ (\(t1, t2) -> setVariance Invariant (unify Nothing t1 t2))
                ((,) <$> tA <*> tA')
              unify Nothing x x'
              unify Nothing y y'
            _ -> err

        AppT _ty f x ->
          case actual' of
            AppT _ty' f' x' -> do
              unify Nothing f f'
              setVariance Invariant $
                unify Nothing x x'
            _ -> err

        LambdaT ty _orig _mparam body ->
          case stripTypeRestrictions (infoType ty) of
            TypeFunT _ty _origF md param mtope _ret ->
              case actual' of
                LambdaT ty' orig' _mparam' body' ->
                  case stripTypeRestrictions (infoType ty') of
                    TypeFunT _ty' _origF' md' param' mtope' _ret' -> do
                      when (md /= md') $
                        issueTypeError (TypeErrorOther $ "modality mismatch in lambda: expected " <> show md <> " but got " <> show md')
                      unify Nothing param param' -- we (should) have already checked this in types!
                      inScope2 orig' md param body body' $ \binder bodyIn bodyIn' -> do
                        scope <- asks ctxScope
                        let openTope = fmap (openWith scope (Foil.nameOf binder))
                        case (openTope mtope, openTope mtope') of
                          (Just tope, Just tope') -> do
                            unify Nothing tope tope' -- we (should) have already checked this in types!
                            localTope tope $ unify Nothing bodyIn bodyIn'
                          (Nothing, Nothing) ->
                            unify Nothing bodyIn bodyIn'
                          _ -> errIn
                    _ -> err
                _ -> err
            _ -> err

        LetT{} -> panicImpossible "let at the root of WHNF"
        LetModT _ orig app inn _ val body ->
          case actual' of
            LetModT _ _ app' inn' _ val' body'
              | app == app', inn == inn' -> do
                unify Nothing val val'
                bty <- typeOf val >>= \case
                  TypeModalT _ _ t -> pure t
                  _ -> panicImpossible "not modal in letmod"
                inScope2 orig (comp app inn) bty body body' $ \_binder bodyIn bodyIn' ->
                  unify Nothing bodyIn bodyIn'
            _ -> err

        ReflT ty _x | TypeIdT _ty x _tA y <- infoType ty ->
          case actual' of
            ReflT ty' _x' | TypeIdT _ty' x' _tA' y' <- infoType ty' -> do
              unify Nothing x x'
              unify Nothing y y'
            _ -> err
        ReflT{} -> panicImpossible "refl with a non-identity type!"

        IdJT _ty a b c d e f ->
          case actual' of
            IdJT _ty' a' b' c' d' e' f' -> do
              unify Nothing a a'
              unify Nothing b b'
              unify Nothing c c'
              unify Nothing d d'
              unify Nothing e e'
              unify Nothing f f'
            _ -> err

        TypeAscT{} -> panicImpossible "type ascription at the root of WHNF"

        TypeRestrictedT _ty ty rs ->
          case actual' of
            TypeRestrictedT _ty' ty' rs' -> do
              unify mterm ty ty'
              -- The faces of the supertype must be covered by the faces of the
              -- subtype (the subtype is at least as specified), with the boundary
              -- terms agreeing on overlaps. Which side is the subtype depends on the
              -- ambient variance.
              variance <- asks ctxCovariance
              let subCoversSuper subRs superRs = sequence_
                    [ localTope tope $ do
                        -- FIXME: can do less entails checks?
                        contextEntails (foldr topeOrT topeBottomT (map fst subRs))
                        forM_ subRs $ \(tope', term') ->
                          localTope tope' $
                            unify Nothing term term'
                    | (tope, term) <- superRs
                    ]
              case variance of
                Covariant     -> subCoversSuper rs' rs
                Contravariant -> subCoversSuper rs rs'
                Invariant     -> do
                  subCoversSuper rs' rs
                  subCoversSuper rs rs'
            _ -> err    -- FIXME: need better unification for restrictions

        TypeModalT _ty m ty ->
          case actual' of
            TypeModalT _ty' m' ty' -> do
              when (m' /= m) err
              enterModality m $ unify Nothing ty ty'
            _ -> err
        ModAppT _ty m ty ->
          case actual' of
            ModAppT _ty' m' ty' -> do
              when (m' /= m) err
              enterModality m $ unify Nothing ty ty'
            _ -> err
        ModExtractT _ty app inn te ->
          case actual' of
            ModExtractT _ty' app' inn' te' -> do
              when (app' /= app) err
              when (inn' /= inn) err
              enterModality app $ unify Nothing te te'
            _ -> err

        -- defensive: a hole nested anywhere also defers here rather than panicking
        -- on an otherwise unexpected shape
        _ | holePresent -> return ()
        _ -> panicImpossible "unexpected term in UNIFY"