Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,9 @@ data DataConstructorDeclaration = DataConstructorDeclaration
, dataCtorFields :: ![(Ident, SourceType)]
} deriving (Show, Eq)

mapDataCtorFields :: ([(Ident, SourceType)] -> [(Ident, SourceType)]) -> DataConstructorDeclaration -> DataConstructorDeclaration
mapDataCtorFields f DataConstructorDeclaration{..} = DataConstructorDeclaration { dataCtorFields = f dataCtorFields, .. }

traverseDataCtorFields :: Monad m => ([(Ident, SourceType)] -> m [(Ident, SourceType)]) -> DataConstructorDeclaration -> m DataConstructorDeclaration
traverseDataCtorFields f DataConstructorDeclaration{..} = DataConstructorDeclaration dataCtorAnn dataCtorName <$> f dataCtorFields

Expand Down
20 changes: 12 additions & 8 deletions lib/purescript-ast/src/Language/PureScript/Environment.hs
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ primBooleanTypes =
primCoerceTypes :: M.Map (Qualified (ProperName 'TypeName)) (SourceType, TypeKind)
primCoerceTypes =
M.fromList $ mconcat
[ primClass (primSubName C.moduleCoerce "Coercible") (\kind -> kindType -:> kindType -:> kind)
[ primClass (primSubName C.moduleCoerce "Coercible") (\kind -> tyForall "k" kindType $ tyVar "k" -:> tyVar "k" -:> kind)
]

primOrderingTypes :: M.Map (Qualified (ProperName 'TypeName)) (SourceType, TypeKind)
Expand Down Expand Up @@ -485,9 +485,10 @@ allPrimClasses = M.unions
primCoerceClasses :: M.Map (Qualified (ProperName 'ClassName)) TypeClassData
primCoerceClasses =
M.fromList
-- class Coercible (a :: k) (b :: k)
[ (primSubName C.moduleCoerce "Coercible", makeTypeClassData
[ ("a", Just kindType)
, ("b", Just kindType)
[ ("a", Just (tyVar "k"))
, ("b", Just (tyVar "k"))
] [] [] [] True)
]

Expand Down Expand Up @@ -635,8 +636,11 @@ nominalRolesForKind :: Type a -> [Role]
nominalRolesForKind k = replicate (kindArity k) Nominal

kindArity :: Type a -> Int
kindArity = go 0 where
go n (TypeApp _ (TypeApp _ fn _) k)
| eqType fn tyFunction = go (n + 1) k
go n (ForAll _ _ _ k _) = go n k
go n _ = n
kindArity = length . fst . unapplyKinds

unapplyKinds :: Type a -> ([Type a], Type a)
unapplyKinds = go [] where
go kinds (TypeApp _ (TypeApp _ fn k1) k2)
| eqType fn tyFunction = go (k1 : kinds) k2
go kinds (ForAll _ _ _ k _) = go kinds k
go kinds k = (reverse kinds, k)
19 changes: 17 additions & 2 deletions src/Language/PureScript/Docs/Prim.hs
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ coercible = primClassOf (P.primSubName "Coerce") "Coercible" $ T.unlines
, ""
, "there is an instance:"
, ""
, " coercibleConstructor :: Coercible a a' => Coercible (D a b) (D a' b')"
, " instance coercibleConstructor :: Coercible a a' => Coercible (D a b) (D a' b')"
, ""
, "Note that, since the type variable `a` plays a role in `D`'s representation,"
, "we require that the types `a` and `a'` are themselves `Coercible`. However,"
Expand All @@ -408,7 +408,22 @@ coercible = primClassOf (P.primSubName "Coerce") "Coercible" $ T.unlines
, " instance coercibleNewtypeRight :: Coercible T b => Coercible NT b"
, ""
, "To prevent breaking abstractions, these instances are only usable if the"
, "constructor `MkNT` is in scope."
, "constructor `MkNT` is exported."
, ""
, "Fourth, every pair of unsaturated type constructors can be coerced if"
, "there is an instance for the fully saturated types. For example,"
, "given the definitions:"
, ""
, "newtype NT1 a = MkNT1 a"
, "newtype NT2 a b = MkNT2 b"
, ""
, "there is an instance:"
, ""
, " instance coercibleUnsaturedTypes :: Coercible (NT1 b) (NT2 a b) => Coercible NT1 (NT2 a)"
, ""
, "This rule may seem puzzling since it is impossible to apply `coerce` to a term"
, "of type `NT1` but it is necessary to coerce types with higher kinded parameters."
, ""
]

kindOrdering :: Declaration
Expand Down
147 changes: 82 additions & 65 deletions src/Language/PureScript/TypeChecker/Entailment.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@ module Language.PureScript.TypeChecker.Entailment
import Prelude.Compat
import Protolude (ordNub)

import Control.Applicative ((<|>))
import Control.Applicative ((<|>), empty)
import Control.Arrow (second, (&&&))
import Control.Monad.Error.Class (MonadError(..))
import Control.Monad.State
import Control.Monad.Supply.Class (MonadSupply(..))
import Control.Monad.Trans.Maybe (MaybeT(..))
import Control.Monad.Writer

import Data.Foldable (for_, fold, toList)
import Data.Foldable (for_, fold, foldl', toList)
import Data.Function (on)
import Data.Functor (($>))
import Data.List (minimumBy, groupBy, nubBy, sortBy)
import Data.List (minimumBy, groupBy, nubBy, sortBy, zipWith4)
import Data.Maybe (fromMaybe, mapMaybe)
import qualified Data.Map as M
import qualified Data.Set as S
Expand Down Expand Up @@ -168,12 +169,18 @@ entails
entails SolverOptions{..} constraint context hints =
solve constraint
where
forClassNameM :: Environment -> InstanceContext -> Qualified (ProperName 'ClassName) -> [SourceType] -> [SourceType] -> m [TypeClassDict]
forClassNameM env ctx cn@C.Coercible kinds args =
solveCoercible env kinds args >>=
pure . fromMaybe (forClassName env ctx cn kinds args)
forClassNameM env ctx cn kinds args =
pure $ forClassName env ctx cn kinds args

forClassName :: Environment -> InstanceContext -> Qualified (ProperName 'ClassName) -> [SourceType] -> [SourceType] -> [TypeClassDict]
forClassName _ ctx cn@C.Warn _ [msg] =
-- Prefer a warning dictionary in scope if there is one available.
-- This allows us to defer a warning by propagating the constraint.
findDicts ctx cn Nothing ++ [TypeClassDictionaryInScope [] 0 (WarnInstance msg) [] C.Warn [] [] [msg] Nothing]
forClassName env _ C.Coercible _ args | Just dicts <- solveCoercible env args = dicts
forClassName _ _ C.IsSymbol _ args | Just dicts <- solveIsSymbol args = dicts
forClassName _ _ C.SymbolCompare _ args | Just dicts <- solveSymbolCompare args = dicts
forClassName _ _ C.SymbolAppend _ args | Just dicts <- solveSymbolAppend args = dicts
Expand Down Expand Up @@ -225,10 +232,12 @@ entails SolverOptions{..} constraint context hints =
Nothing -> throwError . errorMessage $ UnknownClass className'
Just tcd -> pure tcd

dicts <- lift . lift $ forClassNameM env (combineContexts context inferred) className' kinds'' tys''

let instances = do
chain <- groupBy ((==) `on` tcdChain) $
sortBy (compare `on` (tcdChain &&& tcdIndex)) $
forClassName env (combineContexts context inferred) className' kinds'' tys''
dicts
-- process instances in a chain in index order
let found = for chain $ \tcd ->
-- Make sure the type unifies with the type in the type instance definition
Expand Down Expand Up @@ -381,80 +390,88 @@ entails SolverOptions{..} constraint context hints =
subclassDictionaryValue dict className index =
App (Accessor (mkString (superclassName className index)) dict) valUndefined

solveCoercible :: Environment -> [SourceType] -> Maybe [TypeClassDict]
solveCoercible env [a, b] = do
let tySynMap = typeSynonyms env
kindMap = types env
replaceTySyns = either (const Nothing) Just . replaceAllTypeSynonymsM tySynMap kindMap
a' <- replaceTySyns a
b' <- replaceTySyns b
solveCoercible :: Environment -> [SourceType] -> [SourceType] -> m (Maybe [TypeClassDict])
solveCoercible env kinds [a, b] = runMaybeT $ do
let kindOf = lift . (sequence . (id &&& elaborateKind)) <=< replaceAllTypeSynonyms
(a', kind) <- kindOf a
(b', kind') <- kindOf b
lift $ unifyKinds kind kind'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure this is a gap in my own understanding, but it feels a little bit strange to me that we aren't capturing any information as a result of unifying kinds here. Is it possible that we learn something about either kind or kind' that we didn't know until we reached this line? If so, should that information be carried forward?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unifyKinds returns a (MonadError MultipleErrors m, MonadState CheckState m) => m () so I’m not sure how to extract any information from a successful unification 🤔

If unifying kinds extends the current substitution perhaps we should apply it to the kinds we inferred and then refer to [kind, kind'] rather than kinds in the returned type class dictionary? Or perhaps we should rather apply it to the types rewritten by kindOf earlier? I’m completely out of my depth here 😅

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I’m a bit out of my depth too. @natefaubion I’d be interested to hear your thoughts on this if you have a moment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unifyKinds does extend the substitution and at some point it must be applied, yes. I believe it's always applied at the top of the solver loop.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think that suggests that it might be worth exporting apply from TypeChecker.Kinds to use here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I suppose we should resolve the other discussion (#3893 (comment)) first.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applySubstitution as is used everywhere else is fine. I think there's only one in the kind-checker for convenience or module dependencies or something.

-- Solving terminates when the two arguments are the same. Since we
-- currently don't support higher-rank arguments in instance heads, term
-- equality is a sufficient notion of "the same".
if a' == b'
then pure [TypeClassDictionaryInScope [] 0 EmptyClassInstance [] C.Coercible [] [] [a, b] Nothing]
then pure [TypeClassDictionaryInScope [] 0 EmptyClassInstance [] C.Coercible [] kinds [a, b] Nothing]
else do
-- When solving must reduce and recurse, it doesn't matter whether we
-- reduce the first or second argument -- if the constraint is
-- solvable, either path will yield the same outcome. Consequently we
-- just try the first argument first and the second argument second.
ws <- coercibleWanteds env a' b' <|> coercibleWanteds env b' a'
pure [TypeClassDictionaryInScope [] 0 EmptyClassInstance [] C.Coercible [] [] [a, b] (Just ws)]
solveCoercible _ _ = Nothing
ws <- (MaybeT $ coercibleWanteds env a' b') <|> (MaybeT $ coercibleWanteds env b' a')
pure [TypeClassDictionaryInScope [] 0 EmptyClassInstance [] C.Coercible [] kinds [a, b] (Just ws)]
solveCoercible _ _ _ = pure Nothing

-- | Take two types, @a@ and @b@ representing a desired constraint
-- @Coercible a b@ and reduce them to a set of simpler wanted constraints
-- whose satisfaction will yield the goal.
coercibleWanteds :: Environment -> SourceType -> SourceType -> Maybe [SourceConstraint]
coercibleWanteds env a b = case a of
TypeConstructor _ tyName -> do
-- If the first argument is a plain newtype (e.g. @newtype T = T U@ and
-- the constraint @Coercible T b@), look up the type of its wrapped
-- field and yield a new wanted constraint in terms of that type
-- (@Coercible U b@ in the example).
(_, wrappedTy, _) <- lookupNewtypeConstructor env tyName
pure [Constraint nullSourceAnn C.Coercible [] [wrappedTy, b] Nothing]
Comment on lines -410 to -416
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took the liberty to simplify coercibleWanteds by removing the case expression because this first case is a simplification of when the newtype has parameters.

t
| (TypeConstructor _ aTyName, _, axs) <- unapplyTypes a
, (TypeConstructor _ bTyName, _, bxs) <- unapplyTypes b
, not (null axs) && not (null bxs) && aTyName == bTyName -> do
-- If both arguments are applications of the same type constructor
-- (e.g. @data D a b = D a@ in the constraint
-- @Coercible (D a b) (D a' b')@), infer the roles of the type
-- constructor's arguments and generate wanted constraints
-- appropriately (e.g. here @a@ is representational and @b@ is
-- phantom, yielding @Coercible a a'@).
let tyRoles = lookupRoles env aTyName
k role ax bx = case role of
Nominal
-- If we had first-class equality constraints, we'd just
-- emit one of the form @(a ~ b)@ here and let the solver
-- recurse. Since we don't we must compare the types at
-- this point and fail if they don't match. This likely
-- means there are cases we should be able to handle that
-- we currently can't, but is at least sound.
| ax == bx ->
Just []
| otherwise ->
Nothing
Representational ->
Just [Constraint nullSourceAnn C.Coercible [] [ax, bx] Nothing]
Phantom ->
Just []
fmap concat $ sequence $ zipWith3 k tyRoles axs bxs
| (TypeConstructor _ tyName, _, xs) <- unapplyTypes t
, not $ null xs
, Just (tvs, wrappedTy, _) <- lookupNewtypeConstructor env tyName -> do
-- If the first argument is a newtype applied to some other types
-- (e.g. @newtype T a = T a@ in @Coercible (T X) b@), look up the
-- type of its wrapped field and yield a new wanted constraint in
-- terms of that type with the type arguments substituted in (e.g.
-- @Coercible (T[X/a]) b = Coercible X b@ in the example).
let wrappedTySub = replaceAllTypeVars (zip tvs xs) wrappedTy
pure [Constraint nullSourceAnn C.Coercible [] [wrappedTySub, b] Nothing]
_ ->
coercibleWanteds :: Environment -> SourceType -> SourceType -> m (Maybe [SourceConstraint])
coercibleWanteds env a b
| (TypeConstructor _ aTyName, _, axs) <- unapplyTypes a
, (aTyKind, _) <- fromMaybe (internalError "coercibleWanteds: type lookup failed") $ M.lookup aTyName (types env)
, (aks, kind) <- unapplyKinds aTyKind
, length axs < length aks = do
-- If both arguments have kind @k1 -> k2@ (e.g. @data D a b = D a@
-- in the constraint @Coercible (D a) (D a')@), yield a new wanted
-- constraint in terms of the types saturated with the same variables
-- (e.g. @Coercible (D a t0) (D a' t0)@ in the exemple).
tys <- traverse freshTypeWithKind $ drop (length axs) aks
let a' = foldl' srcTypeApp a tys
b' = foldl' srcTypeApp b tys
pure $ Just [srcCoercibleConstraint kind a' b']
| (TypeConstructor _ aTyName, _, axs) <- unapplyTypes a
, (TypeConstructor _ bTyName, _, bxs) <- unapplyTypes b
, not (null axs) && aTyName == bTyName
, (aTyKind, _) <- fromMaybe (internalError "coercibleWanteds: type lookup failed") $ M.lookup aTyName (types env)
= runMaybeT $ do
-- If both arguments are applications of the same type constructor
-- (e.g. @data D a b = D a@ in the constraint
-- @Coercible (D a b) (D a' b')@), infer the roles of the type
-- constructor's arguments and generate wanted constraints
-- appropriately (e.g. here @a@ is representational and @b@ is
-- phantom, yielding @Coercible a a'@).
let roles = lookupRoles env aTyName
kinds = fst $ unapplyKinds aTyKind
f role kx ax bx = case role of
Nominal
-- If we had first-class equality constraints, we'd just
-- emit one of the form @(a ~ b)@ here and let the solver
-- recurse. Since we don't we must compare the types at
-- this point and fail if they don't match. This likely
-- means there are cases we should be able to handle that
-- we currently can't, but is at least sound.
| ax == bx ->
pure []
| otherwise ->
empty
Representational ->
pure [srcCoercibleConstraint kx ax bx]
Phantom ->
pure []
fmap concat $ sequence $ zipWith4 f roles kinds axs bxs
| (TypeConstructor _ tyName, _, xs) <- unapplyTypes a
, Just (tvs, wrappedTy, _) <- lookupNewtypeConstructor env tyName = do
-- If the first argument is a newtype applied to some other types
-- (e.g. @newtype T a = T a@ in @Coercible (T X) b@), look up the
-- type of its wrapped field and yield a new wanted constraint in
-- terms of that type with the type arguments substituted in (e.g.
-- @Coercible (T[X/a]) b = Coercible X b@ in the example).
let wrappedTySub = replaceAllTypeVars (zip tvs xs) wrappedTy
pure $ Just [srcCoercibleConstraint kindType wrappedTySub b]
| otherwise =
-- In all other cases we can't solve the constraint.
Nothing
pure Nothing

srcCoercibleConstraint :: SourceType -> SourceType -> SourceType -> SourceConstraint
srcCoercibleConstraint k a b = srcConstraint C.Coercible [k] [a, b] Nothing

solveIsSymbol :: [SourceType] -> Maybe [TypeClassDict]
solveIsSymbol [TypeLevelString ann sym] = Just [TypeClassDictionaryInScope [] 0 (IsSymbolInstance sym) [] C.IsSymbol [] [] [TypeLevelString ann sym] Nothing]
Expand Down
16 changes: 10 additions & 6 deletions src/Language/PureScript/TypeChecker/Kinds.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module Language.PureScript.TypeChecker.Kinds

import Prelude.Compat

import Control.Arrow ((***))
import Control.Monad
import Control.Monad.Error.Class (MonadError(..))
import Control.Monad.State
Expand Down Expand Up @@ -602,15 +603,17 @@ inferDataDeclaration moduleName (ann, tyName, tyArgs, ctors) = do
tyCtor' = foldl (\ty -> srcTypeApp ty . srcTypeVar . fst) tyCtor tyArgs'
ctorBinders = fmap (fmap (fmap Just)) $ sigBinders <> fmap (nullSourceAnn,) tyArgs'
for ctors $ \ctor ->
fmap ((ctor,) . mkForAll ctorBinders) $ inferDataConstructor tyCtor' ctor
fmap (mkForAll ctorBinders) <$> inferDataConstructor tyCtor' ctor

inferDataConstructor
:: forall m. (MonadError MultipleErrors m, MonadState CheckState m)
=> SourceType
-> DataConstructorDeclaration
-> m SourceType
inferDataConstructor tyCtor =
flip checkKind E.kindType . foldr ((E.-:>) . snd) tyCtor . dataCtorFields
-> m (DataConstructorDeclaration, SourceType)
inferDataConstructor tyCtor DataConstructorDeclaration{..} = do
dataCtorFields' <- traverse (traverse (flip checkKind E.kindType)) dataCtorFields
dataCtor <- flip (foldr ((E.-:>) . snd)) dataCtorFields' <$> checkKind tyCtor E.kindType
pure ( DataConstructorDeclaration { dataCtorFields = dataCtorFields', .. }, dataCtor )

type TypeDeclarationArgs =
( SourceAnn
Expand Down Expand Up @@ -914,7 +917,7 @@ kindsOfAll moduleName syns dats clss = withFreshSubstitution $ do
pure (((synName, synKind'), synBody'), unknowns synKind')
datResultsWithUnks <- for (zip datDict datResults) $ \((datName, datKind), ctors) -> do
datKind' <- apply datKind
ctors' <- traverse (traverse apply) ctors
ctors' <- traverse (bitraverse (traverseDataCtorFields (traverse (traverse apply))) apply) ctors
pure (((datName, datKind'), ctors'), unknowns datKind')
clsResultsWithUnks <- for (zip clsDict clsResults) $ \((clsName, clsKind), (args, supers, decls)) -> do
clsKind' <- apply clsKind
Expand Down Expand Up @@ -951,7 +954,8 @@ kindsOfAll moduleName syns dats clss = withFreshSubstitution $ do
(args', supers', decls', generalizeUnknownsWithVars unkBinders clsKind)
datResultsWithKinds <- for datResultsWithUnks $ \(((datName, datKind), ctors), _) -> do
let tyUnks = snd . fromJust $ lookup (mkQualified datName moduleName) tySubs
ctors' = fmap (fmap (generalizeUnknowns tyUnks . replaceTypeCtors)) ctors
replaceDataCtorField ty = replaceUnknownsWithVars (unknownVarNames (usedTypeVariables ty) tyUnks) $ replaceTypeCtors ty
ctors' = fmap (mapDataCtorFields (fmap (fmap replaceDataCtorField)) *** generalizeUnknowns tyUnks . replaceTypeCtors) ctors
traverse_ (traverse_ checkTypeQuantification) ctors'
pure (ctors', generalizeUnknowns tyUnks datKind)
synResultsWithKinds <- for synResultsWithUnks $ \(((synName, synKind), synBody), _) -> do
Expand Down
1 change: 0 additions & 1 deletion tests/purs/failing/3275-DataBindingGroupErrorPos.out
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ at tests/purs/failing/3275-DataBindingGroupErrorPos.purs:7:19 - 7:22 (line 7, co
while checking that type Bar a
has kind t0 -> t1
while inferring the kind of Bar a a
while inferring the kind of Bar a a -> Foo a
in data binding group Bar, Foo

where t0 is an unknown type
Expand Down
2 changes: 1 addition & 1 deletion tests/purs/failing/CoercibleForeign.out
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ at tests/purs/failing/CoercibleForeign.purs:11:20 - 11:26 (line 11, column 20 -
 (Foreign (Id a0) (Id b1))
 

while checking that type forall (a :: Type) (b :: Type). Coercible a b => a -> b
while checking that type forall (a :: Type) (b :: Type). Coercible @Type a b => a -> b
is at least as general as type Foreign a0 b1 -> Foreign (Id a0) (Id b1)
while checking that expression coerce
has type Foreign a0 b1 -> Foreign (Id a0) (Id b1)
Expand Down
2 changes: 1 addition & 1 deletion tests/purs/failing/CoercibleForeign2.out
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ at tests/purs/failing/CoercibleForeign2.purs:9:20 - 9:26 (line 9, column 20 - li
 (Foreign a0 b1 d3)
 

while checking that type forall (a :: Type) (b :: Type). Coercible a b => a -> b
while checking that type forall (a :: Type) (b :: Type). Coercible @Type a b => a -> b
is at least as general as type Foreign a0 b1 c2 -> Foreign a0 b1 d3
while checking that expression coerce
has type Foreign a0 b1 c2 -> Foreign a0 b1 d3
Expand Down
Loading