scala / scala3

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

Regression (?) in softwaremill/tapir - refined type returned in a macro unable to be matched #19458

Closed jchyb closed 6 months ago

jchyb commented 9 months ago

Compiler version

3.4.0-RC1-bin-20240115-31f837e-NIGHTLY, compiles successfully with 3.3.1

Minimized code

main.scala:

import scala.compiletime._
final class Greater[V]
final class Less[V]

// original codebase used non-transparent inlines in the defs below, but I think this would end up passing an abstract type parameter to the macro, so I don't think that should have worked (but transparent inlines should)
transparent inline given validatorForGreater[N: Numeric, NM <: N](using witness: ValueOf[NM]): ValidatorForPredicate[N, Greater[NM]] = ???
transparent inline given validatorForLess[N: Numeric, NM <: N](using witness: ValueOf[NM]): ValidatorForPredicate[N, Less[NM]] = ???
transparent inline def summonValidators[N, A <: Tuple]: List[ValidatorForPredicate[N, Any]] = {
  inline erasedValue[A] match
    case _: EmptyTuple => Nil
    case _: (head *: tail) =>
      summonInline[ValidatorForPredicate[N, head]]
        .asInstanceOf[ValidatorForPredicate[N, Any]] :: summonValidators[N, tail]
}
transparent inline given validatorForAnd[N, Predicates](using mirror: IntersectionTypeMirror[Predicates]): ValidatorForPredicate[N, Predicates] =
  new ValidatorForPredicate[N, Predicates] {
    summonValidators[N, mirror.ElementTypes]
  }

trait ValidatorForPredicate[Value, Predicate]

@main def main() = validatorForAnd[Int, Greater[1] & Less[3]]

macro.scala:

import scala.quoted.Quotes

import scala.annotation.implicitNotFound
import scala.quoted.*
import scala.collection.View.Empty

trait IntersectionTypeMirror[A] {
  type ElementTypes <: Tuple
}

class IntersectionTypeMirrorImpl[A, T <: Tuple] extends IntersectionTypeMirror[A] {
  override type ElementTypes = T
}

object IntersectionTypeMirror {

  transparent inline given derived[A]: IntersectionTypeMirror[A] = ${ derivedImpl[A] }

  private def derivedImpl[A](using Quotes, Type[A]): Expr[IntersectionTypeMirror[A]] = {
    import quotes.reflect.*

    val tplPrependType = TypeRepr.of[? *: ?]
    val tplConcatType = TypeRepr.of[Tuple.Concat]

    def prependTypes(head: TypeRepr, tail: TypeRepr): TypeRepr =
      AppliedType(tplPrependType, List(head, tail))

    def concatTypes(left: TypeRepr, right: TypeRepr): TypeRepr =
      AppliedType(tplConcatType, List(left, right))

    def rec(tpe: TypeRepr): TypeRepr = {
      tpe.dealias match
        case AndType(left, right) => concatTypes(rec(left), rec(right))
        case t                    => prependTypes(t, TypeRepr.of[EmptyTuple])
    }
    val tupled =
      TypeRepr.of[A].dealias match {
        case and: AndType => rec(and).asType.asInstanceOf[Type[Elems]]
        case tpe          => report.errorAndAbort(s"${tpe.show} is not an intersection type")
      }
    type Elems

    given Type[Elems] = tupled

    Apply(
      TypeApply(
        Select.unique(
          New(
            Applied(
              TypeTree.of[IntersectionTypeMirrorImpl],
              List(
                TypeTree.of[A],
                TypeTree.of[Elems]
              )
            )
          ),
          "<init>"
        ),
        List(
          TypeTree.of[A],
          TypeTree.of[Elems]
        )
      ),
      Nil
    ).asExprOf[IntersectionTypeMirror[A]]
  }
}

Output

[error] ./main.scala:22:20
[error] cannot reduce inline match with
[error]  scrutinee:  scala.compiletime.erasedValue[mirror$proxy1.ElementTypes] : mirror$proxy1.ElementTypes
[error]  patterns :  case _:EmptyTuple
[error]              case _:*:[head @ _, tail @ _]
[error] @main def main() = validatorForAnd[Int, Greater[1] & Less[3]]
[error]                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expectation

I think it should compile. The code used in tapir actually uses non-transparent inlines in code that calls transparent inlines, which I don't think should have worked, but did (probably because of the previous match types). But even after correcting that, the code errors with what seems to be an abstract type member.

WojciechMazur commented 9 months ago

I think this issue origined in the SIP-56 and the new match types https://github.com/lampepfl/dotty/pull/18262#issuecomment-1848377927 @sjrd can you can a look at this issue?

sjrd commented 9 months ago

This is an inline match, not a match type. SIP-56 did not touch those.

nicolasstucki commented 9 months ago

Minimized

macro.scala // same

main.scala

import scala.compiletime._

inline def summonValidators[A <: Tuple]: Int =
  inline erasedValue[A] match
    case _: EmptyTuple => 1
    case _: (head *: tail) => 2

def main() =
  val mirror = IntersectionTypeMirror.derived[Some[Any] & Option[Int]]
  summonValidators[mirror.ElementTypes]
nicolasstucki commented 9 months ago

For the minimized example, the type that we get from rec(and)

        case and: AndType => rec(and).asType.asInstanceOf[Type[Elems]]

is

scala.Tuple.Concat[
  scala.*:[_ >: scala.Nothing <: scala.Any, _ >: scala.Nothing <: scala.Any][scala.Some[scala.Any], scala.Tuple$package.EmptyTuple],
  scala.*:[_ >: scala.Nothing <: scala.Any, _ >: scala.Nothing <: scala.Any][scala.Option[scala.Int], scala.Tuple$package.EmptyTuple]]

This is a nonsensical type. I suspect there is a bug in the macro implementation. My guess is in the use of

    val tplPrependType = TypeRepr.of[? *: ?]
nicolasstucki commented 9 months ago

This is a way to fix prependTypes

    def prependTypes(head: TypeRepr, tail: TypeRepr): TypeRepr =
      (head.asType, tail.asType) match {
        case ('[h], '[type t <: Tuple; t]) => TypeRepr.of[h *: t]
      }
jchyb commented 6 months ago

Fixed and merged in tapir