Reputation: 2454
There is a library X I'm working on, which depends on another library Y. To support multiple versions of Y, X publishes multiple artifacts named X_Y1.0
, X_Y1.1
, etc. This is done using multiple subprojects in SBT with version-specific source directories like src/main/scala-Y1.0
and src/main/scala-Y1.1
.
So far, it worked well. One minor problem is that sometimes version-specific source directories are too much. Sometimes they require a lot of code duplication because it's syntactically impossible to extract just the tiny differences into separate files. Sometimes doing so introduces performance overhead or makes the code unreadable.
Trying to solve the issue, I've added macro annotations to selectively delete a part of the code. It works like this:
class MyClass {
@UntilB1_0
def f: Int = 1
@SinceB1_1
def f: Int = 2
}
However, it seems it only works for methods. When I try to use the macro on fields, compilation fails with an error saying "f is already defined as value f". Also, it doesn't work for classes and objects.
My suspicion is that macros are applied during compilation before resolving method overloads, but after basic checks like checking duplicate names.
Is there a way to make the macros work for fields, classes, and objects too?
Here's an example macro to demonstrate the issue.
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)
}
}
When the annotation @Delete
is used on methods, it works.
class MyClass {
@Delete
def f: Int = 1
def f: Int = 2
}
// new MyClass().f == 2
However, it doesn't work for fields.
class MyClass {
@Delete
val f: Int = 1
val f: Int = 2
}
// error: f is already defined as value f
Upvotes: 4
Views: 108
Reputation: 1724
First of all, good idea :)
It is a strange (and quite uncontrollable) behaviour, and I think that what you want to do is difficult to perform with macros.
To understand why you expansions doesn't work, I tried to print all the scalac phases.
Your expansion works, indeed giving this code:
class Foo {
@Delete
lazy val x : Int = 12
val x : Int = 10
@Delete
def a : Int = 10
def a : Int = 12
}
the code printed after typer
is:
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
};
...
}
But, unfortunately, the error will be thrown anyway, I'm going to explain why this happens.
In scalac, macros are expanded -- at least in Scala 2.13 -- during the packageobjects phases (so after the parser and namer phases).
Here, different things happen, such as (as said here):
The essential problem here is that we cannot change the order, so it happens that invalid val references are checked before the method overloading, and macros expansion happen before method overloading check. For this reason @delete works with methods but it doesn't work with vals.
To solve your problem, I think that is necessary to use compiler plugin, here you can add a phase before the namer, so no error will be thrown. Build compiler plugin is more difficult of writing macros, but I think that is the best option for your case.
Upvotes: 2