MUnit

MUnit

  • Docs
  • Blog
  • GitHub

›All Blog Posts

All Blog Posts

  • Publish Scala 2 and Scala 3 macros together
  • Using ScalaCheck with MUnit
  • Cross-platform async testing support
  • MUnit is a new Scala testing library

Publish Scala 2 and Scala 3 macros together

January 5, 2021

Ólafur Páll Geirsson

Ólafur Páll Geirsson

The next release of MUnit makes use of a new compiler feature that allows you to publish Scala 2 and Scala 3 macros together in a single artifact. The blog post Forward Compatibility for the Scala 3 Transition by Jamie Thompson explains this feature in more detail. The Scala 3 Migration Guide contains a hands-on tutorial on how to use this feature. In this post, I want to share a small example to motivate why I think this feature will be critical for a smooth Scala 3 transition.

This blog post was written as part of a collaboration with the Scala Center.

While it's standard practice to cross-build a Scala library between 2.12 and 2.13, you should think twice before cross-building for Scala 2.13 and Scala 3. There is a chance you can skip 2.13 and publish only for Scala 3 instead. The reason you may want to skip 2.13 is to prevent unexpected runtime crashes.

To demonstrate how runtime crashes can happen, consider the following dependency graph for a Scala 3 application.

G app_3.0 app_3.0 a_library_3.0 a_library_3.0 app_3.0->a_library_3.0 b_library_2.13 b_library_2.13 app_3.0->b_library_2.13 munit_3.0 munit_3.0 a_library_3.0->munit_3.0 munit_2.13 munit_2.13 b_library_2.13->munit_2.13

The application has two direct dependencies (a_library_3.0, b_library_2.13) and one transitive dependency on MUnit. The problem is that the transitive MUnit dependency appears twice on the classpath: once for Scala 3 (munit_3.0) and once for Scala 2.13 (munit_2.13). If munit_3.0 and munit_2.13 have binary incompatibilities then the application may compile successfully but crash at runtime with a MethodNotFoundException or ClassNotFoundException.

The next release of MUnit avoids this problem by including Scala 2 macros in the munit_3.0 artifact. With this change, b_library_2.13 can depend on munit_3.0 and the dependency graph becomes like this instead.

G app_3.0 app_3.0 a_library_3.0 a_library_3.0 app_3.0->a_library_3.0 b_library_2.13 b_library_2.13 app_3.0->b_library_2.13 munit_3.0 munit_3.0 a_library_3.0->munit_3.0 b_library_2.13->munit_3.0

This change is possible thanks to the new -Ytasty-reader flag in the Scala 2.13.4 compiler that enables the Scala 2 compiler to read Scala 3 libraries.

This feature is new and has some limitations that's good to be aware of:

  • Scala 2.13.4 can only understand libraries that are published with the old Scala 3.0.0-M1 version (latest is 3.0.0-M3 at the time of this writing). The upcoming Scala 2.13.5 release will be able to understand newer Scala 3.x releases.
  • Some common features in Scala 2 macros don't work in Scala 3. Most notably, you can't use quasiquotes. In the case of MUnit, we had to replace typeOf[Location] with c.mirror.staticClass(classOf[Location].getName) because typeOf is itself implemented as a Scala 2 macro.
  • Your Scala 3 library needs a compile-time dependecy on scala-reflect:2.13.x,

Given these limitations, MUnit will continue to publish for 2.13. Nevertheless, it's a phenomenal achievement that it's at all possible to publish Scala 2 and Scala 3 macros together. For certain libraries, I'm optimistic this feature will be a critical component to smoothen the Scala 3 transition.

Recent Posts
MUnit
Overview
Getting started
Social
Copyright © 2023 Scalameta