0

I'm running into problems in my open-source project using Macros to generate some code. Everything works fine if I use c.untypecheck, but ideally I'd prefer not to have to do that.

This is the relevant code: https://github.com/outr/reactify/blob/master/shared/src/main/scala/com/outr/reactify/Macros.scala#L46

If I remove the c.untypecheck I get the following compile-time error:

[error] (reactifyJVM/test:compileIncremental) java.lang.AssertionError: assertion failed: 
[error]   transformCaseApply: name = previousVal tree = previousVal / class scala.reflect.internal.Trees$Ident
[error]      while compiling: /home/mhicks/projects/open-source/reactify/shared/src/test/scala/specs/BasicSpec.scala
[error]         during phase: refchecks
[error]      library version: version 2.12.1
[error]     compiler version: version 2.12.1
[error]   reconstructed args: -classpath /home/mhicks/projects/open-source/reactify/jvm/target/scala-2.12/test-classes:/home/mhicks/projects/open-source/reactify/jvm/target/scala-2.12/classes:/home/mhicks/.ivy2/cache/org.scala-lang/scala-reflect/jars/scala-reflect-2.12.1.jar:/home/mhicks/.ivy2/cache/org.scalatest/scalatest_2.12/bundles/scalatest_2.12-3.0.1.jar:/home/mhicks/.ivy2/cache/org.scalactic/scalactic_2.12/bundles/scalactic_2.12-3.0.1.jar:/home/mhicks/.ivy2/cache/org.scala-lang.modules/scala-xml_2.12/bundles/scala-xml_2.12-1.0.5.jar:/home/mhicks/.ivy2/cache/org.scala-lang.modules/scala-parser-combinators_2.12/bundles/scala-parser-combinators_2.12-1.0.4.jar -bootclasspath /usr/java/jdk1.8.0_92/jre/lib/resources.jar:/usr/java/jdk1.8.0_92/jre/lib/rt.jar:/usr/java/jdk1.8.0_92/jre/lib/sunrsasign.jar:/usr/java/jdk1.8.0_92/jre/lib/jsse.jar:/usr/java/jdk1.8.0_92/jre/lib/jce.jar:/usr/java/jdk1.8.0_92/jre/lib/charsets.jar:/usr/java/jdk1.8.0_92/jre/lib/jfr.jar:/usr/java/jdk1.8.0_92/jre/classes:/home/mhicks/.ivy2/cache/org.scala-lang/scala-library/jars/scala-library-2.12.1.jar
[error] 
[error]   last tree to typer: TypeTree(class Position)
[error]        tree position: line 148 of /home/mhicks/projects/open-source/reactify/shared/src/test/scala/specs/BasicSpec.scala
[error]             tree tpe: org.scalactic.source.Position
[error]               symbol: case class Position in package source
[error]    symbol definition: case class Position extends Product with Serializable (a ClassSymbol)
[error]       symbol package: org.scalactic.source
[error]        symbol owners: class Position
[error]            call site: <$anon: com.outr.reactify.ChangeListener[Int]> in package specs
[error] 
[error] == Source file context for tree position ==
[error] 
[error]    145       current should be(15)
[error]    146     }
[error]    147     "observe a complex change" in {
[error]    148       val v1 = Var(5)
[error]    149       val v2 = Var(10)
[error]    150       val v3 = Var(v1 + v2)
[error]    151       var changed = 0
[error] Total time: 1 s, completed Jan 31, 2017 4:43:03 PM

If I add it back everything compiles and works just fine. In more complex use-cases I've been encountering some issues at compile-time Could not find proxy for ... and I think this might be the reason.

Any suggestions would be greatly appreciated.

darkfrog
  • 1,053
  • 8
  • 29

1 Answers1

3

You're introducing an untyped tree into a typed tree.

The incoming tree is typechecked, and then the outgoing tree (that your macro emits) is typechecked again, but the typer does not descend into a tree that is already typechecked (i.e., that has a type already assigned to it).

Because you're introducing new symbols, you can't just use the incoming context to typecheck your reference.

So, the simplest solution is what you arrived at, to untypecheck the outgoing tree. It's also sufficient to untypecheck the transformed tree, to allow typer to descend to your new, untyped tree.

I had to reduce the exploding test by commenting out code. It's unfortunate that it's not immediately obvious what source line causes the error. Maybe it's more obvious if you're familiar with the macro involved.

class Sample {
  def sample(): Unit = {
    val v = Var(5)
    v := v + 5
  }
}

The tree in question, from -Xprint:typer -Yshow-trees:

            Apply( // def +(x: Int): Int in class Int, tree.tpe=Int
              com.outr.reactify.`package`.state2Value[Int](previousVal)."$plus" // def +(x: Int): Int in class Int, tree.tpe=(x: Int)Int
              5
            )

Also worth mentioning that it was easier to write a quick compile script with the "reconstructed args" in the error message, to eliminate sbt incremental compilation, ScalaTest macros and other mysteries.

Edit, the API for setting by hand:

def setStateChannel(value: c.Tree): c.Tree = {
  val observables = retrieveObservables(c)(value)
  val channel = c.prefix.tree
  val selfReference = observables.exists(_.equalsStructure(channel))

  val untyped =
    q"""
      val previousValue = com.outr.reactify.State.internalFunction($channel)
      val previousVal = com.outr.reactify.Val(previousValue())
    """
  val retyped = c.typecheck(untyped)
  val transformed = if (selfReference) {
    val transformer = new Transformer {
      override def transform(tree: c.universe.Tree): c.universe.Tree = if (tree.equalsStructure(channel)) {
        val t = q"previousVal"
        val Block(_ :: v :: Nil, _) = retyped
        c.internal.setSymbol(t, v.symbol)
        c.internal.setType(t, v.tpe)
      } else {
        super.transform(tree)
      }
    }
    transformer.transform(value)
  } else {
    value
  }
  val res = q"$channel.update(List(..$observables), $transformed)"
  q"$retyped ; $res"
}
Seth Tisue
  • 29,985
  • 11
  • 82
  • 149
som-snytt
  • 39,429
  • 2
  • 47
  • 129
  • https://github.com/outr/reactify/blob/master/shared/src/main/scala/com/outr/reactify/Macros.scala#L36 – som-snytt Feb 05 '17 at 21:27
  • I appreciate the feedback, but I'm a little unsure how this will get me closer to the solution. I recognize that it's the `previousVal` that it's exploding on, my issue is how to resolve that without using untyped trees. The purpose for the `previousVal` is to avoid recursive self-calling since `:=` takes in an anonymous function, so `v + 5` would create a recursive scenario. The `previousVal` grabs the value at assignment time and stores it as a static value representing the value at call-time keeping that from happening. – darkfrog Feb 06 '17 at 21:25
  • You can typecheck the assignments, then assign type and symbol to your Ident explicitly from those typed trees, but that's gnarly. Why don't you want to untypecheck again? – som-snytt Feb 06 '17 at 22:06
  • Initially I thought it was the cause of the `Could not find proxy for ...` on more complex use-cases, but the more I consider it the less sure I am. Perhaps this is just a red herring. – darkfrog Feb 07 '17 at 04:10
  • 1
    I don't know about that possible second problem. Did you know red herring is the same word in Swedish as def macro? It's an actual fish in the Baltic Sea that once sustained Viking expeditions as they explored or respectively marauded. Possibly it's a corruption of the Scandinavian word for deep mackerel. Or possibly I just made that up. – som-snytt Feb 07 '17 at 05:53