Skip to content

Commit bdab077

Browse files
Merge pull request #12102 from dotty-staging/add-experimental-annotation
Add `@experimental` annotation
2 parents 6a41a2b + 8c0fe71 commit bdab077

26 files changed

+351
-18
lines changed

compiler/src/dotty/tools/dotc/config/Feature.scala

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,33 @@ object Feature:
9999

100100
private val assumeExperimentalIn = Set("dotty.tools.vulpix.ParallelTesting")
101101

102-
def checkExperimentalFeature(which: String, srcPos: SrcPos = NoSourcePosition)(using Context) =
103-
def hasSpecialPermission =
104-
new Exception().getStackTrace.exists(elem =>
105-
assumeExperimentalIn.exists(elem.getClassName().startsWith(_)))
106-
if !(Properties.experimental || hasSpecialPermission)
107-
|| ctx.settings.YnoExperimental.value
108-
then
109-
//println(i"${new Exception().getStackTrace.map(_.getClassName).toList}%\n%")
110-
report.error(i"Experimental feature$which may only be used with nightly or snapshot version of compiler", srcPos)
102+
def checkExperimentalFeature(which: String, srcPos: SrcPos)(using Context) =
103+
if !isExperimentalEnabled then
104+
report.error(i"Experimental $which may only be used with a nightly or snapshot version of the compiler", srcPos)
105+
106+
def checkExperimentalDef(sym: Symbol, srcPos: SrcPos)(using Context) =
107+
if !isExperimentalEnabled then
108+
val symMsg =
109+
if sym eq defn.ExperimentalAnnot then
110+
i"use of @experimental is experimental"
111+
else if sym.hasAnnotation(defn.ExperimentalAnnot) then
112+
i"$sym is marked @experimental"
113+
else if sym.owner.hasAnnotation(defn.ExperimentalAnnot) then
114+
i"${sym.owner} is marked @experimental"
115+
else
116+
i"$sym inherits @experimental"
117+
report.error(s"$symMsg and therefore may only be used with a nightly or snapshot version of the compiler", srcPos)
111118

112119
/** Check that experimental compiler options are only set for snapshot or nightly compiler versions. */
113120
def checkExperimentalSettings(using Context): Unit =
114121
for setting <- ctx.settings.language.value
115122
if setting.startsWith("experimental.") && setting != "experimental.macros"
116-
do checkExperimentalFeature(s" $setting")
123+
do checkExperimentalFeature(s"feature $setting", NoSourcePosition)
124+
125+
def isExperimentalEnabled(using Context): Boolean =
126+
def hasSpecialPermission =
127+
Thread.currentThread.getStackTrace.exists(elem =>
128+
assumeExperimentalIn.exists(elem.getClassName().startsWith(_)))
129+
(Properties.experimental || hasSpecialPermission) && !ctx.settings.YnoExperimental.value
117130

