Skip to content

Commit 8294257

Browse files
committed
Add @experimental annotation
The `@exerimental` annotation marks definitions as _experimental_ feature. These can be used in the same situattions where `languange.experimental` can be used. * A class is experimental if * It is annotated with `@experimental` * It is a nested class of an experimental class. Annotation `@experimental` is inferred. * It extends an experimental class. An error is emitted if it does not have @experimental. * A member definition is experimental if * It is annotated with `@experimental` * Its a member of an experimental class * `class experimental` is experimental
1 parent 2318a81 commit 8294257

18 files changed

+279
-15
lines changed

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,24 @@ 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+
report.error(i"$sym is marked @experimental and therefore may only be used with a nightly or snapshot version of the compiler", srcPos)
111109

112110
/** Check that experimental compiler options are only set for snapshot or nightly compiler versions. */
113111
def checkExperimentalSettings(using Context): Unit =
114112
for setting <- ctx.settings.language.value
115113
if setting.startsWith("experimental.") && setting != "experimental.macros"
116-
do checkExperimentalFeature(s" $setting")
114+
do checkExperimentalFeature(s"feature $setting", NoSourcePosition)
115+
116+
def isExperimentalEnabled(using Context): Boolean =
117+
def hasSpecialPermission =
118+
Thread.currentThread.getStackTrace.exists(elem =>
119+
assumeExperimentalIn.exists(elem.getClassName().startsWith(_)))
120+
(Properties.experimental || hasSpecialPermission) && !ctx.settings.YnoExperimental.value
117121

