scalascala-macros

Crashing the compiler with a "MatchError: AnyRef" when I call my scala macro


Edit: I've fixed the problem - I was incorrectly calling .map(f => f.typeSignature.asInstanceOf[TypeRef].args.head) on recursiveOpt, which meant that field.name was giving me the wrong field name in my copy method. I've removed the map and everything is working now.


I am writing a macro that will create a map of update methods for a case class, e.g.

case class Inner(innerStr: String)
case class Outer(outerStr: String, inner: Inner, innerOpt: Option[Inner])

should produce an update map for Outer that is something like

val innerMap = Map("innerStr" -> {(json: JsValue) => Try{(inner: Inner) => inner.copy(innerStr = json)}})

val outerMap = Map("outerStr" -> {(json: JsValue) => Try{(outer: Outer) => outer.copy(outerStr = json)}},
  "inner.innerStr" -> {(json: JsValue) => Try{(outer: Outer) => innerMap.get("innerStr").get(json).flatMap(update => outer.copy(inner = update(outer.inner)))},
  "innerOpt.innerStr" -> {(json: JsValue) => Try{(outer: Outer) => innerMap.get("innerStr").get(json).flatMap(update => outer.copy(inner = outer.inner.map(inner => update(inner))))})

which would then be called like

val oldOuter = Outer("str", Inner("str"), Some(Inner("str")))
val updatedOuter = outerMap.get("inner.innerStr").get(JsString("newStr")).get(oldOuter)

The idea is that given a json kv pair, I can retrieve the appropriate update method from the map using the key and then apply the update using the value, using implicit conversions to convert from the json value to the appropriate type.

My macro is working for the case of a flat case class, e.g. Inner(innerStr: String), and for a nested case class, e.g. Outer(outerStr: String, inner: Inner). However, the case of the nested option case class, Outer(outerStr: String, innerOpt: Option[Inner]), is crashing the compiler. I'm not sure if I'm doing something disastrously wrong, or if there's a bug in the compiler, or third option. This was done using the Scala 2.11.0-M7 REPL

Below is my code - I'm constructing a Map that accepts String input instead of JsValue input so that I don't need to import the play framework into my REPL. The blacklist filters out fields that should not be in the update map (e.g. one of the case classes we're applying this to has fields like "crypted_password" and "salt" that should never be updated via json sent in through a REST route). baseMethods constructs the key -> method tuples for the flat case, recursiveMethods constructs the key-method tuples for the nested case, and recursiveOptMethods constructs the key-value tuples for the nested option case; at the bottom of the macro these are all merged into a flat sequence and a placed in a Map.

I've tested the code in the recursiveOptMethods quasiquotes to ensure that I'm constructing a properly typed sequence of tuples and haven't found an error (also, this code is extremely similar to the recursiveMethods quasiquotes, which are functioning correctly), and I've tested the code that constructs the base, recursive, and recursiveOpt sequences of symbols and they seem to be doing their job.

Any help as to why I'm crashing the compiler would be greatly appreciated.


import scala.language.experimental.macros

def copyTestImpl[T: c.WeakTypeTag](c: scala.reflect.macros.Context)(blacklist: c.Expr[String]*): c.Expr[Map[String, (String) => scala.util.Try[(T) => T]]] = {
  import c.universe._

  val blacklistList: Seq[String] = blacklist.map(e => c.eval(c.Expr[String](c.resetAllAttrs(e.tree))))

  def isCaseClass(tpe: Type): Boolean = tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isCaseClass

  def isCaseClassOpt(tpe: Type): Boolean = tpe.typeSymbol.name.decoded == "Option" && isCaseClass(tpe.asInstanceOf[TypeRef].args.head)

  def rec(tpe: Type): c.Expr[Map[String, (String) => scala.util.Try[(T) => T]]] = {
    val typeName = tpe.typeSymbol.name.decoded

    val fields = tpe.declarations.collectFirst {
      case m: MethodSymbol if m.isPrimaryConstructor => m
    }.get.paramss.head.filterNot(field => blacklistList.contains(typeName + "." + field.name.decoded))

    val recursive = fields.filter(f => isCaseClass(f.typeSignature))
    val recursiveOpt = fields.filter(f => isCaseClassOpt(f.typeSignature))
    val base = fields.filterNot(f => isCaseClass(f.typeSignature) || isCaseClassOpt(f.typeSignature))

    val recursiveMethods = recursive.map {
      field => {
        val fieldName = field.name
        val fieldNameDecoded = fieldName.decoded
        val map = rec(field.typeSignature)
        q"""{
            val innerMap = $map
            innerMap.toSeq.map(tuple => ($fieldNameDecoded + "." + tuple._1) -> {
            (str: String) => {
              val innerUpdate = tuple._2(str)
              innerUpdate.map(innerUpdate => (outer: $tpe) => outer.copy($fieldName = innerUpdate(outer.$fieldName)))
            }
        })}"""
      }}

    val recursiveOptMethods = recursiveOpt.map {
      field => {
        val fieldName = field.name
        val fieldNameDecoded = fieldName.decoded
        val map = rec(field.typeSignature.asInstanceOf[TypeRef].args.head)
        q"""{
            val innerMap = $map
            innerMap.toSeq.map(tuple => ($fieldNameDecoded + "." + tuple._1) -> {
            (str: String) => {
              val innerUpdate = tuple._2(str)
              innerUpdate.map(innerUpdate => (outer: $tpe) => outer.copy($fieldName = (outer.$fieldName).map(inner => innerUpdate(inner))))
            }
        })}"""
      }}

    val baseMethods = base.map {
      field => {
        val fieldName = field.name
        val fieldNameDecoded = fieldName.decoded
        val fieldType = field.typeSignature
        val fieldTypeName = fieldType.toString
        q"""{
          $fieldNameDecoded -> {
            (str: String) => scala.util.Try {
              val x: $fieldType = str
              (t: $tpe) => t.copy($fieldName = x)
            }.recoverWith {
              case e: Exception => scala.util.Failure(new IllegalArgumentException("Failed to parse " + str + " as " + $typeName + "." + $fieldNameDecoded + ": " + $fieldTypeName))
            }
         }}"""
      }}

    c.Expr[Map[String, (String) => scala.util.Try[(T) => T]]] {
      q"""{ Map((List(..$recursiveMethods).flatten ++ List(..$recursiveOptMethods).flatten ++ List(..$baseMethods)):_*) }"""
    }
  }

  rec(weakTypeOf[T])
}

def copyTest[T](blacklist: String*) = macro copyTestImpl[T]

And the top and bottom of my error from the 2.11.0-M7 REPL when calling copyTest[Outer]() (where Outer has an Option[Inner] field)

scala> copyTest[Outer]()

scala.MatchError: AnyRef
    with Product
    with Serializable {
  val innerStr: String
  private[this] val innerStr: String
  def <init>(innerStr: String): Inner
  def copy(innerStr: String): Inner
  def copy$default$1: String @scala.annotation.unchecked.uncheckedVariance
  override def productPrefix: String
  def productArity: Int
  def productElement(x$1: Int): Any
  override def productIterator: Iterator[Any]
  def canEqual(x$1: Any): Boolean
  override def hashCode(): Int
  override def toString(): String
  override def equals(x$1: Any): Boolean
} (of class scala.reflect.internal.Types$ClassInfoType)
    at scala.reflect.internal.Variances$class.inType$1(Variances.scala:181)
    at scala.reflect.internal.Variances$$anonfun$inArgs$1$1.apply(Variances.scala:176)
    at scala.reflect.internal.Variances$$anonfun$inArgs$1$1.apply(Variances.scala:176)
    at scala.reflect.internal.util.Collections$class.map2(Collections.scala:55)
    at scala.reflect.internal.SymbolTable.map2(SymbolTable.scala:14)
    at scala.reflect.internal.Variances$class.inArgs$1(Variances.scala:176)
    at scala.reflect.internal.Variances$class.inType$1(Variances.scala:189)
    at scala.reflect.internal.Variances$$anonfun$inArgs$1$1.apply(Variances.scala:176)
    at scala.reflect.internal.Variances$$anonfun$inArgs$1$1.apply(Variances.scala:176)
    at scala.reflect.internal.util.Collections$class.map2(Collections.scala:55)
    at scala.reflect.internal.SymbolTable.map2(SymbolTable.scala:14)

at scala.tools.nsc.typechecker.Analyzer$typerFactory$$anon$3.run(Analyzer.scala:93)
at scala.tools.nsc.Global$Run.compileUnitsInternal(Global.scala:1603)
at scala.tools.nsc.Global$Run.compileUnits(Global.scala:1588)
at scala.tools.nsc.Global$Run.compileSources(Global.scala:1583)
at scala.tools.nsc.interpreter.IMain.compileSourcesKeepingRun(IMain.scala:387)
at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.compileAndSaveRun(IMain.scala:816)
at scala.tools.nsc.interpreter.IMain$ReadEvalPrint.compile(IMain.scala:775)
at scala.tools.nsc.interpreter.IMain$Request.compile$lzycompute(IMain.scala:951)
at scala.tools.nsc.interpreter.IMain$Request.compile(IMain.scala:946)
at scala.tools.nsc.interpreter.IMain.compile(IMain.scala:530)
at scala.tools.nsc.interpreter.IMain.interpret(IMain.scala:518)
at scala.tools.nsc.interpreter.IMain.interpret(IMain.scala:516)
at scala.tools.nsc.interpreter.ILoop.reallyInterpret$1(ILoop.scala:748)
at scala.tools.nsc.interpreter.ILoop.interpretStartingWith(ILoop.scala:793)
at scala.tools.nsc.interpreter.ILoop.command(ILoop.scala:660)
at scala.tools.nsc.interpreter.ILoop.processLine(ILoop.scala:427)
at scala.tools.nsc.interpreter.ILoop.loop(ILoop.scala:444)
at scala.tools.nsc.interpreter.ILoop$$anonfun$process$1.apply$mcZ$sp(ILoop.scala:862)
at scala.tools.nsc.interpreter.ILoop$$anonfun$process$1.apply(ILoop.scala:848)
at scala.tools.nsc.interpreter.ILoop$$anonfun$process$1.apply(ILoop.scala:848)
at scala.reflect.internal.util.ScalaClassLoader$.savingContextLoader(ScalaClassLoader.scala:95)
at scala.tools.nsc.interpreter.ILoop.process(ILoop.scala:848)
at scala.tools.nsc.MainGenericRunner.runTarget$1(MainGenericRunner.scala:81)
at scala.tools.nsc.MainGenericRunner.process(MainGenericRunner.scala:94)
at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:103)
at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)

That entry seems to have slain the compiler.  Shall I replay
your session? I can re-run each line except the last one.

Solution

  • I found the problem - originally I had val recursiveOpt = fields.filter(f => isCaseClassOpt(f.typeSignature)).map(f => f.typeSignature.asInstanceOf[TypeRef].args.head), which meant that when I called field.name on the recursiveOpt fields I was getting the wrong name back.