From aa2f5444b5acde52607f8768f1fdc34b14cb07d5 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 26 Aug 2021 16:23:18 +0200 Subject: [PATCH 1/5] Allow experimental language imports in experimental scopes An experimental language import is permitted of the enclosing scope is an experimental scopes, or if all statements in that scope are @experimental definitions. --- .../dotty/tools/dotc/parsing/Parsers.scala | 4 --- .../tools/dotc/transform/PostTyper.scala | 26 +++++++++++++++++++ .../dotty/tools/dotc/CompilationTests.scala | 2 +- .../no-experimental/experimental-erased.scala | 7 +++++ .../no-experimental/experimental.scala | 6 ----- tests/pos/experimental-erased-2.scala | 10 +++++++ tests/pos/experimental-erased.scala | 6 +++++ 7 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 tests/neg-custom-args/no-experimental/experimental-erased.scala create mode 100644 tests/pos/experimental-erased-2.scala create mode 100644 tests/pos/experimental-erased.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index effbfe22f0e4..6821aaa9ca96 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -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) - then - Feature.checkExperimentalFeature("features", imp.srcPos) for case ImportSelector(id @ Ident(imported), EmptyTree, _) <- selectors if allSourceVersionNames.contains(imported) diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index da386525511a..ed8f45a33345 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -13,6 +13,7 @@ import Decorators._ import Symbols._, SymUtils._, NameOps._ import ContextFunctionResults.annotateContextResults import config.Printers.typr +import config.Feature import reporting._ object PostTyper { @@ -448,6 +449,31 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase throw ex } + /** In addition to normal processing, check that experimental language + * imports are done only in experimental scopes, or in scopes where + * all statements are @experimental definitions. + */ + override def transformStats(trees: List[Tree], exprOwner: Symbol)(using Context): List[Tree] = + + def onlyExperimentalDefs = trees.forall { + case _: Import | EmptyTree => true + case stat: MemberDef => stat.symbol.isExperimental + case _ => false + } + + def checkExperimentalImports = + for case imp @ Import(qual, selectors) <- trees do + languageImport(qual) match + case Some(nme.experimental) + if !ctx.owner.isInExperimentalScope && !onlyExperimentalDefs + && selectors.exists(sel => Feature.experimental(sel.name) != Feature.scala2macros) => + Feature.checkExperimentalFeature("features", imp.srcPos) + case _ => + + try super.transformStats(trees, exprOwner) + finally checkExperimentalImports + end transformStats + /** 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. */ diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index b9e5198b0f24..ae90317bcbea 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -178,7 +178,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() } diff --git a/tests/neg-custom-args/no-experimental/experimental-erased.scala b/tests/neg-custom-args/no-experimental/experimental-erased.scala new file mode 100644 index 000000000000..91f84ba3f85f --- /dev/null +++ b/tests/neg-custom-args/no-experimental/experimental-erased.scala @@ -0,0 +1,7 @@ +import language.experimental.erasedDefinitions // error +import annotation.experimental + +@experimental +erased class CanThrow[-E <: Exception] + +def other = 1 diff --git a/tests/neg-custom-args/no-experimental/experimental.scala b/tests/neg-custom-args/no-experimental/experimental.scala index 26f9ba3d21c7..4787e68b4a5f 100644 --- a/tests/neg-custom-args/no-experimental/experimental.scala +++ b/tests/neg-custom-args/no-experimental/experimental.scala @@ -25,10 +25,4 @@ 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 } \ No newline at end of file diff --git a/tests/pos/experimental-erased-2.scala b/tests/pos/experimental-erased-2.scala new file mode 100644 index 000000000000..44d644e5db16 --- /dev/null +++ b/tests/pos/experimental-erased-2.scala @@ -0,0 +1,10 @@ +import language.experimental.erasedDefinitions +import annotation.experimental + +@experimental object Test: + + erased class CanThrow[-E <: Exception] + + def other = 1 + + diff --git a/tests/pos/experimental-erased.scala b/tests/pos/experimental-erased.scala new file mode 100644 index 000000000000..b8a52645ec9a --- /dev/null +++ b/tests/pos/experimental-erased.scala @@ -0,0 +1,6 @@ +import language.experimental.erasedDefinitions +import annotation.experimental + +@experimental +erased class CanThrow[-E <: Exception] + From e0ff1628ae5e497ea06e0ad6dacd300c9f12152d Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 26 Aug 2021 16:34:01 +0200 Subject: [PATCH 2/5] Refactoring: move experimental imports checking to Checking.scala --- .../tools/dotc/transform/PostTyper.scala | 24 +------------------ .../src/dotty/tools/dotc/typer/Checking.scala | 19 +++++++++++++++ 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index ed8f45a33345..c7e02a5c6837 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -13,7 +13,6 @@ import Decorators._ import Symbols._, SymUtils._, NameOps._ import ContextFunctionResults.annotateContextResults import config.Printers.typr -import config.Feature import reporting._ object PostTyper { @@ -449,30 +448,9 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase throw ex } - /** In addition to normal processing, check that experimental language - * imports are done only in experimental scopes, or in scopes where - * all statements are @experimental definitions. - */ override def transformStats(trees: List[Tree], exprOwner: Symbol)(using Context): List[Tree] = - - def onlyExperimentalDefs = trees.forall { - case _: Import | EmptyTree => true - case stat: MemberDef => stat.symbol.isExperimental - case _ => false - } - - def checkExperimentalImports = - for case imp @ Import(qual, selectors) <- trees do - languageImport(qual) match - case Some(nme.experimental) - if !ctx.owner.isInExperimentalScope && !onlyExperimentalDefs - && selectors.exists(sel => Feature.experimental(sel.name) != Feature.scala2macros) => - Feature.checkExperimentalFeature("features", imp.srcPos) - case _ => - try super.transformStats(trees, exprOwner) - finally checkExperimentalImports - end transformStats + 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. diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 5534d0c795fc..bb78cef2681f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -719,6 +719,24 @@ object Checking { checkValue(tree) case _ => tree + + /** Check that experimental language imports in `trees` + * are done only in experimental scopes, or in scopes where + * all statements are @experimental definitions. + */ + def checkExperimentalImports(trees: List[Tree])(using Context): Unit = + def onlyExperimentalDefs = trees.forall { + case _: Import | EmptyTree => true + case stat: MemberDef => stat.symbol.isExperimental + case _ => false + } + for case imp @ Import(qual, selectors) <- trees do + languageImport(qual) match + case Some(nme.experimental) + if !ctx.owner.isInExperimentalScope && !onlyExperimentalDefs + && selectors.exists(sel => experimental(sel.name) != scala2macros) => + checkExperimentalFeature("features", imp.srcPos) + case _ => } trait Checking { @@ -830,6 +848,7 @@ trait Checking { em"Implementation restriction: ${path.tpe.classSymbol} is not a valid prefix " + "for a wildcard export, as it is a package.", path.srcPos) + /** Check that module `sym` does not clash with a class of the same name * that is concurrently compiled in another source file. */ From 00f77aa5b282d7253ce63cb5a3b1d3d7a5950cd1 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 26 Aug 2021 17:45:24 +0200 Subject: [PATCH 3/5] Disregard synthetic definitions from experimental imports checks --- compiler/src/dotty/tools/dotc/typer/Checking.scala | 2 +- tests/pos/experimental-erased.scala | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index bb78cef2681f..1135ac61acc8 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -727,7 +727,7 @@ object Checking { def checkExperimentalImports(trees: List[Tree])(using Context): Unit = def onlyExperimentalDefs = trees.forall { case _: Import | EmptyTree => true - case stat: MemberDef => stat.symbol.isExperimental + case stat: MemberDef => stat.symbol.isExperimental || stat.symbol.is(Synthetic) case _ => false } for case imp @ Import(qual, selectors) <- trees do diff --git a/tests/pos/experimental-erased.scala b/tests/pos/experimental-erased.scala index b8a52645ec9a..041145bbf261 100644 --- a/tests/pos/experimental-erased.scala +++ b/tests/pos/experimental-erased.scala @@ -2,5 +2,14 @@ import language.experimental.erasedDefinitions import annotation.experimental @experimental -erased class CanThrow[-E <: Exception] +erased class CanThrow[-E <: Exception](val i: Int = 0) + +@experimental +object Foo + +@experimental +def bar = 1 + + + From e2701f89f71b8742ba8e46b67a860266aa0d4902 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 26 Aug 2021 23:47:46 +0200 Subject: [PATCH 4/5] Also check definitions inside package objects # Conflicts: # compiler/src/dotty/tools/dotc/typer/Checking.scala --- .../src/dotty/tools/dotc/typer/Checking.scala | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 1135ac61acc8..78281506dc63 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -725,15 +725,20 @@ object Checking { * all statements are @experimental definitions. */ def checkExperimentalImports(trees: List[Tree])(using Context): Unit = - def onlyExperimentalDefs = trees.forall { - case _: Import | EmptyTree => true - case stat: MemberDef => stat.symbol.isExperimental || stat.symbol.is(Synthetic) - case _ => false + def onlyExperimentalDefs(trees: List[Tree]): Boolean = trees.forall { + case _: Import | EmptyTree => + true + case tree @ TypeDef(_, impl: Template) if tree.symbol.isPackageObject => + onlyExperimentalDefs(impl.body) + case stat: MemberDef => + stat.symbol.isExperimental || stat.symbol.is(Synthetic) + case _ => + false } for case imp @ Import(qual, selectors) <- trees do languageImport(qual) match case Some(nme.experimental) - if !ctx.owner.isInExperimentalScope && !onlyExperimentalDefs + if !ctx.owner.isInExperimentalScope && !onlyExperimentalDefs(trees) && selectors.exists(sel => experimental(sel.name) != scala2macros) => checkExperimentalFeature("features", imp.srcPos) case _ => From 5891a73eadc537920d369038c2a9632465a6de33 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 27 Aug 2021 11:55:16 +0200 Subject: [PATCH 5/5] Explain why enclosing scope is not experimental --- .../src/dotty/tools/dotc/config/Feature.scala | 4 +- .../src/dotty/tools/dotc/typer/Checking.scala | 39 +++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index df2cab2c3375..ff93d31e71fa 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -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 diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 78281506dc63..86745606d847 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -725,23 +725,38 @@ object Checking { * all statements are @experimental definitions. */ def checkExperimentalImports(trees: List[Tree])(using Context): Unit = - def onlyExperimentalDefs(trees: List[Tree]): Boolean = trees.forall { - case _: Import | EmptyTree => - true - case tree @ TypeDef(_, impl: Template) if tree.symbol.isPackageObject => - onlyExperimentalDefs(impl.body) - case stat: MemberDef => - stat.symbol.isExperimental || stat.symbol.is(Synthetic) - case _ => - false - } + + 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 && !onlyExperimentalDefs(trees) + if !ctx.owner.isInExperimentalScope && selectors.exists(sel => experimental(sel.name) != scala2macros) => - checkExperimentalFeature("features", imp.srcPos) + 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") + nonExperimentalStat(trees) match + case EmptyTree => + case tree: MemberDef => check(i"${tree.symbol}") + case tree => check(i"expression ${tree}") case _ => + end checkExperimentalImports } trait Checking {