Skip to content

Avoid unrepresentable InfixCon state in GADT constructors

Currently, the ASTs for both Haskell98-style data constructors and GADT contructors share the same data type for representing their arguments:

data ConDecl pass
  = ConDeclGADT
      { ...
      , con_args    :: HsConDeclDetails pass   -- ^ Arguments; never InfixCon
      , ...
      }

  | ConDeclH98
      { ...
      , con_args   :: HsConDeclDetails pass           -- ^ Arguments; can be InfixCon
      , ...
      }

Where:

type HsConDeclDetails pass
   = HsConDetails (HsScaled pass (LBangType pass)) (Located [LConDeclField pass])

And:

data HsConDetails arg rec
  = PrefixCon [arg]             -- C p1 p2 p3
  | RecCon    rec               -- C { x = p1, y = p2 }
  | InfixCon  arg arg           -- p1 `C` p2

There's something peculiar about this representation, however. Notice that the comments for the ConDeclGADT versionof con_args mention that it can never be InfixCon. Indeed, GADT constructors can only be written in prefix or record form, never infix. Nevertheless, the types don't reflect this, and as a consequence, many code paths involving HsConDeclDetails for GADTs have to awkwardly dance around the fact that InfixCon is unreachable. These include:

  • Here,

        get_args (InfixCon {})    = pprPanic "pprConDecl:GADT" (ppr cons)
  • Here,

                InfixCon _ _ -> panic "ConDeclGADT InfixCon"
  • And here:

    repConstr (InfixCon {}) (Just _) _ =
        panic "repConstr: infix GADT constructor should be in a PrefixCon"

The root cause of this awkwardness is that we are using HsConDeclDetails for both Haskell98 data constructors and GADT constructors, when in reality, the details for the two are different. Accordingly, I propose that we use different data types in the two con_args fields of ConDecl. Concretely, I propose that we:

  • Add a third type parameter to HsConDetails:

    data HsConDetails arg rec inf
      = PrefixCon [arg]             -- C p1 p2 p3
      | RecCon    rec               -- C { x = p1, y = p2 }
      | InfixCon  inf inf           -- p1 `C` p2
  • Factor out today's HsConDeclDetails into two further type synonyms: HsConDeclH98Details (for Haskell98-style data constructors) and HsConDeclGADTDetails (for GADT constructors):

    type HsConDeclDetails pass inf
      = HsConDetails (HsScaled pass (LBangType pass))
                     (XRec pass [LConDeclField pass])
                     inf
    
    type HsConDeclH98Details pass
       = HsConDeclDetails pass (HsScaled pass (LBangType pass))
    
    type HsConDeclGADTDetails pass
       = HsConDeclDetails pass Void

    Note that the two only differ in their treatment of the inf type parameter. (If we implement #18389, this would change further, but for now, it's only a small difference.) In HsConDeclH98Details, inf is instantiated to be HsScaled pass (LBangType pass). In HsConDeclGADTDetails, however, InfixCon is unreachable, so that is enforced by instantiating inf with Void.

This refactor was originally motivated by #18389, where we need to separate HsConDeclH98Details and HsConDeclGADTDetails for other reasons. But this is a worthwhile refactoring in its own right, so I decided to make a dedicated issue for this.

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