Skip to content

Commit

Permalink
Add @scala.annotation.preview annotation and -preview flag.
Browse files Browse the repository at this point in the history
  • Loading branch information
WojciechMazur committed Jan 7, 2025
1 parent 7441791 commit cb46a43
Show file tree
Hide file tree
Showing 20 changed files with 287 additions and 18 deletions.
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] =>
case TypeDefs(_) => true
case _ => false

private val languageSubCategories = Set(nme.experimental, nme.deprecated)
private val languageSubCategories = Set(nme.experimental, nme.preview, nme.deprecated)

/** If `path` looks like a language import, `Some(name)` where name
* is `experimental` if that sub-module is imported, and the empty
Expand Down
41 changes: 40 additions & 1 deletion compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ import SourceVersion.*
import reporting.Message
import NameKinds.QualifiedName
import Annotations.ExperimentalAnnotation
import Annotations.PreviewAnnotation
import Settings.Setting.ChoiceWithHelp

object Feature:

def experimental(str: PreName): TermName =
QualifiedName(nme.experimental, str.toTermName)

def preview(str: PreName): TermName =
QualifiedName(nme.preview, str.toTermName)

private def deprecated(str: PreName): TermName =
QualifiedName(nme.deprecated, str.toTermName)
Expand Down Expand Up @@ -44,6 +48,10 @@ object Feature:
defn.languageExperimentalFeatures
.map(sym => experimental(sym.name))
.filterNot(_ == captureChecking) // TODO is this correct?

def previewAutoEnableFeatures(using Context): List[TermName] =
defn.languagePreviewFeatures
.map(sym => preview(sym.name))

val values = List(
(nme.help, "Display all available features"),
Expand Down Expand Up @@ -224,7 +232,7 @@ object Feature:

def isExperimentalEnabledByImport(using Context): Boolean =
experimentalAutoEnableFeatures.exists(enabledByImport)

/** Handle language import `import language.<prefix>.<imported>` if it is one
* of the global imports `pureFunctions` or `captureChecking`. In this case
* make the compilation unit's and current run's fields accordingly.
Expand All @@ -242,4 +250,35 @@ object Feature:
true
else
false

def isPreviewEnabled(using Context): Boolean =
ctx.settings.preview.value ||
previewAutoEnableFeatures.exists(enabled)

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(markedPreview + "\n\n" + previewUseSite("definition"), srcPos)

private def previewUseSite(which: String): String =
s"""Preview $which may only be used under preview mode:
| 1. in a definition marked as @preview, or
| 2. a preview feature is imported at the package level, or
| 3. compiling with the -preview compiler flag.
|""".stripMargin
end Feature

3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ 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"))
val coverageExcludeClasslikes: Setting[List[String]] = MultiStringSetting(RootSetting, "coverage-exclude-classlikes", "packages, classes and modules", "List of regexes for packages, classes and modules to exclude from coverage.", aliases = List("--coverage-exclude-classlikes"))
Expand Down
13 changes: 12 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
}
}
6 changes: 6 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@ class Definitions {
@tu lazy val LanguageModule: Symbol = requiredModule("scala.language")
@tu lazy val LanguageModuleClass: Symbol = LanguageModule.moduleClass.asClass
@tu lazy val LanguageExperimentalModule: Symbol = requiredModule("scala.language.experimental")
@tu lazy val LanguagePreviewModule: Symbol = requiredModule("scala.language.preview")
@tu lazy val LanguageDeprecatedModule: Symbol = requiredModule("scala.language.deprecated")
@tu lazy val NonLocalReturnControlClass: ClassSymbol = requiredClass("scala.runtime.NonLocalReturnControl")
@tu lazy val SelectableClass: ClassSymbol = requiredClass("scala.Selectable")
Expand Down Expand Up @@ -1052,6 +1053,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.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")
Expand Down Expand Up @@ -2063,6 +2065,10 @@ class Definitions {
@tu lazy val languageExperimentalFeatures: List[TermSymbol] =
LanguageExperimentalModule.moduleClass.info.decls.toList.filter(_.isAllOf(Lazy | Module)).map(_.asTerm)

/** Preview language features defined in `scala.runtime.stdLibPatches.language.preview` */
@tu lazy val languagePreviewFeatures: List[TermSymbol] =
LanguagePreviewModule.moduleClass.info.decls.toList.filter(_.isAllOf(Lazy | Module)).map(_.asTerm)

// ----- primitive value class machinery ------------------------------------------

