Skip to content

Commit 9ad82d0

Browse files
committed
Enable experimental mode when experimental feature is imported
The `@experimental` flag is added to definitions in the scope where the the experimental language feature is imported.
1 parent 49e3250 commit 9ad82d0

20 files changed

+152
-129
lines changed

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

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ object Feature:
3232
val pureFunctions = experimental("pureFunctions")
3333
val captureChecking = experimental("captureChecking")
3434
val into = experimental("into")
35+
val relaxedExtensionImports = experimental("relaxedExtensionImports")
36+
37+
// TODO compute this list
38+
// TODO remove features that do not enable experimental mode
39+
val experimentalAutoEnableFeatures: List[TermName] = List(
40+
namedTypeArguments,
41+
genericNumberLiterals,
42+
erasedDefinitions,
43+
fewerBraces,
44+
saferExceptions,
45+
clauseInterleaving,
46+
pureFunctions,
47+
captureChecking,
48+
into,
49+
relaxedExtensionImports,
50+
)
3551

3652
val globalOnlyImports: Set[TermName] = Set(pureFunctions, captureChecking)
3753

@@ -132,19 +148,20 @@ object Feature:
132148
false
133149

134150
def checkExperimentalFeature(which: String, srcPos: SrcPos, note: => String = "")(using Context) =
135-
if !isExperimentalEnabled then
151+
if !isExperimentalGloballyEnabled && !isExperimentalByImportEnabled && !isExperimentalUnstableEnabled then
136152
report.error(
137153
em"""Experimental $which may only be used under experimental mode:
138154
| 1. In a definition marked as @experimental
139-
| 2. Compiling with the -experimental compiler flag
140-
| 3. With a nightly or snapshot version of the compiler$note
155+
| 2. An experimental language import is in scope
156+
| 3. Compiling with the -experimental compiler flag
157+
| 4. With a nightly or snapshot version of the compiler$note
141158
""", srcPos)
142159

143160
private def ccException(sym: Symbol)(using Context): Boolean =
144161
ccEnabled && defn.ccExperimental.contains(sym)
145162

146163
def checkExperimentalDef(sym: Symbol, srcPos: SrcPos)(using Context) =
147-
if !isExperimentalEnabled then
164+
if !isExperimentalGloballyEnabled && !isExperimentalByImportEnabled && !isExperimentalUnstableEnabled then
148165
val experimentalSym =
149166
if sym.hasAnnotation(defn.ExperimentalAnnot) then sym
150167
else if sym.owner.hasAnnotation(defn.ExperimentalAnnot) then sym.owner
@@ -162,8 +179,22 @@ object Feature:
162179
if setting.startsWith("experimental.") && setting != "experimental.macros"
163180
do checkExperimentalFeature(s"feature $setting", NoSourcePosition)
164181

165-
def isExperimentalEnabled(using Context): Boolean =
166-
(Properties.experimental && !ctx.settings.YnoExperimental.value) || ctx.settings.experimental.value
182+
183+
/** Experimental mode enabled by default in nightly or snapshot version of the compiler without `-Yno-experimental` */
184+
def isExperimentalUnstableEnabled(using Context): Boolean =
185+
Properties.experimental && !ctx.settings.YnoExperimental.value
186+
187+
/** Experimental mode enabled in this compilation unit
188+
* - Compiled with `-experimental`
189+
* - Compiled with `-language:experimental.xyz`
190+
*/
191+
def isExperimentalGloballyEnabled(using Context): Boolean =
192+
ctx.settings.experimental.value ||
193+
experimentalAutoEnableFeatures.exists(enabledBySetting)
194+
195+
/** Experimental mode enabled by a `language.experimental` import in scope */
196+
def isExperimentalByImportEnabled(using Context): Boolean =
197+
experimentalAutoEnableFeatures.exists(enabledByImport)
167198

168199
/** Handle language import `import language.<prefix>.<imported>` if it is one
169200
* of the global imports `pureFunctions` or `captureChecking`. In this case

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,6 +1254,19 @@ object SymDenotations {
12541254
final def isTopLevelClass(using Context): Boolean =
12551255
!this.exists || this.isEffectiveRoot || this.is(PackageClass) || this.owner.is(PackageClass)
12561256

1257+
/** The top-level definition containing this denotation.
1258+
* Returns top-level class or members of top-level package objects.
1259+
* If this is not in a definition, returns NoSymbol.
1260+
*/
1261+
@tailrec final def topLevelDefinition(using Context): Symbol =
1262+
if !this.exists || this.is(Package) then NoSymbol
1263+
else if this.isTopLevelDefinition then this.symbol
1264+
else this.owner.topLevelDefinition
1265+
1266+
final def isTopLevelDefinition(using Context): Boolean =
1267+
!this.is(Package) && !this.name.isPackageObjectName &&
1268+
(this.owner.is(Package) || (this.owner.isPackageObject && !this.isConstructor))
1269+
12571270
/** The package class containing this denotation */
12581271
final def enclosingPackageClass(using Context): Symbol =
12591272
if (this.is(PackageClass)) symbol else owner.enclosingPackageClass

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -545,15 +545,22 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
545545
report.error("`erased` definition cannot be implemented with en expression of type Null", tree.srcPos)
546546

