Skip to content

Commit 2fa92c4

Browse files
committed
Enable experimental mode when experimental feature is imported
The `@experimental` flag is added to top-level definitions in the package where the language feature is imported.
1 parent 18645ee commit 2fa92c4

22 files changed

+92
-110
lines changed

compiler/src/dotty/tools/dotc/Driver.scala

-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ class Driver {
7474
val ictx = rootCtx.fresh
7575
val summary = command.distill(args, ictx.settings)(ictx.settingsState)(using ictx)
7676
ictx.setSettings(summary.sstate)
77-
Feature.checkExperimentalSettings(using ictx)
7877
MacroClassLoader.init(ictx)
7978
Positioned.init(using ictx)
8079

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

+16-11
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ object Feature:
3333
val captureChecking = experimental("captureChecking")
3434
val into = experimental("into")
3535

36+
def experimentalAutoEnableFeatures(using Context): List[TermName] =
37+
defn.languageExperimentalFeatures
38+
.map(sym => experimental(sym.name))
39+
.filterNot(_ == captureChecking) // TODO is this correct?
40+
3641
val globalOnlyImports: Set[TermName] = Set(pureFunctions, captureChecking)
3742

3843
/** Is `feature` enabled by by a command-line setting? The enabling setting is
@@ -132,13 +137,14 @@ object Feature:
132137
false
133138

134139
def checkExperimentalFeature(which: String, srcPos: SrcPos, note: => String = "")(using Context) =
135-
if !isExperimentalEnabled then
140+
if !isExperimentalEnabledBySetting && !isExperimentalEnabledByImport then
136141
report.error(
137142
em"""Experimental $which may only be used under experimental mode:
138143
| 1. in a definition marked as @experimental, or
139-
| 2. compiling with the -experimental compiler flag, or
140-
| 3. with a nightly or snapshot version of the compiler.$note
141-
""", srcPos)
144+
| 2. an experimental feature is imported in at the package level, or
145+
| 3. compiling with the -experimental compiler flag, or
146+
| 4. with a nightly or snapshot version of the compiler.$note
147+
|""", srcPos)
142148

143149
private def ccException(sym: Symbol)(using Context): Boolean =
144150
ccEnabled && defn.ccExperimental.contains(sym)
@@ -155,14 +161,13 @@ object Feature:
155161
else i"$sym inherits @experimental"
156162
checkExperimentalFeature("definition", srcPos, s"\n\n$note")
157163

158-
/** Check that experimental compiler options are only set for snapshot or nightly compiler versions. */
159-
def checkExperimentalSettings(using Context): Unit =
160-
for setting <- ctx.settings.language.value
161-
if setting.startsWith("experimental.") && setting != "experimental.macros"
162-
do checkExperimentalFeature(s"feature $setting", NoSourcePosition)
164+
def isExperimentalEnabledBySetting(using Context): Boolean =
165+
(Properties.unstableExperimentalEnabled && !ctx.settings.YnoExperimental.value) ||
166+
ctx.settings.experimental.value ||
167+
experimentalAutoEnableFeatures.exists(enabledBySetting)
163168

164-
def isExperimentalEnabled(using Context): Boolean =
165-
(Properties.unstableExperimentalEnabled && !ctx.settings.YnoExperimental.value) || ctx.settings.experimental.value
169+
def isExperimentalEnabledByImport(using Context): Boolean =
170+
experimentalAutoEnableFeatures.exists(enabledByImport)
166171

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

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

+7
Original file line numberDiff line numberDiff line change
@@ -2013,6 +2013,13 @@ class Definitions {
20132013
CapabilityAnnot, RequiresCapabilityAnnot,
20142014
RetainsAnnot, RetainsCapAnnot, RetainsByNameAnnot)
20152015

2016+
/** Experimental language fetures defined in `scala.runtime.stdLibPatches.language.experimental`.
2017+
*
2018+
* This list does not incluede `scala.language.experimental.macros`.
2019+
*/
2020+
@tu lazy val languageExperimentalFeatures: List[TermSymbol] =
2021+
LanguageExperimentalModule.moduleClass.info.decls.toList.filter(_.isAllOf(Lazy | Module)).map(_.asTerm)
2022+
20162023
// ----- primitive value class machinery ------------------------------------------
20172024

20182025
class PerRun[T](generate: Context ?=> T) {

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -513,8 +513,8 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
513513
}
514514

515515
override def transformStats[T](trees: List[Tree], exprOwner: Symbol, wrapResult: List[Tree] => Context ?=> T)(using Context): T =
516-
try super.transformStats(trees, exprOwner, wrapResult)
517-
finally Checking.checkExperimentalImports(trees)
516+
Checking.checkExperimentalImports(trees)
517+
super.transformStats(trees, exprOwner, wrapResult)
518518

519519
/** Transforms the rhs tree into a its default tree if it is in an `erased` val/def.
520520
* Performed to shrink the tree that is known to be erased later.
@@ -549,7 +549,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
549549
!sym.is(Package) && !sym.name.isPackageObjectName &&
550550
(sym.owner.is(Package) || (sym.owner.isPackageObject && !sym.isConstructor))
551551
if !sym.hasAnnotation(defn.ExperimentalAnnot)
552-
&& (ctx.settings.experimental.value && isTopLevelDefinitionInSource(sym))
552+
&& (Feature.isExperimentalEnabledBySetting && isTopLevelDefinitionInSource(sym))
553553
|| (sym.is(Module) && sym.companionClass.hasAnnotation(defn.ExperimentalAnnot))
554554
then
555555
sym.addAnnotation(Annotation(defn.ExperimentalAnnot, sym.span))

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

+22-21
Original file line numberDiff line numberDiff line change
@@ -806,44 +806,45 @@ object Checking {
806806
* scope with only @experimental definitions.
807807
*/
808808
def checkExperimentalImports(trees: List[Tree])(using Context): Unit =
809-
810-
def nonExperimentalStat(trees: List[Tree]): Tree = trees match
811-
case (_: Import | EmptyTree) :: rest =>
812-
nonExperimentalStat(rest)
809+
def nonExperimentalStats(trees: List[Tree]): List[Tree] = trees match
810+
case (_: ImportOrExport | EmptyTree) :: rest =>
811+
nonExperimentalStats(rest)
813812
case (tree @ TypeDef(_, impl: Template)) :: rest if tree.symbol.isPackageObject =>
814-
nonExperimentalStat(impl.body).orElse(nonExperimentalStat(rest))
813+
nonExperimentalStats(impl.body) ::: nonExperimentalStats(rest)
815814
case (tree: PackageDef) :: rest =>
816-
nonExperimentalStat(tree.stats).orElse(nonExperimentalStat(rest))
815+
nonExperimentalStats(tree.stats) ::: nonExperimentalStats(rest)
817816
case (tree: MemberDef) :: rest =>
818817
if tree.symbol.isExperimental || tree.symbol.is(Synthetic) then
819-
nonExperimentalStat(rest)
818+
nonExperimentalStats(rest)
820819
else
821-
tree
820+
tree :: nonExperimentalStats(rest)
822821
case tree :: rest =>
823-
tree
822+
tree :: nonExperimentalStats(rest)
824823
case Nil =>
825-
EmptyTree
824+
Nil
826825

827826
for case imp @ Import(qual, selectors) <- trees do
828827
def isAllowedImport(sel: untpd.ImportSelector) =
829828
val name = Feature.experimental(sel.name)
830829
name == Feature.scala2macros
831-
|| name == Feature.erasedDefinitions
832830
|| name == Feature.captureChecking
833831

834832
languageImport(qual) match
835833
case Some(nme.experimental)
836834
if !ctx.owner.isInExperimentalScope && !selectors.forall(isAllowedImport) =>
837-
def check(stable: => String) =
838-
Feature.checkExperimentalFeature("features", imp.srcPos,
839-
s"\n\nNote: the scope enclosing the import is not considered experimental because it contains the\nnon-experimental $stable")
840-
if ctx.owner.is(Package) then
841-
// allow top-level experimental imports if all definitions are @experimental
842-
nonExperimentalStat(trees) match
843-
case EmptyTree =>
844-
case tree: MemberDef => check(i"${tree.symbol}")
845-
case tree => check(i"expression ${tree}")
846-
else Feature.checkExperimentalFeature("features", imp.srcPos)
835+
if ctx.owner.is(Package) || ctx.owner.name.startsWith(str.REPL_SESSION_LINE) then
836+
// mark all top-level definitions as @experimental
837+
for tree <- nonExperimentalStats(trees) do
838+
tree match
839+
case tree: MemberDef =>
840+
// TODO move this out of checking (into posttyper?)
841+
val sym = tree.symbol
842+
if !sym.isExperimental then
843+
sym.addAnnotation(Annotations.Annotation(defn.ExperimentalAnnot, sym.span))
844+
case tree =>
845+
// There is no definition to attach the @experimental annotation
846+
report.error("Implementation restriction: top-level `val _ = ...` is not supported with experimental language imports.", tree.srcPos)
847+
else Feature.checkExperimentalFeature("feature local import", imp.srcPos)
847848
case _ =>
848849
end checkExperimentalImports
849850

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

+8-6
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,14 @@ class CrossVersionChecks extends MiniPhase:
172172
}
173173

