1

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!

4
  • inline defs with handwritten code and inline defs with macros have some differences - one of them is that normal inline def expands other inlines and summons things that are visible in scope (don't confuse with summonInline), while macros' quotes only constructs raw, desugared expressions (it doesn't automatically summon things from scope, it doesn't expand other macros, it doesn't infer types). It can desugar some code, sure, but it has to be doable without any magically provided information from the callsite. Commented May 9 at 22:03
  • If you need to expand macro in macro... your best bet is to express that other macro via a type class that you can summon. Or directly call that def that takes Exprs, Types and Quotes and returns another Expr. Commented May 9 at 22:04
  • @MateuszKubuszok Are you saying that it is not possible to call (from within a macro) an inline member method of a summoned type-class instance? Commented May 9 at 23:09
  • I haven't checked, but I'd expect that unless this inline def is the same for all instances then, no. It is not possible. Commented May 10 at 10:37

1 Answer 1

1

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

  • A workaround in your use case could be to introduce an intermediate trait with a default implementation (or add a default implementation to the original trait)
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)
  }
Sign up to request clarification or add additional context in comments.

12 Comments

Clearly, it’s time for me to retire—I spent two days on this and never even thought to test it outside a macro. I don’t deserve your time. Sorry.
@DmytroMinin if the given is defined given animalDiscriminatorCriteria.type = animalDiscriminatorCriteria then val dogDiscriminator: 1 = summon[animalDiscriminatorCriteria.type].discriminator[Dog] compiles. So, if the given was transient inline then, perhaps, the (discriminatorCriteriaExpr: Exprs[DiscriminatorCriteria[SumType]]) expression, in the macro, may be upcasted to the actual singleton type (provided by the given) before calling discriminator. Is that possible?
@Readren See the update
It worked. Thank you again, and again.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.