class PerRun[T](generate: Context ?=> T) {
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ object StdNames {
val parts: N = "parts"
val postfixOps: N = "postfixOps"
val prefix : N = "prefix"
val preview: N = "preview"
val processEscapes: N = "processEscapes"
val productArity: N = "productArity"
val productElement: N = "productElement"
Expand Down
29 changes: 18 additions & 11 deletions compiler/src/dotty/tools/dotc/core/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -366,24 +366,31 @@ 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 isInExperimentalScope(using Context): Boolean =
def isDefaultArgumentOfExperimentalMethod =
def isExperimental(using Context): Boolean = isFeatureAnnotated(defn.ExperimentalAnnot)
def isInExperimentalScope(using Context): Boolean = isInFeatureScope(defn.ExperimentalAnnot, _.isExperimental, _.isInExperimentalScope)

/** 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.
*/
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,7 @@ trait Checking {
case Some(prefix) =>
val required =
if prefix == nme.experimental then defn.LanguageExperimentalModule
else if prefix == nme.preview then defn.LanguagePreviewModule
else if prefix == nme.deprecated then defn.LanguageDeprecatedModule
else defn.LanguageModule
if path.symbol != required then
Expand All @@ -1064,6 +1065,7 @@ trait Checking {
val foundClasses = path.tpe.classSymbols
if foundClasses.contains(defn.LanguageModule.moduleClass)
|| foundClasses.contains(defn.LanguageExperimentalModule.moduleClass)
|| foundClasses.contains(defn.LanguagePreviewModule.moduleClass)
then
report.error(em"no aliases can be used to refer to a language import", path.srcPos)

Expand Down
14 changes: 12 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class CrossVersionChecks extends MiniPhase:
if sym.exists && !sym.isInExperimentalScope then
for annot <- sym.annotations if annot.symbol.isExperimental 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 @@ -122,10 +122,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 _ =>
}
tree
Expand All @@ -149,18 +151,26 @@ 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
*/
private[CrossVersionChecks] def checkExperimentalRef(sym: Symbol, pos: SrcPos)(using Context): Unit =
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.
Expand Down
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/RefChecks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -643,8 +644,10 @@ object RefChecks {
MigrationVersion.OverrideValParameter)
else if !other.isExperimental && member.hasAnnotation(defn.ExperimentalAnnot) then // (1.13)
overrideError("may not override non-experimental member")
else if !member.hasAnnotation(defn.PublicInBinaryAnnot) && other.hasAnnotation(defn.PublicInBinaryAnnot) then // (1.14)
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
Expand Down
38 changes: 38 additions & 0 deletions docs/_docs/reference/other-new-features/preview-defs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
layout: doc-page
title: "Preview Definitions"
nightlyOf: https://docs.scala-lang.org/scala3/reference/other-new-features/preview-defs.html
---

The [`@preview`](https://scala-lang.org/api/3.x/scala/annotation/preview.html) annotation allows the definition of an API that is not guaranteed backward binary, but might become stable in next minor version of the compiler.

New Scala language features or standard library APIs initially introduced as experimental can become a preview features when they have become fully implemented and acceppted by the [SIP](https://docs.scala-lang.org/sips/) before they're accepted as standard features.
Such definitions can be used by early adopters that can accept possibility of binary compatibility breakage, for example these can be used for project internal tools and applications, but are discouraged to be used by libraries.

The [`@preview`](https://scala-lang.org/api/3.x/scala/annotation/preview.html) definitions follows similar rules as the [`@experimental`](https://scala-lang.org/api/3.x/scala/annotation/experimental.html) - to enable access to preview feature or API in given compilation unit Scala compiler requires either:

- explicit `-preview` flag passed to the compiler,
- top level import for explicit `scala.language.preview.<feature>`,
- annotating defintion that referes to preview feature with `@preview`

The biggest difference of preview features when compared with experimental features is their non-viral behaviour.
Any defintion that was compiles in the preview scope (using `-preview` flag or `scala.language.preview` top-level import) is not annotated as `@preview` defintion itself. It behaviour allows to use preview features transitively in other compilation units without enabled preview mode.

```scala
//> using options -preview
import scala.annotation.preview

@preview def previewFeature: Unit = ()

// Can be used in non-preview scope
def usePreviewFeature = previewFeature
```

```scala
def usePreviewFeatureTransitively = usePreviewFeature
def usePreviewFeatureDirectly = previewFeature // error - refering to preview definition outside preview scope
def useWrappedPreviewFeature = wrappedPreviewFeature // error - refering to preview definition outside preview scope

@scala.annotation.preview
def wrappedPreviewFeature = previewFeature
```
9 changes: 9 additions & 0 deletions library/src/scala/annotation/preview.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package scala.annotation

/** 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
*/
final class preview(message: String) extends StaticAnnotation:
def this() = this("")
15 changes: 15 additions & 0 deletions library/src/scala/runtime/stdLibPatches/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ import scala.annotation.compileTimeOnly
/** Scala 3 additions and replacements to the `scala.language` object.
*/
object language:

/** The preview object contains previously experimental features that are fully implemented
* but are awaiting to be stablized as a standard features.
*
* Preview features '''may undergo binary compatibility changes''' in future releases,
* but their API is unlikely to change. These can be used by early adopters that do don't care
* about the binary breakage, i.e. applications, but not libraries.
*
* Programmers are encouraged to try out preview features and
* [[https://github.com/scala/scala3/issues report any bugs or API inconsistencies]]
* they encounter so they can be improved in future releases.
*
* @group preview
*/
object preview

/** The experimental object contains features that have been recently added but have not
* been thoroughly tested in production yet.
Expand Down
27 changes: 27 additions & 0 deletions tests/neg/preview-message.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- Error: tests/neg/preview-message.scala:15:2 -------------------------------------------------------------------------
15 | f1() // error
| ^^
| method f1 is marked @preview
|
| Preview definition may only be used under preview mode:
| 1. in a definition marked as @preview, or
| 2. a preview feature is imported at the package level, or
| 3. 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 under preview mode:
| 1. in a definition marked as @preview, or
| 2. a preview feature is imported at the package level, or
| 3. 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 under preview mode:
| 1. in a definition marked as @preview, or
| 2. a preview feature is imported at the package level, or
| 3. compiling with the -preview compiler flag.
17 changes: 17 additions & 0 deletions tests/neg/preview-message.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@


import scala.annotation.preview

@preview
def f1() = ???

@preview()
def f2() = ???

@preview("not yet stable")
def f3() = ???

def g() =
f1() // error
f2() // error
f3() // error
6 changes: 6 additions & 0 deletions tests/neg/preview-non-viral/defs_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//> using options -preview
import scala.annotation.preview

@preview def previewFeature = 42

def usePreviewFeature = previewFeature
Loading

0 comments on commit cb46a43

Please sign in to comment.