Skip to content

Commit 7ed0907

Browse files
oderskynicolasstucki
authored andcommitted
Allow experimental language imports in experimental scopes
Also allow top-level experimental language imports if all top-level definitions are experimental
1 parent f8dec07 commit 7ed0907

16 files changed

+245
-13
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ object Feature:
9898
else
9999
false
100100

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

105105
def checkExperimentalDef(sym: Symbol, srcPos: SrcPos)(using Context) =
106106
if !isExperimentalEnabled then

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3114,10 +3114,6 @@ object Parsers {
31143114
languageImport(tree) match
31153115
case Some(prefix) =>
31163116
in.languageImportContext = in.languageImportContext.importContext(imp, NoSymbol)
3117-
if prefix == nme.experimental
3118-
&& selectors.exists(sel => Feature.experimental(sel.name) != Feature.scala2macros && Feature.experimental(sel.name) != Feature.erasedDefinitions)
3119-
then
3120-
Feature.checkExperimentalFeature("features", imp.srcPos)
31213117
for
31223118
case ImportSelector(id @ Ident(imported), EmptyTree, _) <- selectors
31233119
if allSourceVersionNames.contains(imported)

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,10 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
448448
throw ex
449449
}
450450

451+
override def transformStats(trees: List[Tree], exprOwner: Symbol)(using Context): List[Tree] =
452+
try super.transformStats(trees, exprOwner)
453+
finally Checking.checkExperimentalImports(trees)
454+
451455
/** Transforms the rhs tree into a its default tree if it is in an `erased` val/def.
452456
* Performed to shrink the tree that is known to be erased later.
453457
*/

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,50 @@ object Checking {
721721
checkValue(tree)
722722
case _ =>
723723
tree
724+
725+
/** Check that experimental language imports in `trees`
726+
* are done only in experimental scopes, or in a top-level
727+
* scope with only @experimental definitions.
728+
*/
729+
def checkExperimentalImports(trees: List[Tree])(using Context): Unit =
730+
731+
def nonExperimentalStat(trees: List[Tree]): Tree = trees match
732+
case (_: Import | EmptyTree) :: rest =>
733+
nonExperimentalStat(rest)
734+
case (tree @ TypeDef(_, impl: Template)) :: rest if tree.symbol.isPackageObject =>
735+
nonExperimentalStat(impl.body).orElse(nonExperimentalStat(rest))
736+
case (tree: PackageDef) :: rest =>
737+
nonExperimentalStat(tree.stats).orElse(nonExperimentalStat(rest))
738+
case (tree: MemberDef) :: rest =>
739+
if tree.symbol.isExperimental || tree.symbol.is(Synthetic) then
740+
nonExperimentalStat(rest)
741+
else
742+
tree
743+
case tree :: rest =>
744+
tree
745+
case Nil =>
746+
EmptyTree
747+
748+
for case imp @ Import(qual, selectors) <- trees do
749+
def isAllowedImport(sel: untpd.ImportSelector) =
750+
val name = Feature.experimental(sel.name)
751+
name == Feature.scala2macros || name == Feature.erasedDefinitions
752+
753+
languageImport(qual) match
754+
case Some(nme.experimental)
755+
if !ctx.owner.isInExperimentalScope && !selectors.forall(isAllowedImport) =>
756+
def check(stable: => String) =
757+
Feature.checkExperimentalFeature("features", imp.srcPos,
758+
s"\n\nNote: the scope enclosing the import is not considered experimental because it contains the\nnon-experimental $stable")
759+
if ctx.owner.is(Package) then
760+
// allow top-level experimental imports if all definitions are @experimental
761+
nonExperimentalStat(trees) match
762+
case EmptyTree =>
763+
case tree: MemberDef => check(i"${tree.symbol}")
764+
case tree => check(i"expression ${tree}")
765+
else Feature.checkExperimentalFeature("features", imp.srcPos)
766+
case _ =>
767+
end checkExperimentalImports
724768
}
725769

