Skip to content

Commit 3920665

Browse files
authored
Enable experimental mode when experimental feature is imported (#19807)
Make it easier to use experimental language features out of the box. We already have `-experimental` that enables experimental mode globally by adding the `@experimental` annotation to all top-level definitions. The complication was that this flag was necessary to use experimental language features. It was not always easy to set (for example in the REPL) and it is all or nothing. Users can also annotate explicitly all top-level definitions that use language features with `@experimental`. Now, the import of an experimental language feature will automatically enable experimental mode. If the import is at the top-level, all definitions in the file are experimental. We also remove the special case that makes all code generated with a nightly or snapshot compiler compile as if in experimental mode. This implies that now the behavior of nightlies is identical to the behavior of release versions. The `-Yno-experimental` becomes a no-op and will be removed in a later PR. [test_scala2_library_tasty]
2 parents 48aac2c + 803603b commit 3920665

File tree

210 files changed

+339
-331
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

210 files changed

+339
-331
lines changed

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

-1
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

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

+15-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,19 @@ 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.
162167
|""".stripMargin
163168

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-
170169
def isExperimentalEnabled(using Context): Boolean =
171-
(Properties.unstableExperimentalEnabled && !ctx.settings.YnoExperimental.value) || ctx.settings.experimental.value
170+
ctx.settings.experimental.value ||
171+
experimentalAutoEnableFeatures.exists(enabled)
172+
173+
def experimentalEnabledByLanguageSetting(using Context): Option[TermName] =
174+
experimentalAutoEnableFeatures.find(enabledBySetting)
175+
176+
def isExperimentalEnabledByImport(using Context): Boolean =
177+
experimentalAutoEnableFeatures.exists(enabledByImport)
172178

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

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

-6
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,6 @@ trait PropertiesTrait {
8484
*/
8585
val versionString: String = "version " + simpleVersionString
8686

87-
/** Whether the current version of compiler is experimental
88-
*
89-
* Snapshot, nightly releases and non-bootstrapped compiler are experimental.
90-
*/
91-
val unstableExperimentalEnabled: Boolean = versionString.contains("SNAPSHOT") || versionString.contains("NIGHTLY") || versionString.contains("nonbootstrapped")
92-
9387
/** Whether the current version of compiler supports research plugins. */
9488
val researchPluginEnabled: Boolean = versionString.contains("SNAPSHOT") || versionString.contains("NIGHTLY") || versionString.contains("nonbootstrapped")
9589

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

-1
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,6 @@ private sealed trait YSettings:
406406
val YretainTrees: Setting[Boolean] = BooleanSetting(ForkSetting, "Yretain-trees", "Retain trees for top-level classes, accessible from ClassSymbol#tree")
407407
val YshowTreeIds: Setting[Boolean] = BooleanSetting(ForkSetting, "Yshow-tree-ids", "Uniquely tag all tree nodes in debugging output.")
408408
val YfromTastyIgnoreList: Setting[List[String]] = MultiStringSetting(ForkSetting, "Yfrom-tasty-ignore-list", "file", "List of `tasty` files in jar files that will not be loaded when using -from-tasty.")
409-
val YnoExperimental: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-experimental", "Disable experimental language features by default in NIGHTLY/SNAPSHOT versions of the compiler.")
410409
val YlegacyLazyVals: Setting[Boolean] = BooleanSetting(ForkSetting, "Ylegacy-lazy-vals", "Use legacy (pre 3.3.0) implementation of lazy vals.")
411410
val YcompileScala2Library: Setting[Boolean] = BooleanSetting(ForkSetting, "Ycompile-scala2-library", "Used when compiling the Scala 2 standard library.")
412411
val YoutputOnlyTasty: Setting[Boolean] = BooleanSetting(ForkSetting, "Youtput-only-tasty", "Used to only generate the TASTy file without the classfiles")

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

+7
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) {

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

+5-11
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
417417
)
418418
}
419419
case tree: ValDef =>
420-
annotateExperimental(tree.symbol)
420+
annotateExperimentalCompanion(tree.symbol)
421421
registerIfHasMacroAnnotations(tree)
422422
checkErasedDef(tree)
423423
Checking.checkPolyFunctionType(tree.tpt)
@@ -426,7 +426,6 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
426426
checkStableSelection(tree.rhs)
427427
processValOrDefDef(super.transform(tree1))
428428
case tree: DefDef =>
429-
annotateExperimental(tree.symbol)
430429
registerIfHasMacroAnnotations(tree)
431430
checkErasedDef(tree)
432431
Checking.checkPolyFunctionType(tree.tpt)
@@ -438,7 +437,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
438437
val sym = tree.symbol
439438
if (sym.isClass)
440439
VarianceChecker.check(tree)
441-
annotateExperimental(sym)
440+
annotateExperimentalCompanion(sym)
442441
checkMacroAnnotation(sym)
443442
if sym.isOneOf(GivenOrImplicit) then
444443
sym.keepAnnotationsCarrying(thisPhase, Set(defn.CompanionClassMetaAnnot), orNoneOf = defn.MetaAnnots)
@@ -546,8 +545,8 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
546545
}
547546

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

552551
/** Transforms the rhs tree into a its default tree if it is in an `erased` val/def.
553552
* Performed to shrink the tree that is known to be erased later.
@@ -584,14 +583,9 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
584583
else if tpe.derivesFrom(defn.NullClass) then
585584
report.error("`erased` definition cannot be implemented with en expression of type Null", tree.srcPos)
586585

587-
private def annotateExperimental(sym: Symbol)(using Context): Unit =
588-
def isTopLevelDefinitionInSource(sym: Symbol) =
589-
!sym.is(Package) && !sym.name.isPackageObjectName &&
590-
(sym.owner.is(Package) || (sym.owner.isPackageObject && !sym.isConstructor))
586+
private def annotateExperimentalCompanion(sym: Symbol)(using Context): Unit =
591587
if sym.is(Module) then
592588
ExperimentalAnnotation.copy(sym.companionClass).foreach(sym.addAnnotation)
593-
if !sym.hasAnnotation(defn.ExperimentalAnnot) && ctx.settings.experimental.value && isTopLevelDefinitionInSource(sym) then
594-
sym.addAnnotation(ExperimentalAnnotation("Added by -experimental", sym.span))
595589

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

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

+45-37
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.*
@@ -797,50 +798,57 @@ object Checking {
797798
tree
798799

799800
/** Check that experimental language imports in `trees`
800-
* are done only in experimental scopes, or in a top-level
801-
* scope with only @experimental definitions.
801+
* are done only in experimental scopes. For top-level
802+
* experimental imports, all top-level definitions are transformed
803+
* to @experimental definitions.
804+
*
802805
*/
803-
def checkExperimentalImports(trees: List[Tree])(using Context): Unit =
804-
805-
def nonExperimentalStat(trees: List[Tree]): Tree = trees match
806-
case (_: Import | EmptyTree) :: rest =>
807-
nonExperimentalStat(rest)
808-
case (tree @ TypeDef(_, impl: Template)) :: rest if tree.symbol.isPackageObject =>
809-
nonExperimentalStat(impl.body).orElse(nonExperimentalStat(rest))
810-
case (tree: PackageDef) :: rest =>
811-
nonExperimentalStat(tree.stats).orElse(nonExperimentalStat(rest))
812-
case (tree: MemberDef) :: rest =>
813-
if tree.symbol.isExperimental || tree.symbol.is(Synthetic) then
814-
nonExperimentalStat(rest)
815-
else
816-
tree
817-
case tree :: rest =>
818-
tree
819-
case Nil =>
820-
EmptyTree
821-
822-
for case imp @ Import(qual, selectors) <- trees do
806+
def checkAndAdaptExperimentalImports(trees: List[Tree])(using Context): Unit =
807+
def nonExperimentalTopLevelDefs(pack: Symbol): Iterator[Symbol] =
808+
def isNonExperimentalTopLevelDefinition(sym: Symbol) =
809+
!sym.isExperimental
810+
&& sym.source == ctx.compilationUnit.source
811+
&& !sym.isConstructor // not constructor of package object
812+
&& !sym.is(Package) && !sym.name.isPackageObjectName
813+
814+
pack.info.decls.toList.iterator.flatMap: sym =>
815+
if sym.isClass && (sym.is(Package) || sym.isPackageObject) then
816+
nonExperimentalTopLevelDefs(sym)
817+
else if isNonExperimentalTopLevelDefinition(sym) then
818+
sym :: Nil
819+
else Nil
820+
821+
def unitExperimentalLanguageImports =
823822
def isAllowedImport(sel: untpd.ImportSelector) =
824823
val name = Feature.experimental(sel.name)
825824
name == Feature.scala2macros
826-
|| name == Feature.erasedDefinitions
827825
|| name == Feature.captureChecking
826+
trees.filter {
827+
case Import(qual, selectors) =>
828+
languageImport(qual) match
829+
case Some(nme.experimental) =>
830+
!selectors.forall(isAllowedImport) && !ctx.owner.isInExperimentalScope
831+
case _ => false
832+
case _ => false
833+
}
828834

829-
languageImport(qual) match
830-
case Some(nme.experimental)
831-
if !ctx.owner.isInExperimentalScope && !selectors.forall(isAllowedImport) =>
832-
def check(stable: => String) =
833-
Feature.checkExperimentalFeature("features", imp.srcPos,
834-
s"\n\nNote: the scope enclosing the import is not considered experimental because it contains the\nnon-experimental $stable")
835-
if ctx.owner.is(Package) then
836-
// allow top-level experimental imports if all definitions are @experimental
837-
nonExperimentalStat(trees) match
838-
case EmptyTree =>
839-
case tree: MemberDef => check(i"${tree.symbol}")
840-
case tree => check(i"expression ${tree}")
841-
else Feature.checkExperimentalFeature("features", imp.srcPos)
835+
if ctx.owner.is(Package) || ctx.owner.name.startsWith(str.REPL_SESSION_LINE) then
836+
def markTopLevelDefsAsExperimental(why: String): Unit =
837+
for sym <- nonExperimentalTopLevelDefs(ctx.owner) do
838+
sym.addAnnotation(ExperimentalAnnotation(s"Added by $why", sym.span))
839+
840+
unitExperimentalLanguageImports match
841+
case imp :: _ => markTopLevelDefsAsExperimental(i"top level $imp")
842842
case _ =>
843-
end checkExperimentalImports
843+
Feature.experimentalEnabledByLanguageSetting match
844+
case Some(sel) => markTopLevelDefsAsExperimental(i"-language:experimental.$sel")
845+
case _ if ctx.settings.experimental.value => markTopLevelDefsAsExperimental(i"-experimental")
846+
case _ =>
847+
else
848+
for imp <- unitExperimentalLanguageImports do
849+
Feature.checkExperimentalFeature("feature local import", imp.srcPos)
850+
851+
end checkAndAdaptExperimentalImports
844852

845853
/** Checks that PolyFunction only have valid refinements.
846854
*

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: compiler/test/dotty/tools/backend/jvm/PublicInBinaryTests.scala

-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ class PublicInBinaryTests extends DottyBytecodeTest {
4242
override def initCtx =
4343
val ctx0 = super.initCtx
4444
ctx0.setSetting(ctx0.settings.experimental, true)
45-
ctx0.setSetting(ctx0.settings.YnoExperimental, true)
4645

4746
@Test
4847
def publicInBinaryDef(): Unit = {

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,7 @@ 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.
269-
Can use the `-Yno-experimental` compiler flag to disable it and run as a proper release.
268+
6. An experimental language feature is imported in at the package level. All top-level definitions will be marked as `@experimental`.
270269

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

Diff for: project/Build.scala

+3-4
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,6 @@ object Build {
10831083
Compile / doc / scalacOptions += "-Ydocument-synthetic-types",
10841084
scalacOptions += "-Ycompile-scala2-library",
10851085
scalacOptions += "-Yscala2Unpickler:never",
1086-
scalacOptions += "-Yno-experimental",
10871086
scalacOptions -= "-Xfatal-warnings",
10881087
Compile / compile / logLevel.withRank(KeyRanks.Invisible) := Level.Error,
10891088
ivyConfigurations += SourceDeps.hide,
@@ -1757,6 +1756,9 @@ object Build {
17571756
SourceLinksIntegrationTest / scalaSource := baseDirectory.value / "test-source-links",
17581757
SourceLinksIntegrationTest / test:= ((SourceLinksIntegrationTest / test) dependsOn generateScalaDocumentation.toTask("")).value,
17591758
).
1759+
settings(
1760+
scalacOptions += "-experimental" // workaround use of experimental .info in Scaladoc2AnchorCreator
1761+
).
17601762
settings(
17611763
Compile / resourceGenerators ++= Seq(
17621764
generateStaticAssetsTask.taskValue,
@@ -2170,9 +2172,6 @@ object Build {
21702172
settings(
21712173
versionScheme := Some("semver-spec"),
21722174
libraryDependencies += "org.scala-lang" % "scala-library" % stdlibVersion,
2173-
// Make sure we do not refer to experimental features outside an experimental scope.
2174-
// In other words, disable NIGHTLY/SNAPSHOT experimental scope.
2175-
scalacOptions += "-Yno-experimental",
21762175
).
21772176
settings(dottyLibrarySettings)
21782177
if (mode == Bootstrapped) {

Diff for: scaladoc-testcases/src/tests/hugetype.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ trait E:
3131
@deprecated
3232
protected implicit def same[A](a: A): A
3333

34-
trait XD extends E:
34+
@experimental trait XD extends E:
3535
/**
3636
* Some important information :o
3737
*

Diff for: scaladoc-testcases/src/tests/methodsAndConstructors.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package tests.methodsAndConstructors
22

3+
import scala.language.experimental.clauseInterleaving
4+
35
class A
46
class B extends A
57
class C
@@ -60,8 +62,6 @@ class Methods:
6062
def withImplicitParam2(v: String)(implicit ab: Double, a: Int, b: String): String
6163
= ???
6264

63-
import scala.language.experimental.clauseInterleaving
64-
6565
def clauseInterleaving[T](x: T)[U](y: U)(using (T, U)): (T, U)
6666
= ???
6767

Diff for: scaladoc/src/dotty/tools/scaladoc/snippets/SnippetCompiler.scala

-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ class SnippetCompiler(
2727
object SnippetDriver extends Driver:
2828
val currentCtx =
2929
val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions).addMode(Mode.Interactive)
30-
rootCtx.setSetting(rootCtx.settings.YnoExperimental, true)
3130
rootCtx.setSetting(rootCtx.settings.experimental, true)
3231
rootCtx.setSetting(rootCtx.settings.YretainTrees, true)
3332
rootCtx.setSetting(rootCtx.settings.YcookComments, true)

Diff for: tests/init-global/pos/global-region1.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//> using options -experimental -Yno-experimental
1+
//> using options -experimental
22

33
import scala.annotation.init.region
44

Diff for: tests/init-global/warn/i18628_3.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//> using options -experimental -Yno-experimental
1+
//> using options -experimental
22

33
import scala.annotation.init.widen
44

Diff for: tests/neg-macros/i18677-a/Macro_1.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//> using options -experimental -Yno-experimental
1+
//> using options -experimental
22

33
import annotation.MacroAnnotation
44
import quoted.*

Diff for: tests/neg-macros/i18677-a/Test_2.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//> using options -experimental -Yno-experimental
1+
//> using options -experimental
22

33
@extendFoo
44
class AFoo // error

Diff for: tests/neg-macros/i18677-b/Macro_1.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//> using options -experimental -Yno-experimental
1+
//> using options -experimental
22

33
import annotation.MacroAnnotation
44
import quoted.*

Diff for: tests/neg-macros/i18677-b/Test_2.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//> using options -experimental -Yno-experimental
1+
//> using options -experimental
22

33
@extendFoo
44
class AFoo // error

Diff for: tests/neg-macros/i19676/Macro_1.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//> using options -experimental -Yno-experimental
1+
//> using options -experimental
22

33
import scala.annotation.MacroAnnotation
44
import scala.quoted.*

Diff for: tests/neg-macros/i19676/Test_2.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//> using options -experimental -Yno-experimental
1+
//> using options -experimental
22

33
@buggy // error
44
case class Foo()

Diff for: tests/neg-macros/i19842-a/Macro.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//> using options -experimental -Yno-experimental
1+
//> using options -experimental
22

33
import scala.annotation.{experimental, targetName}
44
import scala.quoted.*

0 commit comments

Comments
 (0)