scala3
scala3 copied to clipboard
Case object type class derivation from inside a macro fails
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.
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.
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.