diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 2103b329d89c..91f228bca560 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -11,6 +11,7 @@ import SourceVersion.* import reporting.Message import NameKinds.QualifiedName import Annotations.ExperimentalAnnotation +import Annotations.PreviewAnnotation import Settings.Setting.ChoiceWithHelp object Feature: @@ -233,4 +234,29 @@ object Feature: true else false + + def isPreviewEnabled(using Context): Boolean = + ctx.settings.preview.value + + def checkPreviewFeature(which: String, srcPos: SrcPos, note: => String = "")(using Context) = + if !isPreviewEnabled then + report.error(previewUseSite(which) + note, srcPos) + + def checkPreviewDef(sym: Symbol, srcPos: SrcPos)(using Context) = if !isPreviewEnabled then + val previewSym = + if sym.hasAnnotation(defn.PreviewAnnot) then sym + else if sym.owner.hasAnnotation(defn.PreviewAnnot) then sym.owner + else NoSymbol + val msg = + previewSym.getAnnotation(defn.PreviewAnnot).collectFirst { + case PreviewAnnotation(msg) if msg.nonEmpty => s": $msg" + }.getOrElse("") + val markedPreview = + if previewSym.exists + then i"$previewSym is marked @preview$msg" + else i"$sym inherits @preview$msg" + report.error(i"${markedPreview}\n\n${previewUseSite("definition")}", srcPos) + + private def previewUseSite(which: String): String = + s"Preview $which may only be used when compiling with the `-preview` compiler flag" end Feature diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 7058a9c4ab6d..986e3f3b9c26 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -116,6 +116,7 @@ trait CommonScalaSettings: val unchecked: Setting[Boolean] = BooleanSetting(RootSetting, "unchecked", "Enable additional warnings where generated code depends on assumptions.", initialValue = true, aliases = List("--unchecked")) val language: Setting[List[ChoiceWithHelp[String]]] = MultiChoiceHelpSetting(RootSetting, "language", "feature", "Enable one or more language features.", choices = ScalaSettingsProperties.supportedLanguageFeatures, legacyChoices = ScalaSettingsProperties.legacyLanguageFeatures, default = Nil, aliases = List("--language")) val experimental: Setting[Boolean] = BooleanSetting(RootSetting, "experimental", "Annotate all top-level definitions with @experimental. This enables the use of experimental features anywhere in the project.") + val preview: Setting[Boolean] = BooleanSetting(RootSetting, "preview", "Enable the use of preview features anywhere in the project.") /* Coverage settings */ val coverageOutputDir = PathSetting(RootSetting, "coverage-out", "Destination for coverage classfiles and instrumentation data.", "", aliases = List("--coverage-out")) diff --git a/compiler/src/dotty/tools/dotc/core/Annotations.scala b/compiler/src/dotty/tools/dotc/core/Annotations.scala index 1a5cf2b03e06..1615679a036e 100644 --- a/compiler/src/dotty/tools/dotc/core/Annotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Annotations.scala @@ -303,5 +303,16 @@ object Annotations { case annot @ ExperimentalAnnotation(msg) => ExperimentalAnnotation(msg, annot.tree.span) } } - + + object PreviewAnnotation { + /** Matches and extracts the message from an instance of `@preview(msg)` + * Returns `Some("")` for `@preview` with no message. + */ + def unapply(a: Annotation)(using Context): Option[String] = + if a.symbol ne defn.PreviewAnnot then + None + else a.argumentConstant(0) match + case Some(Constant(msg: String)) => Some(msg) + case _ => Some("") + } } diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 4eada2446cd4..b72f2ee4b9ef 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1058,6 +1058,7 @@ class Definitions { @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 PreviewAnnot: ClassSymbol = requiredClass("scala.annotation.internal.preview") @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/core/SymUtils.scala b/compiler/src/dotty/tools/dotc/core/SymUtils.scala index 1a762737d52f..54ba0e3bdd06 100644 --- a/compiler/src/dotty/tools/dotc/core/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/SymUtils.scala @@ -366,23 +366,30 @@ class SymUtils: && self.owner.linkedClass.isDeclaredInfix /** Is symbol declared or inherits @experimental? */ - def isExperimental(using Context): Boolean = - self.hasAnnotation(defn.ExperimentalAnnot) - || (self.maybeOwner.isClass && self.owner.hasAnnotation(defn.ExperimentalAnnot)) + def isExperimental(using Context): Boolean = isFeatureAnnotated(defn.ExperimentalAnnot) + def isInExperimentalScope(using Context): Boolean = isInFeatureScope(defn.ExperimentalAnnot, _.isExperimental, _.isInExperimentalScope) - def isInExperimentalScope(using Context): Boolean = - def isDefaultArgumentOfExperimentalMethod = + /** Is symbol declared or inherits @preview? */ + def isPreview(using Context): Boolean = isFeatureAnnotated(defn.PreviewAnnot) + def isInPreviewScope(using Context): Boolean = isInFeatureScope(defn.PreviewAnnot, _.isPreview, _.isInPreviewScope) + + private inline def isFeatureAnnotated(checkAnnotaton: ClassSymbol)(using Context): Boolean = + self.hasAnnotation(checkAnnotaton) + || (self.maybeOwner.isClass && self.owner.hasAnnotation(checkAnnotaton)) + + private inline def isInFeatureScope(checkAnnotation: ClassSymbol, checkSymbol: Symbol => Boolean, checkOwner: Symbol => Boolean)(using Context): Boolean = + def isDefaultArgumentOfCheckedMethod = self.name.is(DefaultGetterName) && self.owner.isClass && { val overloads = self.owner.asClass.membersNamed(self.name.firstPart) overloads.filterWithFlags(HasDefaultParams, EmptyFlags) match - case denot: SymDenotation => denot.symbol.isExperimental + case denot: SymDenotation => checkSymbol(denot.symbol) case _ => false } - self.hasAnnotation(defn.ExperimentalAnnot) - || isDefaultArgumentOfExperimentalMethod - || (!self.is(Package) && self.owner.isInExperimentalScope) + self.hasAnnotation(checkAnnotation) + || isDefaultArgumentOfCheckedMethod + || (!self.is(Package) && checkOwner(self.owner)) /** 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/CrossVersionChecks.scala b/compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala index 8f8a68aa5735..f0d4d617bb74 100644 --- a/compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala @@ -141,10 +141,12 @@ class CrossVersionChecks extends MiniPhase: if tree.span.isSourceDerived then checkDeprecatedRef(sym, tree.srcPos) checkExperimentalRef(sym, tree.srcPos) + checkPreviewFeatureRef(sym, tree.srcPos) case TermRef(_, sym: Symbol) => if tree.span.isSourceDerived then checkDeprecatedRef(sym, tree.srcPos) checkExperimentalRef(sym, tree.srcPos) + checkPreviewFeatureRef(sym, tree.srcPos) case AnnotatedType(_, annot) => checkUnrollAnnot(annot.symbol, tree.srcPos) case _ => @@ -174,11 +176,12 @@ object CrossVersionChecks: val description: String = "check issues related to deprecated and experimental" /** Check that a reference to an experimental definition with symbol `sym` meets cross-version constraints - * for `@deprecated` and `@experimental`. + * for `@deprecated`, `@experimental` and `@preview`. */ def checkRef(sym: Symbol, pos: SrcPos)(using Context): Unit = checkDeprecatedRef(sym, pos) checkExperimentalRef(sym, pos) + checkPreviewFeatureRef(sym, pos) /** Check that a reference to an experimental definition with symbol `sym` is only * used in an experimental scope @@ -187,6 +190,13 @@ object CrossVersionChecks: if sym.isExperimental && !ctx.owner.isInExperimentalScope then Feature.checkExperimentalDef(sym, pos) + /** Check that a reference to a preview definition with symbol `sym` is only + * used in a preview mode. + */ + private[CrossVersionChecks] def checkPreviewFeatureRef(sym: Symbol, pos: SrcPos)(using Context): Unit = + if sym.isPreview && !ctx.owner.isInPreviewScope then + Feature.checkPreviewDef(sym, pos) + /** If @deprecated is present, and the point of reference is not enclosed * in either a deprecated member or a scala bridge method, issue a warning. * diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index d96b37dd3c55..51929bf4fbc9 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -306,6 +306,7 @@ object RefChecks { * that passes its value on to O. * 1.13. If O is non-experimental, M must be non-experimental. * 1.14. If O has @publicInBinary, M must have @publicInBinary. + * 1.15. If O is non-preview, M must be non-preview * 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. @@ -645,6 +646,8 @@ object RefChecks { overrideError("may not override non-experimental member") else if !member.hasAnnotation(defn.PublicInBinaryAnnot) && other.hasAnnotation(defn.PublicInBinaryAnnot) then // (1.14) overrideError("also needs to be declared with @publicInBinary") + else if !other.isPreview && member.hasAnnotation(defn.PreviewAnnot) then // (1.15) + overrideError("may not override non-preview member") else if other.hasAnnotation(defn.DeprecatedOverridingAnnot) then overrideDeprecation("", member, other, "removed or renamed") end checkOverride diff --git a/docs/_docs/reference/other-new-features/preview-defs.md b/docs/_docs/reference/other-new-features/preview-defs.md new file mode 100644 index 000000000000..3fc25bc48f9e --- /dev/null +++ b/docs/_docs/reference/other-new-features/preview-defs.md @@ -0,0 +1,34 @@ +--- +layout: doc-page +title: "Preview Definitions" +nightlyOf: https://docs.scala-lang.org/scala3/reference/other-new-features/preview-defs.html +--- + +New Scala language features or standard library APIs are initially introduced as experimental, but once they become fully implemented and accepted by the [SIP](https://docs.scala-lang.org/sips/) these can become a preview features. +Preview language features and APIs are guaranteed to be standardized in some next Scala minor release, but allow the compiler team to introduce small, possibly binary incompatible, changes based on the community feedback. +These can be used by early adopters who can accept the possibility of binary compatibility breakage. For instance, preview features could be used in some internal tool or application. On the other hand, preview features are discouraged in publicly available libraries. + +Users can enable access to preview features and definitions by compiling with the `-preview` flag. The flag would enable all preview features and definitions. There is no scheme for enabling only a subset of preview features. + +The biggest difference of preview features compared to experimental features is their non-viral behavior. +A definition compiled in preview mode (using the `-preview` flag) is not marked as a preview definition itself. +This behavior allows to use preview features transitively in other compilation units without explicitly enabled preview mode, as long as it does not directly reference APIs or features marked as preview. + +The [`@preview`](https://scala-lang.org/api/3.x/scala/annotation/internal/preview.html) annotation is used to mark Scala 3 standard library APIs currently available under preview mode. +The rules for `@preview` are similar to [`@experimental`](https://scala-lang.org/api/3.x/scala/annotation/experimental.html) when it comes to accessing, subtyping, overriding or overloading definitions marked with this annotation - all of these can only be performed in compilation units that enable preview mode. + +```scala +//> using options -preview +package scala.stdlib +import scala.annotation.internal.preview + +@preview def previewFeature: Unit = () + +// Can be used in non-preview scope +def usePreviewFeature = previewFeature +``` + +```scala +def usePreviewFeatureTransitively = scala.stdlib.usePreviewFeature +def usePreviewFeatureDirectly = scala.stdlib.previewFeature // error - referring to preview definition outside preview scope +``` diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 8d3d800554f5..edfa86554d7f 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -85,6 +85,7 @@ subsection: - page: reference/other-new-features/safe-initialization.md - page: reference/other-new-features/type-test.md - page: reference/other-new-features/experimental-defs.md + - page: reference/other-new-features/preview-defs.md - page: reference/other-new-features/binary-literals.md - title: Other Changed Features directory: changed-features diff --git a/library/src/scala/annotation/internal/preview.scala b/library/src/scala/annotation/internal/preview.scala new file mode 100644 index 000000000000..a6e797d78e97 --- /dev/null +++ b/library/src/scala/annotation/internal/preview.scala @@ -0,0 +1,11 @@ +package scala.annotation +package internal + + +/** An annotation that can be used to mark a definition as preview. + * + * @see [[https://dotty.epfl.ch/docs/reference/other-new-features/preview-defs]] + * @syntax markdown + */ +private[scala] final class preview(message: String) extends StaticAnnotation: + def this() = this("") diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index ad2c796393a1..8427b4398c5f 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -13,6 +13,7 @@ object MiMaFilters { ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.quotedPatternsWithPolymorphicFunctions"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$quotedPatternsWithPolymorphicFunctions$"), ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.runtime.Patterns.higherOrderHoleWithTypes"), + ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.preview"), ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.packageObjectValues"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$packageObjectValues$"), ), diff --git a/tests/neg/preview-message.check b/tests/neg/preview-message.check new file mode 100644 index 000000000000..c3478c27fd46 --- /dev/null +++ b/tests/neg/preview-message.check @@ -0,0 +1,18 @@ +-- Error: tests/neg/preview-message.scala:15:2 ------------------------------------------------------------------------- +15 | f1() // error + | ^^ + | method f1 is marked @preview + | + | Preview definition may only be used when compiling with the `-preview` compiler flag +-- Error: tests/neg/preview-message.scala:16:2 ------------------------------------------------------------------------- +16 | f2() // error + | ^^ + | method f2 is marked @preview + | + | Preview definition may only be used when compiling with the `-preview` compiler flag +-- Error: tests/neg/preview-message.scala:17:2 ------------------------------------------------------------------------- +17 | f3() // error + | ^^ + | method f3 is marked @preview: not yet stable + | + | Preview definition may only be used when compiling with the `-preview` compiler flag diff --git a/tests/neg/preview-message.scala b/tests/neg/preview-message.scala new file mode 100644 index 000000000000..99bdd72cd936 --- /dev/null +++ b/tests/neg/preview-message.scala @@ -0,0 +1,17 @@ +package scala // @preview is private[scala] + +import scala.annotation.internal.preview + +@preview +def f1() = ??? + +@preview() +def f2() = ??? + +@preview("not yet stable") +def f3() = ??? + +def g() = + f1() // error + f2() // error + f3() // error diff --git a/tests/neg/preview-non-viral/defs_1.scala b/tests/neg/preview-non-viral/defs_1.scala new file mode 100644 index 000000000000..434f39e13c94 --- /dev/null +++ b/tests/neg/preview-non-viral/defs_1.scala @@ -0,0 +1,7 @@ +//> using options -preview +package scala // @preview is private[scala] +import scala.annotation.internal.preview + +@preview def previewFeature = 42 + +def usePreviewFeature = previewFeature diff --git a/tests/neg/preview-non-viral/usage_2.scala b/tests/neg/preview-non-viral/usage_2.scala new file mode 100644 index 000000000000..50404e582dff --- /dev/null +++ b/tests/neg/preview-non-viral/usage_2.scala @@ -0,0 +1,2 @@ +def usePreviewFeatureTransitively = scala.usePreviewFeature +def usePreviewFeatureDirectly = scala.previewFeature // error diff --git a/tests/neg/previewOverloads.scala b/tests/neg/previewOverloads.scala new file mode 100644 index 000000000000..e324bc535772 --- /dev/null +++ b/tests/neg/previewOverloads.scala @@ -0,0 +1,13 @@ +package scala // @preview is private[scala] + +import scala.annotation.internal.preview + +trait A: + def f: Int + def g: Int = 3 +trait B extends A: + @preview + def f: Int = 4 // error + + @preview + override def g: Int = 5 // error diff --git a/tests/neg/previewOverride.scala b/tests/neg/previewOverride.scala new file mode 100644 index 000000000000..4a772506f7b6 --- /dev/null +++ b/tests/neg/previewOverride.scala @@ -0,0 +1,41 @@ +package scala // @preview is private[scala] + +import scala.annotation.internal.preview + +@preview +class A: + def f() = 1 + +@preview +class B extends A: + override def f() = 2 + +class C: + @preview + def f() = 1 + +class D extends C: + override def f() = 2 + +trait A2: + @preview + 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 + () diff --git a/tests/pos/preview-flag.scala b/tests/pos/preview-flag.scala new file mode 100644 index 000000000000..dbcfcaaff5b3 --- /dev/null +++ b/tests/pos/preview-flag.scala @@ -0,0 +1,18 @@ +//> using options -preview +package scala // @preview is private[scala] +import scala.annotation.internal.preview + +@preview def previewDef: Int = 42 + +class Foo: + def foo: Int = previewDef + +class Bar: + def bar: Int = previewDef +object Bar: + def bar: Int = previewDef + +object Baz: + def bar: Int = previewDef + +def toplevelMethod: Int = previewDef