11

我在 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' 的含义(它是输入表达式中的定义,但将是输出表达式中的 case LHS 变量),但它会启动一个长错误总结在:

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

我当前的解决方案包括解析 xExp 以用新的标识替换所有标识,但它完全依赖于编译器内部,因此是一种临时解决方法。很明显,xExp 附带了 showRaw 提供的更多信息。如何清理该 xExp 以允许“x”扮演案例变量的角色?谁能解释这个错误的全貌?

PS:我一直在尝试使用TreeApi中的替代 * 方法系列,但没有成功,但我缺少了解其含义的基础知识。

4

1 回答 1

22

分解输入表达式并以不同的方式重新组装它们是宏观学中的一个重要场景(这是我们在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 保留了原始符号,因此后续类型检查不会重新绑定 reified 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,但不清理sym相应 Ident 的字段。随后的类型检查为新创建的 ValDef 分配一个新符号,但不会触及逐字复制到结果的原始 Ident。

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

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

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

另一种选择是摆弄符号。例如,您可以编写自己的resetLocalAttrs,只删除损坏的绑定而不触及有效绑定。您也可以尝试自己分配符号,但这是一条通往疯狂的捷径,尽管有时人们被迫走它。

一点都不酷,我同意。我们意识到这一点,并打算有时尝试解决这个基本问题。然而,现在我们的手在 2.10.0 最终版本之前进行了错误修复,所以我们无法在最近的将来解决这个问题。更新。有关其他信息,请参阅https://groups.google.com/forum/#!topic/scala-internals/rIyJ4yHdPDU 。


底线。坏事发生了,因为绑定搞砸了。先试试resetLocalAttrs,如果还是不行,就为自己做点家务吧。

于 2012-06-26T15:18:35.060 回答