Macro libraries must be re-implemented from the ground-up.
Before starting you should be familiar with the Scala 3 migration as described in Migrating to Scala 3.3. This page shows how 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 Mixing Scala 2.13 and Scala 3 Macros. You are encouraged to read both solutions to choose the technique that is best suited for your needs.
Introduction
As an example, consider the minimal macro library defined below.
// build.sbt
lazy val example = project
.in(file("example"))
.settings(
scalaVersion := "2.13.18",
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.13for Scala 2.13 usersexample_3for 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.18", "3.3.8")
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.8.
At any point you can go back to the Scala 2.13 context by running ++2.13.18.
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:
sbt:example> ++3.3.8
sbt:example> example / compile
[error] -- Error: /example/src/main/scala/location/Location.scala:15:35
[error] 15 | val location = typeOf[Location]
[error] | ^
[error] | No TypeTag available for location.Location
[error] -- Error: /example/src/main/scala/location/Location.scala:18:4
[error] 18 | q"new $location($path, $line)"
[error] | ^
[error] |Scala 2 macro cannot be used in Dotty. See https://nightly.scala-lang.org/docs/reference/dropped-features/macros.html
[error] |To turn this error into a warning, pass -Xignore-scala2-macros to the compilerTo 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.
sbt:example> ++2.13.18
sbt:example> example / test
location.MacrosSpec:
+ location
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success]
sbt:example> ++3.3.8
sbt:example> example / test
location.MacrosSpec:
+ location
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success]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.13for Scala 2.13 usersexample_3for Scala 3 users