Skip to content
33 changes: 23 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,33 @@ 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
val symMsg =
if sym eq defn.ExperimentalAnnot then
i"use of @experimental is experimental"
else if sym.hasAnnotation(defn.ExperimentalAnnot) then
i"$sym is marked @experimental"
else if sym.owner.hasAnnotation(defn.ExperimentalAnnot) then
i"${sym.owner} is marked @experimental"
else
i"$sym inherits @experimental"
report.error(s"$symMsg 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
8 changes: 8 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,11 @@ 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(Module) && sym.companionClass.hasAnnotation(defn.ExperimentalAnnot) then
sym.addAnnotation(defn.ExperimentalAnnot)
sym.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)
|| (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 checkExperimentalSignature(sym: Symbol, 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 !sym.owner.isExperimental && !pos.span.isSynthetic then // avoid double errors
checker.traverse(sym.info)

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)
checkExperimentalSignature(tree.symbol, 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)
checkExperimentalSignature(tree.symbol, 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
6 changes: 3 additions & 3 deletions library/src/scala/annotation/internal/ErasedParam.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package scala.annotation.internal

import scala.annotation.Annotation
package scala.annotation
package internal

/** An annotation produced by Namer to indicate an erased parameter */
@experimental
final class ErasedParam() extends Annotation
2 changes: 2 additions & 0 deletions library/src/scala/quoted/Quotes.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package scala.quoted

import scala.annotation.experimental
import scala.reflect.TypeTest

/** Current Quotes in scope
Expand Down Expand Up @@ -2179,6 +2180,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
/** Is this a given parameter clause `(using X1, ..., Xn)` or `(using x1: X1, ..., xn: Xn)` */
def isGiven: Boolean
/** Is this a erased parameter clause `(erased x1: X1, ..., xn: Xn)` */
@experimental
def isErased: Boolean
end TermParamClauseMethods

Expand Down
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 = ()
26 changes: 26 additions & 0 deletions tests/neg-custom-args/no-experimental/experimentalCaseClass.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import scala.annotation.experimental

@experimental // error
case class Foo(a: Int)

@experimental // error
case class Bar(a: Int)

object Bar:
def f(): Unit = ()

def test: Unit =
Foo(2) // error
val x: Foo = ??? // error

x match
case Foo(a) => // error


Bar(2) // error
val y: Bar = ??? // error

y match
case Bar(a) => // error

Bar.f() // error
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
()
Loading