Skip to content

Commit 7f14d85

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 fe1d703 commit 7f14d85

29 files changed

+145
-124
lines changed

Diff for: compiler/src/dotty/tools/dotc/Driver.scala

-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ class Driver {
7777
val ictx = rootCtx.fresh
7878
val summary = command.distill(args, ictx.settings)(ictx.settingsState)(using ictx)
7979
ictx.setSettings(summary.sstate)
80-
Feature.checkExperimentalSettings(using ictx)
8180
MacroClassLoader.init(ictx)
8281
Positioned.init(using ictx)
8382

Diff for: compiler/src/dotty/tools/dotc/config/Feature.scala

+19-9
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ object Feature:
3434
val captureChecking = experimental("captureChecking")
3535
val into = experimental("into")
3636

37+
def experimentalAutoEnableFeatures(using Context): List[TermName] =
38+
defn.languageExperimentalFeatures
39+
.map(sym => experimental(sym.name))
40+
.filterNot(_ == captureChecking) // TODO is this correct?
41+
3742
/** Is `feature` enabled by by a command-line setting? The enabling setting is
3843
*
3944
* -language:<prefix>feature
@@ -157,18 +162,23 @@ object Feature:
157162
private def experimentalUseSite(which: String): String =
158163
s"""Experimental $which may only be used under experimental mode:
159164
| 1. in a definition marked as @experimental, or
160-
| 2. compiling with the -experimental compiler flag, or
161-
| 3. with a nightly or snapshot version of the compiler.
165+
| 2. an experimental feature is imported at the package level, or
166+
| 3. compiling with the -experimental compiler flag, or
167+
| 4. with a nightly or snapshot version of the compiler.
162168
|""".stripMargin
163169

164-
/** Check that experimental compiler options are only set for snapshot or nightly compiler versions. */
165-
def checkExperimentalSettings(using Context): Unit =
166-
for setting <- ctx.settings.language.value
167-
if setting.startsWith("experimental.") && setting != "experimental.macros"
168-
do checkExperimentalFeature(s"feature $setting", NoSourcePosition)
169-
170170
def isExperimentalEnabled(using Context): Boolean =
171-
(Properties.unstableExperimentalEnabled && !ctx.settings.YnoExperimental.value) || ctx.settings.experimental.value
171+
(Properties.unstableExperimentalEnabled && !ctx.settings.YnoExperimental.value) ||
172+
ctx.settings.experimental.value ||
173+
experimentalAutoEnableFeatures.exists(enabled)
174+
175+
def isExperimentalEnabledBySetting(using Context): Boolean =
176+
(Properties.unstableExperimentalEnabled && !ctx.settings.YnoExperimental.value) ||
177+
ctx.settings.experimental.value ||
178+
experimentalAutoEnableFeatures.exists(enabledBySetting)
179+
180+
def isExperimentalEnabledByImport(using Context): Boolean =
181+
experimentalAutoEnableFeatures.exists(enabledByImport)
172182

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

Diff for: compiler/src/dotty/tools/dotc/core/Definitions.scala

+7
Original file line numberDiff line numberDiff line change
@@ -2007,6 +2007,13 @@ class Definitions {
20072007
CapabilityAnnot, RequiresCapabilityAnnot,
20082008
RetainsAnnot, RetainsCapAnnot, RetainsByNameAnnot)
20092009

2010+
/** Experimental language features defined in `scala.runtime.stdLibPatches.language.experimental`.
2011+
*
2012+
* This list does not include `scala.language.experimental.macros`.
2013+
*/
2014+
@tu lazy val languageExperimentalFeatures: List[TermSymbol] =
2015+
LanguageExperimentalModule.moduleClass.info.decls.toList.filter(_.isAllOf(Lazy | Module)).map(_.asTerm)
2016+
20102017
// ----- primitive value class machinery ------------------------------------------
20112018

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

Diff for: compiler/src/dotty/tools/dotc/transform/PostTyper.scala

+6-4
Original file line numberDiff line numberDiff line change
@@ -545,8 +545,8 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
545545
}
546546

547547
override def transformStats[T](trees: List[Tree], exprOwner: Symbol, wrapResult: List[Tree] => Context ?=> T)(using Context): T =
548-
try super.transformStats(trees, exprOwner, wrapResult)
549-
finally Checking.checkExperimentalImports(trees)
548+
Checking.checkAndAdaptExperimentalImports(trees)
549+
super.transformStats(trees, exprOwner, wrapResult)
550550

