8

Let's imagine passing these two equivalent expressions to a Scala macro:

  • with compiler-inferred implicit conversion: 1+"foo"
  • with explicitly invoked implicit conversion: any2stringadd(1)+"foo"

Is there a way to distinguish between these two inside the macro?

ghik
  • 10,706
  • 1
  • 37
  • 50

2 Answers2

3

First of all, the 1 + "foo" case is going to be tricky because there isn't actually any implicit conversion happening there: Int itself really, truly does have this + method (unfortunately).

So you're out of luck if that's your use case, but it is possible to do what you're describing more generally. I'll assume the following setup in my examples below:

case class Foo(i: Int)
case class Bar(s: String)
implicit def foo2bar(foo: Foo) = Bar(foo.i.toString)

First for the elegant approach:

object ConversionDetector {
  import scala.language.experimental.macros
  import scala.reflect.macros.Context

  def sniff[A](tree: _): Boolean = macro sniff_impl[A]
  def sniff_impl[A: c.WeakTypeTag](c: Context)(tree: c.Tree) = {
    // First we confirm that the code typechecks at all:
    c.typeCheck(tree, c.universe.weakTypeOf[A])

    // Now we try it without views:
    c.literal(
      c.typeCheck(tree, c.universe.weakTypeOf[A], true, true, false).isEmpty
    )
  }
}

Which works as desired:

scala> ConversionDetector.sniff[Bar](Foo(42))
res1: Boolean = true

scala> ConversionDetector.sniff[Bar](foo2bar(Foo(42)))
res2: Boolean = false

Unfortunately this requires untyped macros, which are currently only available in Macro Paradise.

You can get what you want with plain old def macros in 2.10, but it's a bit of a hack:

object ConversionDetector {
  import scala.language.experimental.macros
  import scala.reflect.macros.Context

  def sniff[A](a: A) = macro sniff_impl[A]
  def sniff_impl[A: c.WeakTypeTag](c: Context)(a: c.Expr[A]) = {
    import c.universe._

    c.literal(
      a.tree.exists {
        case app @ Apply(fun, _) => app.pos.column == fun.pos.column
        case _ => false
      }
    )
  }
}

And again:

scala> ConversionDetector.sniff[Bar](Foo(42))
res1: Boolean = true

scala> ConversionDetector.sniff[Bar](foo2bar(Foo(42)))
res2: Boolean = false

The trick is to look for places where we see function application in our abstract syntax tree, and then to check whether the positions of the Apply node and its fun child have the same column, which indicates that the method call isn't explicitly present in the source.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • Thanks for answering. I didn't know that `Int` has a method `+` that accepts `String` but I meant the general case anyway. I was thinking about using `Position`s too, but this indeed smells a bit 'hacky'. I think it would be cool if `Tree`s had some flag that would say "this tree was automatically inferred by compiler". – ghik Mar 20 '13 at 17:18
  • 1
    I just found a [comment](https://github.com/scala/scala/blob/master/src/reflect/scala/reflect/internal/Trees.scala#L431) in `scala-reflect` sources that talks about some potential flag on `Apply` AST that will indicate implicit conversion. And it looks like that now there is a separate class to indicate this (it is internal, unfortunately). – ghik Mar 21 '13 at 00:01
  • Unfortunately using the `Position` trick will mean your code blows up when used in the REPL. @ghik, I think you're on the right track with your last comment, which @EugeneBurmako's answer elaborates on. – Tom Crockett Apr 24 '13 at 06:47
2

That's a hack, but it might help you:

import scala.reflect.macros.Context
import language.experimental.macros

object Macros {
  def impl(c: Context)(x: c.Expr[Int]) = {
    import c.universe._
    val hasInferredImplicitArgs = x.tree.isInstanceOf[scala.reflect.internal.Trees#ApplyToImplicitArgs]
    val isAnImplicitConversion = x.tree.isInstanceOf[scala.reflect.internal.Trees#ApplyImplicitView]
    println(s"x = ${x.tree}, args = $hasInferredImplicitArgs, view = $isAnImplicitConversion")
    c.literalUnit
  }

  def foo(x: Int) = macro impl
}

import language.implicitConversions
import scala.reflect.ClassTag

object Test extends App {
  def bar[T: ClassTag](x: T) = x
  implicit def foo(x: String): Int = augmentString(x).toInt
  Macros.foo(2)
  Macros.foo(bar(2))
  Macros.foo("2")
}

08:30 ~/Projects/210x/sandbox (2.10.x)$ ss
x = 2, args = false, view = false
x = Test.this.bar[Int](2)(ClassTag.Int), args = true, view = false
x = Test.this.foo("2"), args = false, view = true
Eugene Burmako
  • 13,028
  • 1
  • 46
  • 59