Macro libraries must be re-implemented from the ground-up.
Before starting you should be familiar with the Scala 3 migration as described in the Porting an sbt Project tutorial. The purpose of the current tutorial is to cross-build an existing Scala 2.13 macro library so that it becomes available in both Scala 3 and Scala 2.13.
An alternative solution called Mixing Macros is explained in the next tutorial. You are encouraged to read both solutions to choose the technique that is best suited for your needs.
Introduction
In order to exemplify this tutorial, we will consider the minimal macro library defined below.
// build.sbt
lazy val example = project
.in(file("example"))
.settings(
scalaVersion := "2.13.11",
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-reflect" % scalaVersion.value
)
)
// example/src/main/scala/location/Location.scala
package location
import scala.reflect.macros.blackbox.Context
import scala.language.experimental.macros
case class Location(path: String, line: Int)
object Macros {
def location: Location = macro locationImpl
private def locationImpl(c: Context): c.Tree = {
import c.universe._
val location = typeOf[Location]
val line = Literal(Constant(c.enclosingPosition.line))
val path = Literal(Constant(c.enclosingPosition.source.path))
q"new $location($path, $line)"
}
}
You should recognize some similarities with your library:
one or more macro methods, in our case the location
method, are implemented by consuming a macro Context
and returning a Tree
from this context.
We can make this library available for Scala 3 users by using the Cross Building technique provided by sbt.
The main idea is to build the artifact twice and to publish two releases:
example_2.13
for Scala 2.13 usersexample_3
for Scala 3 users
1. Set cross-building up
You can add Scala 3 to the list of crossScalaVersions
of your project:
crossScalaVersions := Seq("2.13.11", "3.3.1")
The scala-reflect
dependency won’t be useful in Scala 3.
Remove it conditionally with something like:
// build.sbt
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, 13)) => Seq(
"org.scala-lang" % "scala-reflect" % scalaVersion.value
)
case _ => Seq.empty
}
}
After reloading sbt, you can switch to the Scala 3 context by running ++3.3.1
.
At any point you can go back to the Scala 2.13 context by running ++2.13.11
.
2. Rearrange the code in version-specific source directories
If you try to compile with Scala 3 you should see some errors of the same kind as:
To provide a Scala 3 alternative while preserving the Scala 2 implementation, we are going to rearrange the code in version-specific source directories.
All the code that cannot be compiled by the Scala 3 compiler goes to the src/main/scala-2
folder.
Scala version-specific source directories is an sbt feature that is available by default. Learn more about it in the sbt documentation.
In our example, the Location
class stays in the src/main/scala
folder but the Macros
object is moved to the src/main/scala-2
folder:
// example/src/main/scala-2/location/Macros.scala
package location
import scala.reflect.macros.blackbox.Context
import scala.language.experimental.macros
object Macros {
def location: Location = macro locationImpl
private def locationImpl(c: Context): c.Tree = {
import c.universe._
val location = typeOf[Location]
val line = Literal(Constant(c.enclosingPosition.line))
val path = Literal(Constant(c.enclosingPosition.source.path))
q"new $location($path, $line)"
}
}
Now we can initialize each of our Scala 3 macro definitions in the src/main/scala-3
folder.
They must have the exact same signature than their Scala 2.13 counterparts.
// example/src/main/scala-3/location/Macros.scala
package location
object Macros:
inline def location: Location = ???
3. Implement the Scala 3 macro
There is no magic formula to port a Scala 2 macro into Scala 3. One needs to learn about the new Metaprogramming features.
We eventually come up with this implementation:
// example/src/main/scala-3/location/Macros.scala
package location
import scala.quoted.{Quotes, Expr}
object Macros:
inline def location: Location = ${locationImpl}
private def locationImpl(using quotes: Quotes): Expr[Location] =
import quotes.reflect.Position
val pos = Position.ofMacroExpansion
val file = Expr(pos.sourceFile.path.toString)
val line = Expr(pos.startLine + 1)
'{new Location($file, $line)}
4. Cross-validate the macro
Adding some tests is important to check that the macro method works the same in both Scala versions.
In our example, we add a single test.
You should now be able to run the tests in both versions.
Final overview
Your macro project should now contain the following source files:
src/main/scala/*.scala
: Cross-compatible classessrc/main/scala-2/*.scala
: The Scala 2 implementation of the macro methodssrc/main/scala-3/*.scala
: The Scala 3 implementation of the macro methodssrc/test/scala/*.scala
: Common tests
You are now ready to publish your library by creating two releases:
example_2.13
for Scala 2.13 usersexample_3
for Scala 3 users