scalascalacscala-compilerscala-quasiquotes

How does the Scala compiler perform implicit conversion?


I have a custom class, A, and I have defined some operations within the class as follows:

def +(that: A) = ...
def -(that: A) = ...
def *(that: A) = ...

def +(that: Double) = ...
def -(that: Double) = ...
def *(that: Double) = ...

In order to have something like 2.0 + x make sense when x is of type A, I have defined the following implicit class:

object A {
  implicit class Ops (lhs: Double) {
    def +(rhs: A) = ...
    def -(rhs: A) = ...
    def *(rhs: A) = ...
  }
}

This all works fine normally. Now I introduce a compiler plugin with a TypingTransformer that performs some optimizations. Specifically, let's say I have a ValDef:

val x = y + a * z

where x, y, and z are of type A, and a is a Double. Normally, this compiles fine. I put it through the optimizer, which uses quasiquotes to change y + a * z into something else. BUT in this particular example, the expression is unchanged (there are no optimizations to perform). Suddenly, the compiler no longer does an implicit conversion for a * z.

To summarize, I have a compiler plugin that takes an expression that would normally have implicit conversions applied to it. It creates a new expression via quasiquotes, which syntactically appears the same as the old expression. But for this new expression, the compiler fails to perform implicit conversion.

So my question — how does the compiler determine that an implicit conversion must take place? Is there a specific flag or something that needs to be set in the AST that quasiquotes are failing to set?


UPDATE

The plugin phase looks something like this:

override def transform(tree: Tree) = tree match {
  case ClassDef(classmods, classname, classtparams, impl) if classname.toString == "Module" => {
    var implStatements: List[Tree] = List()
    for (node <- impl.body) node match {
      case DefDef(mods, name, tparams, vparamss, tpt, body) if name.toString == "loop" => {
        var statements: List[Tree] = List()
        for (statement <- body.children.dropRight(1)) statement match {
          case Assign(opd, rhs) => {
            val optimizedRHS = optimizeStatement(rhs)
            statements = statements ++ List(Assign(opd, optimizedRHS))
          }
          case ValDef(mods, opd, tpt, rhs) => {
            val optimizedRHS = optimizeStatement(rhs)
            statements = statements ++
              List(ValDef(mods, opd, tpt, optimizedRHS))
          }
          case Apply(Select(src1, op), List(src2)) if op.toString == "push" => {
            val optimizedSrc2 = optimizeStatement(src2)
            statements = statements ++
              List(Apply(Select(src1, op), List(optimizedSrc2)))
          }
          case _ => statements = statements ++ List(statement)
        }

        val newBody = Block(statements, body.children.last)
        implStatements = implStatements ++
          List(DefDef(mods, name, tparams, vparamss, tpt, newBody))
      }
      case _ => implStatements = implStatements ++ List(node)
    }
    val newImpl = Template(impl.parents, impl.self, implStatements)
    ClassDef(classmods, classname, classtparams, newImpl)
  }
  case _ => super.transform(tree)
}

def optimizeStatement(tree: Tree): Tree = {
  // some logic that transforms
  // 1.0 * x + 2.0 * (x + y)
  // into
  // 3.0 * x + 2.0 * y
  // (i.e. distribute multiplication & collect like terms)
  //
  // returned trees are always newly created
  // returned trees are create w/ quasiquotes
  // something like
  // 1.0 * x + 2.0 * y
  // will return
  // 1.0 * x + 2.0 * y
  // (i.e. syntactically unchanged)
}

UPDATE 2

Please refer to this GitHub repo for a minimum working example: https://github.com/darsnack/compiler-plugin-demo

The issue is that a * z turns into a.<$times: error>(z) after I optimize the statement.


Solution

  • The issue is related to the pos field associated with trees. Even though everything is happening before the namer, and the tree with and without the compiler plugin is syntactically the same, the compiler will not be able to infer implicit conversion due to this pesky line in the compiler source:

    val retry = typeErrors.forall(_.errPos != null) && (errorInResult(fun) || errorInResult(tree) || args.exists(errorInResult))
    

    (credit to hrhino for finding this).

    The solution is to always use treeCopy when creating a new tree so that all the internal flags/fields are copied:

    case Assign(opd, rhs) => {
      val optimizedRHS = optimizeStatement(rhs)
      statements = statements ++ List(treeCopy.Assign(statement, opd, optimizedRHS))
    }
    

    And when generating a tree using quasiquotes, remember to set the position:

    var optimizedNode = atPos(statement.pos.focus)(q"$optimizedSrc1.$newOp")
    

    I updated my MWP Github repo with the fixed solution: https://github.com/darsnack/compiler-plugin-demo