In a scala macro, how to get the full name that a class will have at runtime?

779 views Asked by At

The intention is to get at run-time some info of particular classes that is available only at compile-time.

My approach was to generate the info at compile time with a macro that expands to a map that contains the info indexed by the runtime class name. Something like this:

object macros {
    def subClassesOf[T]: Map[String, Info] = macro subClassesOfImpl[T];

    def subClassesOfImpl[T: ctx.WeakTypeTag](ctx: blackbox.Context): ctx.Expr[Map[String, Info]] = {
        import ctx.universe._

        val classSymbol = ctx.weakTypeTag[T].tpe.typeSymbol.asClass
        val addEntry_codeLines: List[Tree] =
            for {baseClassSymbol <- classSymbol.knownDirectSubclasses.toList} yield {
                val key = baseClassSymbol.asType.toType.erasure.typeSymbol.fullName
                q"""builder.addOne($key -> new Info("some info"));"""
            }
        q"""
            val builder = Map.newBuilder[String, Info];
            {..$addEntry_codeLines}
            builder.result();""";
        ctx.Expr[Map[String, Info]](body_code);
    }
}

Which would we used like this:

object shapes {
    trait Shape;
    case class Box(h: Int, w: Int);
    case class Sphere(r: Int);
}

val infoMap = subclassesOf[shapes.Shape];
val box = Box(3, 7);
val infoOfBox = infoMap.get(box.getClass.getName)

The problem is that the names of the erased classes given by that macro are slightly different from the ones obtained at runtime by the someInstance.getClass.getName method. The first uses dots to separate container from members, and the second uses dollars.

scala> infoMap.mkString("\n")
val res7: String =
shapes.Box -> Info(some info)
shapes.Sphere -> Info(some info)

scala> box.getClass.getName
val res8: String = shapes$Box

How is the correct way to obtain at compile time the name that a class will have at runtime?

2

There are 2 answers

5
Dmytro Mitin On BEST ANSWER

Vice versa, at runtime, having Java name of a class (with dollars) you can obtain Scala name (with dots).

box.getClass.getName 
// com.example.App$shapes$Box

import scala.reflect.runtime
val runtimeMirror = runtime.currentMirror

runtimeMirror.classSymbol(box.getClass).fullName // com.example.App.shapes.Box

This can be done even with replace

val nameWithDot = box.getClass.getName.replace('$', '.')
if (nameWithDot.endsWith(".")) nameWithDot.init else nameWithDot 
// com.example.App.shapes.Box

Anyway, at compile time you can try

def javaName[T]: String = macro javaNameImpl[T]

def javaNameImpl[T: ctx.WeakTypeTag](ctx: blackbox.Context): ctx.Expr[String] = {
  import ctx.universe._
  val symbol = weakTypeOf[T].typeSymbol
  val owners = Seq.unfold(symbol)(symb => 
    if (symb != ctx.mirror.RootClass) Some((symb, symb.owner)) else None
  )
  val nameWithDollar = owners.foldRight("")((symb, str) => {
    val sep = if (symb.isPackage) "." else "$"
    s"$str${symb.name}$sep"
  })
  val name = if (symbol.isModuleClass) nameWithDollar else nameWithDollar.init
  ctx.Expr[String](q"${name: String}")
}

javaName[shapes.Shape] // com.example.App$shapes$Shape

One more option is to use runtime reflection inside a macro. Replace

val key = baseClassSymbol.asType.toType.erasure.typeSymbol.fullName

with

val key = javaName(baseClassSymbol.asType.toType.erasure.typeSymbol.asClass)

where

def subClassesOfImpl[T: ctx.WeakTypeTag](ctx: blackbox.Context): ctx.Expr[Map[String, Info]] = {
  import ctx.universe._

  def javaName(symb: ClassSymbol): String = {
    val rm = scala.reflect.runtime.currentMirror
    rm.runtimeClass(symb.asInstanceOf[scala.reflect.runtime.universe.ClassSymbol]).getName
  }

  ...
}

This works only with classes existing at compile time. So the project should be organized as follows

  • subproject common. Shape, Box, Sphere
  • subproject macros (depends on common). def subClassesOf...
  • subproject core (depends on macros and common). subclassesOf[shapes.Shape]...
0
Readren On

The answer from @Dmytro_Mitin helped me to notice that the scala reflect API does not offer a fast one line method to get the name that a class will have at runtime, and guided me solve my particular problem in another way.

If what you need to know is not the run-time class name itself but only if it matches the name accesible at compile-time, efficiently, then this answer may be useful.

Instead of figuring out what the name will be at runtime, which is apparently not possible before knowing all the classes; just find out if the name we know at compile time corresponds or not to one obtained at runtime. This can be achieved with a string comparator that considers the relevant character and ignores whatever the compiler appends later.

/** Compares two class names based on the length and, if both have the same length, by alphabetic order of the reversed names.
 * If the second name (`b`) ends with a dollar, or a dollar followed by digits, they are removed before the comparison begins. This is necessary because the runtime class name of: module classes have an extra dollar at the end, local classes have a dollar followed by digits, and local object digits surrounded by dollars.
 * Differences between dots and dollars are ignored if the dot is in the first name (`a`) and the dollar in the second name (`b`). This is necessary because the runtime class name of nested classes use a dollar instead of a dot to separate the container from members.
 * The names are considered equal if the fragments after the last dot of the second name (`b`) are equal. */
val productNameComparator: Comparator[String] = { (a, b) =>
    val aLength = a.length;
    var bLength = b.length;
    var bChar: Char = 0;
    var index: Int = 0;

    // Ignore the last segment of `b` if it matches "(\$\d*)+". This is necessary because the runtime class name of: module classes have an extra dollar at the end, local classes have a dollar followed by a number, and local object have a number surrounded by dollars.
    // Optimized versiĆ³n
    var continue = false;
    do {
        index = bLength - 1;
        continue = false;
        //  find the index of the last non digit character
        while ( {bChar = b.charAt(index); Character.isDigit(bChar)}) {
            index -= 1;
        }
        // if the index of the last non digit character is a dollar, remove it along with the succeeding digits for the comparison.
        if (b.charAt(index) == '$') {
            bLength = index;
            // if something was removed, repeat the process again to support combinations of edge cases. It is not necessary to know all the edge cases if it's known that any dollar or dollar followed by digits at the end are not part of the original class name. So we can remove combinations of them without fear.
            continue = true;
        }
    } while(continue)

    // here starts the comparison
    var diff = aLength - bLength;
    if (diff == 0 && aLength > 0) {
        index = aLength - 1;
        do {
            val aChar = a.charAt(index);
            bChar = b.charAt(index);
            diff = if (aChar == '.' && bChar == '$') {
                0 // Ignore difference between dots and dollars. This assumes that the first name (an) is obtained by the macro, and the second (bn) may be obtained at runtime from the Class object.
            } else {
                aChar - bChar;
            }
            index -= 1;
        } while (diff == 0 && index >= 0 && bChar != '.')
    }
    diff
}

Note that it is designed to compare names of classes that extend the same sealed trait or abstract class. Which means that the names may differ only after the last dot.

Also note that the first argument only supports a compile time name (only dots), while the second supports both, a compile-time or a run-time name.