如何在宏中重用定义(AST)子树?

时间:2012-06-26 13:48:48

标签: scala macros abstract-syntax-tree scala-2.10

我正在使用Scala嵌入式DSL,宏正在成为实现我的目的的主要工具。我在尝试将传入宏表达式中的子树重用到结果宏中时遇到错误。情况非常复杂,但(我希望)我已将其简化为理解。

假设我们有这段代码:

val y = transform {
  val x = 3
  x
}
println(y) // prints 3

其中'transform'是涉及的宏。虽然看起来它什么都没有,但它确实将显示的块转换为这个表达式:

3 match { case x => x }

这是通过这个宏实现完成的:

def transform(c: Context)(block: c.Expr[Int]): c.Expr[Int] = {
  import c.universe._
  import definitions._

  block.tree match {
    /* {
     *   val xNam = xVal
     *   xExp
     * }
     */
    case Block(List(ValDef(_, xNam, _, xVal)), xExp) =>
      println("# " + showRaw(xExp)) // prints Ident(newTermName("x"))
      c.Expr(
        Match(
          xVal, 
          List(CaseDef(
            Bind(xNam, Ident(newTermName("_"))),
            EmptyTree,
            /* xExp */ Ident(newTermName("x")) ))))
    case _ => 
      c.error(c.enclosingPosition, "Can't transform block to function")
      block  // keep original expression
  }
}

请注意, xNam 与变量名对应, xVal 与其关联值对应,最后 xExp 与包含变量的表达式对应。好吧,如果我打印xExp原始树,我得到 Ident(newTermName(“x”)),这正是RHS案例中设置的内容。由于表达式可以修改(例如x + 2而不是x),这对我来说不是一个有效的解决方案。我想要做的是重新使用xExp树(参见xExp注释),同时改变'x'的意思(它是输入表达式中的一个定义,但在输出表达式中将是一个LHS变量的情况),但是它启动了长错误总结如下:

symbol value x does not exist in org.habla.main.Main$delayedInit$body.apply); see the error output for details.

我当前的解决方案在于解析xExp以使用新的Ident替换所有Idents,但它完全依赖于编译器内部,因此,是一种时间性的解决方法。很明显,xExp附带了showRaw提供的更多信息。如何清除xExp以允许'x'作为case变量的角色?任何人都可以解释这个错误的全貌吗?

PS:我一直试图使用TreeApi中的替代*方法系列失败,但我遗漏了基础知识来理解它的含义。

1 个答案:

答案 0 :(得分:21)

反汇编输入表达式并以不同方式重新组合它们是宏观中的一个重要场景(这是我们在reify宏内部执行的操作)。但不幸的是,目前并不是特别容易。

问题是宏到达宏实现的输入参数已经被类型化了。这既是一种祝福,也是一种诅咒。

我们特别感兴趣的是,已经建立了与参数对应的树中的变量绑定。这意味着所有IdentSelect个节点都填充了sym个字段,指向这些节点所引用的定义。

以下是符号如何工作的示例。我将从我的一个演讲中复制/粘贴打印输出(我在这里没有给出链接,因为我的谈话中的大部分信息现在已被弃用,但这个特定的打印输出具有永久的用处):

>cat Foo.scala
def foo[T: TypeTag](x: Any) = x.asInstanceOf[T]
foo[Long](42)

>scalac -Xprint:typer -uniqid Foo.scala
[[syntax trees at end of typer]]// Scala source: Foo.scala
def foo#8339
  [T#8340 >: Nothing#4658 <: Any#4657]
  (x#9529: Any#4657)
  (implicit evidence$1#9530: TypeTag#7861[T#8341])
  : T#8340 =
x#9529.asInstanceOf#6023[T#8341];
Test#14.this.foo#8339[Long#1641](42)(scala#29.reflect#2514.`package`#3414.mirror#3463.TypeTag#10351.Long#10361)

回顾一下,我们编写一个小片段然后用scalac编译它,要求编译器在typer阶段之后转储树,打印分配给树的符号的唯一ID(如果有的话)。

在生成的打印输出中,我们可以看到标识符已链接到相应的定义。例如,一方面,ValDef("x", ...)(表示方法foo的参数)定义了id = 9529的方法符号。另一方面,方法体中的Ident("x")将其sym字段设置为相同的符号,从而建立绑定。

好的,我们已经看到了绑定在scalac中是如何工作的,现在是引入基本事实的最佳时机。

If a symbol has been assigned to an AST node, 
then subsequent typechecks will never reassign it. 

这就是为什么reify是卫生的。您可以获取reify的结果并将其插入任意树(可能定义具有冲突名称的变量) - 原始绑定将保持不变。这是因为reify保留了原始符号,因此后续的类型检查不会重新绑定已确定的AST节点。

现在我们已准备好解释您所面临的错误:

symbol value x does not exist in org.habla.main.Main$delayedInit$body.apply); see the error output for details.

transform宏的参数包含定义和对变量x的引用。正如我们刚刚了解到的,这意味着相应的ValDef和Ident将同步sym个字段。到目前为止,非常好。

然而不幸的是,宏破坏了既定的绑定。它重新创建ValDef,但不清除相应Ident的sym字段。随后的类型检查会为新创建的ValDef指定一个新符号,但不会触及逐字复制到结果的原始Ident。

在类型检查之后,原始的Ident指向一个不再存在的符号(这正是错误消息所说的:)),这会导致字节码生成期间崩溃。

那么我们如何修复错误呢?不幸的是,没有简单的答案。

一种选择是利用c.resetLocalAttrs,它递归地擦除给定AST节点中的所有符号。然后,后续的类型检查将重新建立绑定,因为您生成的代码不会弄乱它们(例如,如果将xExp包装在一个块中,该块本身定义了一个名为x的值,那么您就遇到了麻烦)。

另一种选择是摆弄符号。例如,您可以编写自己的resetLocalAttrs,它只会删除损坏的绑定并且不会触及有效的绑定。你也可以尝试自己分配符号,但这是一条通向疯狂的道路,虽然有时会被迫走路。

根本不酷,我同意。我们意识到这一点,并打算有时尝试解决这个根本问题。但是现在我们的手在最终的2.10.0发布之前已经完成了错误修正,因此我们将无法在最近的将来解决问题。 UPD。有关其他信息,请参阅https://groups.google.com/forum/#!topic/scala-internals/rIyJ4yHdPDU


底线。糟糕的事情发生了,因为绑定搞砸了。首先尝试resetLocalAttrs,如果它不起作用,请准备好自己做家务。