Skip to content

Commit

Permalink
Enable experimental mode when experimental feature is imported
Browse files Browse the repository at this point in the history
The `@experimental` flag is added to top-level definitions in the package
where the language feature is imported.
  • Loading branch information
nicolasstucki committed Mar 8, 2024
1 parent 18645ee commit 2fa92c4
Show file tree
Hide file tree
Showing 22 changed files with 92 additions and 110 deletions.
1 change: 0 additions & 1 deletion compiler/src/dotty/tools/dotc/Driver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ class Driver {
val ictx = rootCtx.fresh
val summary = command.distill(args, ictx.settings)(ictx.settingsState)(using ictx)
ictx.setSettings(summary.sstate)
Feature.checkExperimentalSettings(using ictx)
MacroClassLoader.init(ictx)
Positioned.init(using ictx)

Expand Down
27 changes: 16 additions & 11 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ object Feature:
val captureChecking = experimental("captureChecking")
val into = experimental("into")

def experimentalAutoEnableFeatures(using Context): List[TermName] =
defn.languageExperimentalFeatures
.map(sym => experimental(sym.name))
.filterNot(_ == captureChecking) // TODO is this correct?

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

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

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

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

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

def isExperimentalEnabled(using Context): Boolean =
(Properties.unstableExperimentalEnabled && !ctx.settings.YnoExperimental.value) || ctx.settings.experimental.value
def isExperimentalEnabledByImport(using Context): Boolean =
experimentalAutoEnableFeatures.exists(enabledByImport)

/** Handle language import `import language.<prefix>.<imported>` if it is one
* of the global imports `pureFunctions` or `captureChecking`. In this case
Expand Down
7 changes: 7 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2013,6 +2013,13 @@ class Definitions {
CapabilityAnnot, RequiresCapabilityAnnot,
RetainsAnnot, RetainsCapAnnot, RetainsByNameAnnot)

/** Experimental language fetures defined in `scala.runtime.stdLibPatches.language.experimental`.
*
* This list does not incluede `scala.language.experimental.macros`.
*/
@tu lazy val languageExperimentalFeatures: List[TermSymbol] =
LanguageExperimentalModule.moduleClass.info.decls.toList.filter(_.isAllOf(Lazy | Module)).map(_.asTerm)

// ----- primitive value class machinery ------------------------------------------

class PerRun[T](generate: Context ?=> T) {
Expand Down
6 changes: 3 additions & 3 deletions compiler/src/dotty/tools/dotc/transform/PostTyper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -513,8 +513,8 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
}

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

/** Transforms the rhs tree into a its default tree if it is in an `erased` val/def.
* Performed to shrink the tree that is known to be erased later.
Expand Down Expand Up @@ -549,7 +549,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
!sym.is(Package) && !sym.name.isPackageObjectName &&
(sym.owner.is(Package) || (sym.owner.isPackageObject && !sym.isConstructor))
if !sym.hasAnnotation(defn.ExperimentalAnnot)
&& (ctx.settings.experimental.value && isTopLevelDefinitionInSource(sym))
&& (Feature.isExperimentalEnabledBySetting && isTopLevelDefinitionInSource(sym))
|| (sym.is(Module) && sym.companionClass.hasAnnotation(defn.ExperimentalAnnot))
then
sym.addAnnotation(Annotation(defn.ExperimentalAnnot, sym.span))
Expand Down
43 changes: 22 additions & 21 deletions compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -806,44 +806,45 @@ object Checking {
* scope with only @experimental definitions.
*/
def checkExperimentalImports(trees: List[Tree])(using Context): Unit =

def nonExperimentalStat(trees: List[Tree]): Tree = trees match
case (_: Import | EmptyTree) :: rest =>
nonExperimentalStat(rest)
def nonExperimentalStats(trees: List[Tree]): List[Tree] = trees match
case (_: ImportOrExport | EmptyTree) :: rest =>
nonExperimentalStats(rest)
case (tree @ TypeDef(_, impl: Template)) :: rest if tree.symbol.isPackageObject =>
nonExperimentalStat(impl.body).orElse(nonExperimentalStat(rest))
nonExperimentalStats(impl.body) ::: nonExperimentalStats(rest)
case (tree: PackageDef) :: rest =>
nonExperimentalStat(tree.stats).orElse(nonExperimentalStat(rest))
nonExperimentalStats(tree.stats) ::: nonExperimentalStats(rest)
case (tree: MemberDef) :: rest =>
if tree.symbol.isExperimental || tree.symbol.is(Synthetic) then
nonExperimentalStat(rest)
nonExperimentalStats(rest)
else
tree
tree :: nonExperimentalStats(rest)
case tree :: rest =>
tree
tree :: nonExperimentalStats(rest)
case Nil =>
EmptyTree
Nil

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

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

Expand Down
14 changes: 8 additions & 6 deletions compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,14 @@ class CrossVersionChecks extends MiniPhase:
}