551551
/** Transforms the rhs tree into a its default tree if it is in an `erased` val/def.
552552
* Performed to shrink the tree that is known to be erased later.
@@ -589,8 +589,10 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
589589
(sym.owner.is(Package) || (sym.owner.isPackageObject && !sym.isConstructor))
590590
if sym.is(Module) then
591591
ExperimentalAnnotation.copy(sym.companionClass).foreach(sym.addAnnotation)
592-
if !sym.hasAnnotation(defn.ExperimentalAnnot) && ctx.settings.experimental.value && isTopLevelDefinitionInSource(sym) then
593-
sym.addAnnotation(ExperimentalAnnotation("Added by -experimental", sym.span))
592+
if !sym.hasAnnotation(defn.ExperimentalAnnot)
593+
&& Feature.isExperimentalEnabledBySetting && isTopLevelDefinitionInSource(sym)
594+
then
595+
sym.addAnnotation(ExperimentalAnnotation("Added by -experimental or -language:experimental.*", sym.span))
594596

595597
// It needs to run at the phase of the postTyper --- otherwise, the test of the symbols will use
596598
// the transformed denotation with added `Serializable` and `AbstractFunction1`.

Diff for: compiler/src/dotty/tools/dotc/typer/Checking.scala

+29-25
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import cc.{isCaptureChecking, isRetainsLike}
4242

4343
import collection.mutable
4444
import reporting.*
45+
import Annotations.ExperimentalAnnotation
4546

4647
object Checking {
4748
import tpd.*
@@ -802,50 +803,53 @@ object Checking {
802803
tree
803804

804805
/** Check that experimental language imports in `trees`
805-
* are done only in experimental scopes, or in a top-level
806-
* scope with only @experimental definitions.
806+
* are done only in experimental scopes. For for top-level
807+
* experimental imports, all top-level definitions are transformed
808+
* to @experimental definitions.
809+
*
807810
*/
808-
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)
811+
def checkAndAdaptExperimentalImports(trees: List[Tree])(using Context): Unit =
812+
def nonExperimentalStats(trees: List[Tree]): List[Tree] = trees match
813+
case (_: ImportOrExport | EmptyTree) :: rest =>
814+
nonExperimentalStats(rest)
813815
case (tree @ TypeDef(_, impl: Template)) :: rest if tree.symbol.isPackageObject =>
814-
nonExperimentalStat(impl.body).orElse(nonExperimentalStat(rest))
816+
nonExperimentalStats(impl.body) ::: nonExperimentalStats(rest)
815817
case (tree: PackageDef) :: rest =>
816-
nonExperimentalStat(tree.stats).orElse(nonExperimentalStat(rest))
818+
nonExperimentalStats(tree.stats) ::: nonExperimentalStats(rest)
817819
case (tree: MemberDef) :: rest =>
818820
if tree.symbol.isExperimental || tree.symbol.is(Synthetic) then
819-
nonExperimentalStat(rest)
821+
nonExperimentalStats(rest)
820822
else
821-
tree
823+
tree :: nonExperimentalStats(rest)
822824
case tree :: rest =>
823-
tree
825+
tree :: nonExperimentalStats(rest)
824826
case Nil =>
825-
EmptyTree
827+
Nil
826828

827829
for case imp @ Import(qual, selectors) <- trees do
828830
def isAllowedImport(sel: untpd.ImportSelector) =
829831
val name = Feature.experimental(sel.name)
830832
name == Feature.scala2macros
831-
|| name == Feature.erasedDefinitions
832833
|| name == Feature.captureChecking
833834

834835
languageImport(qual) match
835836
case Some(nme.experimental)
836837
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)
838+
if ctx.owner.is(Package) || ctx.owner.name.startsWith(str.REPL_SESSION_LINE) then
839+
// mark all top-level definitions as @experimental
840+
for tree <- nonExperimentalStats(trees) do
841+
tree match
842+
case tree: MemberDef =>
843+
// TODO move this out of checking (into posttyper?)
844+
val sym = tree.symbol
845+
if !sym.isExperimental then
846+
sym.addAnnotation(ExperimentalAnnotation(i"Added by top level $imp", sym.span))
847+
case tree =>
848+
// There is no definition to attach the @experimental annotation
849+
report.error("Implementation restriction: top-level `val _ = ...` is not supported with experimental language imports.", tree.srcPos)
850+
else Feature.checkExperimentalFeature("feature local import", imp.srcPos)
847851
case _ =>
848-
end checkExperimentalImports
852+
end checkAndAdaptExperimentalImports
849853

