darkfrog darkfrog - 2 years ago 154
Scala Question

Macro untypecheck required

I'm running into problems in my open-source project using Macros to generate some code. Everything works fine if I use

, 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
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] 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] == Source file context for tree position ==
[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.

Answer Source

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

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" }

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download