Skip to content

Commit c4edfde

Browse files
committed
Change @experimental spec
1 parent 35eb5a9 commit c4edfde

27 files changed

+553
-71
lines changed

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,13 @@ object Feature:
106106
def checkExperimentalDef(sym: Symbol, srcPos: SrcPos)(using Context) =
107107
if !isExperimentalEnabled then
108108
val symMsg =
109-
if sym eq defn.ExperimentalAnnot then
110-
i"use of @experimental is experimental"
111-
else if sym.hasAnnotation(defn.ExperimentalAnnot) then
109+
if sym.hasAnnotation(defn.ExperimentalAnnot) then
112110
i"$sym is marked @experimental"
113111
else if sym.owner.hasAnnotation(defn.ExperimentalAnnot) then
114112
i"${sym.owner} is marked @experimental"
115113
else
116114
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)
115+
report.error(s"$symMsg and therefore may only be used in an experimental scope.", srcPos)
118116

119117
/** Check that experimental compiler options are only set for snapshot or nightly compiler versions. */
120118
def checkExperimentalSettings(using Context): Unit =

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,11 +263,13 @@ object SymUtils:
263263

264264
/** Is symbol declared or inherits @experimental? */
265265
def isExperimental(using Context): Boolean =
266-
// TODO should be add `@experimental` to `class experimental` in PostTyper?
267-
self.eq(defn.ExperimentalAnnot)
268-
|| self.hasAnnotation(defn.ExperimentalAnnot)
266+
self.hasAnnotation(defn.ExperimentalAnnot)
269267
|| (self.maybeOwner.isClass && self.owner.hasAnnotation(defn.ExperimentalAnnot))
270268