override def transformOther(tree: Tree)(using Context): Tree =
tree.foreachSubTree { // Find references in type trees and imports
case tree: Ident => transformIdent(tree)
case tree: Select => transformSelect(tree)
case tree: TypeTree => transformTypeTree(tree)
case _ =>
}
val inPackage = ctx.owner.is(Package) || ctx.owner.isPackageObject
if !(inPackage && tree.isInstanceOf[ImportOrExport] && Feature.isExperimentalEnabledByImport) then
tree.foreachSubTree { // Find references in type trees and imports
case tree: Ident => transformIdent(tree)
case tree: Select => transformSelect(tree)
case tree: TypeTree => transformTypeTree(tree)
case _ =>
}
tree

end CrossVersionChecks
Expand Down
2 changes: 2 additions & 0 deletions docs/_docs/reference/other-new-features/experimental-defs.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ Experimental definitions can only be referenced in an experimental scope. Experi
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.
Can use the `-Yno-experimental` compiler flag to disable it and run as a proper release.

7. An experimental language feature is imported in at the package level. All top-level definitions will be marked as `@experimental`.

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

## Experimental overriding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ Experimental definitions can only be referenced in an experimental scope. Experi

<details>
<summary>Example 1</summary>

```scala
import scala.annotation.experimental

Expand All @@ -242,7 +242,7 @@ Experimental definitions can only be referenced in an experimental scope. Experi
}
}
```

</details>

5. Annotations of an experimental definition are in experimental scopes. Examples:
Expand All @@ -268,6 +268,8 @@ Experimental definitions can only be referenced in an experimental scope. Experi
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.
Can use the `-Yno-experimental` compiler flag to disable it and run as a proper release.

7. An experimental language feature is imported in at the package level. All top-level definitions will be marked as `@experimental`.

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

## Experimental inheritance
Expand Down
5 changes: 0 additions & 5 deletions tests/neg/expeimental-flag-with-lang-feature-1.scala

This file was deleted.

11 changes: 0 additions & 11 deletions tests/neg/experimental-erased.scala

This file was deleted.

6 changes: 3 additions & 3 deletions tests/neg/experimental-imports.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ object Object2:
import language.experimental.fewerBraces // error
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions
import language.experimental.erasedDefinitions // error
erased def f = 1

@experimental
Expand All @@ -29,7 +29,7 @@ object Class2:
import language.experimental.fewerBraces // error
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions
import language.experimental.erasedDefinitions // error
erased def f = 1

@experimental
Expand All @@ -44,5 +44,5 @@ def fun2 =
import language.experimental.fewerBraces // error
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions
import language.experimental.erasedDefinitions // error
erased def f = 1
8 changes: 4 additions & 4 deletions tests/neg/experimental-nested-imports-2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@ import annotation.experimental
class Class1:
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
import language.experimental.erasedDefinitions // error
@experimental def f = 1
def g = 1

object Object1:
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
import language.experimental.erasedDefinitions // error
@experimental def f = 1
def g = 1

def fun1 =
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
import language.experimental.erasedDefinitions // error
@experimental def f = 1
def g = 1

val value1 =
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
import language.experimental.erasedDefinitions // error
@experimental def f = 1
def g = 1
8 changes: 4 additions & 4 deletions tests/neg/experimental-nested-imports-3.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import annotation.experimental
class Class1:
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
import language.experimental.erasedDefinitions // error

object Object1:
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
import language.experimental.erasedDefinitions // error

def fun1 =
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
import language.experimental.erasedDefinitions // error

val value1 =
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
import language.experimental.erasedDefinitions // error
8 changes: 4 additions & 4 deletions tests/neg/experimental-nested-imports.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@ import annotation.experimental
class Class1:
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
import language.experimental.erasedDefinitions // error
@experimental def f = 1

object Object1:
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
import language.experimental.erasedDefinitions // error
@experimental def f = 1

def fun1 =
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
import language.experimental.erasedDefinitions // error
@experimental def f = 1

val value1 =
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
import language.experimental.erasedDefinitions // error
@experimental def f = 1
2 changes: 1 addition & 1 deletion tests/neg/experimental.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class Test0 {
}

class Test1 {
import scala.language.experimental.erasedDefinitions
import scala.language.experimental.erasedDefinitions // error
import scala.compiletime.erasedValue
type UnivEq[A]
object UnivEq:
Expand Down
24 changes: 0 additions & 24 deletions tests/neg/experimentalErased.scala

This file was deleted.

2 changes: 2 additions & 0 deletions tests/neg/experimentalOverloads.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//> using options -Yno-experimental

import scala.annotation.experimental

trait A:
Expand Down
6 changes: 3 additions & 3 deletions tests/neg/use-experimental-def.check
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
| ^^^
| Experimental definition may only be used under experimental mode:
| 1. in a definition marked as @experimental, or
| 2. compiling with the -experimental compiler flag, or
| 3. with a nightly or snapshot version of the compiler.
| 2. an experimental feature is imported in at the package level, or
| 3. compiling with the -experimental compiler flag, or
| 4. with a nightly or snapshot version of the compiler.
|
| method foo is marked @experimental
|
Loading

0 comments on commit 2fa92c4

Please sign in to comment.