diff options
author | sheaf <sam.derbyshire@gmail.com> | 2022-07-21 15:37:22 +0200 |
---|---|---|
committer | sheaf <sam.derbyshire@gmail.com> | 2022-07-21 15:37:22 +0200 |
commit | 81d65f7f358fdbd1d13b89c43fc4cbe3ac82d24b (patch) | |
tree | 54254e42d773a4b9567204f49367627f5b4d77d9 | |
parent | e2f0094c315746ff15b8d9650cf318f81d8416d7 (diff) | |
download | haskell-81d65f7f358fdbd1d13b89c43fc4cbe3ac82d24b.tar.gz |
Make withDict opaque to the specialiser
As pointed out in #21575, it is not sufficient to set withDict to inline
after the typeclass specialiser, because we might inline withDict in one
module and then import it in another, and we run into the same problem.
This means we could still end up with incorrect runtime results because
the typeclass specialiser would assume that distinct typeclass evidence
terms at the same type are equal, when this is not necessarily the case
when using withDict.
Instead, this patch introduces a new magicId, 'nospec', which is only
inlined in CorePrep. We make use of it in the definition of withDict
to ensure that the typeclass specialiser does not common up distinct
typeclass evidence terms.
Fixes #21575
-rw-r--r-- | compiler/GHC/Builtin/Names.hs | 3 | ||||
-rw-r--r-- | compiler/GHC/CoreToStg/Prep.hs | 18 | ||||
-rw-r--r-- | compiler/GHC/Tc/Instance/Class.hs | 94 | ||||
-rw-r--r-- | compiler/GHC/Types/Id/Make.hs | 26 | ||||
-rw-r--r-- | testsuite/tests/simplCore/should_run/T21575b.hs | 69 | ||||
-rw-r--r-- | testsuite/tests/simplCore/should_run/T21575b.stdout | 12 | ||||
-rw-r--r-- | testsuite/tests/simplCore/should_run/T21575b_aux.hs | 16 | ||||
-rw-r--r-- | testsuite/tests/simplCore/should_run/all.T | 1 |
8 files changed, 193 insertions, 46 deletions
diff --git a/compiler/GHC/Builtin/Names.hs b/compiler/GHC/Builtin/Names.hs index 115a7f53f4..f97103a90f 100644 --- a/compiler/GHC/Builtin/Names.hs +++ b/compiler/GHC/Builtin/Names.hs @@ -2332,6 +2332,9 @@ runRWKey = mkPreludeMiscIdUnique 107 traceKey :: Unique traceKey = mkPreludeMiscIdUnique 108 +nospecIdKey :: Unique +nospecIdKey = mkPreludeMiscIdUnique 109 + inlineIdKey, noinlineIdKey :: Unique inlineIdKey = mkPreludeMiscIdUnique 120 -- see below diff --git a/compiler/GHC/CoreToStg/Prep.hs b/compiler/GHC/CoreToStg/Prep.hs index d03df0eedb..3078d26969 100644 --- a/compiler/GHC/CoreToStg/Prep.hs +++ b/compiler/GHC/CoreToStg/Prep.hs @@ -123,26 +123,24 @@ The goal of this pass is to prepare for code generation. We want curried definitions for all of these in case they aren't inlined by some caller. -9. Replace (lazy e) by e. See Note [lazyId magic] in GHC.Types.Id.Make - Also replace (noinline e) by e. + 9. Convert bignum literals into their core representation. -10. Convert bignum literals into their core representation. - -11. Uphold tick consistency while doing this: We move ticks out of +10. Uphold tick consistency while doing this: We move ticks out of (non-type) applications where we can, and make sure that we annotate according to scoping rules when floating. -12. Collect cost centres (including cost centres in unfoldings) if we're in +11. Collect cost centres (including cost centres in unfoldings) if we're in profiling mode. We have to do this here beucase we won't have unfoldings after this pass (see `trimUnfolding` and Note [Drop unfoldings and rules]. -13. Eliminate case clutter in favour of unsafe coercions. +12. Eliminate case clutter in favour of unsafe coercions. See Note [Unsafe coercions] -14. Eliminate some magic Ids, specifically +13. Eliminate some magic Ids, specifically runRW# (\s. e) ==> e[readWorldId/s] - lazy e ==> e + lazy e ==> e (see Note [lazyId magic] in GHC.Types.Id.Make) noinline e ==> e + nospec e ==> e ToDo: keepAlive# ... This is done in cpeApp @@ -1052,6 +1050,8 @@ cpeApp top_env expr -- See Note [lazyId magic] in GHC.Types.Id.Make || f `hasKey` noinlineIdKey -- Replace (noinline a) with a -- See Note [noinlineId magic] in GHC.Types.Id.Make + || f `hasKey` nospecIdKey -- Replace (nospec a) with a + -- See Note [nospecId magic] in GHC.Types.Id.Make -- Consider the code: -- diff --git a/compiler/GHC/Tc/Instance/Class.hs b/compiler/GHC/Tc/Instance/Class.hs index f2ffeea8c6..2993d02ab6 100644 --- a/compiler/GHC/Tc/Instance/Class.hs +++ b/compiler/GHC/Tc/Instance/Class.hs @@ -33,6 +33,7 @@ import GHC.Types.SafeHaskell import GHC.Types.Name ( Name, pprDefinedAt ) import GHC.Types.Var.Env ( VarEnv ) import GHC.Types.Id +import GHC.Types.Id.Make ( nospecId ) import GHC.Types.Var import GHC.Core.Predicate @@ -43,9 +44,7 @@ import GHC.Core.DataCon import GHC.Core.TyCon import GHC.Core.Class -import GHC.Core ( Expr(Var, App, Cast, Let), Bind (NonRec) ) -import GHC.Types.Basic -import GHC.Types.SourceText +import GHC.Core ( Expr(Var, App, Cast, Type) ) import GHC.Utils.Outputable import GHC.Utils.Panic @@ -455,20 +454,26 @@ matchWithDict [cls, mty] = do { sv <- mkSysLocalM (fsLit "withDict_s") Many mty ; k <- mkSysLocalM (fsLit "withDict_k") Many (mkInvisFunTyMany cls openAlphaTy) - ; let evWithDict_type = mkSpecForAllTys [runtimeRep1TyVar, openAlphaTyVar] $ - mkVisFunTysMany [mty, mkInvisFunTyMany cls openAlphaTy] openAlphaTy - - ; wd_id <- mkSysLocalM (fsLit "withDict_wd") Many evWithDict_type - ; let wd_id' = wd_id `setInlinePragma` inlineAfterSpecialiser - -- Given co2 : mty ~N# inst_meth_ty, construct the method of -- the WithDict dictionary: - -- \@(r : RuntimeRep) @(a :: TYPE r) (sv : mty) (k :: cls => a) -> k (sv |> (sub co; sym co2)) + -- + -- \@(r :: RuntimeRep) @(a :: TYPE r) (sv :: mty) (k :: cls => a) -> + -- nospec @(cls => a) k (sv |> (sub co ; sym co2)) + -- + -- where nospec :: forall a. a -> a ensures that the typeclass specialiser + -- doesn't attempt to common up this evidence term with other evidence terms + -- of the same type. + -- + -- See (WD6) in Note [withDict], and Note [nospecId magic] in GHC.Types.Id.Make. ; let evWithDict co2 = - let wd_rhs = mkCoreLams [ runtimeRep1TyVar, openAlphaTyVar, sv, k ] $ - Var k `App` Cast (Var sv) (mkTcTransCo (mkTcSubCo co2) (mkTcSymCo co)) - in Let (NonRec wd_id' wd_rhs) (Var wd_id') - -- Why a Let? See (WD6) in Note [withDict] + mkCoreLams [ runtimeRep1TyVar, openAlphaTyVar, sv, k ] $ + Var nospecId + `App` + (Type $ mkInvisFunTyMany cls openAlphaTy) + `App` + Var k + `App` + (Var sv `Cast` mkTcTransCo (mkTcSubCo co2) (mkTcSymCo co)) ; tc <- tcLookupTyCon withDictClassName ; let Just withdict_data_con @@ -486,12 +491,6 @@ matchWithDict [cls, mty] matchWithDict _ = return NoInstance -inlineAfterSpecialiser :: InlinePragma --- Do not inline before the specialiser; but do so afterwards --- See (WD6) in Note [withDict] -inlineAfterSpecialiser = alwaysInlinePragma `setInlinePragmaActivation` - ActiveAfter NoSourceText 2 - {- Note [withDict] ~~~~~~~~~~~~~~~ @@ -536,7 +535,7 @@ as if the following instance declaration existed: instance (mty ~# inst_meth_ty) => WithDict (C t1..tn) mty where withDict = \@{rr} @(r :: TYPE rr) (sv :: mty) (k :: C t1..tn => r) -> - k (sv |> (sub co2; sym co)) + k (sv |> (sub co2 ; sym co)) That is, it matches on the first (constraint) argument of C; if C is a single-method class, the instance "fires" and emits an equality @@ -582,28 +581,55 @@ Some further observations about `withDict`: (WD5) In earlier implementations, `withDict` was implemented as an identifier with special handling during either constant-folding or desugaring. - The current approach is more robust, previously the type of `withDict` + The current approach is more robust: previously, the type of `withDict` did not have a type-class constraint and was overly polymorphic. See #19915. -(WD6) In fact we desugar `withDict @(C t_1 ... t_n) @mty @{rr} @r` to - let wd = \sv k -> k (sv |> co) - {-# INLINE [2] #-} - in wd - The local `let` and INLINE pragma delays inlining `wd` until after the - type-class Specialiser has run. This is super important. Suppose we - have calls +(WD6) In fact, we desugar `withDict @cls @mty @{rr} @r` to + + \@(r :: RuntimeRep) @(a :: TYPE r) (sv :: mty) (k :: cls => a) -> + nospec @(cls => a) k (sv |> (sub co2 ; sym co))) + + That is, we cast the method using a coercion, and apply k to it. + However, we use the 'nospec' magicId (see Note [nospecId magic] in GHC.Types.Id.Make) + to ensure that the typeclass specialiser doesn't incorrectly common-up distinct + evidence terms. This is super important! Suppose we have calls + withDict A k withDict B k - where k1, k2 :: C T -> blah. If we inline those withDict calls we'll get + + where k1, k2 :: C T -> blah. If we desugared withDict naively, we'd get + k (A |> co1) k (B |> co2) - and the Specialiser will assume that those arguments (of type `C T`) are - the same, will specialise `k` for that type, and will call the same, + + and the Specialiser would assume that those arguments (of type `C T`) are + the same. It would then specialise `k` for that type, and then call the same, specialised function from both call sites. #21575 is a concrete case in point. - Solution: delay inlining `withDict` until after the specialiser; that is, - until Phase 2. This is not a Final Solution -- seee #21575 "Alas..". + To avoid this, we need to stop the typeclass specialiser from seeing this + structure, by using nospec. This function is inlined only in CorePrep; crucially + this means that it still appears in interface files, so that the desugaring of + withDict remains opaque to the typeclass specialiser across modules. + This means the specialiser will always see instead: + + nospec @(cls => a) k (A |> co1) + nospec @(cls => a) k (B |> co2) + + Why does this work? Recall that nospec is not an overloaded function; + it has the type + + nospec :: forall a. a -> a + + This means that there is nothing for the specialiser to do with function calls + such as + + nospec @(cls => a) k (A |> co) + + as the specialiser only looks at calls of the form `f dict` for an + overloaded function `f` (e.g. with a type such as `f :: Eq a => ...`). + + See test-case T21575b. -} diff --git a/compiler/GHC/Types/Id/Make.hs b/compiler/GHC/Types/Id/Make.hs index 9628bea733..e31460de7c 100644 --- a/compiler/GHC/Types/Id/Make.hs +++ b/compiler/GHC/Types/Id/Make.hs @@ -32,7 +32,7 @@ module GHC.Types.Id.Make ( voidPrimId, voidArgId, nullAddrId, seqId, lazyId, lazyIdKey, coercionTokenId, coerceId, - proxyHashId, noinlineId, noinlineIdName, + proxyHashId, noinlineId, noinlineIdName, nospecId, nospecIdName, coerceName, leftSectionName, rightSectionName, ) where @@ -159,7 +159,7 @@ wiredInIds ++ errorIds -- Defined in GHC.Core.Make magicIds :: [Id] -- See Note [magicIds] -magicIds = [lazyId, oneShotId, noinlineId] +magicIds = [lazyId, oneShotId, noinlineId, nospecId] ghcPrimIds :: [Id] -- See Note [ghcPrimIds (aka pseudoops)] ghcPrimIds @@ -1401,10 +1401,11 @@ leftSectionName = mkWiredInIdName gHC_PRIM (fsLit "leftSection") leftSecti rightSectionName = mkWiredInIdName gHC_PRIM (fsLit "rightSection") rightSectionKey rightSectionId -- Names listed in magicIds; see Note [magicIds] -lazyIdName, oneShotName, noinlineIdName :: Name +lazyIdName, oneShotName, noinlineIdName, nospecIdName :: Name lazyIdName = mkWiredInIdName gHC_MAGIC (fsLit "lazy") lazyIdKey lazyId oneShotName = mkWiredInIdName gHC_MAGIC (fsLit "oneShot") oneShotKey oneShotId noinlineIdName = mkWiredInIdName gHC_MAGIC (fsLit "noinline") noinlineIdKey noinlineId +nospecIdName = mkWiredInIdName gHC_MAGIC (fsLit "nospec") nospecIdKey nospecId ------------------------------------------------ proxyHashId :: Id @@ -1472,6 +1473,12 @@ noinlineId = pcMiscPrelId noinlineIdName ty info info = noCafIdInfo ty = mkSpecForAllTys [alphaTyVar] (mkVisFunTyMany alphaTy alphaTy) +nospecId :: Id -- See Note [nospecId magic] +nospecId = pcMiscPrelId nospecIdName ty info + where + info = noCafIdInfo + ty = mkSpecForAllTys [alphaTyVar] (mkVisFunTyMany alphaTy alphaTy) + oneShotId :: Id -- See Note [The oneShot function] oneShotId = pcMiscPrelId oneShotName ty info where @@ -1727,6 +1734,19 @@ special case to the demand analyser to address #16588. However, the special case seemed like a large and expensive hammer to address a rare case and consequently we rather opted to use a more minimal solution. +Note [nospecId magic] +~~~~~~~~~~~~~~~~~~~~~ +The 'nospec' magic Id is used to ensure to make a value opaque to the typeclass +specialiser. In CorePrep, we inline 'nospec', turning (nospec e) into e. +Note that this happens *after* unfoldings are exposed in the interface file. +This is crucial: otherwise, we could import an unfolding in which +'nospec' has been inlined (= erased), and we would lose the benefit. + +'nospec' is used in the implementation of 'withDict': we insert 'nospec' +so that the typeclass specialiser doesn't assume any two evidence terms +of the same type are equal. See Note [withDict] in GHC.Tc.Instance.Class, +and see test case T21575b for an example. + Note [The oneShot function] ~~~~~~~~~~~~~~~~~~~~~~~~~~~ In the context of making left-folds fuse somewhat okish (see ticket #7994 diff --git a/testsuite/tests/simplCore/should_run/T21575b.hs b/testsuite/tests/simplCore/should_run/T21575b.hs new file mode 100644 index 0000000000..da6362edaa --- /dev/null +++ b/testsuite/tests/simplCore/should_run/T21575b.hs @@ -0,0 +1,69 @@ +{-# LANGUAGE UndecidableInstances #-} + +module Main (main) where + +import Control.Monad (unless) +import qualified Data.Map as M +import Data.Map (Map) +import T21575b_aux (Given(..), give, unsafeGive) + +main :: IO () +main = do + testCase "Normal" + (give Normal (toJSON (Foo Bar))) + (unsafeGive Normal (toJSON (Foo Bar))) + (Object (M.fromList [("Foo",String "Bar")])) + testCase "ViaShow" + (give ViaShow (toJSON (Foo Bar))) + (unsafeGive ViaShow (toJSON (Foo Bar))) + (Object (M.fromList [("Foo",String "SHOWBAR")])) + +----- + +testCase :: (Eq a, Show a) => String -> a -> a -> a -> IO () +testCase str with_give with_unsafeGive expected = + putStrLn $ unlines + [ str ++ ":" + , " withDict: " ++ show with_give + , "unsafeCoerce: " ++ show with_unsafeGive + , " expected: " ++ show expected + , "" + ] + +data Foo = Foo Bar + +instance Show Foo where + show _ = "SHOWFOO" + +data Bar = Bar | BarBar + +instance Show Bar where + show _ = "SHOWBAR" + +---------------------------------------------------------------------------- +-- ToJSON instances +---------------------------------------------------------------------------- + +instance Given Style => ToJSON Foo where + toJSON (Foo x) = Object $ M.singleton "Foo" (toJSON x) + +instance Given Style => ToJSON Bar where + toJSON x = case given of + Normal -> String $ case x of + Bar -> "Bar" + BarBar -> "BarBar" + ViaShow -> String $ show x + +data Style = Normal | ViaShow + +---------------------------------------------------------------------------- +-- Minimized aeson +---------------------------------------------------------------------------- + +class ToJSON a where + toJSON :: a -> Value + +data Value + = Object !(Map String Value) + | String !String + deriving (Eq, Show) diff --git a/testsuite/tests/simplCore/should_run/T21575b.stdout b/testsuite/tests/simplCore/should_run/T21575b.stdout new file mode 100644 index 0000000000..2cfdf0dcac --- /dev/null +++ b/testsuite/tests/simplCore/should_run/T21575b.stdout @@ -0,0 +1,12 @@ +Normal: + withDict: Object (fromList [("Foo",String "Bar")]) +unsafeCoerce: Object (fromList [("Foo",String "Bar")]) + expected: Object (fromList [("Foo",String "Bar")]) + + +ViaShow: + withDict: Object (fromList [("Foo",String "SHOWBAR")]) +unsafeCoerce: Object (fromList [("Foo",String "SHOWBAR")]) + expected: Object (fromList [("Foo",String "SHOWBAR")]) + + diff --git a/testsuite/tests/simplCore/should_run/T21575b_aux.hs b/testsuite/tests/simplCore/should_run/T21575b_aux.hs new file mode 100644 index 0000000000..556ef26307 --- /dev/null +++ b/testsuite/tests/simplCore/should_run/T21575b_aux.hs @@ -0,0 +1,16 @@ +module T21575b_aux ( Given(..), give, unsafeGive ) where + +import GHC.Exts + ( withDict ) +import Unsafe.Coerce + ( unsafeCoerce ) + +class Given a where + given :: a + +give, unsafeGive :: forall a r. a -> (Given a => r) -> r +give = withDict @(Given a) @a + +unsafeGive a k = unsafeCoerce (Gift k :: Gift a r) a + +newtype Gift a r = Gift (Given a => r) diff --git a/testsuite/tests/simplCore/should_run/all.T b/testsuite/tests/simplCore/should_run/all.T index bebd839724..a887d5cedb 100644 --- a/testsuite/tests/simplCore/should_run/all.T +++ b/testsuite/tests/simplCore/should_run/all.T @@ -105,4 +105,5 @@ test('T19313', normal, compile_and_run, ['']) test('UnliftedArgRule', normal, compile_and_run, ['']) test('T21229', normal, compile_and_run, ['-O']) test('T21575', normal, compile_and_run, ['-O']) +test('T21575b', [], multimod_compile_and_run, ['T21575b', '-O']) test('T20836', normal, compile_and_run, ['-O0']) # Should not time out; See #20836 |