In a scala 3.6.4 serialization library I am developing there is the DiscriminatorCriteria
type-class that allows the user to determine which discriminator value to use for each variant P
of a sum-type S
.
trait DiscriminationCriteria[-S] {
transparent inline def discriminator[P <: S]: Int
}
Its discriminator
method must be inline to allow the user to use the scala.compiletime.erasedValue
tool. And I think it must be transparent so that the returned values be constant (single instances of singleton types).
The instances of the DiscriminatorCriteria
type-class are summoned by a serializer/deserializer derivation macro which then calls the discriminator
method from within a quote. Here is how the summoning and the call is done in the macros:
...
val discriminatorExpr: Expr[Int] = Expr.summon[DiscriminationCriteria[SumType]] match {
case Some(discriminatorCriteriaExpr) =>
'{ $discriminatorCriteriaExpr.discriminator[VariantType & SumType] }
case None =>
// Fall back to the alphanumerical index
Expr[Int](alphanumericIndex)
}
Where VariantType
is the type of a variant of the SumType
.
The expression resulting from the quote expansion would be:
(discriminatorCriteria: DiscriminatorCriteria[Animal]).discriminator[Dog]
which is a call to an transparent inline method that expands to a singleton Int instance.
The problem is that it does not compile because: "Deferred inline method discriminator
in trait DiscriminationCriteria
cannot be invoked."
The question is, how to call an inline method (discriminator
in this case) from within a macro? I was unable to create the correct prompt for a free AI to respond correctly.
@DmytroMitin said "Your issue is not related to macros. It relates to ordinary inline methods (not necessarily macros)." But that is not totally true. If the given returns a singleton like in
sealed trait Animal
case class Dog(field: String) extends Animal
object animalDc extends DiscriminationCriteria[Animal] {
override transparent inline def discriminator[S <: Animal]: Int = 32
}
given animalDc.type = animalDc
val thirtyTwo: 32 = summon[DiscriminationCriteria[Animal]].discriminator[Dog]
the summon
returns a singleton object and the call to discriminator
compiles and returns a constant.
He proposed two solutions in his answer. I didn't understand the first one, so I tried the second:
//// Library code
trait DiscriminationCriteria[-S] {
transparent inline def discriminator[P <: S]: Int
}
inline def summonDiscriminator[SumType, VariantType <: SumType]: Int =
summonInline[DiscriminationCriteria[SumType]].discriminator[VariantType]
transparent inline def someMacro[SumType, VariantType]: Int = ${ someMacroImpl[SumType, VariantType] }
def someMacroImpl[SumType: Type, VariantType: Type](using quotes: Quotes): Expr[Int] = {
val discriminatorExpr: Expr[Int] = Expr.summon[DiscriminationCriteria[SumType]] match {
case Some(_) => '{ summonDiscriminator[SumType, VariantType & SumType] }
case None => Expr[Int](77)
}
discriminatorExpr
}
//// User code
sealed trait Animal
case class Dog(dogField: Int) extends Animal
case class Cat(catField: String) extends Animal
object animalDc extends DiscriminationCriteria[Animal] {
override transparent inline def discriminator[P <: Animal]: Int =
inline erasedValue[P] match {
case _: Dog => 1
case _: Cat => 2
}
}
transparent inline given animalDc.type = animalDc
@main def s2(): Unit = {
println(someMacro[Animal, Cat]) // Error: Deferred inline method discriminator in trait DiscriminationCriteria cannot be invoked
}
Unfortunately it produces the same compilation error but in the call site.
@DmytroMitin found a solution using the low level Implicits.search
method. It is at the end of his answer. Congratulations!
Your issue is not related to macros. It relates to ordinary inline methods (not necessarily macros).
3. Inline methods can also be abstract. An abstract inline method can be implemented only by other inline methods. It cannot be invoked directly:
abstract class A: inline def f: Int object B extends A: inline def f: Int = 22 B.f // compiles val a: A = B a.f // doesn't compile: Deferred inline method f in class A cannot be invoked
trait A:
inline def f: Int
implicit object B extends A:
inline def f: Int = 22
B.f // compiles
val a: A = B
// a.f // doesn't compile: Deferred inline method f in class A cannot be invoked
summon[A].f // compiles since `summon` returns precise type i.e. B
"Deferred inline method `foo` in trait `Foo` cannot be invoked": Pairs
https://docs.scala-lang.org/scala3/reference/metaprogramming/inline.html#rules-for-overriding-1
Jasper-M: It means
foreach
can’t be inlined because it is abstract and he doesn’t know which implementation to pick.charpov: Makes sense. Can’t have both inline and dynamic binding.
https://users.scala-lang.org/t/deferred-inline-in-a-specific-case/7698
trait DiscriminationCriteria1[-S] extends DiscriminationCriteria[S] {
transparent inline def discriminator[P <: S]: Int = ??? // e.g. just throwing
}
and call an overridden version of this implementation
val discriminatorExpr: Expr[Int] = Expr.summon[DiscriminationCriteria1[SumType]] match {
case Some(discriminatorCriteriaExpr) =>
'{ $discriminatorCriteriaExpr.discriminator[VariantType & SumType] }
summon
with summonInline
)inline def summonDiscriminator[SumType, VariantType <: SumType]: Int =
summonInline[DiscriminationCriteria[SumType]].discriminator[VariantType] // (*)
and then use this helper function rather than making a call directly on a quoted expression
val discriminatorExpr: Expr[Int] = Expr.summon[DiscriminationCriteria[SumType]] match {
case Some(_) =>
'{ summonDiscriminator[SumType, VariantType & SumType] }
(*) The above rule 3 is not violated in this case because summon
/summonInline
returns an implicit of precise type (like shapeless.the
/c.inferImplicitValue
), not like Scala-2 implicitly
. But Expr.summon
returns just type T
def summon[T](using Type[T])(using Quotes): Option[Expr[T]] = {
import quotes.reflect.*
Implicits.search(TypeRepr.of[T]) match {
case iss: ImplicitSearchSuccess => Some(iss.tree.asExpr.asInstanceOf[Expr[T]])
// ^^^^^^^^^^^^^^^^^^^^^
case isf: ImplicitSearchFailure => None
}
}
https://github.com/scala/scala3/blob/3.6.4/library/src/scala/quoted/Expr.scala#L275-L280
Update. Thanks for the MCVE.
Yeah, the first approach with intermediate trait doesn't seem to work because as soon as we add a default implementation of inline method discriminator
we can no longer override it since non-abstract inline methods are effectively final: https://docs.scala-lang.org/scala3/guides/macros/inline.html#inline-method-overriding
The issue with the second approach with inline helper function summonDiscriminator
is that summonInline
doesn't seem to be as precise as summon
. It resolves at proper time (summonInline
resolves at the inlining site/call site of inline method while summon
resolves at the current site/definition site) but seems to return less precise type. Indeed, summon[DiscriminationCriteria[Animal]].discriminator[Cat]
(aka summon[DiscriminationCriteria[Animal]](using animalDc).discriminator[Cat]
aka animalDc.discriminator[Cat]
) compiles and returns 2
but summonDiscriminator[Animal, Cat]
doesn't compile with the same Deferred inline method discriminator in trait DiscriminationCriteria cannot be invoked
, so in the definition of summonDiscriminator
, summonInline[DiscriminationCriteria[SumType]]
seems to return type DiscriminationCriteria[SumType]
upon inlining rather than precise animalDc.type
.
As we already found out Expr.summon
is not precise either.
The third approach. Low-level Implicits.search
seems to return precise type. Try
import quotes.reflect.*
val discriminatorExpr: Expr[Int] =
Implicits.search(TypeRepr.of[DiscriminationCriteria[SumType]]) match {
case iss: ImplicitSearchSuccess =>
Select.unique(iss.tree, "discriminator")
.appliedToType(TypeRepr.of[VariantType]).asExprOf[Int]
case _: ImplicitSearchFailure => Expr[Int](77)
}