Skip to content

Add @scala.annotation.internal.preview annotation and -preview flag. #22317

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

Merged
merged 7 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import SourceVersion.*
import reporting.Message
import NameKinds.QualifiedName
import Annotations.ExperimentalAnnotation
import Annotations.PreviewAnnotation
import Settings.Setting.ChoiceWithHelp

object Feature:
Expand Down Expand Up @@ -242,4 +243,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(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
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
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("")
}
}
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 @@ -1053,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.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")
Expand Down
25 changes: 16 additions & 9 deletions compiler/src/dotty/tools/dotc/core/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 11 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 _ =>
Expand Down Expand Up @@ -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
Expand All @@ -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.
*
Expand Down
3 changes: 3 additions & 0 deletions 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 @@ -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
Expand Down
34 changes: 34 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,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 acceppted by the [SIP](https://docs.scala-lang.org/sips/) these can become a preview features.
Preview language features and APIs are guaranteed to be standarized in some next Scala minor release, but allow compiler team to introduce small, possibly binary incompatible, changes based on the community feedback.
These can be used by early adopters who can accept possibility of binary compatibility breakage. As an example these can be used for project internal tools and applications, but are discouraged to be used by libraries.

Users can enable access to preview features and definitions by compiling with `-preview` flag. The flag would enable all preview features and definitions. There is no way for enabling only a subset of preview features.

The biggest difference of preview features when compared with experimental features is their non-viral behaviour.
Any defintion compiled in the preview mode (using `-preview` flag) is not marked as preview defintion itself.
This behaviour allows to use preview features transitively in other compilation units without explicitlly 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) annotations are used to mark Scala 3 standard library APIs currently available under enabled preview mode.
The definitions follows similar rules as the [`@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 unit that enables 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 - refering to preview definition outside preview scope
```
1 change: 1 addition & 0 deletions docs/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions library/src/scala/annotation/internal/preview.scala
Original file line number Diff line number Diff line change
@@ -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("")
1 change: 1 addition & 0 deletions project/MiMaFilters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
),

// Additions since last LTS
Expand Down
18 changes: 18 additions & 0 deletions tests/neg/preview-message.check
Original file line number Diff line number Diff line change
@@ -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
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 @@
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
7 changes: 7 additions & 0 deletions tests/neg/preview-non-viral/defs_1.scala
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions tests/neg/preview-non-viral/usage_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def usePreviewFeatureTransitively = scala.usePreviewFeature
def usePreviewFeatureDirectly = scala.previewFeature // error
13 changes: 13 additions & 0 deletions tests/neg/previewOverloads.scala
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions tests/neg/previewOverride.scala
Original file line number Diff line number Diff line change
@@ -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
()
18 changes: 18 additions & 0 deletions tests/pos/preview-flag.scala
Original file line number Diff line number Diff line change
@@ -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
Loading