How does the Scala compiler perform implicit conversion?
Asked Answered
P

1

7

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.

Paola answered 14/4, 2019 at 23:11 Comment(6)
In what phase does this plugin work? docs.scala-lang.org/overviews/plugins/index.htmlAitchbone
Can you provide code of plugin (or of its simplified version that still remains such erroneous behavior)?Aitchbone
Unfortunately, the plugin/custom types/etc. are too large of a codebase to easily display here. This is why I have struggled with a minimum working example. I will update my question to provide more detail.Paola
Probably this depends on optimizeStatement. With trivial def optimizeStatement(tree: Tree): Tree = tree implicit conversions compile github.com/DmytroMitin/compiler-plugin-demoAitchbone
Yes, it does have to do with how I generate the optimized statements. I am having trouble understanding why. I have forked your repo and created a minimum working example of the issue. Do you mind taking a look?Paola
@DmytroMitin I am new to Stack Overflow as a member. Is it reasonable to post a new question linking to this one? It doesn't seem like this post is getting much traction anymore.Paola
P
4

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

Paola answered 4/5, 2019 at 17:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.