Skip to content
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

Add @experimental annotation #12102

Merged
merged 13 commits into from
May 10, 2021
24 changes: 14 additions & 10 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 a nightly or snapshot version of the compiler", srcPos)

def checkExperimentalDef(sym: Symbol, srcPos: SrcPos)(using Context) =
if !isExperimentalEnabled then
report.error(i"$sym is marked @experimental and therefore may only be used with a nightly or snapshot version of the 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
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,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")
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3079,7 +3079,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)
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/plugins/Plugins.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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)
}
Expand Down
9 changes: 9 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/PostTyper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase

override def transform(tree: Tree)(using Context): Tree =
try tree match {
// TODO move CaseDef case lower: keep most probable trees first for performance
case CaseDef(pat, _, _) =>
val gadtCtx =
pat.removeAttachment(typer.Typer.InferredGadtConstraints) match
Expand Down Expand Up @@ -353,6 +354,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
val sym = tree.symbol
if (sym.isClass)
VarianceChecker.check(tree)
annotateExperimental(sym)
// Add SourceFile annotation to top-level classes
if sym.owner.is(Package)
&& ctx.compilationUnit.source.exists
Expand Down Expand Up @@ -443,5 +445,12 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
*/
private def normalizeErasedRhs(rhs: Tree, sym: Symbol)(using Context) =
if (sym.isEffectivelyErased) dropInlines.transform(rhs) else rhs

private def annotateExperimental(sym: Symbol)(using Context): Unit =
if sym.is(Enum) && sym.hasAnnotation(defn.ExperimentalAnnot) then
// Add @experimental annotation to enum class definitions
Copy link
Member

Choose a reason for hiding this comment

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

Why is this needed if we also have checkExperimentalInheritance?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not needed anymore. Removed the code. Though now there is similar looking logic for case class modules.

val compMod = sym.companionModule.moduleClass
compMod.addAnnotation(defn.ExperimentalAnnot)
compMod.companionModule.addAnnotation(defn.ExperimentalAnnot)
}
}
7 changes: 7 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,13 @@ object SymUtils:
&& self.owner.linkedClass.is(Case)
&& self.owner.linkedClass.isDeclaredInfix

/** Is symbol declared or inherits @experimental? */
def isExperimental(using Context): Boolean =
// TODO should be add `@experimental` to `class experimental` in PostTyper?
self.eq(defn.ExperimentalAnnot)
|| self.hasAnnotation(defn.ExperimentalAnnot)
nicolasstucki marked this conversation as resolved.
Show resolved Hide resolved
|| (self.maybeOwner.isClass && self.owner.hasAnnotation(defn.ExperimentalAnnot))

/** The declared self type of this class, as seen from `site`, stripping
* all refinements for opaque types.
*/
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Inliner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -93,6 +94,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
Expand Down
54 changes: 54 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/RefChecks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import reporting._
import scala.util.matching.Regex._
import Constants.Constant
import NullOpsDecorator._
import dotty.tools.dotc.config.Feature

