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