Search code examples
scalamacrosscala-macrosscala-3

scala 3 macros: returning Map[String, Class[Any]] from a Macro


I am trying to write a macro against scala 3.0.0-M3. I want the macro to return the inner types of the Option[_] fields of a Type. For example, given:

class Professor(val lastName: String, val id: Option[Int], val bossId: Option[Long])

I want to associate id with Int, and bossId with Long.

I have some code that does that for primitive types and compiles ok:

import scala.quoted._
import scala.quoted.staging._
import scala.quoted.{Quotes, Type}

object TypeInfo {


  inline def fieldsInfo[T <: AnyKind]: Map[String, Class[Any]] = ${ fieldsInfo[T] }

  def fieldsInfo[T <: AnyKind: Type](using qctx0: Quotes): Expr[Map[String, Class[Any]]] = {
    given qctx0.type = qctx0
    import qctx0.reflect.{given, _}

    val uns = TypeTree.of[T]
    val symbol = uns.symbol
    val innerClassOfOptionFields: Map[String, Class[Any]] = symbol.memberFields.flatMap { m =>
      // we only support val fields for now
      if(m.isValDef){
        val tpe = ValDef(m, None).tpt.tpe
        // only if the field is an Option[_]
        if(tpe.typeSymbol == TypeRepr.of[Option[Any]].typeSymbol){
          val containedClass: Option[Class[Any]] =
            if(tpe =:= TypeRepr.of[Option[Int]]) Some(classOf[Int].asInstanceOf[Class[Any]])
            else if(tpe =:= TypeRepr.of[Option[Short]])  Some(classOf[Short].asInstanceOf[Class[Any]])
            else if(tpe =:= TypeRepr.of[Option[Long]])  Some(classOf[Long].asInstanceOf[Class[Any]])
            else if(tpe =:= TypeRepr.of[Option[Double]])  Some(classOf[Double].asInstanceOf[Class[Any]])
            else if(tpe =:= TypeRepr.of[Option[Float]])  Some(classOf[Float].asInstanceOf[Class[Any]])
            else if(tpe =:= TypeRepr.of[Option[Boolean]])  Some(classOf[Boolean].asInstanceOf[Class[Any]])
            else if(tpe =:= TypeRepr.of[Option[Byte]])  Some(classOf[Byte].asInstanceOf[Class[Any]])
            else if(tpe =:= TypeRepr.of[Option[Char]])  Some(classOf[Char].asInstanceOf[Class[Any]])
            else None

          containedClass.map(clazz => (m.name -> clazz))
        } else None
      } else None
    }.toMap

    println(innerClassOfOptionFields)

    Expr(innerClassOfOptionFields)
  }

But if I try to use it, like this:

class Professor(val lastName: String, val id: Option[Int], val bossId: Option[Long])

object Main extends App {

  val fields = TypeInfo.fieldsInfo[Professor]

}

the compiler first prints Map(id -> int, bossId -> long) because of the println in the macro code which looks alright, but then fails with:

[error] 16 |  val fields = TypeInfo.fieldsInfo[Professor]
[error]    |               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error]    |               Found:    (classOf[Int] : Class[Int])
[error]    |               Required: Class[Any]
[error]    | This location contains code that was inlined from Main.scala:34

What I am doing wrong? Am I not supposed to be able to return a Map from a macro, or maybe not this way?

Note that the if/else logic in my macro doesn't really matter, the problem can be reduced to (everything else being equal):

    val result: Map[String, Class[Any]] = Map(
      "bossId" -> classOf[scala.Long].asInstanceOf[Class[Any]],
      "id" -> classOf[scala.Int].asInstanceOf[Class[Any]]
    )
    Expr(result)

Solution

  • You can define this missing given, based on the one from the standard library.

    import scala.quoted._
    
    given ToExpr[Class[?]] with {                               
      def apply(x: Class[?])(using Quotes) = {
        import quotes.reflect._
        Ref(defn.Predef_classOf).appliedToType(TypeRepr.typeConstructorOf(x)).asExpr.asInstanceOf[Expr[Class[?]]]
      }
    }
    

    In the next release of Scala 3, this should no longer be necessary. The given instance of the standard library has been adapted to work for Class[?] too.

    Then you can return a well typed Map[String, Class[?]].

    inline def fieldsInfo: Map[String, Class[?]] = ${ fieldsInfoMacro }
    
    def fieldsInfoMacro(using Quotes): Expr[Map[String, Class[?]]] = {
      val result: Map[String, Class[?]] = Map(
        "bossId" -> classOf[scala.Long],
        "id" -> classOf[scala.Int]
      )
      Expr(result)
    }
    

    And everything works:

    scala> fieldsInfo                                                               
    val res1: Map[String, Class[?]] = Map(bossId -> long, id -> int)