object RefChecks {
import tpd._
Expand Down Expand Up @@ -212,6 +213,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.
Expand Down Expand Up @@ -477,6 +479,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()
}
Expand Down Expand Up @@ -924,6 +928,7 @@ object RefChecks {
// arbitrarily choose one as more important than the other.
private def checkUndesiredProperties(sym: Symbol, pos: SrcPos)(using Context): Unit =
checkDeprecated(sym, pos)
checkExperimental(sym, pos)

val xMigrationValue = ctx.settings.Xmigration.value
if xMigrationValue != NoScalaVersion then
Expand Down Expand Up @@ -964,6 +969,29 @@ object RefChecks {
val since = annot.argumentConstant(1).map(" since " + _.stringValue).getOrElse("")
report.deprecationWarning(s"${sym.showLocated} is deprecated${since}${msg}", pos)

private def checkExperimental(sym: Symbol, pos: SrcPos)(using Context): Unit =
if sym.isExperimental
&& !sym.isConstructor // already reported on the class
&& !ctx.owner.isExperimental // already reported on the @experimental of the owner
&& !sym.is(ModuleClass) // already reported on the module
&& (sym.span.exists || sym != defn.ExperimentalAnnot) // already reported on inferred annotations
then
Feature.checkExperimentalDef(sym, pos)

private def checkExperimentalTypes(tpe: Type, pos: SrcPos)(using Context): Unit =
val checker = new TypeTraverser:
def traverse(tp: Type): Unit =
if tp.typeSymbol.isExperimental then
Feature.checkExperimentalDef(tp.typeSymbol, pos)
else
traverseChildren(tp)
if !pos.span.isSynthetic then // avoid double errors
checker.traverse(tpe)

private def checkExperimentalAnnots(sym: Symbol)(using Context): Unit =
for annot <- sym.annotations if annot.symbol.isExperimental && annot.tree.span.exists do
Feature.checkExperimentalDef(annot.symbol, annot.tree)

/** If @migration is present (indicating that the symbol has changed semantics between versions),
* emit a warning.
*/
Expand Down Expand Up @@ -1136,6 +1164,15 @@ object RefChecks {

end checkImplicitNotFoundAnnotation


/** Check that classes extending experimental classes or nested in experimental classes have the @experimental annotation. */
private def checkExperimentalInheritance(cls: ClassSymbol)(using Context): Unit =
if !cls.hasAnnotation(defn.ExperimentalAnnot) then
cls.info.parents.find(_.typeSymbol.isExperimental) match
case Some(parent) =>
report.error(em"extension of experimental ${parent.typeSymbol} must have @experimental annotation", cls.srcPos)
case _ =>
end checkExperimentalInheritance
}
import RefChecks._

Expand Down Expand Up @@ -1192,6 +1229,8 @@ class RefChecks extends MiniPhase { thisPhase =>
override def transformValDef(tree: ValDef)(using Context): ValDef = {
checkNoPrivateOverrides(tree)
checkDeprecatedOvers(tree)
checkExperimentalAnnots(tree.symbol)
checkExperimentalTypes(tree.symbol.info, tree)
val sym = tree.symbol
if (sym.exists && sym.owner.isTerm) {
tree.rhs match {
Expand All @@ -1212,6 +1251,8 @@ class RefChecks extends MiniPhase { thisPhase =>
override def transformDefDef(tree: DefDef)(using Context): DefDef = {
checkNoPrivateOverrides(tree)
checkDeprecatedOvers(tree)
checkExperimentalAnnots(tree.symbol)
checkExperimentalTypes(tree.symbol.info, tree)
checkImplicitNotFoundAnnotation.defDef(tree.symbol.denot)
tree
}
Expand All @@ -1224,6 +1265,8 @@ class RefChecks extends MiniPhase { thisPhase =>
checkCompanionNameClashes(cls)
checkAllOverrides(cls)
checkImplicitNotFoundAnnotation.template(cls.classDenot)
checkExperimentalInheritance(cls)
checkExperimentalAnnots(cls)
tree
}
catch {
Expand Down Expand Up @@ -1268,6 +1311,17 @@ class RefChecks extends MiniPhase { thisPhase =>
}
tree
}

override def transformTypeTree(tree: TypeTree)(using Context): TypeTree = {
checkExperimental(tree.symbol, tree.srcPos)
tree
}

override def transformTypeDef(tree: TypeDef)(using Context): TypeDef = {
checkExperimental(tree.symbol, tree.srcPos)
checkExperimentalAnnots(tree.symbol)
tree
}
}

/* todo: rewrite and re-enable
Expand Down
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/dotc/CompilationTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
14 changes: 14 additions & 0 deletions library/src/scala/annotation/experimental.scala
Original file line number Diff line number Diff line change
@@ -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
nicolasstucki marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 2 additions & 1 deletion library/src/scala/util/FromDigits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import scala.math.{BigInt}
import quoted._
import annotation.internal.sharable


/** A type class for types that admit numeric literals.
*/
trait FromDigits[T] {
Expand All @@ -28,7 +29,7 @@ object FromDigits {
trait WithRadix[T] extends FromDigits[T] {
def fromDigits(digits: String): T = fromDigits(digits, 10)

/** Convert digits string with given radix to numberof type `T`.
/** Convert digits string with given radix to number of type `T`.
* E.g. if radix is 16, digits `a..f` and `A..F` are also allowed.
*/
def fromDigits(digits: String, radix: Int): T
Expand Down
7 changes: 7 additions & 0 deletions tests/neg-custom-args/no-experimental/experimentalAnnot.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import scala.annotation.experimental

@experimental // error
class myExperimentalAnnot extends scala.annotation.Annotation

@myExperimentalAnnot // error
def test: Unit = ()
12 changes: 12 additions & 0 deletions tests/neg-custom-args/no-experimental/experimentalEnum.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import scala.annotation.experimental

@experimental // error
enum E:
case A
case B

def test: Unit =
E.A // error
E.B // error
val e: E = ??? // error
()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import scala.annotation.experimental

class MyExperimentalAnnot // error
extends experimental // error
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import scala.annotation.experimental

@experimental
inline def g() = ()

def test: Unit =
g() // errors
()
39 changes: 39 additions & 0 deletions tests/neg-custom-args/no-experimental/experimentalOverride.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import scala.annotation.experimental

@experimental // error
class A:
def f() = 1

@experimental // error
class B extends A:
override def f() = 2

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

def test: Unit =
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() // ok because D.f is a stable API
c2.f() // ok because B2.f is a stable API
()
11 changes: 11 additions & 0 deletions tests/neg-custom-args/no-experimental/experimentalSam.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import scala.annotation.experimental

@experimental // error
trait ExpSAM {
def foo(x: Int): Int
}
def bar(f: ExpSAM): Unit = {} // error

def test: Unit =
bar(x => x) // error
()
Loading