PRE-SIP: requireCompilerPlugin annotation

SIP-XXX: requireCompilerPlugin annotation.

Summary:

Problem: compiler-plugin can introduce binary incompatibility between code generated with and without plugin. To check such cases in the compile-time we can introduce method annotation “scala.annotation.requireCompilerPlugin(name, version),” which checks that the annotated method should be called only if the compiler plugin with the given name is enabled.

Motivation

Scala3 allows developers to write standalone compiler plugins, which are used for different purposes: generation of hosted code, simplified representation of computations, generation of boilerplate integration API, etc.

Usually, libraries are built with plugins distributed similarly, and any client can freely mix code built with and without compiler plugins. But often, compiler plugins change the binary call interface for some subset of processed code or enforce some constraints about the call interface. For example, scalus (GitHub - nau/scalus: Scalus - Scala implementation of Cardano Plutus) assumes that the caller of the compile function should use a compiler plugin; dotty-cps-async (GitHub - rssh/dotty-cps-async: experimental CPS transformer for dotty ) changes the binary representation of the functions which use direct context encoding; scout (GitHub - xebia-functional/scourt: Scala Coroutines Compiler Plugin ) change the binary representation of the coroutines, etc.

Application developers can call in the library method, which some compiler plugins change the binary interface, and the Scala compiler will accept this method. The error will be in runtime during the linking or calling of changed methods.

Proposed Solution:

Add a static annotation “scala.annotation.requireCompilerPlugin” to the standard library, which should be put on methods or objects whose signature is changed after erasure by a compiler plugin.

   final class requireCompilerPlugin(name: String, version: String) extends StaticAnnotation

Let’s define ‘method is required compiler plugin’’ if

  • the method is annotated by requireCompilerPlugin [or]

  • the template in which the method is defined is annotated by requireCompilerPlugin [or]

  • some of the types of parameters are annotated by requireCompilerPlugin [or]

  • method are annotated with annotations, which annotated by requireCompilerPlugin

    Add to the compiler check that compiler plugins are loaded with versions, compatible in terms of semantic versioning or report an appropriate error for all method invocations required.

Alternatives:

  1. Allow changing the tasty representation of the compiled code between typing and pickling for compiler plugins. In the current compiler code, the changes of the symbol signatures after typing are possible only after erasure. (see comment in compiler/src/dotty/tools/dotc/Compiler.scala ).
  • Pro:
    • In the long-term, the ability to transform a typed tree into another typed tree and write a transformed type tree into tasty is worth having. This direction is superior to the requireCompilerPlugin annotation in most cases.
  • Cons:
    • The compiler has relatively big changes (change the search of the overloaded terms).
    • This can require additional implementation effort from plugin authors.
    • Some analog of requireComplierPlugin is still needed when the compiler plugin requires interaction with some custom infrastructure at compile-time.
  1. Disallow changing the signatures by plugins.
  • Pro:

    • Simple
  • Cons:

    • Probably, this will kill all untrivial compiler plugins.

I think this is a too fine-grained approach. If a library is dependent on a compiler plugin then everything that is dependent on that library should also implicitly be dependent on that compiler plugin. This will also simplify the build files of applications that are dependent on a plugin-dependent library. They just need to include the library and the plugin will be implicitly included and turned on.
So I propose adding some kind of “requirePlugin” flag to the build process to initiate this instead.

The coarse-grained approach will make the requireCompilerPlugin property transitive, which I want to avoid: this means that developers will be not able to encapsulate the functionality of custom plugins in their code and export API which is usable without plugins.

(to include in the next version: also, we have some notion of platform, and plugins can be needed only for some platforms. Example: JUnit)