Case-of-known-constructor is broken in GHC 8.10 for floated DataCon wrappers
As the issue title states, case-of-known-constructor does not fire if a case
expression scrutinizes a floated-out value built from a DataCon wrapper. This program reproduces the issue:
module KnownCon where
data T = D !Bool
f :: T -> T -> T
f (D a) (D b) = D (a && b)
{-# INLINE [100] f #-} -- so it isn’t inlined before FloatOut
g :: Bool -> T
g x = f (D True) (D x)
To see the bad behavior, we have to compile with -dverbose-core2core
, since we need to see the output of simplifier phase 2 (or phase 1). Inspecting it shows that D True
is floated out of g
by FloatOut, and even though f
is inlined, case-of-known-constructor does not fire!
==================== Simplifier ====================
Max iterations = 4
SimplMode {Phase = 2 [main],
inline,
rules,
eta-expand,
case-of-case}
lvl_s1gm :: T
[LclId,
Unf=Unf{Src=<vanilla>, TopLvl=True, Value=False, ConLike=False,
WorkFree=False, Expandable=False, Guidance=IF_ARGS [] 20 0}]
lvl_s1gm = $WD True
g :: Bool -> T
[LclIdX,
Arity=1,
Unf=Unf{Src=<vanilla>, TopLvl=True, Value=True, ConLike=True,
WorkFree=True, Expandable=True, Guidance=IF_ARGS [20] 80 0}]
g = \ (x_a1eV :: Bool) ->
case lvl_s1gm of { D a_atX ->
case x_a1eV of dt_X0 { __DEFAULT ->
case a_atX of {
False -> $WD False;
True -> $WD dt_X0
}
}
}
This seems quite bad to me. It is a regression from GHC 8.8, since GHC 8.8 inlines DataCon wrappers much more aggressively.
The source of this issue appears to be the Expandable=False
in the floated-out binding’s unfolding. The root cause is isExpandableApp
, which does not classify applications of DataCon wrappers as expandable:
isExpandableApp :: CheapAppFun
isExpandableApp fn n_val_args
| isWorkFreeApp fn n_val_args = True
| otherwise
= case idDetails fn of
DataConWorkId {} -> True -- Actually handled by isWorkFreeApp
RecSelId {} -> n_val_args == 1 -- See Note [Record selection]
ClassOpId {} -> n_val_args == 1
PrimOpId {} -> False
_ | isBottomingId fn -> False
-- See Note [isExpandableApp: bottoming functions]
| isConLike (idRuleMatchInfo fn) -> True
| all_args_are_preds -> True
| otherwise -> False
One point of note is that DataCon wrappers do not appear to be considered “conlike”; isConLikeId
returns True
on DataCon workers and ids with user-defined CONLIKE
pragmas, but it returns False
on DataCon wrappers. I suppose it is possible to argue in favor of this behavior, as it’s true that a DataCon wrapper is not work-free! But it is not an especially large amount of work, and the purpose of CONLIKE
is to annotate something as worth duplicating if and only if it exposes further optimizations. From that perspective, DataCon wrappers certainly seem conlike to me.
In any case, DataCon wrappers definitely ought to be expandable, even if they are not conlike. In fact, Note [exprIsConApp_maybe on data constructors with wrappers]
assumes they are:
1. Inline $WMkT on-the-fly. That's why data-constructor wrappers are marked
as expandable. (See GHC.Core.Utils.isExpandableApp.) Now we have
So this seems clearly a mistake to me.