-
Notifications
You must be signed in to change notification settings - Fork 571
Saturate higher kinded types in Coercible constraints
#3893
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
560a1b7
Saturate higher kinded types in `Coercible` constraints
kl0tl e4c5304
Apply the polymorphic kind of `Coercible` constraints when solving them
kl0tl a465784
Forbid heterogenously kinded `Coercible` constraints arguments
kl0tl d2405fe
Don’t rewrite thrown away types when unifying `Coercible` arguments k…
kl0tl 8552334
Compare `Coercible` arguments rewritten with their inferred kinds
kl0tl d9b8da8
Throw on failed type lookups when solving `Coercible` constraints
kl0tl cf6d84e
Remove redundant checks from `coercibleWanteds` guards
kl0tl eea4f60
Support `Coercible` constraints on unsaturated type constructors with…
kl0tl 7e8c171
Rewrite constructor fields when checking the kind of their data type
kl0tl e1b50bc
Fix CoercibleKindMismatch golden test
kl0tl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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' | ||
| -- 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I took the liberty to simplify |
||
| 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] | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
kindorkind'that we didn't know until we reached this line? If so, should that information be carried forward?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unifyKindsreturns 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 thankindsin the returned type class dictionary? Or perhaps we should rather apply it to the types rewritten bykindOfearlier? I’m completely out of my depth here 😅There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unifyKindsdoes 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.There was a problem hiding this comment.
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
applyfrom TypeChecker.Kinds to use here?There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
applySubstitutionas is used everywhere else is fine. I think there's only one in the kind-checker for convenience or module dependencies or something.