scala3 icon indicating copy to clipboard operation
scala3 copied to clipboard

Case object type class derivation from inside a macro fails

Open jchyb opened this issue 3 years ago • 1 comments

Compiler version

3.2.0 and nightly

Minimized code

TypeClass.scala

//> using options "-Xcheck-macros"
import scala.quoted._

trait TypeClass[A]:
  def handle(value: A): String

object TypeClass:

  def instance[A](f: A => String): TypeClass[A] = f(_)

  inline def derive[A]: TypeClass[A] = ${ deriveImpl[A] }

  object Auto:
    implicit inline def deriveAutomatic[A]: TypeClass[A] = ${ deriveImpl[A] }
  end Auto

  @scala.annotation.experimental
  def deriveImpl[A: Type](using quotes: Quotes): Expr[TypeClass[A]] =
    import quotes.reflect.*

    val sym = TypeRepr.of[A].typeSymbol

    if (sym.isClassDef && sym.flags.is(Flags.Case)) {
      // case object and case class
      '{ TypeClass.instance[A](_.toString) }
    } else if (sym.flags.is(Flags.Sealed)) {
      // sealed trait / enum
      def generate(using Quotes)(): Expr[String] = sym.children
        .map { subtypeSym =>
          subtypeSym.typeRef.asType match 
            case '[t] =>
              '{ scala.compiletime.summonInline[TypeClass[t]] } // cause of the error
              // Expr.summon below does not work with case objects either
              // Expr
              //   .summon[TypeClass[t]]
              //   .getOrElse(report.errorAndAbort(s"Cannot summon TypeClass[${TypeRepr.of[t].show}]"))
        }.foldLeft('{""})((l, r) => '{ $l + $r.toString() })
      '{ TypeClass.instance[A](a => ${ generate() }) }
    } else {
      report.errorAndAbort(s"${TypeRepr.of[A].show} is neither sum nor product type")
    }

end TypeClass

Main.scala

//> using scala "3.2.0"

sealed trait Bar
object Bar:
  case class A(a: Int) extends Bar
end Bar

sealed trait Baz
object Baz:
  case object A extends Baz // <- problematic case
end Baz

@main def main() =
  TypeClass.derive[Bar.A]
  TypeClass.derive[Baz.A.type]

  // automatic derivation for case class/enum case works
  {
    import TypeClass.Auto.deriveAutomatic
    TypeClass.derive[Bar]
  }
  // automatic derivation for case object doesn't
  {
    import TypeClass.Auto.deriveAutomatic
    // but uncommenting the code below make it works again
    // implicit val bazA: TypeClass[Baz.A.type] = deriveAutomatic[Baz.A.type]
    TypeClass.derive[Baz]
  }

Output

-- Error: /Users/jchyb/Documents/workspace/scala-3-failing-macro-repro/Main.scala:27:20 
27 |    TypeClass.derive[Baz]
   |    ^^^^^^^^^^^^^^^^^^^^^
   |No given instance of type TypeClass[Baz.A] was found.
   |I found:
   |
   |    TypeClass.Auto.deriveAutomatic[Baz.A]
   |
   |But method deriveAutomatic in object Auto does not match type TypeClass[Baz.A].
   |----------------------------------------------------------------------------
   |Inline stack trace
   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   |This location contains code that was inlined from TypeClass.scala:32
32 |              '{ scala.compiletime.summonInline[TypeClass[t]] }
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   |This location contains code that was inlined from TypeClass.scala:32
32 |              '{ scala.compiletime.summonInline[TypeClass[t]] }
   |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ----------------------------------------------------------------------------

Expectation

Should be able to summon the correct typeclass and compile. To me it seems like perhaps TypeRef.asType for case objects internally resolves as a case object and not its type (Baz.A instead of Baz.A.type), and perhaps that causes the confusing error.

jchyb avatar Sep 06 '22 09:09 jchyb

I investigated this for a bit more and came up with some surprising conclusions. Symbol.children for case objects specifically returns a term symbol like object A instead of a type symbol module class A$, like it does with case classes. And so perhaps it makes sense that implicits cannot be found for terms, in which case I feel like it would be better to have an earlier, more verbose error for calling a typeRef on a term.

