Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enum and equivalent class hierarchy do not generate same type signature #16470

Closed
hmf opened this issue Dec 6, 2022 · 5 comments
Closed

Enum and equivalent class hierarchy do not generate same type signature #16470

hmf opened this issue Dec 6, 2022 · 5 comments

Comments

@hmf
Copy link

hmf commented Dec 6, 2022

Compiler version

Version 2.3.1

Minimized code

I have created two examples of an HList using this Enum:

enum Tup:
  case EmpT
  case TCons[H, T <: Tup](head: H, tail: T)

import Tup.*

and what I assume is the equivalent case class hierarchy:

sealed trait Tup
case object EmpT extends Tup
case class TCons[H, T <: Tup](head: H, tail: T) extends Tup

//import Tup.*

In both cases I use the same code:

val tup1: TCons[1, TCons[2,  EmpT.type]] = TCons(1, TCons(2,  EmpT))
val tup3 = TCons(3, TCons(3,  EmpT))

println(split(tup1))
println(split(tup3))

The full code can be found here:

Output

I expected both examples to compile and run. However in the Enum version, in the last line I get:

    cannot reduce inline match with
     scrutinee:  Playground.tup3 : (Playground.tup3 : Playground.Tup)
     patterns :  case _:Playground.Tup#EmpT.type
                 case cons @ _:Playground.Tup.TCons[_ @  >: Nothing <: Any, _ @  >: Nothing <: Any]

Expectation

I expect both versions to generate the same type inference. My IDE shows that for the Enum version, the HLIst will have the the generic type Tup. For the sealed trait version, the complete definition of HList will be retained.

I would also expect the HList to retain its value types. For example, My IDE shows, which I assume is what the compiler inferred:

   val tup2: TCons[Int, TCons[Int, EmpT.type]] = 1 +: 2 +: EmpT

in my opinion should be:

   val tup2: TCons[1, TCons[2, EmpT.type]] = 1 +: 2 +: EmpT

For the first issue this may not be a bug. If it is not, and maybe I missed it, this difference could be documented.

The second is a question. If it is warranted, I can open another issue.

TIA

@hmf hmf added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Dec 6, 2022
@WojciechMazur
Copy link
Contributor

WojciechMazur commented Dec 6, 2022

In terms of type inference you are missing the Singleton bound for your types. Otherwise the result type would be narrowed

scala> import scala.compiletime.*
     | 
     | enum Tup:
     |   case EmpT
     |   case TCons[H <: Singleton, T <: Tup](head: H, tail: T)
     | 
     | import Tup.*
     | 
     | type +:[A<: Singleton,T<:Tup] = TCons[A, T]
     | extension [A<: Singleton, T <: Tup] (a: A) def +: (t: T): TCons[A, T] =  TCons(a, t)
     | 
     | val tup2 = TCons(1, TCons(2, EmpT))
     | val tup3 = 1 +: 2 +: EmpT
// defined class Tup
// defined alias type +:[A <: Singleton,T <: Tup] = Tup.TCons[A, T]
def +:[A <: Singleton, T <: Tup](t: T)(a: A): Tup.TCons[A, T]
val tup2: Tup = TCons(1,TCons(2,EmpT))
val tup3: Tup.TCons[1, Tup.TCons[2, Tup]] = TCons(1,TCons(2,EmpT))

without singleton type it would infer to:

val tup2: Tup = TCons(1,TCons(2,EmpT))
val tup3: Tup.TCons[Int, Tup.TCons[Int, Tup]] = TCons(1,TCons(2,EmpT))

What's really interesting is the fact that apply method of Tup.TCons has the same result type as extension +: both return TCons[A, T] yet, infered type for tup2 is narrowed, but for tup3 it is not

@hmf
Copy link
Author

hmf commented Dec 6, 2022

@WojciechMazur I was unaware of the singleton bound. I confirm the results above using an IDE. When using apply, I get a Tup. Using +: I get the type values.

However, using the singleton bound on the sealed trait version, does not show the issue you describe above. Both apply and +: produce the same expected results.

Thanks for the feedback.

@WojciechMazur
Copy link
Contributor

@odersky I think there was a similar case reported recently but I'm not sure. It seems that the main problem is widening the types which happen for enums but not for traits. My question is are enums always widened / down casted on purpose? Is it needed to simplify types for further phases or there is some other reason for that?

Example:

val tup1: TCons[1, TCons[2,  EmpT.type]] = TCons(1, TCons(2,  EmpT))
val tup2                                 = TCons(1, TCons(2,  EmpT))
val tup3                                 = 1 +: 2 +: EmpT

would be inferred as the following:

val tup1: Tup.TCons[1, Tup.TCons[2, Tup.EmpT.type]] = TCons(1,TCons(2,EmpT))
val tup2: Tup = TCons(1,TCons(2,EmpT))
val tup3: Tup.TCons[1, Tup.TCons[2, Tup]] = TCons(1,TCons(2,EmpT))

In the compiler after the typer phase they're defined as following:

 val tup1: test.Tup.TCons[1.type, test.Tup.TCons[2.type, test.Tup#EmpT.type]]
       = 
    test.Tup.TCons.apply[(1 : Int), 
      test.Tup.TCons[(2 : Int), (test.Tup.EmpT : test.Tup)]
    ](1, 
      test.Tup.TCons.apply[(2 : Int), (test.Tup.EmpT : test.Tup)](2, 
        test.Tup#EmpT
      )
    )
    val tup2: test.Tup.TCons[Int, test.Tup.TCons[Int, test.Tup]] = 
      test.+:[Int, test.Tup.TCons[Int, test.Tup]](
        test.+:[Int, test.Tup](test.Tup#EmpT.asInstanceOf[test.Tup#EmpT.type])(2
          )
      )(1)
    val tup3: test.Tup = 
      test.Tup.TCons.apply[Int, test.Tup](1, 
        test.Tup.TCons.apply[Int, test.Tup](2, test.Tup#EmpT):test.Tup
      ):test.Tup

The most important aspect here is widening exact type info using (...):test.Tup

The split method is defined as

inline def split[L <: Tup](inline left:L): List[String] =
  inline left match
    case EmpT              => List("Empty")
    case cons: TCons[_, _] => "NonEmpty" :: split(cons.tail)

Due to the widenning of the type, at the time of inlining would see a generic type Tup and though the match would not be handled.

@WojciechMazur WojciechMazur added area:typer stat:needs spec area:enums and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Dec 7, 2022
@bishabosha
Copy link
Member

bishabosha commented Dec 7, 2022

This is as documented image

Note that the type of the expressions above is always Option. Generally, the type of a enum case constructor application will be widened to the underlying enum type, unless a more specific type is expected. This is a subtle difference with respect to normal case classes. The classes making up the cases do exist, and can be unveiled, either by constructing them directly with a new, or by explicitly providing an expected type.

@bishabosha
Copy link
Member

You can use a union type of the enum cases as an upper bound if you want precise types

@bishabosha bishabosha closed this as not planned Won't fix, can't repro, duplicate, stale Dec 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants