Skip to content

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.

Edited by Alexis King
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information