There are cases however, when calling typeRef on a object A termSymbol leads to summoning a correct implicit, like in the example below:

Main.scala

//> using scala "3.2.0"

@main def main() =
  import TypeClass.Auto.*
  TypeClass.derive()

TypeClass.scala

//> using options "-Xcheck-macros"
import scala.quoted._

sealed trait Baz
object Baz:
  case object A extends Baz
end Baz

trait TypeClass[T]:
  def handle(value: T): String

object TypeClass:

  def instance[T](f: T => String): TypeClass[T] = f(_)
  
  object Auto:
    implicit inline def deriveAutomatic[T]: TypeClass[T] = ${ deriveAutomaticImpl[T] } // nested call
  end Auto

  private def deriveAutomaticImpl[T: Type](using Quotes) =
    '{ instance[T](_.toString) }


  inline def derive[T]() = ${ deriveImpl[T]() }

  private def deriveImpl[T: Type](using Quotes)(): Expr[String] =
    import quotes.reflect._
    val s1 = TypeRepr.of[Baz.A.type].typeSymbol
    val s2 = TypeRepr.of[Baz].typeSymbol.children.head
    val s3 = TypeRepr.of[T].typeSymbol.children.head

    println(s1) // module class A$
    println(s2) // object A
    println(s3) // object A

    s1.typeRef.asType match
      case '[t] => println(Expr.summon[TypeClass[t]].map(_.show)) 
    // prints Some((TypeClass.instance[Baz.A.type](((_$2: Baz.A.type) => _$2.toString())): TypeClass[Baz.A]))

    s2.typeRef.asType match
      case '[t] => println(Expr.summon[TypeClass[t]].map(_.show))
    // prints Some((TypeClass.instance[Baz.A](((_$2: Baz.A) => _$2.toString())): TypeClass[Baz.A]))
    
    s3.typeRef.asType match
      case '[t] => println(Expr.summon[TypeClass[t]].map(_.show))
    // prints Some((TypeClass.instance[Baz.A](((_$2: Baz.A) => _$2.toString())): TypeClass[Baz.A]))

    '{"placeholder"}

end TypeClass

As you can see, both changing type symbol and term symbol to typeRef manages to resolve the correct implicits, both located by expanding a different macro method (here deriveAutomatic instead of derive).

However, when the summoning leads recursively to the same method (like in the initial example, or the example below), things start to break:

//> using options "-Xcheck-macros"
import scala.quoted._

sealed trait Baz
object Baz:
  case object A extends Baz // <- problematic case
end Baz

trait TypeClass[T]:
  def handle(value: T): String

object TypeClass:

  def instance[T](f: T => String): TypeClass[T] = f(_)

  inline def derive[T](): TypeClass[T] = ${ deriveAutomaticImpl[T] } // same method

  object Auto:
    implicit inline def deriveAutomatic[T]: TypeClass[T] = ${ deriveAutomaticImpl[T] } // same method
  end Auto

  def deriveAutomaticImpl[T: Type](using quotes: Quotes): Expr[TypeClass[T]] =
    import quotes.reflect.*

    val sym = TypeRepr.of[T].typeSymbol

    if (sym.isClassDef && sym.flags.is(Flags.Case)) {
      '{ TypeClass.instance[T](_.toString) }
    } else if (sym.flags.is(Flags.Sealed)) {
      sym.children.head.typeRef.asType match // works only with sym.children.head.moduleClass
        case '[t] => println(Expr.summon[TypeClass[t]].map(_.show)) // None

      '{ TypeClass.instance[T](_.toString()) }
    } else {
      report.errorAndAbort(s"${TypeRepr.of[T].show} is neither sum nor product type")
    }

end TypeClass

For me it would make sense to either error on every typeRef call from term symbol (which I imagine would be impractical at this point), or fix summoning for object term symbols.

jchyb avatar Sep 09 '22 10:09 jchyb

Turns out, everything works properly. Both moduleClass (module class A$) and object (object A) are searched for correctly both in recurrent macro expansion and in a different method. What tripped me there is the fact that object A would fail the macro expansion because it is not isClassDef, which is incredibly obvious in hindsight. I apologize for the misleading issue report.

jchyb avatar Jan 27 '23 11:01 jchyb