packages feed

swarm-0.7.0.0: src/swarm-lang/Swarm/Language/Kindcheck.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Kind checking + type name resolution for the Swarm language.
module Swarm.Language.Kindcheck (
  KindError (..),
  processPolytype,
  processType,
) where

import Control.Algebra (Has)
import Control.Effect.Reader (Reader, ask)
import Control.Effect.Throw (Throw, throwError)
import Control.Monad.Extra (unlessM)
import Data.Fix (Fix (..))
import Prettyprinter (hsep, nest, pretty, vsep, (<+>))
import Swarm.Language.Types
import Swarm.Pretty (PrettyPrec (..), ppr)
import Swarm.Util (number)
import Swarm.Util.Effect (withThrow)

------------------------------------------------------------
-- Type processing

-- | Process a polytype, by doing name resolution and kind checking,
--   and returning an appropriate 'TydefInfo' record to be used in the
--   case of a type definition.
processPolytype :: (Has (Reader TDCtx) sig m, Has (Throw KindError) sig m) => Polytype -> m TydefInfo
processPolytype pty@(unPoly -> (xs, _)) = do
  pty' <- traverse processType pty
  pure $ TydefInfo pty' (Arity $ length xs)

-- | Process a type by doing name resolution and kind checking.
processType :: (Has (Reader TDCtx) sig m, Has (Throw KindError) sig m) => Type -> m Type
processType ty = do
  ty' <- resolveTydefs ty
  checkKind ty'
  pure ty'

------------------------------------------------------------
-- Tydef name resolution

-- | Name resolution for user-defined type names: for each
--   user-defined type name found anywhere in the type, resolve it to
--   the correct version number depending on what is in scope, by
--   calling 'resolveUserTy'.
resolveTydefs :: Has (Reader TDCtx) sig m => Type -> m Type
resolveTydefs (Fix tyF) =
  Fix <$> case tyF of
    TyConF tc tys -> do
      tc' <- case tc of
        TCUser u -> TCUser <$> resolveUserTy u
        _ -> pure tc
      TyConF tc' <$> mapM resolveTydefs tys
    TyRcdF m -> TyRcdF <$> mapM resolveTydefs m
    TyRecF x t -> TyRecF x <$> resolveTydefs t
    TyVarF {} -> pure tyF
    TyRecVarF {} -> pure tyF

------------------------------------------------------------
-- Kind checking

-- | Kind checking errors that can occur.
data KindError
  = -- | A type constructor expects n arguments, but was given these
    --   arguments instead.
    ArityMismatch TyCon Int [Type]
  | -- | An undefined type constructor was encountered in the given type.
    UndefinedTyCon TyCon Type
  | -- | A trivial recursive type (one that does not use its bound
    --   variable) was encountered.
    TrivialRecTy Var Type
  | -- | A vacuous recursive type (one that expands immediately to
    --   itself) was encountered.
    VacuousRecTy Var Type
  deriving (Eq, Show)

instance PrettyPrec KindError where
  prettyPrec _ = \case
    ArityMismatch c a tys ->
      nest 2 . vsep $
        [ "Kind error:"
        , hsep
            [ ppr c
            , "requires"
            , pretty a
            , "type"
            , pretty (number a "argument" <> ",")
            , "but was given"
            , pretty (length tys)
            ]
        ]
          ++ ["in the type:" <+> ppr (TyConApp c tys) | not (null tys)]
    UndefinedTyCon tc _ty -> "Undefined type" <+> ppr tc
    TrivialRecTy x ty ->
      nest 2 . vsep $
        [ "Encountered trivial recursive type" <+> ppr (TyRec x ty)
        , "Did you forget to use" <+> ppr x <+> "in the body of the type?"
        ]
    VacuousRecTy x ty ->
      nest 2 . vsep $
        [ "Encountered vacuous recursive type" <+> ppr (TyRec x ty)
        , "Recursive types must be productive, i.e. must not expand to themselves."
        ]

