scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.88k stars 1.06k forks source link

Scala 3 macro cannot match abstract types #16782

Open Atry opened 1 year ago

Atry commented 1 year ago

Compiler version

3.2.2

Minimized code

import scala.quoted.*

given staging.Compiler = staging.Compiler.make(getClass.getClassLoader)

def matchIArray = staging.run {
  Type.of[IArray[Int]] match
    case '[IArray[t]] =>
      '{"IArray"}
    case _ =>
      '{"not IArray"}
  end match
}
println(matchIArray)

def matchArray = staging.run {
  Type.of[Array[Int]] match
    case '[Array[t]] =>
      '{"Array"}
    case _ =>
      '{"not Array"}
  end match
}
println(matchArray)

https://scastie.scala-lang.org/rDUMGIiBQ5Km5vxE7TEl1Q

Output

not IArray
Array

Expectation

IArray
Array
nicolasstucki commented 1 year ago

It fails for all abstract types

import scala.quoted.*

object X:
  opaque type Opaque[T] = Array[T]
  type Invariant[T]
  type Covariant[+T]

class InvariantClass[T]
class CovariantClass[+T]

import X.*

inline def testTypeMatch(): Unit =
  ${testTypeMatchExpr}

def testTypeMatchExpr(using Quotes) =
  Type.of[Opaque[Int]] match
    case '[Opaque[t]] => // fails

  Type.of[Invariant[Int]] match
    case '[Invariant[t]] => // fails

  Type.of[Covariant[Int]] match
    case '[Covariant[t]] => // fails

  Type.of[CovariantClass[Int]] match
    case '[CovariantClass[t]] =>

  Type.of[InvariantClass[Int]] match
    case '[InvariantClass[t]] =>

  Type.of[CovariantClass[Int]] match
    case '[CovariantClass[t]] =>

  '{}
def test = testTypeMatch()
nicolasstucki commented 1 year ago

The issue is that we have a scrutinee.tpe <:< pattern.tpe where we compare F[Int] <:< F[t] with no constraints of on t we get false if F is a generic type. Otherwise, if F is a class we get true, and the constraint is added.

Here: https://github.com/lampepfl/dotty/blob/main/compiler/src/scala/quoted/runtime/impl/QuoteMatcher.scala#L345

@abgruszecki do you have some insight on why this is the case when we use gadt constraints?

abgruszecki commented 1 year ago

It's because classes are known to be injective. It's justifiable to return true in both cases, but if F is abstract, we can't reconstruct the bound Int <: t, since we could have type F[X] = Unit for instance.

What behaviour would you expect?

abgruszecki commented 1 year ago

Looking at the code, it feels to me like pattern matching on quoted types should be based on the structure of the types. The actual type information can be kept in GadtConstraint, if that's useful for type checking. I don't quite know why we involve GADT reasoning here.

nicolasstucki commented 1 year ago

Same issue in #15470

smarter commented 2 months ago

Note that this issue also affects something like:

type Abs[T]
inline def test =
  inline erasedValue[Abs[1]] match
    case _: Abs[a] => valueOf[a]

which is also incorrectly relying on the GADT logic currently: https://github.com/scala/scala3/blob/d2bb85dc6c197a8fd5dea198cf6b7e20a97f63a9/compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala#L279-L283 The same fix should apply to both form of type matching.

We could either use regular subtype checking instead of GADT checking, or reuse the match type algorithm (https://docs.scala-lang.org/sips/match-types-spec.html) without the disjointness checks, but this might be constraining in some situations (/cc @sjrd).

As a workaround (for the inline erasedValue usecase at least, but should also work for the quoted type match), it's possible to define a macro that extracts the type constructor and type arguments and wrap them in a dummy trait for the sole purpose of type matching (just wrote this, no idea if it's robust yet): https://gist.github.com/smarter/7211132efd78b7191fe393800e366835