From cdac487bcd9928d77738f6e79ead7b9bb4bc00fd Mon Sep 17 00:00:00 2001 From: Simon Peyton Jones <simonpj@microsoft.com> Date: Fri, 7 Mar 2014 16:45:55 +0000 Subject: [PATCH] Make -XDeriveFunctor more generous about non-last arguments (Trac #8678) When deriving Functor, Foldable, Traversable, we need only look at the way that the last type argument is treated. It's fine for there to be existentials etc, provided they don't affect the last type argument. See Note [Check that the type variable is truly universal] in TcDeriv. --- compiler/typecheck/TcDeriv.lhs | 88 ++++++++++++++----- compiler/typecheck/TcGenDeriv.lhs | 8 +- docs/users_guide/glasgow_exts.xml | 7 ++ .../tests/deriving/should_compile/T8678.hs | 12 +++ testsuite/tests/deriving/should_compile/all.T | 3 +- .../tests/deriving/should_fail/T3101.stderr | 2 +- .../tests/generics/GenCannotDoRep0_0.stderr | 3 +- .../tests/generics/GenCannotDoRep1_0.stderr | 3 +- .../typecheck/should_fail/tcfail086.stderr | 2 +- 9 files changed, 97 insertions(+), 31 deletions(-) create mode 100644 testsuite/tests/deriving/should_compile/T8678.hs diff --git a/compiler/typecheck/TcDeriv.lhs b/compiler/typecheck/TcDeriv.lhs index db8505c92931..02c0c098055e 100644 --- a/compiler/typecheck/TcDeriv.lhs +++ b/compiler/typecheck/TcDeriv.lhs @@ -1060,7 +1060,7 @@ inferConstraints cls inst_tys rep_tc rep_tc_args | data_con <- tyConDataCons rep_tc, (arg_n, arg_ty) <- ASSERT( isVanillaDataCon data_con ) - zip [1..] $ + zip [1..] $ -- ASSERT is precondition of dataConInstOrigArgTys get_constrained_tys $ dataConInstOrigArgTys data_con all_rep_tc_args, not (isUnLiftedType arg_ty) ] @@ -1171,21 +1171,30 @@ sideConditions mtheta cls | cls_key == ixClassKey = Just (cond_std `andCond` cond_enumOrProduct cls) | cls_key == boundedClassKey = Just (cond_std `andCond` cond_enumOrProduct cls) | cls_key == dataClassKey = Just (checkFlag Opt_DeriveDataTypeable `andCond` - cond_std `andCond` cond_args cls) + cond_std `andCond` + cond_args cls) | cls_key == functorClassKey = Just (checkFlag Opt_DeriveFunctor `andCond` - cond_functorOK True) -- NB: no cond_std! + cond_vanilla `andCond` + cond_functorOK True) | cls_key == foldableClassKey = Just (checkFlag Opt_DeriveFoldable `andCond` + cond_vanilla `andCond` cond_functorOK False) -- Functor/Fold/Trav works ok for rank-n types | cls_key == traversableClassKey = Just (checkFlag Opt_DeriveTraversable `andCond` + cond_vanilla `andCond` cond_functorOK False) - | cls_key == genClassKey = Just (cond_RepresentableOk `andCond` - checkFlag Opt_DeriveGeneric) - | cls_key == gen1ClassKey = Just (cond_Representable1Ok `andCond` - checkFlag Opt_DeriveGeneric) + | cls_key == genClassKey = Just (checkFlag Opt_DeriveGeneric `andCond` + cond_vanilla `andCond` + cond_RepresentableOk) + | cls_key == gen1ClassKey = Just (checkFlag Opt_DeriveGeneric `andCond` + cond_vanilla `andCond` + cond_Representable1Ok) | otherwise = Nothing where cls_key = getUnique cls - cond_std = cond_stdOK mtheta + cond_std = cond_stdOK mtheta False -- Vanilla data constructors, at least one, + -- and monotype arguments + cond_vanilla = cond_stdOK mtheta True -- Vanilla data constructors but + -- allow no data cons or polytype arguments type Condition = (DynFlags, TyCon, [Type]) -> Maybe SDoc -- first Bool is whether or not we are allowed to derive Data and Typeable @@ -1208,13 +1217,18 @@ andCond c1 c2 tc = case c1 tc of Nothing -> c2 tc -- c1 succeeds Just x -> Just x -- c1 fails -cond_stdOK :: DerivContext -> Condition -cond_stdOK (Just _) _ +cond_stdOK :: DerivContext -- Says whether this is standalone deriving or not; + -- if standalone, we just say "yes, go for it" + -> Bool -- True <=> permissive: allow higher rank + -- args and no data constructors + -> Condition +cond_stdOK (Just _) _ _ = Nothing -- Don't check these conservative conditions for -- standalone deriving; just generate the code -- and let the typechecker handle the result -cond_stdOK Nothing (_, rep_tc, _) - | null data_cons = Just (no_cons_why rep_tc $$ suggestion) +cond_stdOK Nothing permissive (_, rep_tc, _) + | null data_cons + , not permissive = Just (no_cons_why rep_tc $$ suggestion) | not (null con_whys) = Just (vcat con_whys $$ suggestion) | otherwise = Nothing where @@ -1224,9 +1238,12 @@ cond_stdOK Nothing (_, rep_tc, _) check_con :: DataCon -> Maybe SDoc check_con con - | isVanillaDataCon con - , all isTauTy (dataConOrigArgTys con) = Nothing - | otherwise = Just (badCon con (ptext (sLit "must have a Haskell-98 type"))) + | not (isVanillaDataCon con) + = Just (badCon con (ptext (sLit "has existentials or constraints in its type"))) + | not (permissive || all isTauTy (dataConOrigArgTys con)) + = Just (badCon con (ptext (sLit "has a higher-rank type"))) + | otherwise + = Nothing no_cons_why :: TyCon -> SDoc no_cons_why rep_tc = quotes (pprSourceTyCon rep_tc) <+> @@ -1244,7 +1261,7 @@ cond_enumOrProduct cls = cond_isEnumeration `orCond` cond_args :: Class -> Condition -- For some classes (eg Eq, Ord) we allow unlifted arg types --- by generating specilaised code. For others (eg Data) we don't. +-- by generating specialised code. For others (eg Data) we don't. cond_args cls (_, tc, _) = case bad_args of [] -> Nothing @@ -1342,11 +1359,16 @@ cond_functorOK allowFunctions (_, rep_tc, _) is_bad pred = last_tv `elemVarSet` tyVarsOfType pred data_cons = tyConDataCons rep_tc - check_con con = msum (check_vanilla con : foldDataConArgs (ft_check con) con) - - check_vanilla :: DataCon -> Maybe SDoc - check_vanilla con | isVanillaDataCon con = Nothing - | otherwise = Just (badCon con existential) + check_con con = msum (check_universal con : foldDataConArgs (ft_check con) con) + + check_universal :: DataCon -> Maybe SDoc + check_universal con + | Just tv <- getTyVar_maybe (last (tyConAppArgs (dataConOrigResTy con))) + , tv `elem` dataConUnivTyVars con + , not (tv `elemVarSet` tyVarsOfTypes (dataConTheta con)) + = Nothing -- See Note [Check that the type variable is truly universal] + | otherwise + = Just (badCon con existential) ft_check :: DataCon -> FFoldType (Maybe SDoc) ft_check con = FT { ft_triv = Nothing, ft_var = Nothing @@ -1358,7 +1380,7 @@ cond_functorOK allowFunctions (_, rep_tc, _) , ft_bad_app = Just (badCon con wrong_arg) , ft_forall = \_ x -> x } - existential = ptext (sLit "must not have existential arguments") + existential = ptext (sLit "must be truly polymorphic in the last argument of the data type") covariant = ptext (sLit "must not use the type variable in a function argument") functions = ptext (sLit "must not contain function types") wrong_arg = ptext (sLit "must use the type variable only as the last argument of a data type") @@ -1420,6 +1442,28 @@ badCon :: DataCon -> SDoc -> SDoc badCon con msg = ptext (sLit "Constructor") <+> quotes (ppr con) <+> msg \end{code} +Note [Check that the type variable is truly universal] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +For Functor, Foldable, Traversable, we must check that the *last argument* +of the type constructor is used truly universally. Example + + data T a b where + T1 :: a -> b -> T a b -- Fine! Vanilla H-98 + T2 :: b -> c -> T a b -- Fine! Existential c, but we can still map over 'b' + T3 :: b -> T Int b -- Fine! Constraint 'a', but 'b' is still polymorphic + T4 :: Ord b => b -> T a b -- No! 'b' is constrained + T5 :: b -> T b b -- No! 'b' is constrained + T6 :: T a (b,b) -- No! 'b' is constrained + +Notice that only the first of these constructors is vanilla H-98. We only +need to take care about the last argument (b in this case). See Trac #8678. +Eg. for T1-T3 we can write + + fmap f (T1 a b) = T1 a (f b) + fmap f (T2 b c) = T2 (f b) c + fmap f (T3 x) = T3 (f x) + + Note [Superclasses of derived instance] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In general, a derived instance decl needs the superclasses of the derived diff --git a/compiler/typecheck/TcGenDeriv.lhs b/compiler/typecheck/TcGenDeriv.lhs index c8b203e8cc90..581cebc9c444 100644 --- a/compiler/typecheck/TcGenDeriv.lhs +++ b/compiler/typecheck/TcGenDeriv.lhs @@ -1714,10 +1714,10 @@ foldDataConArgs :: FFoldType a -> DataCon -> [a] foldDataConArgs ft con = map (functorLikeTraverse tv ft) (dataConOrigArgTys con) where - tv = last (dataConUnivTyVars con) - -- Argument to derive for, 'a in the above description - -- The validity checks have ensured that con is - -- a vanilla data constructor + Just tv = getTyVar_maybe (last (tyConAppArgs (dataConOrigResTy con))) + -- Argument to derive for, 'a in the above description + -- The validity and kind checks have ensured that + -- the Just will match and a::* -- Make a HsLam using a fresh variable from a State monad mkSimpleLam :: (LHsExpr id -> State [id] (LHsExpr id)) -> State [id] (LHsExpr id) diff --git a/docs/users_guide/glasgow_exts.xml b/docs/users_guide/glasgow_exts.xml index 9910d2bd4eb1..dc1fbb5a9ced 100644 --- a/docs/users_guide/glasgow_exts.xml +++ b/docs/users_guide/glasgow_exts.xml @@ -3803,6 +3803,13 @@ data type declaration for <literal>T</literal>, because <literal>T</literal> is a GADT, but you <emphasis>can</emphasis> generate the instance declaration using stand-alone deriving. </para> +<para> +The down-side is that, +if the boilerplate code fails to typecheck, you will get an error message about that +code, which you did not write. Whereas, with a <literal>deriving</literal> clause +the side-conditions are necessarily more conservative, but any error message +may be more comprehensible. +</para> </listitem> <listitem> diff --git a/testsuite/tests/deriving/should_compile/T8678.hs b/testsuite/tests/deriving/should_compile/T8678.hs new file mode 100644 index 000000000000..655f530b5b37 --- /dev/null +++ b/testsuite/tests/deriving/should_compile/T8678.hs @@ -0,0 +1,12 @@ +{-# LANGUAGE DataKinds, DeriveFunctor, FlexibleInstances, GADTs, KindSignatures, StandaloneDeriving #-} +module T8678 where + +data {- kind -} Nat = Z | S Nat + +-- GADT in parameter other than the last +data NonStandard :: Nat -> * -> * -> * where + Standard :: a -> NonStandard (S n) a b + Non :: NonStandard n a b -> b -> NonStandard (S n) a b + +deriving instance (Show a, Show b) => Show (NonStandard n a b) +deriving instance Functor (NonStandard n a) diff --git a/testsuite/tests/deriving/should_compile/all.T b/testsuite/tests/deriving/should_compile/all.T index 8620c36dc5e6..5d9c7337f130 100644 --- a/testsuite/tests/deriving/should_compile/all.T +++ b/testsuite/tests/deriving/should_compile/all.T @@ -44,4 +44,5 @@ test('AutoDeriveTypeable', normal, compile, ['']) test('T8138', reqlib('primitive'), compile, ['-O2']) test('T8631', normal, compile, ['']) test('T8758', extra_clean(['T8758a.o', 'T8758a.hi']), multimod_compile, ['T8758a', '-v0']) -test('T8851', expect_broken(8851), compile, ['']) \ No newline at end of file +test('T8851', expect_broken(8851), compile, ['']) +test('T8678', normal, compile, ['']) diff --git a/testsuite/tests/deriving/should_fail/T3101.stderr b/testsuite/tests/deriving/should_fail/T3101.stderr index 58069283dc7a..7c976178c4ed 100644 --- a/testsuite/tests/deriving/should_fail/T3101.stderr +++ b/testsuite/tests/deriving/should_fail/T3101.stderr @@ -1,6 +1,6 @@ T3101.hs:9:12: Can't make a derived instance of ‘Show Boom’: - Constructor ‘Boom’ must have a Haskell-98 type + Constructor ‘Boom’ has a higher-rank type Possible fix: use a standalone deriving declaration instead In the data declaration for ‘Boom’ diff --git a/testsuite/tests/generics/GenCannotDoRep0_0.stderr b/testsuite/tests/generics/GenCannotDoRep0_0.stderr index 3537dac4d61b..e1292b8e7eb7 100644 --- a/testsuite/tests/generics/GenCannotDoRep0_0.stderr +++ b/testsuite/tests/generics/GenCannotDoRep0_0.stderr @@ -4,7 +4,8 @@ GenCannotDoRep0_0.hs:6:14: Warning: GenCannotDoRep0_0.hs:13:45: Can't make a derived instance of ‘Generic Dynamic’: - Dynamic must be a vanilla data constructor + Constructor ‘Dynamic’ has existentials or constraints in its type + Possible fix: use a standalone deriving declaration instead In the data declaration for ‘Dynamic’ GenCannotDoRep0_0.hs:17:1: diff --git a/testsuite/tests/generics/GenCannotDoRep1_0.stderr b/testsuite/tests/generics/GenCannotDoRep1_0.stderr index e40f35961331..7764f24662cf 100644 --- a/testsuite/tests/generics/GenCannotDoRep1_0.stderr +++ b/testsuite/tests/generics/GenCannotDoRep1_0.stderr @@ -1,5 +1,6 @@ GenCannotDoRep1_0.hs:9:49: Can't make a derived instance of ‘Generic1 Dynamic’: - Dynamic must be a vanilla data constructor + Constructor ‘Dynamic’ has existentials or constraints in its type + Possible fix: use a standalone deriving declaration instead In the data declaration for ‘Dynamic’ diff --git a/testsuite/tests/typecheck/should_fail/tcfail086.stderr b/testsuite/tests/typecheck/should_fail/tcfail086.stderr index 65149ef1f9ea..f88fde164b50 100644 --- a/testsuite/tests/typecheck/should_fail/tcfail086.stderr +++ b/testsuite/tests/typecheck/should_fail/tcfail086.stderr @@ -1,6 +1,6 @@ tcfail086.hs:6:38: Can't make a derived instance of ‘Eq Ex’: - Constructor ‘Ex’ must have a Haskell-98 type + Constructor ‘Ex’ has existentials or constraints in its type Possible fix: use a standalone deriving declaration instead In the data declaration for ‘Ex’ -- GitLab