-- | Check that a type is well-kinded. For now, we don't allow
--   higher-kinded types, *i.e.* all kinds will be of the form @Type
--   -> Type -> ... -> Type@ which can be represented by a number (the
--   arity); every type constructor must also be fully applied. So, we
--   only have to check that each type constructor is applied to the
--   correct number of type arguments.  In the future, we might very
--   well want to generalize to arbitrary higher kinds (e.g. @(Type ->
--   Type) -> Type@ etc.) which would require generalizing this
--   checking code a bit.
--
--   Here we also check that any recursive types are non-vacuous,
--   /i.e./ not of the form @rec t. t@, and non-trivial, /i.e./ the
--   variable bound by the @rec@ actually occurs somewhere in the
--   body.
checkKind :: (Has (Reader TDCtx) sig m, Has (Throw KindError) sig m) => Type -> m ()
checkKind ty@(Fix tyF) = case tyF of
  TyConF c tys -> do
    tdCtx <- ask
    case getArity <$> tcArity tdCtx c of
      Nothing -> throwError $ UndefinedTyCon c ty
      Just a -> case compare (length tys) a of
        EQ -> mapM_ checkKind tys
        _ -> throwError $ ArityMismatch c a tys
  TyVarF _ _ -> return ()
  TyRcdF m -> mapM_ checkKind m
  TyRecF x t -> do
    -- It's important to call checkKind first, to rule out undefined
    -- type constructors. Within the recursive kind check, we
    -- substitute the given variable name for the bound de Bruijn
    -- index 0 in the body.  This doesn't affect the checking but it
    -- does ensure that error messages will use the variable name and
    -- not de Bruijn indices.
    checkKind (substRec (TyVarF x x) t NZ)
    -- Now check that the recursive type is well-formed.  We call this
    -- with the *unsubstituted* t because the check will be looking
    -- for de Bruijn variables specifically.
    checkRecTy x t
  TyRecVarF _ -> return ()

-- | Check that the body of a recursive type actually contains the
--   bound variable at least once (otherwise there's no point in using
--   @rec@) and does not consist solely of that variable.
checkRecTy :: (Has (Reader TDCtx) sig m, Has (Throw KindError) sig m) => Var -> Type -> m ()
checkRecTy x ty = do
  unlessM (containsVar NZ ty) $ throwError (TrivialRecTy x ty)
  unlessM (nonVacuous NZ ty) $ throwError (VacuousRecTy x ty)

-- Note, in theory it would be more efficient to combine containsVar
-- and nonVacuous into a single check that walks over the type only
-- once, but we keep them separate just to simplify things.  This
-- won't make much difference in the grand scheme of things since
-- types are small.

-- | Check whether a type contains a specific bound recursive type
--   variable.
containsVar ::
  (Has (Reader TDCtx) sig m, Has (Throw KindError) sig m) =>
  Nat ->
  Type ->
  m Bool
containsVar i ty@(Fix tyF) = case tyF of
  TyRecVarF j -> pure (i == j)
  TyVarF {} -> pure False
  TyConF (TCUser u) tys -> do
    ty' <-
      withThrow
        (\(UnexpandedUserType _) -> UndefinedTyCon (TCUser u) ty)
        (expandTydef u tys)
    containsVar i ty'
  TyConF _ tys -> or <$> mapM (containsVar i) tys
  TyRcdF m -> or <$> mapM (containsVar i) m
  TyRecF _ ty' -> containsVar (NS i) ty'

-- | @nonVacuous ty@ checks that the recursive type @rec x. ty@ is
--   non-vacuous, /i.e./ that it doesn't look like @rec x. x@.  Put
--   another way, we make sure the recursive type is "productive" in
--   the sense that unfolding it will result in a well-defined
--   infinite type (as opposed to @rec x. x@ which just unfolds to
--   itself).  However, we can't just check whether it literally looks
--   like @rec x. x@ since we must also (1) expand type aliases and
--   (2) ignore additional intervening @rec@s.  For example, given
--   @tydef Id a = a@, the type @rec x. rec y. Id x@ is also vacuous.
nonVacuous ::
  (Has (Reader TDCtx) sig m, Has (Throw KindError) sig m) =>
  Nat ->
  Type ->
  m Bool
nonVacuous i ty@(Fix tyF) = case tyF of
  -- The type simply consists of a variable bound by some @rec@.
  -- Check if it's the variable we're currently looking for.
  TyRecVarF j -> pure (i /= j)
  -- Expand a user-defined type and keep looking.
  TyConF (TCUser u) tys -> do
    ty' <-
      withThrow
        (\(UnexpandedUserType _) -> UndefinedTyCon (TCUser u) ty)
        (expandTydef u tys)
    nonVacuous i ty'
  -- Increment the variable we're looking for when going under a @rec@
  -- binder.
  TyRecF _ ty' -> nonVacuous (NS i) ty'
  -- If we encounter any other kind of type constructor or record
  -- type, rejoice!
  TyConF {} -> pure True
  TyRcdF {} -> pure True
  -- This last case can't actully happen if we already checked that
  -- the recursive type actually contains its bound variable (with
  -- 'containsVar'), since it would correspond to something like @rec
  -- x. y@.  However, it's still correct to return True.
  TyVarF {} -> pure True