850854
/** Checks that PolyFunction only have valid refinements.
851855
*

Diff for: compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala

+8-6
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,14 @@ class CrossVersionChecks extends MiniPhase:
132132
}
133133

134134
override def transformOther(tree: Tree)(using Context): Tree =
135-
tree.foreachSubTree { // Find references in type trees and imports
136-
case tree: Ident => transformIdent(tree)
137-
case tree: Select => transformSelect(tree)
138-
case tree: TypeTree => transformTypeTree(tree)
139-
case _ =>
140-
}
135+
val inPackage = ctx.owner.is(Package) || ctx.owner.isPackageObject
136+
if !(inPackage && tree.isInstanceOf[ImportOrExport] && Feature.isExperimentalEnabledByImport) then
137+
tree.foreachSubTree { // Find references in type trees and imports
138+
case tree: Ident => transformIdent(tree)
139+
case tree: Select => transformSelect(tree)
140+
case tree: TypeTree => transformTypeTree(tree)
141+
case _ =>
142+
}
141143
tree
142144

143145
end CrossVersionChecks

Diff for: 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

Diff for: tests/neg-macros/i18677-a.check

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
|The tree does not conform to the compiler's tree invariants.
88
|
99
|Macro was:
10-
|@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-a/Test_2.scala") @scala.annotation.experimental("Added by -experimental") @extendFoo class AFoo()
10+
|@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-a/Test_2.scala") @scala.annotation.experimental("Added by -experimental or -language:experimental.*") @extendFoo class AFoo()
1111
|
1212
|The macro returned:
13-
|@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-a/Test_2.scala") @scala.annotation.experimental("Added by -experimental") @extendFoo class AFoo() extends Foo
13+
|@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-a/Test_2.scala") @scala.annotation.experimental("Added by -experimental or -language:experimental.*") @extendFoo class AFoo() extends Foo
1414
|
1515
|Error:
1616
|assertion failed: Parents of class symbol differs from the parents in the tree for class AFoo

Diff for: tests/neg-macros/i18677-b.check

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
|The tree does not conform to the compiler's tree invariants.
88
|
99
|Macro was:
10-
|@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-b/Test_2.scala") @scala.annotation.experimental("Added by -experimental") @extendFoo class AFoo()
10+
|@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-b/Test_2.scala") @scala.annotation.experimental("Added by -experimental or -language:experimental.*") @extendFoo class AFoo()
1111
|
1212
|The macro returned:
13-
|@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-b/Test_2.scala") @scala.annotation.experimental("Added by -experimental") @extendFoo class AFoo() extends Foo
13+
|@scala.annotation.internal.SourceFile("tests/neg-macros/i18677-b/Test_2.scala") @scala.annotation.experimental("Added by -experimental or -language:experimental.*") @extendFoo class AFoo() extends Foo
1414
|
1515
|Error:
1616
|assertion failed: Parents of class symbol differs from the parents in the tree for class AFoo

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

-5
This file was deleted.

Diff for: tests/neg/experimental-erased.scala

-11
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Error: tests/neg/experimental-import-with-top-level-val-underscore.scala:4:4 ----------------------------------------
2+
4 |val _ = // error
3+
| ^
4+
| Implementation restriction: top-level `val _ = ...` is not supported with experimental language imports.
5+
5 | println("Hello, world!")
6+
6 | 42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
import language.experimental.erasedDefinitions
3+
4+
val _ = // error
5+
println("Hello, world!")
6+
42

Diff for: 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

Diff for: tests/neg/experimental-message-experimental-flag.check

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
-- Error: tests/neg/experimental-message-experimental-flag/Test_2.scala:3:10 -------------------------------------------
33
3 |def g() = f() // error
44
| ^
5-
| method f is marked @experimental: Added by -experimental
5+
| method f is marked @experimental: Added by -experimental or -language:experimental.*
66
|
77
| Experimental definition may only be used under experimental mode:
88
| 1. in a definition marked as @experimental, or
9-
| 2. compiling with the -experimental compiler flag, or
10-
| 3. with a nightly or snapshot version of the compiler.
9+
| 2. an experimental feature is imported at the package level, or
10+
| 3. compiling with the -experimental compiler flag, or
11+
| 4. with a nightly or snapshot version of the compiler.

Diff for: tests/neg/experimental-message.check

+9-6
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,26 @@
55
|
66
| Experimental definition may only be used under experimental mode:
77
| 1. in a definition marked as @experimental, or
8-
| 2. compiling with the -experimental compiler flag, or
9-
| 3. with a nightly or snapshot version of the compiler.
8+
| 2. an experimental feature is imported at the package level, or
9+
| 3. compiling with the -experimental compiler flag, or
10+
| 4. with a nightly or snapshot version of the compiler.
1011
-- Error: tests/neg/experimental-message.scala:16:2 --------------------------------------------------------------------
1112
16 | f2() // error
1213
| ^^
1314
| method f2 is marked @experimental
1415
|
1516
| Experimental definition may only be used under experimental mode:
1617
| 1. in a definition marked as @experimental, or
17-
| 2. compiling with the -experimental compiler flag, or
18-
| 3. with a nightly or snapshot version of the compiler.
18+
| 2. an experimental feature is imported at the package level, or
19+
| 3. compiling with the -experimental compiler flag, or
20+
| 4. with a nightly or snapshot version of the compiler.
1921
-- Error: tests/neg/experimental-message.scala:17:2 --------------------------------------------------------------------
2022
17 | f3() // error
2123
| ^^
2224
| method f3 is marked @experimental: not yet stable
2325
|
2426
| Experimental definition may only be used under experimental mode:
2527
| 1. in a definition marked as @experimental, or
26-
| 2. compiling with the -experimental compiler flag, or
27-
| 3. with a nightly or snapshot version of the compiler.
28+
| 2. an experimental feature is imported at the package level, or
29+
| 3. compiling with the -experimental compiler flag, or
30+
| 4. with a nightly or snapshot version of the compiler.

Diff for: 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

Diff for: 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

0 commit comments

Comments
 (0)