Skip to content

Commit

Permalink
Allow experimental language imports in experimental scopes
Browse files Browse the repository at this point in the history
Also allow top-level experimental language imports if all top-level definitions are experimental
  • Loading branch information
odersky authored and nicolasstucki committed Aug 30, 2021
1 parent 028c749 commit c92da2e
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 14 deletions.
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 @@ -97,9 +97,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
41 changes: 41 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,47 @@ 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
languageImport(qual) match
case Some(nme.experimental)
if !ctx.owner.isInExperimentalScope
&& selectors.exists(sel => experimental(sel.name) != scala2macros && experimental(sel.name) != erasedDefinitions) =>
def check(stable: => String) =
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 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
6 changes: 5 additions & 1 deletion docs/docs/reference/other-new-features/experimental-defs.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ This annotation can be placed on term or type definitions.

### References to experimental definitions

Experimental definitions can only be referenced in an experimental scope. Experimental scopes are defined as follows.
- Experimental definitions can only be referenced in an experimental scope.
- Experimental language features can be imported in an experimental scope.
- Experimental language features can be imported at the top-level if all top-level definitions are @experimental.

Experimental scopes are defined as follows.

(1) The RHS of an experimental `def`, `val`, `var`, `given` or `type` is an experimental scope.

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

0 comments on commit c92da2e

Please sign in to comment.