174174
override def transformOther(tree: Tree)(using Context): Tree =
175-
tree.foreachSubTree { // Find references in type trees and imports
176-
case tree: Ident => transformIdent(tree)
177-
case tree: Select => transformSelect(tree)
178-
case tree: TypeTree => transformTypeTree(tree)
179-
case _ =>
180-
}
175+
val inPackage = ctx.owner.is(Package) || ctx.owner.isPackageObject
176+
if !(inPackage && tree.isInstanceOf[ImportOrExport] && Feature.isExperimentalEnabledByImport) then
177+
tree.foreachSubTree { // Find references in type trees and imports
178+
case tree: Ident => transformIdent(tree)
179+
case tree: Select => transformSelect(tree)
180+
case tree: TypeTree => transformTypeTree(tree)
181+
case _ =>
182+
}
181183
tree
182184

183185
end CrossVersionChecks

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

+2
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ Experimental definitions can only be referenced in an experimental scope. Experi
268268
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.
269269
Can use the `-Yno-experimental` compiler flag to disable it and run as a proper release.
270270

271+
7. An experimental language feature is imported in at the package level. All top-level definitions will be marked as `@experimental`.
272+
271273
In any other situation, a reference to an experimental definition will cause a compilation error.
272274

273275
## Experimental overriding

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

