使用宏的条件编译适用于方法,但不适用于字段

Conditional compilation with macros work for methods, but not for fields

我正在处理一个库 X,它依赖于另一个库 Y。为了支持 Y 的多个版本,X 发布了多个名为 X_Y1.0X_Y1.1 等的工件。这是在 SBT 中使用多个子项目完成的,这些子项目具有特定于版本的源目录,例如 src/main/scala-Y1.0src/main/scala-Y1.1.

到目前为止,它运行良好。一个小问题是有时版本特定的源目录太多了。有时它们需要大量的代码重复,因为在语法上不可能将微小的差异提取到单独的文件中。有时这样做会引入性能开销或使代码不可读。

为了解决这个问题,我添加了宏注释以选择性地删除部分代码。它是这样工作的:

class MyClass {
  @UntilB1_0
  def f: Int = 1

  @SinceB1_1
  def f: Int = 2
}

但是,它似乎只适用于方法。当我尝试在字段上使用宏时,编译失败并显示错误“f 已定义为值 f”。此外,它不适用于 类 和对象。

我怀疑宏是在解析方法重载之前的编译期间应用的,但在检查重名等基本检查之后。

有没有办法让宏对字段、类 和对象也起作用?

这是一个演示该问题的示例宏。

import scala.annotation.{compileTimeOnly, StaticAnnotation}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

@compileTimeOnly("enable macro paradise to expand macro annotations")
class Delete extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro DeleteMacro.impl
}

object DeleteMacro {
  def impl(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._
    c.Expr[Nothing](EmptyTree)
  }
}

在方法上使用注释 @Delete 时,它会起作用。

class MyClass {
  @Delete
  def f: Int = 1

  def f: Int = 2
}
// new MyClass().f == 2

但是,它不适用于字段。

class MyClass {
  @Delete
  val f: Int = 1

  val f: Int = 2
}
// error: f is already defined as value f

首先,好主意:)

这是一种奇怪的(而且非常无法控制的)行为,我认为你想做的事情很难用宏来执行。

为了理解为什么你的扩展不起作用,我试着打印所有的 scalac 阶段。

你的扩展有效,确实给出了这个代码:

class Foo {
  @Delete
  lazy val x : Int = 12
  val x : Int = 10
  @Delete
  def a : Int = 10
  def a : Int = 12
}

typer后打印的代码是:

package it.unibo {
  class Foo extends scala.AnyRef {
    def <init>(): it.unibo.Foo = {
      Foo.super.<init>();
      ()
    };
    <empty>; //val removed
    private[this] val x: Int = 10;
    <stable> <accessor> def x: Int = Foo.this.x;
    <empty>; //def removed
    def a: Int = 12
  };
  ...
}

但是,不幸的是,无论如何都会抛出错误,我将解释为什么会发生这种情况。

在 scalac 中,宏被扩展——至少在 Scala 2.13 中——在 packageobjects 阶段(因此在解析器和命名器阶段之后)。

在这里,会发生不同的事情,例如(如前所述here):

  • 推断类型,
  • 检查类型是否匹配,
  • 搜索隐式参数并将它们添加到树中,
  • 进行隐式转换,
  • 检查是否允许所有类型操作(例如类型不能是其自身的子类型),
  • 解决重载,
  • 类型检查父引用,
  • 检查类型违规,
  • 搜索隐式,
  • 扩展宏,
  • 并为案例 类 创建额外的方法(如应用或复制)。

这里的本质问题是我们不能改变顺序,所以会发生在方法重载之前检查无效的val引用,而宏扩展发生在方法重载检查之前。由于这个原因,@delete 适用于方法,但不适用于 vals。

为了解决你的问题,我认为有必要使用compiler plugin,这里你可以在namer之前加一个phase,这样就不会报错了。 Build compiler plugin 编写宏比较困难,但我认为这是适合你的情况的最佳选择。