118131
end Feature

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,7 @@ class Definitions {
909909
@tu lazy val ConstructorOnlyAnnot: ClassSymbol = requiredClass("scala.annotation.constructorOnly")
910910
@tu lazy val CompileTimeOnlyAnnot: ClassSymbol = requiredClass("scala.annotation.compileTimeOnly")
911911
@tu lazy val SwitchAnnot: ClassSymbol = requiredClass("scala.annotation.switch")
912+
@tu lazy val ExperimentalAnnot: ClassSymbol = requiredClass("scala.annotation.experimental")
912913
@tu lazy val ThrowsAnnot: ClassSymbol = requiredClass("scala.throws")
913914
@tu lazy val TransientAnnot: ClassSymbol = requiredClass("scala.transient")
914915
@tu lazy val UncheckedAnnot: ClassSymbol = requiredClass("scala.unchecked")

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3088,7 +3088,7 @@ object Parsers {
30883088
if prefix == nme.experimental
30893089
&& selectors.exists(sel => Feature.experimental(sel.name) != Feature.scala2macros)
30903090
then
3091-
Feature.checkExperimentalFeature("s", imp.srcPos)
3091+
Feature.checkExperimentalFeature("features", imp.srcPos)
30923092
for
30933093
case ImportSelector(id @ Ident(imported), EmptyTree, _) <- selectors
30943094
if allSourceVersionNames.contains(imported)

compiler/src/dotty/tools/dotc/plugins/Plugins.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package plugins
33

44
import core._
55
import Contexts._
6-
import config.{ PathResolver, Properties }
6+
import config.{ PathResolver, Feature }
77
import dotty.tools.io._
88
import Phases._
99
import config.Printers.plugins.{ println => debug }
@@ -125,7 +125,7 @@ trait Plugins {
125125
val updatedPlan = Plugins.schedule(plan, pluginPhases)
126126

127127
// add research plugins
128-
if (Properties.experimental)
128+
if (Feature.isExperimentalEnabled)
129129
plugins.collect { case p: ResearchPlugin => p }.foldRight(updatedPlan) {
130130
(plug, plan) => plug.init(options(plug), plan)
131131
}

compiler/src/dotty/tools/dotc/transform/PostTyper.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
255255

256256
override def transform(tree: Tree)(using Context): Tree =
257257
try tree match {
258+
// TODO move CaseDef case lower: keep most probable trees first for performance
258259
case CaseDef(pat, _, _) =>
259260
val gadtCtx =
260261
pat.removeAttachment(typer.Typer.InferredGadtConstraints) match
@@ -353,6 +354,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
353354
val sym = tree.symbol
354355
if (sym.isClass)
355356
VarianceChecker.check(tree)
357+
annotateExperimental(sym)
356358
// Add SourceFile annotation to top-level classes
357359
if sym.owner.is(Package)
358360
&& ctx.compilationUnit.source.exists
@@ -443,5 +445,11 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
443445
*/
444446
private def normalizeErasedRhs(rhs: Tree, sym: Symbol)(using Context) =
445447
if (sym.isEffectivelyErased) dropInlines.transform(rhs) else rhs
448+
449+
private def annotateExperimental(sym: Symbol)(using Context): Unit =
450+
if sym.is(Module) && sym.companionClass.hasAnnotation(defn.ExperimentalAnnot) then
451+
sym.addAnnotation(defn.ExperimentalAnnot)
452+
sym.companionModule.addAnnotation(defn.ExperimentalAnnot)
453+
446454
}
447455
}

compiler/src/dotty/tools/dotc/transform/SymUtils.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,13 @@ object SymUtils:
259259
&& self.owner.linkedClass.is(Case)
260260
&& self.owner.linkedClass.isDeclaredInfix
261261

262+
/** Is symbol declared or inherits @experimental? */
263+
def isExperimental(using Context): Boolean =
264+
// TODO should be add `@experimental` to `class experimental` in PostTyper?
265+
self.eq(defn.ExperimentalAnnot)
266+
|| self.hasAnnotation(defn.ExperimentalAnnot)
267+
|| (self.maybeOwner.isClass && self.owner.hasAnnotation(defn.ExperimentalAnnot))
268+
262269
/** The declared self type of this class, as seen from `site`, stripping
263270
* all refinements for opaque types.
264271
*/

compiler/src/dotty/tools/dotc/typer/Inliner.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Annotations.Annotation
2121
import SymDenotations.SymDenotation
2222
import Inferencing.isFullyDefined
2323
import config.Printers.inlining
24+
import config.Feature
2425
import ErrorReporting.errorTree
2526
import dotty.tools.dotc.util.{SimpleIdentityMap, SimpleIdentitySet, EqHashMap, SourceFile, SourcePosition, SrcPos}
2627
import dotty.tools.dotc.parsing.Parsers.Parser
@@ -93,6 +94,7 @@ object Inliner {
9394
if (tree.symbol == defn.CompiletimeTesting_typeChecks) return Intrinsics.typeChecks(tree)
9495
if (tree.symbol == defn.CompiletimeTesting_typeCheckErrors) return Intrinsics.typeCheckErrors(tree)
9596

97+
Feature.checkExperimentalDef(tree.symbol, tree)
9698

9799
/** Set the position of all trees logically contained in the expansion of
98100
* inlined call `call` to the position of `call`. This transform is necessary

compiler/src/dotty/tools/dotc/typer/RefChecks.scala

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import reporting._
2424
import scala.util.matching.Regex._
2525
import Constants.Constant
2626
import NullOpsDecorator._
27+
import dotty.tools.dotc.config.Feature
2728

2829
object RefChecks {
2930
import tpd._
@@ -212,6 +213,7 @@ object RefChecks {
212213
* 1.9. If M is erased, O is erased. If O is erased, M is erased or inline.
213214
* 1.10. If O is inline (and deferred, otherwise O would be final), M must be inline
214215
* 1.11. If O is a Scala-2 macro, M must be a Scala-2 macro.
216+
* 1.12. If O is non-experimental, M must be non-experimental.
215217
* 2. Check that only abstract classes have deferred members
216218
* 3. Check that concrete classes do not have deferred definitions
217219
* that are not implemented in a subclass.
@@ -477,6 +479,8 @@ object RefChecks {
477479
overrideError(i"needs to be declared with @targetName(${"\""}${other.targetName}${"\""}) so that external names match")
478480
else
479481
overrideError("cannot have a @targetName annotation since external names would be different")
482+
else if !other.isExperimental && member.hasAnnotation(defn.ExperimentalAnnot) then // (1.12)
483+
overrideError("may not override non-experimental member")
480484
else
481485
checkOverrideDeprecated()
482486
}
@@ -924,6 +928,7 @@ object RefChecks {
924928
// arbitrarily choose one as more important than the other.
925929
private def checkUndesiredProperties(sym: Symbol, pos: SrcPos)(using Context): Unit =
926930
checkDeprecated(sym, pos)
931+
checkExperimental(sym, pos)
927932

928933
val xMigrationValue = ctx.settings.Xmigration.value
929934
if xMigrationValue != NoScalaVersion then
@@ -964,6 +969,29 @@ object RefChecks {
964969
val since = annot.argumentConstant(1).map(" since " + _.stringValue).getOrElse("")
965970
report.deprecationWarning(s"${sym.showLocated} is deprecated${since}${msg}", pos)
966971

972+
private def checkExperimental(sym: Symbol, pos: SrcPos)(using Context): Unit =
973+
if sym.isExperimental
974+
&& !sym.isConstructor // already reported on the class
975+
&& !ctx.owner.isExperimental // already reported on the @experimental of the owner
976+
&& !sym.is(ModuleClass) // already reported on the module
977+
&& (sym.span.exists || sym != defn.ExperimentalAnnot) // already reported on inferred annotations
978+
then
979+
Feature.checkExperimentalDef(sym, pos)
980+
981+
private def checkExperimentalSignature(sym: Symbol, pos: SrcPos)(using Context): Unit =
982+
val checker = new TypeTraverser:
983+
def traverse(tp: Type): Unit =
984+
if tp.typeSymbol.isExperimental then
985+
Feature.checkExperimentalDef(tp.typeSymbol, pos)
986+
else
987+
traverseChildren(tp)
988+
if !sym.owner.isExperimental && !pos.span.isSynthetic then // avoid double errors
989+
checker.traverse(sym.info)
990+
991+
private def checkExperimentalAnnots(sym: Symbol)(using Context): Unit =
992+
for annot <- sym.annotations if annot.symbol.isExperimental && annot.tree.span.exists do
993+
Feature.checkExperimentalDef(annot.symbol, annot.tree)
994+
967995
/** If @migration is present (indicating that the symbol has changed semantics between versions),
968996
* emit a warning.
969997
*/
@@ -1136,6 +1164,15 @@ object RefChecks {
11361164

11371165
end checkImplicitNotFoundAnnotation
11381166

1167+
1168+
/** Check that classes extending experimental classes or nested in experimental classes have the @experimental annotation. */
1169+
private def checkExperimentalInheritance(cls: ClassSymbol)(using Context): Unit =
1170+
if !cls.hasAnnotation(defn.ExperimentalAnnot) then
1171+
cls.info.parents.find(_.typeSymbol.isExperimental) match
1172+
case Some(parent) =>
1173+
report.error(em"extension of experimental ${parent.typeSymbol} must have @experimental annotation", cls.srcPos)
1174+
case _ =>
1175+
end checkExperimentalInheritance
11391176
}
11401177
import RefChecks._
11411178

@@ -1192,6 +1229,8 @@ class RefChecks extends MiniPhase { thisPhase =>
11921229
override def transformValDef(tree: ValDef)(using Context): ValDef = {
11931230
checkNoPrivateOverrides(tree)
11941231
checkDeprecatedOvers(tree)
1232+
checkExperimentalAnnots(tree.symbol)
1233+
checkExperimentalSignature(tree.symbol, tree)
11951234
val sym = tree.symbol
11961235
if (sym.exists && sym.owner.isTerm) {
11971236
tree.rhs match {
@@ -1212,6 +1251,8 @@ class RefChecks extends MiniPhase { thisPhase =>
12121251
override def transformDefDef(tree: DefDef)(using Context): DefDef = {
12131252
checkNoPrivateOverrides(tree)
12141253
checkDeprecatedOvers(tree)
1254+
checkExperimentalAnnots(tree.symbol)
1255+
checkExperimentalSignature(tree.symbol, tree)
12151256
checkImplicitNotFoundAnnotation.defDef(tree.symbol.denot)
12161257
tree
12171258
}
@@ -1224,6 +1265,8 @@ class RefChecks extends MiniPhase { thisPhase =>
12241265
checkCompanionNameClashes(cls)
12251266
checkAllOverrides(cls)
12261267
checkImplicitNotFoundAnnotation.template(cls.classDenot)
1268+
checkExperimentalInheritance(cls)
1269+
checkExperimentalAnnots(cls)
12271270
tree
12281271
}
12291272
catch {
@@ -1268,6 +1311,17 @@ class RefChecks extends MiniPhase { thisPhase =>
12681311
}
12691312
tree
12701313
}
1314+
1315+
override def transformTypeTree(tree: TypeTree)(using Context): TypeTree = {
1316+
checkExperimental(tree.symbol, tree.srcPos)
1317+
tree
1318+
}
1319+
1320+
override def transformTypeDef(tree: TypeDef)(using Context): TypeDef = {
1321+
checkExperimental(tree.symbol, tree.srcPos)
1322+
checkExperimentalAnnots(tree.symbol)
1323+
tree
1324+
}
12711325
}
12721326

12731327
/* todo: rewrite and re-enable

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ class CompilationTests {
240240
Properties.compilerInterface, Properties.scalaLibrary, Properties.scalaAsm,
241241
Properties.dottyInterfaces, Properties.jlineTerminal, Properties.jlineReader,
242242
).mkString(File.pathSeparator),
243-
Array("-Ycheck-reentrant", "-language:postfixOps", "-Xsemanticdb", "-Yno-experimental")
243+
Array("-Ycheck-reentrant", "-language:postfixOps", "-Xsemanticdb")
244244
)
245245

246246
val libraryDirs = List(Paths.get("library/src"), Paths.get("library/src-bootstrapped"))
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package scala.annotation
2+
3+
/** An annotation that can be used to mark a definition as experimental.
4+
*
5+
* This class is experimental as well as if it was defined as
6+
* ```scala
7+
* @experimental
8+
* class experimental extends StaticAnnotation
9+
* ```
10+
*
11+
* @syntax markdown
12+
*/
13+
// @experimental
14+
class experimental extends StaticAnnotation
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
package scala.annotation.internal
2-
3-
import scala.annotation.Annotation
1+
package scala.annotation
2+
package internal
43

54
/** An annotation produced by Namer to indicate an erased parameter */
5+
@experimental
66
final class ErasedParam() extends Annotation

library/src/scala/quoted/Quotes.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package scala.quoted
22

3+
import scala.annotation.experimental
34
import scala.reflect.TypeTest
45

56
/** Current Quotes in scope
@@ -2179,6 +2180,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
21792180
/** Is this a given parameter clause `(using X1, ..., Xn)` or `(using x1: X1, ..., xn: Xn)` */
21802181
def isGiven: Boolean
21812182
/** Is this a erased parameter clause `(erased x1: X1, ..., xn: Xn)` */
2183+
@experimental
21822184
def isErased: Boolean
21832185
end TermParamClauseMethods
21842186

library/src/scala/util/FromDigits.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import scala.math.{BigInt}
33
import quoted._
44
import annotation.internal.sharable
55

6+
67
/** A type class for types that admit numeric literals.
78
*/
89
trait FromDigits[T] {
@@ -28,7 +29,7 @@ object FromDigits {
2829
trait WithRadix[T] extends FromDigits[T] {
2930
def fromDigits(digits: String): T = fromDigits(digits, 10)
3031

31-
/** Convert digits string with given radix to numberof type `T`.
32+
/** Convert digits string with given radix to number of type `T`.
3233
* E.g. if radix is 16, digits `a..f` and `A..F` are also allowed.
3334
*/
3435
def fromDigits(digits: String, radix: Int): T
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import scala.annotation.experimental
2+
3+
@experimental // error
4+
class myExperimentalAnnot extends scala.annotation.Annotation
5+
6+
@myExperimentalAnnot // error
7+
def test: Unit = ()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import scala.annotation.experimental
2+
3+
@experimental // error
4+
case class Foo(a: Int)
5+
6+
@experimental // error
7+
case class Bar(a: Int)
8+
9+
object Bar:
10+
def f(): Unit = ()
11+
12+
def test: Unit =
13+
Foo(2) // error
14+
val x: Foo = ??? // error
15+
16+
x match
17+
case Foo(a) => // error
18+
19+
20+
Bar(2) // error
21+
val y: Bar = ??? // error
22+
23+
y match
24+
case Bar(a) => // error
25+
26+
Bar.f() // error
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import scala.annotation.experimental
2+
3+
@experimental // error
4+
enum E:
5+
case A
6+
case B
7+
8+
def test: Unit =
9+
E.A // error
10+
E.B // error
11+
val e: E = ??? // error
12+
()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import scala.annotation.experimental
2+
3+
class MyExperimentalAnnot // error
4+
extends experimental // error
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import scala.annotation.experimental
2+
3+
@experimental
4+
inline def g() = ()
5+
6+
def test: Unit =
7+
g() // errors
8+
()

0 commit comments

Comments
 (0)