+4-2
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:
@@ -268,6 +268,8 @@ Experimental definitions can only be referenced in an experimental scope. Experi
268268
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.
269269
Can use the `-Yno-experimental` compiler flag to disable it and run as a proper release.
270270

271+
7. An experimental language feature is imported in at the package level. All top-level definitions will be marked as `@experimental`.
272+
271273
In any other situation, a reference to an experimental definition will cause a compilation error.
272274

273275
## Experimental inheritance

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

-5
This file was deleted.

tests/neg/experimental-erased.scala

-11
This file was deleted.

tests/neg/experimental-imports.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ object Object2:
1414
import language.experimental.fewerBraces // error
1515
import language.experimental.namedTypeArguments // error
1616
import language.experimental.genericNumberLiterals // error
17-
import language.experimental.erasedDefinitions
17+
import language.experimental.erasedDefinitions // error
1818
erased def f = 1
1919

2020
@experimental
@@ -29,7 +29,7 @@ object Class2:
2929
import language.experimental.fewerBraces // error
3030
import language.experimental.namedTypeArguments // error
3131
import language.experimental.genericNumberLiterals // error
32-
import language.experimental.erasedDefinitions
32+
import language.experimental.erasedDefinitions // error
3333
erased def f = 1
3434

3535
@experimental
@@ -44,5 +44,5 @@ def fun2 =
4444
import language.experimental.fewerBraces // error
4545
import language.experimental.namedTypeArguments // error
4646
import language.experimental.genericNumberLiterals // error
47-
import language.experimental.erasedDefinitions
47+
import language.experimental.erasedDefinitions // error
4848
erased def f = 1

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,27 @@ import annotation.experimental
55
class Class1:
66
import language.experimental.namedTypeArguments // error
77
import language.experimental.genericNumberLiterals // error
8-
import language.experimental.erasedDefinitions // ok: only check at erased definition
8+
import language.experimental.erasedDefinitions // error
99
@experimental def f = 1
1010
def g = 1
1111

