diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 42522d38be51..cebb74bd4551 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -99,20 +99,24 @@ object Feature: private val assumeExperimentalIn = Set("dotty.tools.vulpix.ParallelTesting") - def checkExperimentalFeature(which: String, srcPos: SrcPos = NoSourcePosition)(using Context) = - def hasSpecialPermission = - new Exception().getStackTrace.exists(elem => - assumeExperimentalIn.exists(elem.getClassName().startsWith(_))) - if !(Properties.experimental || hasSpecialPermission) - || ctx.settings.YnoExperimental.value - then - //println(i"${new Exception().getStackTrace.map(_.getClassName).toList}%\n%") - report.error(i"Experimental feature$which may only be used with nightly or snapshot version of compiler", srcPos) + def checkExperimentalFeature(which: String, srcPos: SrcPos)(using Context) = + if !isExperimentalEnabled then + report.error(i"Experimental $which may only be used with nightly or snapshot version of compiler", srcPos) + + def checkExperimentalDef(sym: Symbol, srcPos: SrcPos)(using Context) = + if !isExperimentalEnabled then + report.error(i"Experimental $sym may only be used with nightly or snapshot version of compiler", srcPos) /** 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" $setting") + do checkExperimentalFeature(s"feature $setting", NoSourcePosition) + + def isExperimentalEnabled(using Context): Boolean = + def hasSpecialPermission = + Thread.currentThread.getStackTrace.exists(elem => + assumeExperimentalIn.exists(elem.getClassName().startsWith(_))) + (Properties.experimental || hasSpecialPermission) && !ctx.settings.YnoExperimental.value end Feature \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index db3bad2c54bf..974ab41d7f83 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -910,6 +910,7 @@ class Definitions { @tu lazy val ConstructorOnlyAnnot: ClassSymbol = requiredClass("scala.annotation.constructorOnly") @tu lazy val CompileTimeOnlyAnnot: ClassSymbol = requiredClass("scala.annotation.compileTimeOnly") @tu lazy val SwitchAnnot: ClassSymbol = requiredClass("scala.annotation.switch") + @tu lazy val ExperimentalAnnot: ClassSymbol = requiredClass("scala.annotation.experimental") @tu lazy val ThrowsAnnot: ClassSymbol = requiredClass("scala.throws") @tu lazy val TransientAnnot: ClassSymbol = requiredClass("scala.transient") @tu lazy val UncheckedAnnot: ClassSymbol = requiredClass("scala.unchecked") diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index f5db937f8684..30b2dd268fea 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3083,7 +3083,7 @@ object Parsers { if prefix == nme.experimental && selectors.exists(sel => Feature.experimental(sel.name) != Feature.scala2macros) then - Feature.checkExperimentalFeature("s", imp.srcPos) + 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/plugins/Plugins.scala b/compiler/src/dotty/tools/dotc/plugins/Plugins.scala index a4db7ef44a86..14cb83fb6c6c 100644 --- a/compiler/src/dotty/tools/dotc/plugins/Plugins.scala +++ b/compiler/src/dotty/tools/dotc/plugins/Plugins.scala @@ -3,7 +3,7 @@ package plugins import core._ import Contexts._ -import config.{ PathResolver, Properties } +import config.{ PathResolver, Feature } import dotty.tools.io._ import Phases._ import config.Printers.plugins.{ println => debug } @@ -125,7 +125,7 @@ trait Plugins { val updatedPlan = Plugins.schedule(plan, pluginPhases) // add research plugins - if (Properties.experimental) + if (Feature.isExperimentalEnabled) plugins.collect { case p: ResearchPlugin => p }.foldRight(updatedPlan) { (plug, plan) => plug.init(options(plug), plan) } diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 8e9b95a0c572..fcd63793aec5 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -14,6 +14,8 @@ import Symbols._, SymUtils._, NameOps._ import ContextFunctionResults.annotateContextResults import config.Printers.typr import reporting._ +import util.Experimental + object PostTyper { val name: String = "posttyper" @@ -257,15 +259,19 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase override def transform(tree: Tree)(using Context): Tree = try tree match { - case tree: Ident if !tree.isType => - if tree.symbol.is(Inline) && !Inliner.inInlineMethod then - ctx.compilationUnit.needsInlining = true - checkNoConstructorProxy(tree) - tree.tpe match { - case tpe: ThisType => This(tpe.cls).withSpan(tree.span) - case _ => tree - } + case tree: Ident => + Experimental.checkExperimental(tree) + if tree.isType then super.transform(tree) + else + if tree.symbol.is(Inline) && !Inliner.inInlineMethod then + ctx.compilationUnit.needsInlining = true + checkNoConstructorProxy(tree) + tree.tpe match { + case tpe: ThisType => This(tpe.cls).withSpan(tree.span) + case _ => tree + } case tree @ Select(qual, name) => + Experimental.checkExperimental(tree) if tree.symbol.is(Inline) then ctx.compilationUnit.needsInlining = true if (name.isTypeName) { @@ -382,6 +388,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase Checking.checkRealizable(ref.tpe, ref.srcPos) super.transform(tree) case tree: TypeTree => + Experimental.checkExperimental(tree) tree.withType( tree.tpe match { case AnnotatedType(tpe, annot) => AnnotatedType(tpe, transformAnnot(annot)) diff --git a/compiler/src/dotty/tools/dotc/transform/SymUtils.scala b/compiler/src/dotty/tools/dotc/transform/SymUtils.scala index 3bbcdef68932..c8c9acef8d12 100644 --- a/compiler/src/dotty/tools/dotc/transform/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/transform/SymUtils.scala @@ -259,6 +259,13 @@ object SymUtils: && self.owner.linkedClass.is(Case) && self.owner.linkedClass.isDeclaredInfix + /** Is symbol declared experimental? */ + def isExperimental(using Context): Boolean = + (self eq defn.ExperimentalAnnot) + || self.hasAnnotation(defn.ExperimentalAnnot) + || self.allOverriddenSymbols.nonEmpty && self.allOverriddenSymbols.forall(_.hasAnnotation(defn.ExperimentalAnnot)) // TODO infer @experimental? + || (self.maybeOwner.exists && self.owner.isClass && self.owner.hasAnnotation(defn.ExperimentalAnnot)) // TODO infer @experimental? + /** The declared self type of this class, as seen from `site`, stripping * all refinements for opaque types. */ diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 820f466723e2..44db22831426 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -422,6 +422,14 @@ object Checking { } } + /** Check that classes extending experimental classes have the @experimental annotation */ + def checkExperimentalInheritance(cls: ClassSymbol, parents: List[Type], srcPos: SrcPos)(using Context): Unit = + if !cls.hasAnnotation(defn.ExperimentalAnnot) then + parents.find(_.typeSymbol.isExperimental) match + case Some(parent) => + report.error(em"extension of experimental ${parent.typeSymbol} must have @experimental annotation", srcPos) + case _ => + /** Check that symbol's definition is well-formed. */ def checkWellFormed(sym: Symbol)(using Context): Unit = { def fail(msg: Message) = report.error(msg, sym.srcPos) diff --git a/compiler/src/dotty/tools/dotc/typer/Inliner.scala b/compiler/src/dotty/tools/dotc/typer/Inliner.scala index 0c0de55daa58..67258b7a07df 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inliner.scala @@ -21,6 +21,7 @@ import Annotations.Annotation import SymDenotations.SymDenotation import Inferencing.isFullyDefined import config.Printers.inlining +import config.Feature import ErrorReporting.errorTree import dotty.tools.dotc.util.{SimpleIdentityMap, SimpleIdentitySet, EqHashMap, SourceFile, SourcePosition, SrcPos} import dotty.tools.dotc.parsing.Parsers.Parser @@ -92,6 +93,7 @@ object Inliner { if (tree.symbol == defn.CompiletimeTesting_typeChecks) return Intrinsics.typeChecks(tree) if (tree.symbol == defn.CompiletimeTesting_typeCheckErrors) return Intrinsics.typeCheckErrors(tree) + Feature.checkExperimentalDef(tree.symbol, tree) /** Set the position of all trees logically contained in the expansion of * inlined call `call` to the position of `call`. This transform is necessary diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 7d9e25c33b2d..964b660918de 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -210,6 +210,7 @@ object RefChecks { * 1.9. If M is erased, O is erased. If O is erased, M is erased or inline. * 1.10. If O is inline (and deferred, otherwise O would be final), M must be inline * 1.11. If O is a Scala-2 macro, M must be a Scala-2 macro. + * 1.12. If O is non-experimental, M must be non-experimental. * 2. Check that only abstract classes have deferred members * 3. Check that concrete classes do not have deferred definitions * that are not implemented in a subclass. @@ -475,6 +476,8 @@ object RefChecks { overrideError(i"needs to be declared with @targetName(${"\""}${other.targetName}${"\""}) so that external names match") else overrideError("cannot have a @targetName annotation since external names would be different") + else if !other.isExperimental && member.hasAnnotation(defn.ExperimentalAnnot) then // (1.12) + overrideError("may not override non-experimental member") else checkOverrideDeprecated() } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 4433ac7dee90..4f4041905127 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2352,6 +2352,7 @@ class Typer extends Namer } checkNonCyclicInherited(cls.thisType, cls.info.parents, cls.info.decls, cdef.srcPos) + checkExperimentalInheritance(cls, cls.info.parents, cdef.srcPos) // check value class constraints checkDerivedValueClass(cls, body1) diff --git a/compiler/src/dotty/tools/dotc/util/Experimental.scala b/compiler/src/dotty/tools/dotc/util/Experimental.scala new file mode 100644 index 000000000000..f29e687b8a7a --- /dev/null +++ b/compiler/src/dotty/tools/dotc/util/Experimental.scala @@ -0,0 +1,32 @@ +package dotty.tools.dotc +package util + +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.ast.Trees._ +import dotty.tools.dotc.config.Feature +import dotty.tools.dotc.core.Contexts._ +import dotty.tools.dotc.core.Symbols._ +import dotty.tools.dotc.core.Types._ +import dotty.tools.dotc.core.Flags._ +import dotty.tools.dotc.transform.SymUtils._ + +object Experimental: + import tpd._ + + def checkExperimental(tree: Tree)(using Context): Unit = + if tree.symbol.isExperimental + && !tree.symbol.isConstructor // already reported on the class + && !tree.symbol.is(ModuleClass) // already reported on the module + && (tree.span.exists || tree.symbol != defn.ExperimentalAnnot) // already reported on inferred annotations + then + Feature.checkExperimentalDef(tree.symbol, tree) + + def checkExperimentalTypes(tree: Tree)(using Context): Unit = + val checker = new TypeTraverser: + def traverse(tp: Type): Unit = + if tp.typeSymbol.isExperimental then + Feature.checkExperimentalDef(tp.typeSymbol, tree) + else + traverseChildren(tp) + if !tree.span.isSynthetic then // avoid double errors + checker.traverse(tree.tpe) diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index db05ed611a0e..2d193a457afe 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -240,7 +240,7 @@ class CompilationTests { Properties.compilerInterface, Properties.scalaLibrary, Properties.scalaAsm, Properties.dottyInterfaces, Properties.jlineTerminal, Properties.jlineReader, ).mkString(File.pathSeparator), - Array("-Ycheck-reentrant", "-language:postfixOps", "-Xsemanticdb", "-Yno-experimental") + Array("-Ycheck-reentrant", "-language:postfixOps", "-Xsemanticdb") ) val libraryDirs = List(Paths.get("library/src"), Paths.get("library/src-bootstrapped")) diff --git a/library/src/scala/annotation/experimental.scala b/library/src/scala/annotation/experimental.scala new file mode 100644 index 000000000000..4ccda84d623c --- /dev/null +++ b/library/src/scala/annotation/experimental.scala @@ -0,0 +1,14 @@ +package scala.annotation + +/** An annotation that can be used to mark a definition as experimental. + * + * This class is experimental as well as if it was defined as + * ```scala + * @experimental + * class experimental extends StaticAnnotation + * ``` + * + * @syntax markdown + */ +// @experimental +class experimental extends StaticAnnotation diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 0d4fd95c9e53..4764166540bd 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -18,6 +18,7 @@ object language: * * @group experimental */ + @scala.annotation.experimental object experimental: /* Experimental support for richer dependent types (disabled for now) diff --git a/library/src/scala/util/FromDigits.scala b/library/src/scala/util/FromDigits.scala index 6064116d1443..f7f9829de178 100644 --- a/library/src/scala/util/FromDigits.scala +++ b/library/src/scala/util/FromDigits.scala @@ -2,9 +2,12 @@ package scala.util import scala.math.{BigInt} import quoted._ import annotation.internal.sharable +import annotation.experimental + /** A type class for types that admit numeric literals. */ +@experimental trait FromDigits[T] { /** Convert `digits` string to value of type `T` @@ -20,11 +23,13 @@ trait FromDigits[T] { def fromDigits(digits: String): T } +@experimental object FromDigits { /** A subclass of `FromDigits` that also allows to convert whole number literals * with a radix other than 10 */ + @experimental trait WithRadix[T] extends FromDigits[T] { def fromDigits(digits: String): T = fromDigits(digits, 10) @@ -37,28 +42,34 @@ object FromDigits { /** A subclass of `FromDigits` that also allows to convert number * literals containing a decimal point ".". */ + @experimental trait Decimal[T] extends FromDigits[T] /** A subclass of `FromDigits`that allows also to convert number * literals containing a decimal point "." or an * exponent `('e' | 'E')['+' | '-']digit digit*`. */ + @experimental trait Floating[T] extends Decimal[T] /** The base type for exceptions that can be thrown from * `fromDigits` conversions */ + @experimental abstract class FromDigitsException(msg: String) extends NumberFormatException(msg) /** Thrown if value of result does not fit into result type's range */ + @experimental class NumberTooLarge(msg: String = "number too large") extends FromDigitsException(msg) /** Thrown in case of numeric underflow (e.g. a non-zero * floating point literal that produces a zero value) */ + @experimental class NumberTooSmall(msg: String = "number too small") extends FromDigitsException(msg) /** Thrown if digit string is not legal for the given type */ + @experimental class MalformedNumber(msg: String = "malformed number literal") extends FromDigitsException(msg) /** Convert digits and radix to integer value (either int or Long) @@ -156,10 +167,12 @@ object FromDigits { x } + @experimental given BigIntFromDigits: WithRadix[BigInt] with { def fromDigits(digits: String, radix: Int): BigInt = BigInt(digits, radix) } + @experimental given BigDecimalFromDigits: Floating[BigDecimal] with { def fromDigits(digits: String): BigDecimal = BigDecimal(digits) } diff --git a/tests/neg-custom-args/no-experimental/experimentalAnnotation.scala b/tests/neg-custom-args/no-experimental/experimentalAnnotation.scala new file mode 100644 index 000000000000..1c15df53c672 --- /dev/null +++ b/tests/neg-custom-args/no-experimental/experimentalAnnotation.scala @@ -0,0 +1,94 @@ +import scala.annotation.experimental + +@experimental // error +val x = () + +@experimental // error +def f() = () + +@experimental // error +class A: + def f() = 1 + +@experimental // error +class B extends A: // error + override def f() = 2 + +@experimental // error +type X + +@experimental // error +type Y = Int + +@experimental // error +opaque type Z = Int + +@experimental // error +object X: + def fx() = 1 // error + +class C: + @experimental // error + def f() = 1 + +class D extends C: + override def f() = 2 + +trait A2: + @experimental // error + def f(): Int + +trait B2: + def f(): Int + +class C2 extends A2, B2: + def f(): Int = 1 + +object Extractor1: + def unapply(s: Any): Option[A] = ??? // error + +object Extractor2: + @experimental // error + def unapply(s: Any): Option[Int] = ??? + + +@experimental // error +trait ExpSAM { + def foo(x: Int): Int +} +def bar(f: ExpSAM): Unit = {} // error + +def test( + p1: A, // error + p2: List[A], // error + p3: X, // error + p4: Y, // error + p5: Z, // error +): Unit = + f() // error + x // error + new A // error + new B // error + X.fx() // error + import X.fx // error + fx() // error + val i1 = identity[X] // error // error + val i2 = identity[A] // error // error + val a: A = ??? // error + val b: B = ??? // error + val c: C = ??? + val d: D = ??? + val c2: C2 = ??? + a.f() // error + b.f() // error + c.f() // error + d.f() // error + c2.f() // ok because B2.f is a stable API + () + + (??? : Any) match + case _: A => // error // error + case Extractor1(_) => // error + case Extractor2(_) => // error + + bar(x => x) // error diff --git a/tests/neg-custom-args/no-experimental/experimentalExperimental.scala b/tests/neg-custom-args/no-experimental/experimentalExperimental.scala new file mode 100644 index 000000000000..9011a3e49225 --- /dev/null +++ b/tests/neg-custom-args/no-experimental/experimentalExperimental.scala @@ -0,0 +1 @@ +class MyExperimentalAnnot extends scala.annotation.experimental // error diff --git a/tests/neg-custom-args/no-experimental/experimentalnline.scala b/tests/neg-custom-args/no-experimental/experimentalnline.scala new file mode 100644 index 000000000000..146f93061d28 --- /dev/null +++ b/tests/neg-custom-args/no-experimental/experimentalnline.scala @@ -0,0 +1,8 @@ +import scala.annotation.experimental + +@experimental // error +inline def g() = () + +def test: Unit = + g() // errors + () diff --git a/tests/neg/experimentalInheritance.scala b/tests/neg/experimentalInheritance.scala new file mode 100644 index 000000000000..0712e4775a6d --- /dev/null +++ b/tests/neg/experimentalInheritance.scala @@ -0,0 +1,10 @@ +import scala.annotation.experimental + +@experimental +class A + +@experimental +trait T + +class B extends A // error +class C extends T // error diff --git a/tests/neg/experimentalOverloads.scala b/tests/neg/experimentalOverloads.scala new file mode 100644 index 000000000000..7adaf0b78840 --- /dev/null +++ b/tests/neg/experimentalOverloads.scala @@ -0,0 +1,11 @@ +import scala.annotation.experimental + +trait A: + def f: Int + def g: Int = 3 +trait B extends A: + @experimental + def f: Int = 4 // error + + @experimental + override def g: Int = 5 // error