purescript-0.15.15: src/Language/PureScript/TypeChecker/Subsumption.hs
{-# LANGUAGE GADTs #-}
-- | Subsumption checking
module Language.PureScript.TypeChecker.Subsumption
( subsumes
) where
import Prelude
import Control.Monad (when)
import Control.Monad.Error.Class (MonadError(..))
import Control.Monad.State.Class (MonadState(..))
import Data.Foldable (for_)
import Data.List (uncons)
import Data.List.Ordered (minusBy')
import Data.Ord (comparing)
import Language.PureScript.AST (ErrorMessageHint(..), Expr(..), pattern NullSourceAnn)
import Language.PureScript.Crash (internalError)
import Language.PureScript.Environment (tyFunction, tyRecord)
import Language.PureScript.Errors (MultipleErrors, SimpleErrorMessage(..), errorMessage, internalCompilerError)
import Language.PureScript.TypeChecker.Monad (CheckState, getHints, getTypeClassDictionaries, withErrorMessageHint)
import Language.PureScript.TypeChecker.Skolems (newSkolemConstant, skolemize)
import Language.PureScript.TypeChecker.Unify (alignRowsWith, freshTypeWithKind, unifyTypes)
import Language.PureScript.Types (RowListItem(..), SourceType, Type(..), eqType, isREmpty, replaceTypeVars, rowFromList)
-- | Subsumption can operate in two modes:
--
-- * Elaboration mode, in which we try to insert type class dictionaries
-- * No-elaboration mode, in which we do not insert dictionaries
--
-- Some subsumption rules apply in both modes, and others are specific to
-- certain modes.
--
-- The subsumption algorithm follows the structure of the types in question,
-- and we can switch into no-elaboration mode when we move under a type
-- constructor where we can no longer insert dictionaries, e.g. into the fields
-- of a record.
data Mode = Elaborate | NoElaborate
-- | Value-level proxies for the two modes
data ModeSing (mode :: Mode) where
SElaborate :: ModeSing 'Elaborate
SNoElaborate :: ModeSing 'NoElaborate
-- | This type family tracks what evidence we return from 'subsumes' for each
-- mode.
type family Coercion (mode :: Mode) where
-- When elaborating, we generate a coercion
Coercion 'Elaborate = Expr -> Expr
-- When we're not elaborating, we don't generate coercions
Coercion 'NoElaborate = ()
-- | The default coercion for each mode.
defaultCoercion :: ModeSing mode -> Coercion mode
defaultCoercion SElaborate = id
defaultCoercion SNoElaborate = ()
-- | Check that one type subsumes another, rethrowing errors to provide a better error message
subsumes
:: (MonadError MultipleErrors m, MonadState CheckState m)
=> SourceType
-> SourceType
-> m (Expr -> Expr)
subsumes ty1 ty2 =
withErrorMessageHint (ErrorInSubsumption ty1 ty2) $
subsumes' SElaborate ty1 ty2
-- | Check that one type subsumes another
subsumes'
:: (MonadError MultipleErrors m, MonadState CheckState m)
=> ModeSing mode
-> SourceType
-> SourceType
-> m (Coercion mode)
subsumes' mode (ForAll _ _ ident mbK ty1 _) ty2 = do
u <- maybe (internalCompilerError "Unelaborated forall") freshTypeWithKind mbK
let replaced = replaceTypeVars ident u ty1
subsumes' mode replaced ty2
subsumes' mode ty1 (ForAll _ _ ident mbK ty2 sco) =
case sco of
Just sco' -> do
sko <- newSkolemConstant
let sk = skolemize NullSourceAnn ident mbK sko sco' ty2
subsumes' mode ty1 sk
Nothing -> internalError "subsumes: unspecified skolem scope"
subsumes' mode (TypeApp _ (TypeApp _ f1 arg1) ret1) (TypeApp _ (TypeApp _ f2 arg2) ret2) | eqType f1 tyFunction && eqType f2 tyFunction = do
subsumes' SNoElaborate arg2 arg1
subsumes' SNoElaborate ret1 ret2
-- Nothing was elaborated, return the default coercion
return (defaultCoercion mode)
subsumes' mode (KindedType _ ty1 _) ty2 =
subsumes' mode ty1 ty2
subsumes' mode ty1 (KindedType _ ty2 _) =
subsumes' mode ty1 ty2
-- Only check subsumption for constrained types when elaborating.
-- Otherwise fall back to unification.
subsumes' SElaborate (ConstrainedType _ con ty1) ty2 = do
dicts <- getTypeClassDictionaries
hints <- getHints
elaborate <- subsumes' SElaborate ty1 ty2
let addDicts val = App val (TypeClassDictionary con dicts hints)
return (elaborate . addDicts)
subsumes' mode (TypeApp _ f1 r1) (TypeApp _ f2 r2) | eqType f1 tyRecord && eqType f2 tyRecord = do
let goWithLabel l t1 t2 = withErrorMessageHint (ErrorInRowLabel l) $ subsumes' SNoElaborate t1 t2
let (common, ((ts1', r1'), (ts2', r2'))) = alignRowsWith goWithLabel r1 r2
-- For { ts1 | r1 } to subsume { ts2 | r2 } when r1 is empty (= we're working with a closed row),
-- every property in ts2 must appear in ts1. If not, then the candidate expression is missing a required property.
-- Conversely, when r2 is empty, every property in ts1 must appear in ts2, or else the expression has
-- an additional property which is not allowed.
when (isREmpty r1')
(for_ (firstMissingProp ts2' ts1') (throwError . errorMessage . PropertyIsMissing . rowListLabel))
when (isREmpty r2')
(for_ (firstMissingProp ts1' ts2') (throwError . errorMessage . AdditionalProperty . rowListLabel))
-- Check subsumption for common labels
sequence_ common
-- Inject the info here
unifyTypes (rowFromList (ts1', r1')) (rowFromList (ts2', r2'))
-- Nothing was elaborated, return the default coercion
return (defaultCoercion mode)
where
-- Find the first property that's in the first list (of tuples) but not in the second
firstMissingProp t1 t2 = fst <$> uncons (minusBy' (comparing rowListLabel) t1 t2)
subsumes' mode ty1 ty2@(TypeApp _ obj _) | obj == tyRecord =
subsumes' mode ty2 ty1
subsumes' mode ty1 ty2 = do
unifyTypes ty1 ty2
-- Nothing was elaborated, return the default coercion
return (defaultCoercion mode)