Admin message

Due to a large amount of spam we do not allow new users to create repositories, they are "external" users. If you are a new user and want to create a repository, for example for forking GHC, open a new issue on ghc/ghc using the "get-verified" issue template

Newtypes can destroy join points.
See also * #26157 ## Summary In some cases newtypes currently prevent functions from becoming join points incorrectly. ## Steps to reproduce In the example below if we case the result of the join point via a newtype the call to `should_join` is not turned into a join point. ghc M.hs ```haskell {-# OPTIONS_GHC -O -fforce-recomp -fno-worker-wrapper #-} {-# OPTIONS_GHC -ddump-simpl -ddump-occur-anal -ddump-to-file #-} -- module M where module M (foo,bar) where newtype I2 = I2 Int foo :: Bool -> Int -> Int -> Int -> I2 foo c x y z = case c of True -> I2 $ (should_join :: Int -> Int) x False -> I2 $ (should_join :: Int -> Int) y where {-# NOINLINE should_join #-} should_join = \x -> x + z bar :: Bool -> Int -> Int -> Int -> Int bar c x y z = case c of True -> (should_join :: Int -> Int) x False -> (should_join :: Int -> Int) y where {-# NOINLINE should_join #-} should_join = \x -> x + z ``` Which gives us: ```haskell foo = \ (c_awM :: Bool) (x_awN :: Int) (y_awO :: Int) (z_awP :: Int) -> let { should_join_sDJ [InlPrag=NOINLINE, Dmd=1C(1,!P(L))] :: Int -> Int [LclId, Arity=1, Str=<1L>, Unf=OtherCon []] should_join_sDJ = \ (x1_awR [OS=OneShot] :: Int) -> GHC.Internal.Num.$fNumInt_$c+ x1_awR z_awP } in case c_awM of { False -> (should_join_sDJ y_awO) `cast` (Sym M.N:I2 :: Int ~R# I2); True -> (should_join_sDJ x_awN) `cast` (Sym M.N:I2 :: Int ~R# I2) } -- RHS size: {terms: 17, types: 8, coercions: 0, joins: 1/1} bar :: Bool -> Int -> Int -> Int -> Int bar = \ (c_azA :: Bool) (x_azB :: Int) (y_azC :: Int) (z_azD :: Int) -> join { should_join_sDL [InlPrag=NOINLINE, Dmd=1C(1,!P(L))] :: Int -> Int [LclId[JoinId(1)(Just [!])], Arity=1, Str=<1L>, Unf=OtherCon []] should_join_sDL (eta_B1 [OS=OneShot] :: Int) = GHC.Internal.Num.$fNumInt_$c+ eta_B1 z_azD } in case c_azA of { False -> jump should_join_sDL y_azC; True -> jump should_join_sDL x_azB } ``` ------------------------------ ## Expected behavior `should_join` should be turned into a join point. ## Analysis This seems be due to this snippet: ``` occAnal env (Cast expr co) = let (WUD usage expr') = occAnal env expr usage1 = addManyOccs usage (coVarsOfCo co) -- usage2: see Note [Gather occurrences of coercion variables] usage2 = markAllNonTail usage1 -- usage3: calls inside expr aren't tail calls any more in WUD usage2 (Cast expr' co) ``` In another function I found a reference to Note `Note [Join points and casts]` which seems to talk about something like this which I will reproduce below: ``` {- Note [Join points and casts] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You might think that this should be OK: join j x = rhs in (case e of A -> alt1 B x -> (jump j x) |> co) You might think that, since the cast is ultimately erased, the jump to `j` should still be OK as a join point. But no! See #21716. Suppose newtype Age = MkAge Int -- axAge :: Age ~ Int f :: Int -> ... -- f strict in it's first argument and consider the expression f (join j :: Bool -> Age j x = (rhs1 :: Age) in case v of Just x -> (j x |> axAge :: Int) Nothing -> rhs2) Then, if the Simplifier pushes the strict call into the join points and alternatives we'll get join j' x = f (rhs1 :: Age) in case v of Just x -> j' x |> axAge Nothing -> f rhs2 Utterly bogus. `f` expects an `Int` and we are giving it an `Age`. No no no. Casts destroy the tail-call property. Henc markAllJoinsBad in the (Cast expr co) case of lintCoreExpr. ``` How does this arise? The basic optimization of pushing the continuation inwards is described in `Note [Join points and case-of-case]` It claims that: > Since they're always tail-called and we want to maintain this invariant, we can do this (for any evaluation context E): But this clearly is not the case if casts are involved! (See above). In fact there is a similar problem without case-of-case described in `Note [Join points with -fno-case-of-case]`. But for whatever reason in `Note [Join points with -fno-case-of-case]` we choose not to push the continuation, while for casts we have choosen to turn the join point into a let instead. Either way the statement of being able to push the continuation seems to come with a gigantic asterisk. For the case of casts the issue there seems to be "merely" that the operation as outlined in `Note [Join points and case-of-case]` leads to ill-typed core. The code would *run* fine, it's "merely" the casts that get messed up. This seems like it should be fixable. What we like to do push the call to `f` into the alternative `j x |> co`. "Through" an alternative into the join point. ``` E[join j = e in case ... of A -> jump j 1 B -> jump j 2 C -> f 3] --> join j = E[e] in case ... of A -> jump j 1 B -> jump j 2 C -> E[f 3] ``` If we try this with a concrete strict function call as context and casts this goes wrong: ``` (f :: N T -> Int) [join j = (e :: T) in case ... of A -> (jump j _ |> co :: N T) C -> f 3] --> join j = (f :: N T -> Int) (e :: T) in case ... of A -> (jump j _ |> co :: N T) C -> E[f 3] ``` This is all kinds of wrong, it's not just the function having an argument type missmatch but also the coercion is now casting `Int` to `N T`. ---------------------- I'm not yet sure if there is a great solution. Maybe not pushing the context inwards would be better than simply not creating join points. I might try that for a start. Alternatively maybe we can come up with a clever transformation that allows for the coercion to be pushed along inside the join point, but this get's tricky rather quickly. ## Environment * GHC version used: 9.12 (or any other)
issue