Skip to content

Allow experimental language imports in experimental scopes #13417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ object Feature:
else
false

def checkExperimentalFeature(which: String, srcPos: SrcPos)(using Context) =
def checkExperimentalFeature(which: String, srcPos: SrcPos, note: => String = "")(using Context) =
if !isExperimentalEnabled then
report.error(i"Experimental $which may only be used with a nightly or snapshot version of the compiler", srcPos)
report.error(i"Experimental $which may only be used with a nightly or snapshot version of the compiler$note", srcPos)

def checkExperimentalDef(sym: Symbol, srcPos: SrcPos)(using Context) =
if !isExperimentalEnabled then
Expand Down
4 changes: 0 additions & 4 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3114,10 +3114,6 @@ object Parsers {
languageImport(tree) match
case Some(prefix) =>
in.languageImportContext = in.languageImportContext.importContext(imp, NoSymbol)
if prefix == nme.experimental
&& selectors.exists(sel => Feature.experimental(sel.name) != Feature.scala2macros && Feature.experimental(sel.name) != Feature.erasedDefinitions)
then
Feature.checkExperimentalFeature("features", imp.srcPos)
for
case ImportSelector(id @ Ident(imported), EmptyTree, _) <- selectors
if allSourceVersionNames.contains(imported)
Expand Down
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/PostTyper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,10 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
throw ex
}

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

/** 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
44 changes: 44 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,50 @@ object Checking {
checkValue(tree)
case _ =>
tree

/** Check that experimental language imports in `trees`
* are done only in experimental scopes, or in a top-level
* 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)
case (tree @ TypeDef(_, impl: Template)) :: rest if tree.symbol.isPackageObject =>
nonExperimentalStat(impl.body).orElse(nonExperimentalStat(rest))
case (tree: PackageDef) :: rest =>
nonExperimentalStat(tree.stats).orElse(nonExperimentalStat(rest))
case (tree: MemberDef) :: rest =>
if tree.symbol.isExperimental || tree.symbol.is(Synthetic) then
nonExperimentalStat(rest)
else
tree
case tree :: rest =>
tree
case Nil =>
EmptyTree

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

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main change in this logic is the addition of this condition to detect if we are in a top-level scope.

// 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)
case _ =>
end checkExperimentalImports
}

trait Checking {
Expand Down
3 changes: 2 additions & 1 deletion compiler/test/dotty/tools/dotc/CompilationTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class CompilationTests {
compileFilesInDir("tests/pos-custom-args/erased", defaultOptions.and("-language:experimental.erasedDefinitions")),
compileFilesInDir("tests/pos", defaultOptions.and("-Ysafe-init")),
compileFilesInDir("tests/pos-deep-subtype", allowDeepSubtypes),
compileFilesInDir("tests/pos-custom-args/no-experimental", defaultOptions.and("-Yno-experimental")),
compileDir("tests/pos-special/java-param-names", defaultOptions.withJavacOnlyOptions("-parameters")),
compileFile(
// succeeds despite -Xfatal-warnings because of -nowarn
Expand Down Expand Up @@ -178,7 +179,7 @@ class CompilationTests {
compileFile("tests/neg-custom-args/matchable.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")),
compileFile("tests/neg-custom-args/i7314.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")),
compileFile("tests/neg-custom-args/feature-shadowing.scala", defaultOptions.and("-Xfatal-warnings", "-feature")),
compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")),
compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")),
).checkExpectedErrors()
}

Expand Down
20 changes: 20 additions & 0 deletions docs/docs/reference/experimental/lang-experimental.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
layout: doc-page
title: Experimental language features
author: Nicolas Stucki
---

### Experimental language features

All experimental language features can be found under the `scala.language.experimental` package.
They are enabled by importing the feature or using the `-language` compiler flag.

* [`erasedDefinitions`](./erased-defs.md): Enable support for `erased` modifier.
* `fewerBraces`: Enable support for using indentation for arguments.
* [`genericNumberLiterals`](../changed-features/numeric-literals.md): Enable support for generic number literals.
* [`namedTypeArguments`](../changed-features/named-typeargs.md): Enable support for named type arguments

### Experimental language imports

In general, experimental language features can be imported in an experimental scope (see [experimental definitions](../other-new-features/experimental-defs.md).
They can be imported at the top-level if all top-level definitions are @experimental.
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ class A:
class B extends A:
- @experimental def f: Int = 2
```

### Test frameworks

Tests can be defined as experimental. Tests frameworks can execute tests using reflection even if they are in an experimental class, object or method.
Expand Down
5 changes: 5 additions & 0 deletions tests/neg-custom-args/no-experimental/experimental-2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Test7 {
import scala.language.experimental
import experimental.genericNumberLiterals // error: no aliases can be used to refer to a language import
val x: BigInt = 13232202002020202020202 // error
}
46 changes: 46 additions & 0 deletions tests/neg-custom-args/no-experimental/experimental-imports.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import annotation.experimental

@experimental
object Object1:
import language.experimental.fewerBraces
import language.experimental.namedTypeArguments
import language.experimental.genericNumberLiterals
import language.experimental.erasedDefinitions
erased def f = 1

object Object2:
import language.experimental.fewerBraces // error
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions
erased def f = 1

@experimental
object Class1:
import language.experimental.fewerBraces
import language.experimental.namedTypeArguments
import language.experimental.genericNumberLiterals
import language.experimental.erasedDefinitions
erased def f = 1

object Class2:
import language.experimental.fewerBraces // error
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions
erased def f = 1

@experimental
def fun1 =
import language.experimental.fewerBraces
import language.experimental.namedTypeArguments
import language.experimental.genericNumberLiterals
import language.experimental.erasedDefinitions
erased def f = 1

def fun2 =
import language.experimental.fewerBraces // error
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions
erased def f = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import annotation.experimental

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

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

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

val value1 =
import language.experimental.fewerBraces // error
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
@experimental def f = 1
def g = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import annotation.experimental

class Class1:
import language.experimental.fewerBraces // error
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition

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

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

val value1 =
import language.experimental.fewerBraces // error
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import annotation.experimental

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

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

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

val value1 =
import language.experimental.fewerBraces // error
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition
@experimental def f = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import annotation.experimental

package foo {
import language.experimental.fewerBraces // error
import language.experimental.namedTypeArguments // error
import language.experimental.genericNumberLiterals // error
import language.experimental.erasedDefinitions // ok: only check at erased definition

package bar {
def foo = 1
}
}

package foo2 {
// ok: all definitions are top-level @experimental
import language.experimental.fewerBraces
import language.experimental.namedTypeArguments
import language.experimental.genericNumberLiterals
import language.experimental.erasedDefinitions

package bar {
@experimental def foo = 1
}
}
6 changes: 0 additions & 6 deletions tests/neg-custom-args/no-experimental/experimental.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,3 @@ class Test2 {
class Test6 {
import scala.language.experimental // ok
}

class Test7 {
import scala.language.experimental
import experimental.genericNumberLiterals // error: no aliases can be used to refer to a language import
val x: BigInt = 13232202002020202020202 // error
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import annotation.experimental
import language.experimental.fewerBraces
import language.experimental.namedTypeArguments
import language.experimental.genericNumberLiterals
import language.experimental.erasedDefinitions
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import language.experimental.erasedDefinitions
import annotation.experimental

@experimental
erased def f = 1