scalamacrosinlinescala-macrosscala-3

How to call an inline method from within a scala 3.6.4 macro?


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!


Solution

  • 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] }
    
    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)
      }