Core generated with GHC-9.14.1-rc1 appears to be lazier?!

I noticed that GHC-9.14-rc1 generates different Core for Data.Map.Lazy.mapWithKey than GHC-9.12.2 did:

GHC-9.12.2 generates:

mapWithKey [InlPrag=NOINLINE[1], Occ=LoopBreaker]
  :: forall k a b. (k -> a -> b) -> Map k a -> Map k b
[GblId, Arity=2, Str=<LC(L,C(1,L))><1L>, Unf=OtherCon []]
mapWithKey
  = \ (@k) (@a) (@b) (ds :: k -> a -> b) (ds1 :: Map k a) ->
      case ds1 of {
        Bin bx kx x l r ->
          case mapWithKey @k @a @b ds l of conrep { __DEFAULT ->
          case mapWithKey @k @a @b ds r of conrep1 { __DEFAULT ->
          Bin @k @b bx kx (ds kx x) conrep conrep1
          }
          };
        Tip -> Tip @k @b
      }

Since the Bin constructor is strict in the left and right branch, they are forced before applying the constructor. So far so good.

But GHC-9.14.1-rc1 generates

mapWithKey [InlPrag=NOINLINE[1], Occ=LoopBreaker]
  :: forall k a b. (k -> a -> b) -> Map k a -> Map k b
[GblId, Arity=2, Str=<LC(L,C(1,L))><1L>, Unf=OtherCon []]
mapWithKey
  = \ (@k) (@a) (@b) (ds :: k -> a -> b) (ds1 :: Map k a) ->
      case ds1 of {
        Bin bx kx x l r ->
          Bin
            @k
            @b
            bx
            kx
            (ds kx x)
            (mapWithKey @k @a @b ds l)
            (mapWithKey @k @a @b ds r);
        Tip -> Tip @k @b
      }

Doesn’t this mean that the left and right branches are thunks?! What am I missing?

EDIT: The same pattern shows up in functions like traverseWithKey, mapKeysMonotonic etc

1 Like

Bin is strict in those arguments, so it should all be the same in the end?

Try -ddump-prep instead of -ddump-simpl, maybe those look the same there.

Ah, -ddump-prep is helpful. Indeed the functions look the same now:

GHC-9.12

mapWithKey [InlPrag=NOINLINE[1], Occ=LoopBreaker]
  :: forall k a b. (k -> a -> b) -> Map k a -> Map k b
[GblId, Arity=2, Str=<LC(L,C(1,L))><1L>, Unf=OtherCon []]
mapWithKey
  = \ (@k)
      (@a)
      (@b)
      (ds :: k -> a -> b)
      (ds1 [Occ=Once1!] :: Map k a) ->
      case ds1 of {
        Bin bx [Occ=Once1] kx x [Occ=Once1] l [Occ=Once1] r [Occ=Once1] ->
          case mapWithKey @k @a @b ds l of conrep [Occ=Once1] { __DEFAULT ->
          case mapWithKey @k @a @b ds r of conrep1 [Occ=Once1] { __DEFAULT ->
          let {
            sat [Occ=Once1] :: b
            [LclId]
            sat = ds kx x } in
          Bin @k @b bx kx sat conrep conrep1
          }
          };
        Tip -> Tip @k @b
      }

GHC 9.14:

mapWithKey [InlPrag=NOINLINE[1], Occ=LoopBreaker]
  :: forall k a b. (k -> a -> b) -> Map k a -> Map k b
[GblId, Arity=2, Str=<LC(L,C(1,L))><1L>, Unf=OtherCon []]
mapWithKey
  = \ (@k)
      (@a)
      (@b)
      (ds :: k -> a -> b)
      (ds1 [Occ=Once1!] :: Map k a) ->
      case ds1 of {
        Bin bx [Occ=Once1] kx x [Occ=Once1] l [Occ=Once1] r [Occ=Once1] ->
          case mapWithKey @k @a @b ds r of mapWithKey_sat [Occ=Once1, Dmd=SL]
          { __DEFAULT ->
          case mapWithKey @k @a @b ds l of mapWithKey_sat [Occ=Once1, Dmd=SL]
          { __DEFAULT ->
          let {
            mapWithKey_sat [Occ=Once1] :: b
            [LclId]
            mapWithKey_sat = ds kx x } in
          Bin @k @b bx kx mapWithKey_sat mapWithKey_sat mapWithKey_sat
          }
          };
        Tip -> Tip @k @b
      }

At this stage we also get definitions for the Bin constructor.

Bin [InlPrag=CONLIKE]
  :: forall {k} {a}.
     Int# %1 -> k %1 -> a %1 -> Map k a %1 -> Map k a %1 -> Map k a
[GblId[DataCon],
 Arity=5,
 Caf=NoCafRefs,
 Str=<L><SL><L><SL><SL>,
 Unf=OtherCon []]
Bin
  = (\ (@k[sk:1])
       (@a[sk:1])
       (eta [Occ=Once1] :: Int#)
       (eta [Occ=Once1] :: k)
       (eta [Occ=Once1] :: a)
       (eta [Occ=Once1] :: Map k a)
       (eta [Occ=Once1] :: Map k a) ->
       Bin @k[sk:1] @a[sk:1] eta eta eta eta eta)
    `cast` <Co:14> :: (forall {k} {a}.
                       Int# %1 -> k %1 -> a %1 -> Map k a %1 -> Map k a %1 -> Map k a)
                      ~R# (forall {k} {a}.
                           Int# %1 -> k %1 -> a %1 -> Map k a %1 -> Map k a %1 -> Map k a)

Somehow I was convinced that strict fields in constructors are a “Haskell thing” and don’t exist at the Core level.

1 Like

Nothing has become lazier; Make DataCon workers strict in strict fields (#20749) (!9874) · Merge requests · Glasgow Haskell Compiler / GHC · GitLab just changed the realised Core semantics of data constructor workers with strict fields (such as Bin). These datacon workers now act like unknown function calls that are known to be strict in the respective field. The corresponding evals are inserted late in the pipeline (and have already been inserted since the tag inference work landed). As a result, the eagerly inlined data con wrapper $WBin, which is what Haskell code such as Bin a b c d desugars to, no longer evals strict fields, hence you don’t see them in Core output. (Since $WBin would be a no-op, it is not even defined in the first place.) The case binding you see in CorePrep output is simply a result of using call-by-value for strict function arguments.

6 Likes