118122
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
@@ -3084,7 +3084,7 @@ object Parsers {
30843084
if prefix == nme.experimental
30853085
&& selectors.exists(sel => Feature.experimental(sel.name) != Feature.scala2macros)
30863086
then
3087-
Feature.checkExperimentalFeature("s", imp.srcPos)
3087+
Feature.checkExperimentalFeature("features", imp.srcPos)
30883088
for
30893089
case ImportSelector(id @ Ident(imported), EmptyTree, _) <- selectors
30903090
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import Symbols._, SymUtils._, NameOps._
1414
import ContextFunctionResults.annotateContextResults
1515
import config.Printers.typr
1616
import reporting._
17+
import util.Experimental
18+
1719

1820
object PostTyper {
1921
val name: String = "posttyper"
@@ -263,6 +265,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
263265
ctx
264266
super.transform(tree)(using gadtCtx)
265267
case tree: Ident if !tree.isType =>
268+
Experimental.checkExperimental(tree)
266269
if tree.symbol.is(Inline) && !Inliner.inInlineMethod then
267270
ctx.compilationUnit.needsInlining = true
268271
checkNoConstructorProxy(tree)
@@ -271,6 +274,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
271274
case _ => tree
272275
}
273276
case tree @ Select(qual, name) =>
277+
Experimental.checkExperimental(tree)
274278
if tree.symbol.is(Inline) then
275279
ctx.compilationUnit.needsInlining = true
276280
if (name.isTypeName) {
@@ -353,6 +357,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
353357
val sym = tree.symbol
354358
if (sym.isClass)
355359
VarianceChecker.check(tree)
360+
Experimental.annotateExperimental(sym)
356361
// Add SourceFile annotation to top-level classes
357362
if sym.owner.is(Package)
358363
&& ctx.compilationUnit.source.exists
@@ -388,6 +393,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
388393
Checking.checkRealizable(ref.tpe, ref.srcPos)
389394
super.transform(tree)
390395
case tree: TypeTree =>
396+
Experimental.checkExperimental(tree)
391397
tree.withType(
392398
tree.tpe match {
393399
case AnnotatedType(tpe, annot) => AnnotatedType(tpe, transformAnnot(annot))

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,16 @@ 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+
269+
def isNestedInExperimental(using Context): Boolean =
270+
self.ownersIterator.drop(1).exists(_.hasAnnotation(defn.ExperimentalAnnot))
271+
262272
/** The declared self type of this class, as seen from `site`, stripping
263273
* all refinements for opaque types.
264274
*/

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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ object RefChecks {
212212
* 1.9. If M is erased, O is erased. If O is erased, M is erased or inline.
213213
* 1.10. If O is inline (and deferred, otherwise O would be final), M must be inline
214214
* 1.11. If O is a Scala-2 macro, M must be a Scala-2 macro.
215+
* 1.12. If O is non-experimental, M must be non-experimental.
215216
* 2. Check that only abstract classes have deferred members
216217
* 3. Check that concrete classes do not have deferred definitions
217218
* that are not implemented in a subclass.
@@ -477,6 +478,8 @@ object RefChecks {
477478
overrideError(i"needs to be declared with @targetName(${"\""}${other.targetName}${"\""}) so that external names match")
478479
else
479480
overrideError("cannot have a @targetName annotation since external names would be different")
481+
else if !other.isExperimental && member.hasAnnotation(defn.ExperimentalAnnot) then // (1.12)
482+
overrideError("may not override non-experimental member")
480483
else
481484
checkOverrideDeprecated()
482485
}
@@ -1136,6 +1139,15 @@ object RefChecks {
11361139

11371140
end checkImplicitNotFoundAnnotation
11381141

1142+
1143+
/** Check that classes extending experimental classes or nested in experimental classes have the @experimental annotation. */
1144+
private def checkExperimentalInheritance(cls: ClassSymbol)(using Context): Unit =
1145+
if !cls.hasAnnotation(defn.ExperimentalAnnot) then
1146+
cls.info.parents.find(_.typeSymbol.isExperimental) match
1147+
case Some(parent) =>
1148+
report.error(em"extension of experimental ${parent.typeSymbol} must have @experimental annotation", cls.srcPos)
1149+
case _ =>
1150+
end checkExperimentalInheritance
11391151
}
11401152
import RefChecks._
11411153

@@ -1224,6 +1236,7 @@ class RefChecks extends MiniPhase { thisPhase =>
12241236
checkCompanionNameClashes(cls)
12251237
checkAllOverrides(cls)
12261238
checkImplicitNotFoundAnnotation.template(cls.classDenot)
1239+
checkExperimentalInheritance(cls)
12271240
tree
12281241
}
12291242
catch {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package dotty.tools.dotc
2+
package util
3+
4+
import dotty.tools.dotc.ast.tpd
5+
import dotty.tools.dotc.ast.Trees._
6+
import dotty.tools.dotc.config.Feature
7+
import dotty.tools.dotc.core.Contexts._
8+
import dotty.tools.dotc.core.Symbols._
9+
import dotty.tools.dotc.core.Types._
10+
import dotty.tools.dotc.core.Flags._
11+
import dotty.tools.dotc.transform.SymUtils._
12+
13+
object Experimental:
14+
import tpd._
15+
16+
def checkExperimental(tree: Tree)(using Context): Unit =
17+
if tree.symbol.isExperimental
18+
&& !tree.symbol.isConstructor // already reported on the class
19+
&& !ctx.owner.isExperimental // already reported on the @experimental of the owner
20+
&& !tree.symbol.is(ModuleClass) // already reported on the module
21+
&& (tree.span.exists || tree.symbol != defn.ExperimentalAnnot) // already reported on inferred annotations
22+
then
23+
Feature.checkExperimentalDef(tree.symbol, tree)
24+
25+
def checkExperimentalTypes(tree: Tree)(using Context): Unit =
26+
val checker = new TypeTraverser:
27+
def traverse(tp: Type): Unit =
28+
if tp.typeSymbol.isExperimental then
29+
Feature.checkExperimentalDef(tp.typeSymbol, tree)
30+
else
31+
traverseChildren(tp)
32+
if !tree.span.isSynthetic then // avoid double errors
33+
checker.traverse(tree.tpe)
34+
35+
def annotateExperimental(sym: Symbol)(using Context): Unit =
36+
if sym.isClass && !sym.hasAnnotation(defn.ExperimentalAnnot) then
37+
// Add @experimental annotation to all classes nested in an experimental definition
38+
if !sym.owner.is(Package) && sym.isNestedInExperimental then
39+
sym.addAnnotation(defn.ExperimentalAnnot)
40+
41+
// Add @experimental annotation to enum class definitions
42+
val compSym = sym.companionClass
43+
if compSym.is(Enum) && compSym.hasAnnotation(defn.ExperimentalAnnot) then
44+
sym.addAnnotation(defn.ExperimentalAnnot)
45+
sym.companionModule.addAnnotation(defn.ExperimentalAnnot)

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

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: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import scala.annotation.experimental
2+
3+
@experimental // error
4+
val x = ()
5+
6+
@experimental // error
7+
def f() = ()
8+
9+
@experimental // error
10+
class A:
11+
def f() = 1
12+
13+
@experimental // error
14+
class B extends A:
15+
override def f() = 2
16+
17+
@experimental // error
18+
type X
19+
20+
@experimental // error
21+
type Y = Int
22+
23+
@experimental // error
24+
opaque type Z = Int
25+
26+
@experimental // error
27+
object X:
28+
def fx() = 1
29+
30+
class C:
31+
@experimental // error
32+
def f() = 1
33+
34+
class D extends C:
35+
override def f() = 2
36+
37+
trait A2:
38+
@experimental // error
39+
def f(): Int
40+
41+
trait B2:
42+
def f(): Int
43+
44+
class C2 extends A2, B2:
45+
def f(): Int = 1
46+
47+
object Extractor1:
48+
def unapply(s: Any): Option[A] = ??? // error
49+
50+
object Extractor2:
51+
@experimental // error
52+
def unapply(s: Any): Option[Int] = ???
53+
54+
55+
@experimental // error
56+
trait ExpSAM {
57+
def foo(x: Int): Int
58+
}
59+
def bar(f: ExpSAM): Unit = {} // error
60+
61+
@experimental // error
62+
enum E:
63+
case A
64+
case B
65+
66+
def test(
67+
p1: A, // error
68+
p2: List[A], // error
69+
p3: X, // error
70+
p4: Y, // error
71+
p5: Z, // error
72+
): Unit =
73+
f() // error
74+
x // error
75+
new A // error
76+
new B // error
77+
X.fx() // error
78+
import X.fx // error
79+
fx() // error
80+
val i1 = identity[X] // error // error
81+
val i2 = identity[A] // error // error
82+
val a: A = ??? // error
83+
val b: B = ??? // error
84+
val c: C = ???
85+
val d: D = ???
86+
val c2: C2 = ???
87+
a.f() // error
88+
b.f() // error
89+
c.f() // error
90+
d.f() // ok because D.f is a stable API
91+
c2.f() // ok because B2.f is a stable API
92+
()
93+
94+
(??? : Any) match
95+
case _: A => // error // error
96+
case Extractor1(_) => // error
97+
case Extractor2(_) => // error
98+
99+
bar(x => x) // error
100+
101+
E.A // error
102+
E.B // error
103+
val e: E = ??? // error
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
class MyExperimentalAnnot extends scala.annotation.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 // error
4+
inline def g() = ()
5+
6+
def test: Unit =
7+
g() // errors
8+
()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
class MyExperimentalAnnot extends scala.annotation.experimental // error

0 commit comments

Comments
 (0)