726770
trait Checking {

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class CompilationTests {
4242
compileFilesInDir("tests/pos-custom-args/erased", defaultOptions.and("-language:experimental.erasedDefinitions")),
4343
compileFilesInDir("tests/pos", defaultOptions.and("-Ysafe-init")),
4444
compileFilesInDir("tests/pos-deep-subtype", allowDeepSubtypes),
45+
compileFilesInDir("tests/pos-custom-args/no-experimental", defaultOptions.and("-Yno-experimental")),
4546
compileDir("tests/pos-special/java-param-names", defaultOptions.withJavacOnlyOptions("-parameters")),
4647
compileFile(
4748
// succeeds despite -Xfatal-warnings because of -nowarn
@@ -178,7 +179,7 @@ class CompilationTests {
178179
compileFile("tests/neg-custom-args/matchable.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")),
179180
compileFile("tests/neg-custom-args/i7314.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")),
180181
compileFile("tests/neg-custom-args/feature-shadowing.scala", defaultOptions.and("-Xfatal-warnings", "-feature")),
181-
compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")),
182+
compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")),
182183
).checkExpectedErrors()
183184
}
184185

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
layout: doc-page
3+
title: Experimental language features
4+
author: Nicolas Stucki
5+
---
6+
7+
### Experimental language features
8+
9+
All experimental language features can be found under the `scala.language.experimental` package.
10+
They are enabled by importing the feature or using the `-language` compiler flag.
11+
12+
* [`erasedDefinitions`](./erased-defs.md): Enable support for `erased` modifier.
13+
* `fewerBraces`: Enable support for using indentation for arguments.
14+
* [`genericNumberLiterals`](../changed-features/numeric-literals.md): Enable support for generic number literals.
15+
* [`namedTypeArguments`](../changed-features/named-typeargs.md): Enable support for named type arguments
16+
17+
### Experimental language imports
18+
19+
In general, experimental language features can be imported in an experimental scope (see [experimental definitions](../other-new-features/experimental-defs.md).
20+
They can be imported at the top-level if all top-level definitions are @experimental.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ class A:
276276
class B extends A:
277277
- @experimental def f: Int = 2
278278
```
279+
279280
### Test frameworks
280281

281282
Tests can be defined as experimental. Tests frameworks can execute tests using reflection even if they are in an experimental class, object or method.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class Test7 {
2+
import scala.language.experimental
3+
import experimental.genericNumberLiterals // error: no aliases can be used to refer to a language import
4+
val x: BigInt = 13232202002020202020202 // error
5+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import annotation.experimental
2+
3+
@experimental
4+
object Object1:
5+
import language.experimental.fewerBraces
6+
import language.experimental.namedTypeArguments
7+
import language.experimental.genericNumberLiterals
8+
import language.experimental.erasedDefinitions
9+
erased def f = 1
10+
11+
object Object2:
12+
import language.experimental.fewerBraces // error
13+
import language.experimental.namedTypeArguments // error
14+
import language.experimental.genericNumberLiterals // error
15+
import language.experimental.erasedDefinitions
16+
erased def f = 1
17+
18+
@experimental
19+
object Class1:
20+
import language.experimental.fewerBraces
21+
import language.experimental.namedTypeArguments
22+
import language.experimental.genericNumberLiterals
23+
import language.experimental.erasedDefinitions
24+
erased def f = 1
25+
26+
object Class2:
27+
import language.experimental.fewerBraces // error
28+
import language.experimental.namedTypeArguments // error
29+
import language.experimental.genericNumberLiterals // error
30+
import language.experimental.erasedDefinitions
31+
erased def f = 1
32+
33+
@experimental
34+
def fun1 =
35+
import language.experimental.fewerBraces
36+
import language.experimental.namedTypeArguments
37+
import language.experimental.genericNumberLiterals
38+
import language.experimental.erasedDefinitions
39+
erased def f = 1
40+
41+
def fun2 =
42+
import language.experimental.fewerBraces // error
43+
import language.experimental.namedTypeArguments // error
44+
import language.experimental.genericNumberLiterals // error
45+
import language.experimental.erasedDefinitions
46+
erased def f = 1
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import annotation.experimental
2+
3+
class Class1:
4+
import language.experimental.fewerBraces // error
5+
import language.experimental.namedTypeArguments // error
6+
import language.experimental.genericNumberLiterals // error
7+
import language.experimental.erasedDefinitions // ok: only check at erased definition
8+
@experimental def f = 1
9+
def g = 1
10+
11+
object Object1:
12+
import language.experimental.fewerBraces // error
13+
import language.experimental.namedTypeArguments // error
14+
import language.experimental.genericNumberLiterals // error
15+
import language.experimental.erasedDefinitions // ok: only check at erased definition
16+
@experimental def f = 1
17+
def g = 1
18+
19+
def fun1 =
20+
import language.experimental.fewerBraces // error
21+
import language.experimental.namedTypeArguments // error
22+
import language.experimental.genericNumberLiterals // error
23+
import language.experimental.erasedDefinitions // ok: only check at erased definition
24+
@experimental def f = 1
25+
def g = 1
26+
27+
val value1 =
28+
import language.experimental.fewerBraces // error
29+
import language.experimental.namedTypeArguments // error
30+
import language.experimental.genericNumberLiterals // error
31+
import language.experimental.erasedDefinitions // ok: only check at erased definition
32+
@experimental def f = 1
33+
def g = 1
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import annotation.experimental
2+
3+
class Class1:
4+
import language.experimental.fewerBraces // error
5+
import language.experimental.namedTypeArguments // error
6+
import language.experimental.genericNumberLiterals // error
7+
import language.experimental.erasedDefinitions // ok: only check at erased definition
8+
9+
object Object1:
10+
import language.experimental.fewerBraces // error
11+
import language.experimental.namedTypeArguments // error
12+
import language.experimental.genericNumberLiterals // error
13+
import language.experimental.erasedDefinitions // ok: only check at erased definition
14+
15+
def fun1 =
16+
import language.experimental.fewerBraces // error
17+
import language.experimental.namedTypeArguments // error
18+
import language.experimental.genericNumberLiterals // error
19+
import language.experimental.erasedDefinitions // ok: only check at erased definition
20+
21+
val value1 =
22+
import language.experimental.fewerBraces // error
23+
import language.experimental.namedTypeArguments // error
24+
import language.experimental.genericNumberLiterals // error
25+
import language.experimental.erasedDefinitions // ok: only check at erased definition
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import annotation.experimental
2+
3+
class Class1:
4+
import language.experimental.fewerBraces // error
5+
import language.experimental.namedTypeArguments // error
6+
import language.experimental.genericNumberLiterals // error
7+
import language.experimental.erasedDefinitions // ok: only check at erased definition
8+
@experimental def f = 1
9+
10+
object Object1:
11+
import language.experimental.fewerBraces // error
12+
import language.experimental.namedTypeArguments // error
13+
import language.experimental.genericNumberLiterals // error
14+
import language.experimental.erasedDefinitions // ok: only check at erased definition
15+
@experimental def f = 1
16+
17+
def fun1 =
18+
import language.experimental.fewerBraces // error
19+
import language.experimental.namedTypeArguments // error
20+
import language.experimental.genericNumberLiterals // error
21+
import language.experimental.erasedDefinitions // ok: only check at erased definition
22+
@experimental def f = 1
23+
24+
val value1 =
25+
import language.experimental.fewerBraces // error
26+
import language.experimental.namedTypeArguments // error
27+
import language.experimental.genericNumberLiterals // error
28+
import language.experimental.erasedDefinitions // ok: only check at erased definition
29+
@experimental def f = 1
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import annotation.experimental
2+
3+
package foo {
4+
import language.experimental.fewerBraces // error
5+
import language.experimental.namedTypeArguments // error
6+
import language.experimental.genericNumberLiterals // error
7+
import language.experimental.erasedDefinitions // ok: only check at erased definition
8+
9+
package bar {
10+
def foo = 1
11+
}
12+
}
13+
14+
package foo2 {
15+
// ok: all definitions are top-level @experimental
16+
import language.experimental.fewerBraces
17+
import language.experimental.namedTypeArguments
18+
import language.experimental.genericNumberLiterals
19+
import language.experimental.erasedDefinitions
20+
21+
package bar {
22+
@experimental def foo = 1
23+
}
24+
}

tests/neg-custom-args/no-experimental/experimental.scala

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,3 @@ class Test2 {
2626
class Test6 {
2727
import scala.language.experimental // ok
2828
}
29-
30-
class Test7 {
31-
import scala.language.experimental
32-
import experimental.genericNumberLiterals // error: no aliases can be used to refer to a language import
33-
val x: BigInt = 13232202002020202020202 // error
34-
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import annotation.experimental
2+
import language.experimental.fewerBraces
3+
import language.experimental.namedTypeArguments
4+
import language.experimental.genericNumberLiterals
5+
import language.experimental.erasedDefinitions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import language.experimental.erasedDefinitions
2+
import annotation.experimental
3+
4+
@experimental
5+
erased def f = 1

0 commit comments

Comments
 (0)