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

Desugared lambda expressions involving unlifted types won't typecheck

RyanGlScott opened this issue · comments

This th-desugar example involving unlifted types typechecks:

{-# LANGUAGE MagicHash #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -ddump-splices #-}
module Foo where

import GHC.Exts (Int#)
import Language.Haskell.TH.Desugar

$(do decs <- [d| f :: Int# -> Int# -> ()
                 f 27# 42# = ()
               |]
     ddecs <- dsDecs decs
     pure (sweeten ddecs))

However, if I change f to use a lambda expression:

                 f = \27# 42# -> ()

Then the generated code no longer typechecks:

[1 of 1] Compiling Foo              ( Foo.hs, interpreted )
Foo.hs:(9,2)-(13,26): Splicing declarations
    do decs_a3w1 <- [d| f_a3w0 :: Int# -> Int# -> ()
                        f_a3w0 = \ 27# 42# -> () |]
       ddecs_a3w2 <- dsDecs decs_a3w1
       pure (sweeten ddecs_a3w2)
  ======>
    f_a3xc :: Int# -> Int# -> ()
    f_a3xc
      = \ arg_6989586621679023383_a3xe arg_6989586621679023385_a3xg
          -> case
                 ((,) arg_6989586621679023383_a3xe) arg_6989586621679023385_a3xg
             of {
               (,) 27# 42# -> () }

Foo.hs:9:2: error:
    • Couldn't match a lifted type with an unlifted type
      When matching types
        a0 :: *
        Int# :: TYPE 'GHC.Types.IntRep
    • In the first argument of ‘(,)’, namely
        ‘arg_6989586621679023383_a3xe’
      In the expression:
        ((,) arg_6989586621679023383_a3xe) arg_6989586621679023385_a3xg
      In the expression:
        case
            ((,) arg_6989586621679023383_a3xe) arg_6989586621679023385_a3xg
        of {
          (,) 27# 42# -> () }
  |
9 | $(do decs <- [d| f :: Int# -> Int# -> ()
  |  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...

As the -ddump-splices output indicates, this is because we desugar \27# 42# -> () to \arg1 arg2 -> case (arg1, arg2) of (27#, 42#) -> (). This uses a lifted pair (,), however, which won't work for unlifted types like Int#.

I can think of a couple of solutions to this problem:

  1. Make the desugaring use an unlifted pair (#,#). This would require desugared code to enable UnboxedTuples in surprising places, however. Moreover, the desugared code would not work in old versions of GHCi without enabling -fobject-code.

  2. Instead of using a tuple, instead let-bind an auxiliary function which performs the match:

    f :: Int# -> Int# -> ()
    f = \arg1 arg2 ->
          let aux 27# 42# = ()
           in aux arg1 arg2

    This avoids the use of tuples altogether. I'm not yet clear if this would have ramifications for singletons, however.

Upon further thought, solution (2) won't work in the presence of GADTs. Given this data type:

data T a where
  MkT :: Int -> T Int

This will typecheck:

f :: T a -> T a -> a
f = \(MkT x1) (MkT x2) -> x1 + x2

But this won't:

f :: T a -> T a -> a
f = \t1 t2 ->
      let aux (MkT x1) (MkT x2) = x1 + x2
      in aux t1 t2
Bug.hs:10:25: error:
    • Couldn't match expected type ‘p’ with actual type ‘T a0’
        ‘p’ is untouchable
          inside the constraints: a1 ~ Int
          bound by a pattern with constructor: MkT :: Int -> T Int,
                   in an equation for ‘aux’
          at Bug.hs:10:16-21
      ‘p’ is a rigid type variable bound by
        the inferred type of aux :: T a1 -> p -> p1
        at Bug.hs:10:11-41
      Possible fix: add a type signature for ‘aux’
    • In the pattern: MkT x2
      In an equation for ‘aux’: aux (MkT x1) (MkT x2) = x1 + x2
      In the expression: let aux (MkT x1) (MkT x2) = x1 + x2 in aux t1 t2
    • Relevant bindings include
        aux :: T a1 -> p -> p1 (bound at Bug.hs:10:11)
   |
10 |       let aux (MkT x1) (MkT x2) = x1 + x2
   |                         ^^^^^^

Bug.hs:10:35: error:
    • Couldn't match expected type ‘p1’ with actual type ‘Int’
        ‘p1’ is untouchable
          inside the constraints: a0 ~ Int
          bound by a pattern with constructor: MkT :: Int -> T Int,
                   in an equation for ‘aux’
          at Bug.hs:10:25-30
      ‘p1’ is a rigid type variable bound by
        the inferred type of aux :: T a1 -> p -> p1
        at Bug.hs:10:11-41
      Possible fix: add a type signature for ‘aux’
    • In the expression: x1 + x2
      In an equation for ‘aux’: aux (MkT x1) (MkT x2) = x1 + x2
      In the expression: let aux (MkT x1) (MkT x2) = x1 + x2 in aux t1 t2
    • Relevant bindings include
        aux :: T a1 -> p -> p1 (bound at Bug.hs:10:11)
   |
10 |       let aux (MkT x1) (MkT x2) = x1 + x2
   |                                   ^^^^^^^

I like the unlifted pair idea. Contrary to your assumption, using UnboxedTupE does not require -XUnboxedTuples. So that's good. It is true that you would need -fobject-code on older GHCi's, but I think that's OK (perhaps you disagree). One remaining problem, though, is that you evidently need TupleSections to use e.g. UnboxedTupE [Nothing, Nothing], which is a shame. I was able to get ConE to work with an unboxed tuple constructor name, though, so that might make a way forward, if we had a way to generate those names programmatically -- I didn't do this last experiment.

Contrary to your assumption, using UnboxedTupE does not require -XUnboxedTuples.

Doesn't it? If I try this:

{-# LANGUAGE TemplateHaskell #-}
module Foo where

import Language.Haskell.TH

f :: ()
f = let x = $(pure (UnboxedTupE [Just (LitE (CharL 'a')), Just (LitE (CharL 'b'))])) in ()

Then GHC rejects it without -XUnboxedTuples:

$ ghci-9.2 Foo.hs
GHCi, version 9.2.2: https://www.haskell.org/ghc/  :? for help
Loaded GHCi configuration from /home/rscott/.ghci
[1 of 1] Compiling Foo              ( Foo.hs, interpreted )

Foo.hs:7:9: error:
    • Illegal unboxed tuple type as function argument: (# Char, Char #)
      Perhaps you intended to use UnboxedTuples
    • When checking the inferred type
        x :: (# Char, Char #)
      In the expression: let x = ((# 'a', 'b' #)) in ()
      In an equation for ‘f’: f = let x = ((# 'a', 'b' #)) in ()
  |
7 | f = let x = $(pure (UnboxedTupE [Just (LitE (CharL 'a')), Just (LitE (CharL 'b'))])) in ()
  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

One remaining problem, though, is that you evidently need TupleSections to use e.g. UnboxedTupE [Nothing, Nothing], which is a shame.

This is true, but I don't think solution (1) would ever generate code involving tuple sections. All of the uses of UnboxedTupE would be fully saturated, unless I'm overlooking something.

I've observed the same as you, but the key bit is that the error comes up when validating an inferred type for a variable, x in your case. In the desugaring plan, no variable would have an unboxed-tuple type, so all is well.

Hah, a good observation. If you say case $(pure (UnboxedTupE [Just (LitE (CharL 'a')), Just (LitE (CharL 'b'))])) of ..., for instance, then all is well. I suppose it's a bit fragile to rely on GHC not requiring UnboxedTuples here, but so the cookie crumbles.

But now I understand what you were getting at with the talk of tuple sections. After all, there is no DExp constructor for tuple syntax (boxed or unboxed), so in order to represent an application of an unboxed tuple to arguments, you'd have to do so like DConE <unboxed tuple data constructor> `DAppE` ... `DAppE` .... As you surmised in #158 (comment), however, there is a way to retrieve unboxed tuple data constructor names programmatically: template-haskell's unboxedTupleDataName function. As such, I believe this would avoid needing TupleSection as well.

In short: it's brittle, but it just might work.

In short: it's brittle, but it just might work.

Like the rest of th-desugar and singletons. Excellent. :)

It turns out that there actually is an example of desugared code that would require the use of the UnboxedTuples extension, and it involves match flattening. In this example:

{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -ddump-splices #-}
module Bug where

import Language.Haskell.TH
import Language.Haskell.TH.Desugar

data Pair a b = MkPair a b

$(pure [])

g :: String
g =
  $(do e1 <- [| let f ~(MkPair x y) = x ++ y
                in f (MkPair "a" "b")
             |]
       e2 <- dsExp e1
       e3 <- scExp e2
       pure (sweeten e3))

The desugared code is:

..\Bug.hs:(14,5)-(19,24): Splicing expression
    do e1_aCde <- [| let
                       f_aCdb ~(MkPair x_aCdc y_aCdd) = x_aCdc ++ y_aCdd
                     in f_aCdb (MkPair "a" "b") |]
       e2_aCdf <- dsExp e1_aCde
       e3_aCdg <- scExp e2_aCdf
       pure (sweeten e3_aCdg)
  ======>
    let
      f_aCeK _arg_6989586621679156781_aCeO
        = let
            tuple_6989586621679156789_aCeW
              = case _arg_6989586621679156781_aCeO of {
                  MkPair _x_6989586621679156783_aCeQ _y_6989586621679156785_aCeS
                    -> let x_aCeL = _x_6989586621679156783_aCeQ in
                       let y_aCeM = _y_6989586621679156785_aCeS in ((#,#) x_aCeL) y_aCeM }
            x_aCeL
              = case tuple_6989586621679156789_aCeW of {
                  (#,#) proj_6989586621679156791_aCeY _
                    -> proj_6989586621679156791_aCeY }
            y_aCeM
              = case tuple_6989586621679156789_aCeW of {
                  (#,#) _ proj_6989586621679156793_aCf0
                    -> proj_6989586621679156793_aCf0 }
          in ((++) x_aCeL) y_aCeM
    in f_aCeK ((MkPair "a") "b")

Notice that tuple_6989586621679156789_aCeW returns an unboxed tuple. As a result, GHC infers an unboxed tuple type for it, and it complains as such:

..\Bug.hs:14:5: error:
    • Illegal unboxed tuple type as function argument: (# a0, b0 #)
      Perhaps you intended to use UnboxedTuples
    • When checking the inferred type
        tuple_6989586621679156789_aCeW :: (# a0, b0 #)
      In the expression:
        let
          tuple_6989586621679156789_aCeW
            = case _arg_6989586621679156781_aCeO of {
                MkPair _x_6989586621679156783_aCeQ _y_6989586621679156785_aCeS
                  -> ... }
          x_aCeL
            = case tuple_6989586621679156789_aCeW of {
                (#,#) proj_6989586621679156791_aCeY _ -> ... }
          y_aCeM
            = case tuple_6989586621679156789_aCeW of {
                (#,#) _ proj_6989586621679156793_aCf0 -> ... }
        in ((++) x_aCeL) y_aCeM
      In an equation for ‘f_aCeK’:
          f_aCeK _arg_6989586621679156781_aCeO
            = let
                tuple_6989586621679156789_aCeW = ...
                x_aCeL = ...
                ....
              in ((++) x_aCeL) y_aCeM
   |
14 |   $(do e1 <- [| let f ~(MkPair x y) = x ++ y
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...

The relevant code is in mkSelectorDecs. Interestingly, the comments for that function claim that the code is similar to what GHC itself does during compilation. I wonder how it handles this example?

Ah, I figured out the answer to my own question. Here is a variant of that example which I cooked up to determine how mkSelectorDecs behaves for unlifted types:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# OPTIONS_GHC -ddump-simpl #-}
module Bug where

import GHC.Exts

data Pair (a :: TYPE UnliftedRep) (b :: TYPE UnliftedRep) = MkPair a b

f :: Pair a b -> Pair b a
f ~(MkPair x y) = MkPair y x

The answer is surprisingly straightforward: it just chooses not to even try:

$ ghc Bug.hs
[1 of 1] Compiling Bug              ( Bug.hs, Bug.o )

Bug.hs:11:4: error:
    A lazy (~) pattern cannot bind variables of unlifted type.
    Unlifted variables:
      x :: a
      y :: b
   |
11 | f ~(MkPair x y) = MkPair y x
   |    ^^^^^^^^^^^^

In that case, I'll apply the same restriction to th-desugar's implementation of mkSelectorDecs as well.

I've pushed a fix to the T158 branch, and I've also verified that this does not regress singletons. (That is unsurprising, since singletons has very special treatment of unboxed tuples, but it never hurts to check.)

That being said, there is still one small snag that I've run into: this approach doesn't work on GHC 7.10, and only on 7.10. (It even works on 7.8, to make things more annoying.) Here is an excerpt from the CI for the 7.10 job:

[1 of 8] Compiling T158Exp          ( Test/T158Exp.hs, /__w/th-desugar/th-desugar/dist-newstyle/build/x86_64-linux/ghc-7.10.3/th-desugar-1.13/t/spec/build/spec/spec-tmp/T158Exp.o )

Test/T158Exp.hs:14:5:
    Illegal data constructor name: ‘(#,#)’
    When splicing a TH expression:
      (\arg_1627423212_0 arg_1627423214_1 -> case GHC.Tuple.(#,#) arg_1627423212_0 arg_1627423214_1 of
                                           GHC.Tuple.(#,#) 27# 42# -> GHC.Tuple.()) 27# 42#
    In the splice:
      $([| (\ 27# 42# -> ()) 27# 42# |] >>= dsExp >>= return . expToTH)

I wonder if we should just drop support for pre-8.0 versions of GHC at this point.

I think dropping support for pre-8.0 is indeed reasonable. Maybe note in the README that if someone finds this problematic, they should submit a bug report.

Sounds good to me. I've opened #163 to make GHC 8.0 the minimum. Once that lands, I can implement a fix for this issue without remorse.