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
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.
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.