1212
object Object1:
1313
import language.experimental.namedTypeArguments // error
1414
import language.experimental.genericNumberLiterals // error
15-
import language.experimental.erasedDefinitions // ok: only check at erased definition
15+
import language.experimental.erasedDefinitions // error
1616
@experimental def f = 1
1717
def g = 1
1818

1919
def fun1 =
2020
import language.experimental.namedTypeArguments // error
2121
import language.experimental.genericNumberLiterals // error
22-
import language.experimental.erasedDefinitions // ok: only check at erased definition
22+
import language.experimental.erasedDefinitions // error
2323
@experimental def f = 1
2424
def g = 1
2525

2626
val value1 =
2727
import language.experimental.namedTypeArguments // error
2828
import language.experimental.genericNumberLiterals // error
29-
import language.experimental.erasedDefinitions // ok: only check at erased definition
29+
import language.experimental.erasedDefinitions // error
3030
@experimental def f = 1
3131
def g = 1

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ import annotation.experimental
55
class Class1:
66
import language.experimental.namedTypeArguments // error
77
import language.experimental.genericNumberLiterals // error
8-
import language.experimental.erasedDefinitions // ok: only check at erased definition
8+
import language.experimental.erasedDefinitions // error
99

1010
object Object1:
1111
import language.experimental.namedTypeArguments // error
1212
import language.experimental.genericNumberLiterals // error
13-
import language.experimental.erasedDefinitions // ok: only check at erased definition
13+
import language.experimental.erasedDefinitions // error
1414

1515
def fun1 =
1616
import language.experimental.namedTypeArguments // error
1717
import language.experimental.genericNumberLiterals // error
18-
import language.experimental.erasedDefinitions // ok: only check at erased definition
18+
import language.experimental.erasedDefinitions // error
1919

2020
val value1 =
2121
import language.experimental.namedTypeArguments // error
2222
import language.experimental.genericNumberLiterals // error
23-
import language.experimental.erasedDefinitions // ok: only check at erased definition
23+
import language.experimental.erasedDefinitions // error

tests/neg/experimental-nested-imports.scala

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@ import annotation.experimental
55
class Class1:
66
import language.experimental.namedTypeArguments // error
77
import language.experimental.genericNumberLiterals // error
8-
import language.experimental.erasedDefinitions // ok: only check at erased definition
8+
import language.experimental.erasedDefinitions // error
99
@experimental def f = 1
1010

1111
object Object1:
1212
import language.experimental.namedTypeArguments // error
1313
import language.experimental.genericNumberLiterals // error
14-
import language.experimental.erasedDefinitions // ok: only check at erased definition
14+
import language.experimental.erasedDefinitions // error
1515
@experimental def f = 1
1616

1717
def fun1 =
1818
import language.experimental.namedTypeArguments // error
1919
import language.experimental.genericNumberLiterals // error
20-
import language.experimental.erasedDefinitions // ok: only check at erased definition
20+
import language.experimental.erasedDefinitions // error
2121
@experimental def f = 1
2222

2323
val value1 =
2424
import language.experimental.namedTypeArguments // error
2525
import language.experimental.genericNumberLiterals // error
26-
import language.experimental.erasedDefinitions // ok: only check at erased definition
26+
import language.experimental.erasedDefinitions // error
2727
@experimental def f = 1

tests/neg/experimental.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class Test0 {
99
}
1010

1111
class Test1 {
12-
import scala.language.experimental.erasedDefinitions
12+
import scala.language.experimental.erasedDefinitions // error
1313
import scala.compiletime.erasedValue
1414
type UnivEq[A]
1515
object UnivEq:

tests/neg/experimentalErased.scala

-24
This file was deleted.

tests/neg/experimentalOverloads.scala

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//> using options -Yno-experimental
2+
13
import scala.annotation.experimental
24

35
trait A:

tests/neg/use-experimental-def.check

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
| ^^^
44
| Experimental definition may only be used under experimental mode:
55
| 1. in a definition marked as @experimental, or
6-
| 2. compiling with the -experimental compiler flag, or
7-
| 3. with a nightly or snapshot version of the compiler.
6+
| 2. an experimental feature is imported in at the package level, or
7+
| 3. compiling with the -experimental compiler flag, or
8+
| 4. with a nightly or snapshot version of the compiler.
89
|
910
| method foo is marked @experimental
10-
|

0 commit comments

Comments
 (0)