547547
private def annotateExperimental(sym: Symbol)(using Context): Unit =
548-
def isTopLevelDefinitionInSource(sym: Symbol) =
549-
!sym.is(Package) && !sym.name.isPackageObjectName &&
550-
(sym.owner.is(Package) || (sym.owner.isPackageObject && !sym.isConstructor))
551-
if !sym.hasAnnotation(defn.ExperimentalAnnot)
552-
&& (ctx.settings.experimental.value && isTopLevelDefinitionInSource(sym))
553-
|| (sym.is(Module) && sym.companionClass.hasAnnotation(defn.ExperimentalAnnot))
554-
then
548+
def companionOfExperimental =
549+
sym.is(Module) && sym.companionClass.hasAnnotation(defn.ExperimentalAnnot)
550+
def topLevelDefinitionWithExperimentalEnabledInUnit =
551+
sym.isTopLevelDefinition && (Feature.isExperimentalGloballyEnabled || Feature.isExperimentalByImportEnabled)
552+
if !sym.hasAnnotation(defn.ExperimentalAnnot) && (companionOfExperimental || topLevelDefinitionWithExperimentalEnabledInUnit) then
555553
sym.addAnnotation(Annotation(defn.ExperimentalAnnot, sym.span))
556554

555+
// TODO do this on import statement
556+
val topLevelDefinition = sym.topLevelDefinition
557+
if topLevelDefinition.exists &&
558+
!topLevelDefinition.hasAnnotation(defn.ExperimentalAnnot)
559+
&& Feature.isExperimentalByImportEnabled
560+
then
561+
topLevelDefinition.addAnnotation(Annotation(defn.ExperimentalAnnot, sym.span))
562+
if topLevelDefinition.is(Module) then topLevelDefinition.companionModule.addAnnotation(Annotation(defn.ExperimentalAnnot, sym.span))
563+
557564
private def scala2LibPatch(tree: TypeDef)(using Context) =
558565
val sym = tree.symbol
559566
if compilingScala2StdLib && sym.is(ModuleClass) then

docs/_docs/reference/other-new-features/experimental-defs.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,9 @@ Experimental definitions can only be referenced in an experimental scope. Experi
265265

266266
</details>
267267

