Allow Experimental 0.1.0 - implementation-scoped access to Scala 3 `@experimental` APIs

I’ve released the first version of Allow Experimental, a small Scala 3 compiler plugin:

The motivation is to separate two ideas that Scala’s normal @experimental mechanism deliberately couples:

  • an API is experimental;

  • a consumer intentionally accepts the risk of using that API internally.

For example:

import scala.annotation.experimental
import io.github.dmytromitin.allowexperimental.allowExperimental

@experimental
def provider(): Int = 1

@allowExperimental
def allowed(): Int =
  provider()

def ordinaryCaller(): Int =
  allowed()

allowed may use the experimental API in its implementation, but callers of allowed do not themselves become experimental. A direct unmarked call to provider() still fails with the normal Scala experimental-use diagnostic.

The release currently supports exactly Scala 3.3.8, 3.8.4, and 3.9.0. The compiler plugin is full-crossed because it depends on compiler internals.

ThisBuild / scalaVersion := "3.9.0" // 3.3.8, 3.8.4

libraryDependencies ++= Seq(
  "com.github.dmytromitin" %% "allow-experimental-annotation" % "0.1.0" % Provided,
  compilerPlugin(("com.github.dmytromitin" % "allow-experimental-plugin" % "0.1.0").cross(CrossVersion.full)),
)

One practical use case is macro implementations: a public inline macro frontend can delegate to a private non-inline @allowExperimental implementation that uses an experimental compiler/reflection API, without leaking that requirement to downstream users.

The scope is intentionally conservative rather than a general replacement for scalacOptions += "-experimental". Public inline permission owners, experimental signatures/types, constructors, and several other placements remain unsupported.

0.1.0 is available from Maven Central and the release is at Github.

Feedback on the semantics, implementation approach, and useful real-world cases would be very welcome.

Cross-posted at Reddit

Hello, while a cool project, I would highly recommend against using it !

As you hinted at, there is a very good reason these things are coupled, it goes as follows:
Assume a library uses experimental feature F to do X

  • If your library requires F to implement X, then it must be experimental as well, since X will need to evolve as F evolves !
  • If your library can implement X without F, then you refactor it, experimental features are bound to change, and you should not rely on them for your stable library

This was a hard-learned lesson from Scala 2’s macros, which were experimental, but could be used by regular libraries, and so spread over the whole ecosystem.
Since some things were only possible using them, even changing them could require vast amount of code to need to be updated, and removing them entirely was impossible.
AFAIK the most difficult part in translating code from Scala 2 to Scala 3 is the macros.
(If I made any mistake in the above, feel free to correct me)

11 Likes