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 @scala.annotation.internal.preview annotation and -preview flag. #22317

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 @@ -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
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 @@ -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")
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 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
```
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"),
ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.packageObjectValues"),
ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$packageObjectValues$"),
),
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