268-
6. Any code compiled using a [_Nightly_](https://search.maven.org/artifact/org.scala-lang/scala3-compiler_3) or _Snapshot_ version of the compiler is considered to be in an experimental scope.
268+
6. An experimental language feature is imported in the current scope
269+
270+
7. Any code compiled using a [_Nightly_](https://search.maven.org/artifact/org.scala-lang/scala3-compiler_3) or _Snapshot_ version of the compiler is considered to be in an experimental scope.
269271
Can use the `-Yno-experimental` compiler flag to disable it and run as a proper release.
270272

271273
In any other situation, a reference to an experimental definition will cause a compilation error.

docs/_spec/TODOreference/other-new-features/experimental-defs.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ Experimental definitions can only be referenced in an experimental scope. Experi
216216

217217
<details>
218218
<summary>Example 1</summary>
219-
219+
220220
```scala
221221
import scala.annotation.experimental
222222

@@ -242,7 +242,7 @@ Experimental definitions can only be referenced in an experimental scope. Experi
242242
}
243243
}
244244
```
245-
245+
246246
</details>
247247

248248
5. Annotations of an experimental definition are in experimental scopes. Examples:
@@ -265,7 +265,9 @@ Experimental definitions can only be referenced in an experimental scope. Experi
265265

266266
</details>
267267

268-
6. Any code compiled using a [_Nightly_](https://search.maven.org/artifact/org.scala-lang/scala3-compiler_3) or _Snapshot_ version of the compiler is considered to be in an experimental scope.
268+
6. An experimental language feature is imported in the current scope
269+
270+
7. Any code compiled using a [_Nightly_](https://search.maven.org/artifact/org.scala-lang/scala3-compiler_3) or _Snapshot_ version of the compiler is considered to be in an experimental scope.
269271
Can use the `-Yno-experimental` compiler flag to disable it and run as a proper release.
270272

271273
In any other situation, a reference to an experimental definition will cause a compilation error.

tests/neg/experimental-erased.scala

Lines changed: 0 additions & 11 deletions
This file was deleted.

tests/neg/experimental-nested-imports-2.scala

Lines changed: 0 additions & 31 deletions
This file was deleted.

tests/neg/experimental-nested-imports.scala

Lines changed: 0 additions & 27 deletions
This file was deleted.

tests/neg/experimentalErased.scala

Lines changed: 0 additions & 24 deletions
This file was deleted.

tests/neg/expeimental-flag-with-lang-feature-1.scala renamed to tests/pos/expeimental-flag-with-lang-feature-1.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
import scala.language.experimental.erasedDefinitions
44

5-
erased def erasedFun(erased x: Int): Int = x // error // error
5+
erased def erasedFun(erased x: Int): Int = x

tests/neg/expeimental-flag-with-lang-feature-2.scala renamed to tests/pos/expeimental-flag-with-lang-feature-2.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//> using options -Yno-experimental
22

3-
import scala.language.experimental.namedTypeArguments // error
3+
import scala.language.experimental.namedTypeArguments
44

55
def namedTypeArgumentsFun[T, U]: Int =
66
namedTypeArgumentsFun[T = Int, U = Int]

tests/neg/experimental.scala renamed to tests/pos/experimental-imports-2.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//> using options -Yno-experimental
22

33
class Test0 {
4-
import language.experimental.namedTypeArguments // error
4+
import language.experimental.namedTypeArguments
55
object Foo {
66
inline def f[S, T](x: S): T = ???
77
def g(x: Int) = f[T = Any](x)

tests/neg/experimental-imports.scala renamed to tests/pos/experimental-imports.scala

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ object Object1:
1111
erased def f = 1
1212

1313
object Object2:
14-
import language.experimental.fewerBraces // error
15-
import language.experimental.namedTypeArguments // error
16-
import language.experimental.genericNumberLiterals // error
14+
import language.experimental.fewerBraces
15+
import language.experimental.namedTypeArguments
16+
import language.experimental.genericNumberLiterals
1717
import language.experimental.erasedDefinitions
1818
erased def f = 1
1919

@@ -26,9 +26,9 @@ object Class1:
2626
erased def f = 1
2727

2828
object Class2:
29-
import language.experimental.fewerBraces // error
30-
import language.experimental.namedTypeArguments // error
31-
import language.experimental.genericNumberLiterals // error
29+
import language.experimental.fewerBraces
30+
import language.experimental.namedTypeArguments
31+
import language.experimental.genericNumberLiterals
3232
import language.experimental.erasedDefinitions
3333
erased def f = 1
3434

@@ -41,8 +41,8 @@ def fun1 =
4141
erased def f = 1
4242

4343
def fun2 =
44-
import language.experimental.fewerBraces // error
45-
import language.experimental.namedTypeArguments // error
46-
import language.experimental.genericNumberLiterals // error
44+
import language.experimental.fewerBraces
45+
import language.experimental.namedTypeArguments
46+
import language.experimental.genericNumberLiterals
4747
import language.experimental.erasedDefinitions
4848
erased def f = 1
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//> using options -Yno-experimental
2+
3+
import annotation.experimental
4+
5+
class Class1:
6+
import language.experimental.namedTypeArguments
7+
import language.experimental.genericNumberLiterals
8+
import language.experimental.erasedDefinitions
9+
@experimental def f = 1
10+
def g = 1
11+
12+
object Object1:
13+
import language.experimental.namedTypeArguments
14+
import language.experimental.genericNumberLiterals
15+
import language.experimental.erasedDefinitions
16+
@experimental def f = 1
17+
def g = 1
18+
19+
def fun1 =
20+
import language.experimental.namedTypeArguments
21+
import language.experimental.genericNumberLiterals
22+
import language.experimental.erasedDefinitions
23+
@experimental def f = 1
24+
def g = 1
25+
26+
val value1 =
27+
import language.experimental.namedTypeArguments
28+
import language.experimental.genericNumberLiterals
29+
import language.experimental.erasedDefinitions
30+
@experimental def f = 1
31+
def g = 1
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//> using options -Yno-experimental
2+
3+
import annotation.experimental
4+
5+
class Class1:
6+
import language.experimental.namedTypeArguments
7+
import language.experimental.genericNumberLiterals
8+
import language.experimental.erasedDefinitions
9+
@experimental def f = 1
10+
11+
object Object1:
12+
import language.experimental.namedTypeArguments
13+
import language.experimental.genericNumberLiterals
14+
import language.experimental.erasedDefinitions
15+
@experimental def f = 1
16+
17+
def fun1 =
18+
import language.experimental.namedTypeArguments
19+
import language.experimental.genericNumberLiterals
20+
import language.experimental.erasedDefinitions
21+
@experimental def f = 1
22+
23+
val value1 =
24+
import language.experimental.namedTypeArguments
25+
import language.experimental.genericNumberLiterals
26+
import language.experimental.erasedDefinitions
27+
@experimental def f = 1

0 commit comments

Comments
 (0)