goldfirere / th-desugar

Desugars Template Haskell abstract syntax to a simpler format without changing semantics

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Replace `DLamE`/`DCaseE` with lambda cases (`\cases`)

RyanGlScott opened this issue · comments

Currently, the th-desugar AST has a DLamE construct for binding variables in expressions, and a DCaseE construct for scrutinizing expressions. I propose that we remove both of these in favor of a single DLamCasesE construct, as proposed in #174 (comment):

data DExp
  = ...
  | DLamCasesE [DClause]

Advantages

  • Doing so would make the th-desugar AST more minimal, as we can desugar lambda expressions, case expressions, \case expressions, and \cases expressions to a single language construct (DLamCasesE). (See #204 (comment) for examples of how each of these desugarings would work.) We could also get rid of DMatch in favor of DClause.

  • Doing so would avoid the need to desugar expressions like this:

    \(Foo x) (Bar y) -> f x y

    Into this:

    \fooX barY ->
      case (# fooX, barY #) of
        (# Foo x, Bar y #) -> f x y

    That is, it would avoid the need to "pack" arguments into an unboxed tuple just for the sake of pattern-matching on them in a single clause. Instead, we can avoid all of this tuple-packing business by simply using \cases' built-in pattern-matching capabilities.

  • By not performing tuple-packing, we would make it much, much simpler to desugar expressions that bind embedded type patterns or invisible type patterns (#204).

Potential downsides

  • This would be a pretty big breaking API change, even by th-desugar standards. It may not be entirely straightforward to migrate all existing th-desugar clients over to the DLamCasesE approach, so we may even want to consider a deprecation period for one release before removing DLamE and DCaseE entirely.
  • I haven't attempted to port singletons-th (by far the most sophisticated th-desugar client I know of) over to this new approach. We should make sure that this is viable before committing to this design.

One obstacle to making this work is that \cases (as well as its template-haskell counterpart, LamCasesE) has only been around since GHC 9.4. When sweetening a DLamCasesE expression, this means we have to think carefully about what to do on pre-9.4 versions of GHC, as we can't simply convert DLamCasesE to LamCasesE. In many cases, we can convert simple DLamCasesE expressions to LamE (when there is only a single \cases clause) or LamCaseE (when each \cases clause only has a single pattern), which provides at least partial backwards compatibility. This wouldn't be quite as straightforward for \cases expressions like this this one, however:

\cases
  True  (Just x) -> x
  False Nothing  -> 2
  _     _        -> 3

Note that you wouldn't actually be able to write such an expression directly with a pre-9.4 version of GHC—you'd only be able to construct one by splicing in a DLamCasesE. Still, it's conceivable that a user might try this, so we should have a story for how to handle it. Some options:

  1. Throw an error. We can say that sweetening \cases expressions like the one above are simply unsupported on pre-9.4 versions of GHC, and you'd need to upgrade your GHC if you want to do this.

  2. We could cleverly sweeten this \cases expression to something like:

    \case
      True ->
        \case
          Just x -> x
          _      -> 3
      False ->
        \case
          Nothing -> 2
          _       -> 3

    Note how we have inlined the 3 case in certain spots. While we could do this, this would require a significant amount of additional complexity in the sweetening pass. This feels a bit wrong, as th-desugar's usual approach is to put all of the complicated logic in desugaring, which then makes sweetening back to the template-haskell AST nearly trivial.

  3. We could take a page from how \cases expressions are currently desugared and sweeten the expression above to something like this:

    \arg1 arg2 ->
      case (arg1, arg2) of
        (True,  Just x)  -> x
        (False, Nothing) -> 2
        (_,     _)       -> 3

    This is still a bit involved, but not nearly as complicated as option (2).

    This option comes with a more severe drawback, however. In order to ensure that arg1 and arg2 don't shadow anything currently in scope (and run the risk of emitting -Wname-shadowing warnings), we need to use newName to ensure that arg1 and arg2 are fresh names. newName is a monadic operation, however, and sweetening is a completely pure operation. This means that if we wanted to call newName during sweetening, we would need to change the type signatures of all sweetening-related operations to be monadic. This is doable, but this it at odds with th-desugar's current approach of making sweetening as simple as possible.

For now, I am inclined to pick option (1). We can revisit if someone specifically asks for the ability to sweeten DLamCasesE expressions with pre-9.4 versions of GHC.

Reading the options, I was more excited about (3). But actually I agree that (1) is most expedient.

See #218 for the changes on the th-desugar side, as well as goldfirere/singletons#595 for the changes on the singletons-th side. Happily, migrating singletons-th over to DLamCasesE proved extremely straightforward, and it even simplified some tricky parts of how singling works.