Skip to content

Commit 0544d7c

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 1cf779c commit 0544d7c

29 files changed

+145
-124
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ class Driver {
8080
val ictx = rootCtx.fresh
8181
val summary = command.distill(args, ictx.settings)(ictx.settingsState)(using ictx)
8282
ictx.setSettings(summary.sstate)
83-
Feature.checkExperimentalSettings(using ictx)
8483
MacroClassLoader.init(ictx)
8584
Positioned.init(using ictx)
8685

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

Lines changed: 19 additions & 9 deletions
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

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2008,6 +2008,13 @@ class Definitions {
20082008
CapabilityAnnot, RequiresCapabilityAnnot,
20092009
RetainsAnnot, RetainsCapAnnot, RetainsByNameAnnot)
20102010

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

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

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

Lines changed: 6 additions & 4 deletions
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`.

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

Lines changed: 29 additions & 25 deletions
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
*

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

Lines changed: 8 additions & 6 deletions
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

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

Lines changed: 2 additions & 0 deletions
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

tests/neg-macros/i18677-a.check

Lines changed: 2 additions & 2 deletions
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

tests/neg-macros/i18677-b.check

Lines changed: 2 additions & 2 deletions
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

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

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

0 commit comments

Comments
 (0)