269+
def isInExperimentalScope(using Context): Boolean =
270+
self.hasAnnotation(defn.ExperimentalAnnot)
271+
|| (!self.is(Package) && self.owner.isInExperimentalScope)
272+
271273
/** The declared self type of this class, as seen from `site`, stripping
272274
* all refinements for opaque types.
273275
*/

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

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -948,12 +948,7 @@ object RefChecks {
948948
report.deprecationWarning(s"${sym.showLocated} is deprecated${since}${msg}", pos)
949949

950950
private def checkExperimental(sym: Symbol, pos: SrcPos)(using Context): Unit =
951-
if sym.isExperimental
952-
&& !sym.isConstructor // already reported on the class
953-
&& !ctx.owner.isExperimental // already reported on the @experimental of the owner
954-
&& !sym.is(ModuleClass) // already reported on the module
955-
&& (sym.span.exists || sym != defn.ExperimentalAnnot) // already reported on inferred annotations
956-
then
951+
if sym.isExperimental && !ctx.owner.isInExperimentalScope then
957952
Feature.checkExperimentalDef(sym, pos)
958953

959954
private def checkExperimentalSignature(sym: Symbol, pos: SrcPos)(using Context): Unit =
@@ -963,12 +958,14 @@ object RefChecks {
963958
Feature.checkExperimentalDef(tp.typeSymbol, pos)
964959
else
965960
traverseChildren(tp)
966-
if !sym.owner.isExperimental && !pos.span.isSynthetic then // avoid double errors
961+
962+
if !sym.isInExperimentalScope then
967963
checker.traverse(sym.info)
968964

969965
private def checkExperimentalAnnots(sym: Symbol)(using Context): Unit =
970-
for annot <- sym.annotations if annot.symbol.isExperimental && annot.tree.span.exists do
971-
Feature.checkExperimentalDef(annot.symbol, annot.tree)
966+
if !sym.isExperimental then
967+
for annot <- sym.annotations if annot.symbol.isExperimental && annot.tree.span.exists do
968+
Feature.checkExperimentalDef(annot.symbol, annot.tree)
972969

973970
/** If @migration is present (indicating that the symbol has changed semantics between versions),
974971
* emit a warning.
@@ -1338,7 +1335,6 @@ class RefChecks extends MiniPhase { thisPhase =>
13381335
}
13391336

13401337
override def transformTypeDef(tree: TypeDef)(using Context): TypeDef = {
1341-
checkExperimental(tree.symbol, tree.srcPos)
13421338
checkExperimentalAnnots(tree.symbol)
13431339
tree
13441340
}
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
---
2+
layout: doc-page
3+
title: "Experimental definitions"
4+
---
5+
6+
## Experimental definitions
7+
8+
The `@experimental` annotation allows the definition of an API that is not guaranteed backward binary or source compatibility.
9+
This annotation can be placed on term or type definitions.
10+
11+
### References to experimental definitions
12+
13+
Experimental definitions can only be referenced in an experimental scope. Experimental scopes are defined as follows.
14+
15+
(1) The RHS of an experimental `def`, `val`, `var`, `given` or `type` is an experimental scope.
16+
17+
<details>
18+
<summary>Examples</summary>
19+
20+
```scala
21+
import scala.annotation.experimental
22+
23+
@experimental
24+
def x = ()
25+
26+
def d1 = x // error: value x is marked @experimental and therefore ...
27+
@experimental def d2 = x
28+
29+
val v1 = x // error: value x is marked @experimental and therefore ...
30+
@experimental val v2 = x
31+
32+
var vr1 = x // error: value x is marked @experimental and therefore ...
33+
@experimental var vr2 = x
34+
35+
lazy val lv1 = x // error: value x is marked @experimental and therefore ...
36+
@experimental lazy val lv2 = x
37+
```
38+
39+
```scala
40+
import scala.annotation.experimental
41+
42+
@experimental
43+
val x = ()
44+
45+
@experimental
46+
def f() = ()
47+
48+
@experimental
49+
object X:
50+
def fx() = 1
51+
52+
def test1: Unit =
53+
f() // error: def f is marked @experimental and therefore ...
54+
x // error: value x is marked @experimental and therefore ...
55+
X.fx() // error: object X is marked @experimental and therefore ...
56+
import X.fx
57+
fx() // error: object X is marked @experimental and therefore ...
58+
59+
@experimental
60+
def test2: Unit =
61+
// references to f, x and X are ok because `test2` is experimental
62+
f()
63+
x
64+
X.fx()
65+
import X.fx
66+
fx()
67+
```
68+
69+
```scala
70+
import scala.annotation.experimental
71+
72+
@experimental type E
73+
74+
type A = E // error
75+
@experimental type B = E
76+
```
77+
78+
```scala
79+
import scala.annotation.experimental
80+
81+
@experimental class A
82+
@experimental type X
83+
@experimental type Y = Int
84+
@experimental opaque type Z = Int
85+
86+
def test: Unit =
87+
new A // error: class A is marked @experimental and therefore ...
88+
val i0: A = ??? // error: class A is marked @experimental and therefore ...
89+
val i1: X = ??? // error: type X is marked @experimental and therefore ...
90+
val i2: Y = ??? // error: type Y is marked @experimental and therefore ...
91+
val i2: Z = ??? // error: type Y is marked @experimental and therefore ...
92+
()
93+
```
94+
95+
```scala
96+
@experimental
97+
trait ExpSAM {
98+
def foo(x: Int): Int
99+
}
100+
def bar(f: ExpSAM): Unit = {} // error: error form rule 2
101+
102+
def test: Unit =
103+
bar(x => x) // error: reference to experimental SAM
104+
()
105+
```
106+
107+
</details>
108+
109+
(2.) The signatures of an experimental `def`, `val`, `var`, `given` and `type`, or constructors of `class` and `trait` are experimental scopes.
110+
111+
<details>
112+
<summary>Examples</summary>
113+
114+
```scala
115+
import scala.annotation.experimental
116+
117+
@experimental def x = 2
118+
@experimental class A
119+
@experimental type X
120+
@experimental type Y = Int
121+
@experimental opaque type Z = Int
122+
123+
def test1(
124+
p1: A, // error: class A is marked @experimental and therefore ...
125+
p2: List[A], // error: class A is marked @experimental and therefore ...
126+
p3: X, // error: type X is marked @experimental and therefore ...
127+
p4: Y, // error: type Y is marked @experimental and therefore ...
128+
p5: Z, // error: type Z is marked @experimental and therefore ...
129+
p6: Any = x // error: def x is marked @experimental and therefore ...
130+
): A = ??? // error: class A is marked @experimental and therefore ...
131+
132+
@experimental def test2(
133+
p1: A,
134+
p2: List[A],
135+
p3: X,
136+
p4: Y,
137+
p5: Z,
138+
p6: Any = x
139+
): A = ???
140+
141+
class Test1(
142+
p1: A, // error
143+
p2: List[A], // error
144+
p3: X, // error
145+
p4: Y, // error
146+
p5: Z, // error
147+
p6: Any = x // error
148+
) {}
149+
150+
@experimental class Test2(
151+
p1: A,
152+
p2: List[A],
153+
p3: X,
154+
p4: Y,
155+
p5: Z,
156+
p6: Any = x
157+
) {}
158+
159+
trait Test1(
160+
p1: A, // error
161+
p2: List[A], // error
162+
p3: X, // error
163+
p4: Y, // error
164+
p5: Z, // error
165+
p6: Any = x // error
166+
) {}
167+
168+
@experimental trait Test2(
169+
p1: A,
170+
p2: List[A],
171+
p3: X,
172+
p4: Y,
173+
p5: Z,
174+
p6: Any = x
175+
) {}
176+
```
177+
178+
</details>
179+
180+
(3.) The extension clause of an experimental `class`, `trait`, `object` are experimental scopes.
181+
182+
<details>
183+
<summary>Examples</summary>
184+
185+
```scala
186+
import scala.annotation.experimental
187+
188+
@experimental def x = 2
189+
190+
@experimental class A1(x: Any)
191+
class A2(x: Any)
192+
193+
194+
@experimental class B1 extends A1(1)
195+
class B2 extends A1(1) // error: class A1 is marked @experimental and therefore marked @experimental and therefore ...
196+
197+
@experimental class C1 extends A2(x)
198+
class C2 extends A2(x) // error def x is marked @experimental and therefore
199+
```
200+
201+
</details>
202+
203+
(4.) Members of an experimental `class`, `trait` or `object` are in experimental scopes.
204+
205+
<details>
206+
<summary>Examples</summary>
207+
208+
```scala
209+
import scala.annotation.experimental
210+
211+
@experimental def x = 2
212+
213+
@experimental class A {
214+
def f = x // ok because A is experimental
215+
}
216+
217+
@experimental class B {
218+
def f = x // ok because A is experimental
219+
}
220+
221+
@experimental object C {
222+
def f = x // ok because A is experimental
223+
}
224+
225+
@experimental class D {
226+
def f = {
227+
object B {
228+
x // ok because A is experimental
229+
}
230+
}
231+
}
232+
```
233+
234+
</details>
235+
236+
(5.) Annotations of an experimental definition are in experimental scopes.
237+
238+
<details>
239+
<summary>Examples</summary>
240+
241+
```scala
242+
import scala.annotation.experimental
243+
244+
@experimental class myExperimentalAnnot extends scala.annotation.Annotation
245+
246+
@myExperimentalAnnot // error
247+
def test: Unit = ()
248+
249+
@experimental
250+
@myExperimentalAnnot
251+
def test: Unit = ()
252+
```
253+
254+
</details>
255+
256+
(6.) Any code compiled using a _Nightly_ or _Snapshot_ version of the compiler is considered to be in an experimental scope.
257+
Can use the `-Yno-experimental` compiler flag to disable it and run as a proper release.
258+
259+
In any other situation, a reference to an experimental definition will cause a compilation error.
260+
261+
> Question: Should we disallow `@experimental def main(...)`?
262+
263+
### Experimental inheritance
264+
265+
All subclasses of an experimental `class` or `trait` must be marked as `@experimental` even if they are in an experimental scope.
266+
Anonymous classes and SAMs of experimental classes are considered experimental.
267+
268+
We require explicit annotations to make sure we do not have completion or cycles issues with nested classes. This restriction could be relaxed in the future.
269+
270+
### Experimental overriding
271+
272+
For an overriding member `M` and overridden member `O`, if `O` is non-experimental then `M` must be non-experimental.
273+
274+
This makes sure that we cannot have accidental binary incompatibilities such as the following change.
275+
```diff
276+
class A:
277+
def f: Any = 1
278+
class B extends A:
279+
- @experimental def f: Int = 2
280+
```
281+
### Test frameworks
282+
283+
Tests can be defined as experimental. Tests frameworks can execute tests using reflection even if they are in an experimental class, object or method.
284+
285+
Test that touch experimental APIs can be written as follows
286+
287+
```scala
288+
import scala.annotation.experimental
289+
290+
@experimental def x = 2
291+
292+
class MyTests {
293+
/*@Test*/ def test1 = x // error
294+
@experimental /*@Test*/ def test2 = x
295+
}
296+
297+
@experimental
298+
class MyExperimentalTests {
299+
/*@Test*/ def test1 = x
300+
/*@Test*/ def test2 = x
301+
}
302+
```

docs/sidebar.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ sidebar:
6767
- page: docs/reference/other-new-features/explicit-nulls.md
6868
- page: docs/reference/other-new-features/safe-initialization.md
6969
- page: docs/reference/other-new-features/type-test.md
70+
- page: docs/reference/other-new-features/experimental-defs.md
7071
- title: Other Changed Features
7172
subsection:
7273
- page: docs/reference/changed-features/numeric-literals.md

0 commit comments

Comments
 (0)