Skip to content

Commit 5971674

Browse files
Add @scala.annotation.internal.preview annotation and -preview flag. (#22317)
This adds `@scala.annotation.internal.preview` to mark Scala 3 stdlib API as preview-only and accessors for easy marking Scala features in the compiler as preview (similarly to experimental features) Access to preview features/API is granted to user using the `-preview` compiler flag - it enables all access to preview features/API without possibility to enable only their subset. `@preview` annotated definitions follows the same rules as `@experimental` with exception of being non viral - we're not automatically adding `@preview` annotation to when refering to preview definitions, but still require preview scope (by `-preview` flag), we still check if class overloads, overrides or externs a definition marked as `@preview`. Other difference is that `@preview` is `private[scala]` to prevent users from defining their own preview definitions as it was happening with `@experimental`
1 parent 4dc4668 commit 5971674

File tree

18 files changed

+233
-11
lines changed

18 files changed

+233
-11
lines changed

compiler/src/dotty/tools/dotc/config/Feature.scala

+26
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import SourceVersion.*
1111
import reporting.Message
1212
import NameKinds.QualifiedName
1313
import Annotations.ExperimentalAnnotation
14+
import Annotations.PreviewAnnotation
1415
import Settings.Setting.ChoiceWithHelp
1516

1617
object Feature:
@@ -233,4 +234,29 @@ object Feature:
233234
true
234235
else
235236
false
237+
238+
def isPreviewEnabled(using Context): Boolean =
239+
ctx.settings.preview.value
240+
241+
def checkPreviewFeature(which: String, srcPos: SrcPos, note: => String = "")(using Context) =
242+
if !isPreviewEnabled then
243+
report.error(previewUseSite(which) + note, srcPos)
244+
245+
def checkPreviewDef(sym: Symbol, srcPos: SrcPos)(using Context) = if !isPreviewEnabled then
246+
val previewSym =
247+
if sym.hasAnnotation(defn.PreviewAnnot) then sym
248+
else if sym.owner.hasAnnotation(defn.PreviewAnnot) then sym.owner
249+
else NoSymbol
250+
val msg =
251+
previewSym.getAnnotation(defn.PreviewAnnot).collectFirst {
252+
case PreviewAnnotation(msg) if msg.nonEmpty => s": $msg"
253+
}.getOrElse("")
254+
val markedPreview =
255+
if previewSym.exists
256+
then i"$previewSym is marked @preview$msg"
257+
else i"$sym inherits @preview$msg"
258+
report.error(i"${markedPreview}\n\n${previewUseSite("definition")}", srcPos)
259+
260+
private def previewUseSite(which: String): String =
261+
s"Preview $which may only be used when compiling with the `-preview` compiler flag"
236262
end Feature

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ trait CommonScalaSettings:
116116
val unchecked: Setting[Boolean] = BooleanSetting(RootSetting, "unchecked", "Enable additional warnings where generated code depends on assumptions.", initialValue = true, aliases = List("--unchecked"))
117117
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"))
118118
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.")
119+
val preview: Setting[Boolean] = BooleanSetting(RootSetting, "preview", "Enable the use of preview features anywhere in the project.")
119120

120121
/* Coverage settings */
121122
val coverageOutputDir = PathSetting(RootSetting, "coverage-out", "Destination for coverage classfiles and instrumentation data.", "", aliases = List("--coverage-out"))

compiler/src/dotty/tools/dotc/core/Annotations.scala

+12-1
Original file line numberDiff line numberDiff line change
@@ -303,5 +303,16 @@ object Annotations {
303303
case annot @ ExperimentalAnnotation(msg) => ExperimentalAnnotation(msg, annot.tree.span)
304304
}
305305
}
306-
306+
307+
object PreviewAnnotation {
308+
/** Matches and extracts the message from an instance of `@preview(msg)`
309+
* Returns `Some("")` for `@preview` with no message.
310+
*/
311+
def unapply(a: Annotation)(using Context): Option[String] =
312+
if a.symbol ne defn.PreviewAnnot then
313+
None
314+
else a.argumentConstant(0) match
315+
case Some(Constant(msg: String)) => Some(msg)
316+
case _ => Some("")
317+
}
307318
}

compiler/src/dotty/tools/dotc/core/Definitions.scala

+1
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,7 @@ class Definitions {
10581058
@tu lazy val CompileTimeOnlyAnnot: ClassSymbol = requiredClass("scala.annotation.compileTimeOnly")
10591059
@tu lazy val SwitchAnnot: ClassSymbol = requiredClass("scala.annotation.switch")
10601060
@tu lazy val ExperimentalAnnot: ClassSymbol = requiredClass("scala.annotation.experimental")
1061+
@tu lazy val PreviewAnnot: ClassSymbol = requiredClass("scala.annotation.internal.preview")
10611062
@tu lazy val ThrowsAnnot: ClassSymbol = requiredClass("scala.throws")
10621063
@tu lazy val TransientAnnot: ClassSymbol = requiredClass("scala.transient")
10631064
@tu lazy val UncheckedAnnot: ClassSymbol = requiredClass("scala.unchecked")

compiler/src/dotty/tools/dotc/core/SymUtils.scala

+16-9
Original file line numberDiff line numberDiff line change
@@ -366,23 +366,30 @@ class SymUtils:
366366
&& self.owner.linkedClass.isDeclaredInfix
367367

368368
/** Is symbol declared or inherits @experimental? */
369-
def isExperimental(using Context): Boolean =
370-
self.hasAnnotation(defn.ExperimentalAnnot)
371-
|| (self.maybeOwner.isClass && self.owner.hasAnnotation(defn.ExperimentalAnnot))
369+
def isExperimental(using Context): Boolean = isFeatureAnnotated(defn.ExperimentalAnnot)
370+
def isInExperimentalScope(using Context): Boolean = isInFeatureScope(defn.ExperimentalAnnot, _.isExperimental, _.isInExperimentalScope)
372371

373-
def isInExperimentalScope(using Context): Boolean =
374-
def isDefaultArgumentOfExperimentalMethod =
372+
/** Is symbol declared or inherits @preview? */
373+
def isPreview(using Context): Boolean = isFeatureAnnotated(defn.PreviewAnnot)
374+
def isInPreviewScope(using Context): Boolean = isInFeatureScope(defn.PreviewAnnot, _.isPreview, _.isInPreviewScope)
375+
376+
private inline def isFeatureAnnotated(checkAnnotaton: ClassSymbol)(using Context): Boolean =
377+
self.hasAnnotation(checkAnnotaton)
378+
|| (self.maybeOwner.isClass && self.owner.hasAnnotation(checkAnnotaton))
379+
380+
private inline def isInFeatureScope(checkAnnotation: ClassSymbol, checkSymbol: Symbol => Boolean, checkOwner: Symbol => Boolean)(using Context): Boolean =
381+
def isDefaultArgumentOfCheckedMethod =
375382
self.name.is(DefaultGetterName)
376383
&& self.owner.isClass
377384
&& {
378385
val overloads = self.owner.asClass.membersNamed(self.name.firstPart)
379386
overloads.filterWithFlags(HasDefaultParams, EmptyFlags) match
380-
case denot: SymDenotation => denot.symbol.isExperimental
387+
case denot: SymDenotation => checkSymbol(denot.symbol)
381388
case _ => false
382389
}
383-
self.hasAnnotation(defn.ExperimentalAnnot)
384-
|| isDefaultArgumentOfExperimentalMethod
385-
|| (!self.is(Package) && self.owner.isInExperimentalScope)
390+
self.hasAnnotation(checkAnnotation)
391+
|| isDefaultArgumentOfCheckedMethod
392+
|| (!self.is(Package) && checkOwner(self.owner))
386393

387394
/** The declared self type of this class, as seen from `site`, stripping
388395
* all refinements for opaque types.

compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala

+11-1
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,12 @@ class CrossVersionChecks extends MiniPhase:
141141
if tree.span.isSourceDerived then
142142
checkDeprecatedRef(sym, tree.srcPos)
143143
checkExperimentalRef(sym, tree.srcPos)
144+
checkPreviewFeatureRef(sym, tree.srcPos)
144145
case TermRef(_, sym: Symbol) =>
145146
if tree.span.isSourceDerived then
146147
checkDeprecatedRef(sym, tree.srcPos)
147148
checkExperimentalRef(sym, tree.srcPos)
149+
checkPreviewFeatureRef(sym, tree.srcPos)
148150
case AnnotatedType(_, annot) =>
149151
checkUnrollAnnot(annot.symbol, tree.srcPos)
150152
case _ =>
@@ -174,11 +176,12 @@ object CrossVersionChecks:
174176
val description: String = "check issues related to deprecated and experimental"
175177

176178
/** Check that a reference to an experimental definition with symbol `sym` meets cross-version constraints
177-
* for `@deprecated` and `@experimental`.
179+
* for `@deprecated`, `@experimental` and `@preview`.
178180
*/
179181
def checkRef(sym: Symbol, pos: SrcPos)(using Context): Unit =
180182
checkDeprecatedRef(sym, pos)
181183
checkExperimentalRef(sym, pos)
184+
checkPreviewFeatureRef(sym, pos)
182185

183186
/** Check that a reference to an experimental definition with symbol `sym` is only
184187
* used in an experimental scope
@@ -187,6 +190,13 @@ object CrossVersionChecks:
187190
if sym.isExperimental && !ctx.owner.isInExperimentalScope then
188191
Feature.checkExperimentalDef(sym, pos)
189192

193+
/** Check that a reference to a preview definition with symbol `sym` is only
194+
* used in a preview mode.
195+
*/
196+
private[CrossVersionChecks] def checkPreviewFeatureRef(sym: Symbol, pos: SrcPos)(using Context): Unit =
197+
if sym.isPreview && !ctx.owner.isInPreviewScope then
198+
Feature.checkPreviewDef(sym, pos)
199+
190200
/** If @deprecated is present, and the point of reference is not enclosed
191201
* in either a deprecated member or a scala bridge method, issue a warning.
192202
*

compiler/src/dotty/tools/dotc/typer/RefChecks.scala

+3
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ object RefChecks {
306306
* that passes its value on to O.
307307
* 1.13. If O is non-experimental, M must be non-experimental.
308308
* 1.14. If O has @publicInBinary, M must have @publicInBinary.
309+
* 1.15. If O is non-preview, M must be non-preview
309310
* 2. Check that only abstract classes have deferred members
310311
* 3. Check that concrete classes do not have deferred definitions
311312
* that are not implemented in a subclass.
@@ -645,6 +646,8 @@ object RefChecks {
645646
overrideError("may not override non-experimental member")
646647
else if !member.hasAnnotation(defn.PublicInBinaryAnnot) && other.hasAnnotation(defn.PublicInBinaryAnnot) then // (1.14)
647648
overrideError("also needs to be declared with @publicInBinary")
649+
else if !other.isPreview && member.hasAnnotation(defn.PreviewAnnot) then // (1.15)
650+
overrideError("may not override non-preview member")
648651
else if other.hasAnnotation(defn.DeprecatedOverridingAnnot) then
649652
overrideDeprecation("", member, other, "removed or renamed")
650653
end checkOverride
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
layout: doc-page
3+
title: "Preview Definitions"
4+
nightlyOf: https://docs.scala-lang.org/scala3/reference/other-new-features/preview-defs.html
5+
---
6+
7+
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.
8+
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.
9+
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.
10+
11+
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.
12+
13+
The biggest difference of preview features compared to experimental features is their non-viral behavior.
14+
A definition compiled in preview mode (using the `-preview` flag) is not marked as a preview definition itself.
15+
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.
16+
17+
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.
18+
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.
19+
20+
```scala
21+
//> using options -preview
22+
package scala.stdlib
23+
import scala.annotation.internal.preview
24+
25+
@preview def previewFeature: Unit = ()
26+
27+
// Can be used in non-preview scope
28+
def usePreviewFeature = previewFeature
29+
```
30+
31+
```scala
32+
def usePreviewFeatureTransitively = scala.stdlib.usePreviewFeature
33+
def usePreviewFeatureDirectly = scala.stdlib.previewFeature // error - referring to preview definition outside preview scope
34+
```

docs/sidebar.yml

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ subsection:
8585
- page: reference/other-new-features/safe-initialization.md
8686
- page: reference/other-new-features/type-test.md
8787
- page: reference/other-new-features/experimental-defs.md
88+
- page: reference/other-new-features/preview-defs.md
8889
- page: reference/other-new-features/binary-literals.md
8990
- title: Other Changed Features
9091
directory: changed-features
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package scala.annotation
2+
package internal
3+
4+
5+
/** An annotation that can be used to mark a definition as preview.
6+
*
7+
* @see [[https://dotty.epfl.ch/docs/reference/other-new-features/preview-defs]]
8+
* @syntax markdown
9+
*/
10+
private[scala] final class preview(message: String) extends StaticAnnotation:
11+
def this() = this("")

project/MiMaFilters.scala

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ object MiMaFilters {
1313
ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.quotedPatternsWithPolymorphicFunctions"),
1414
ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$quotedPatternsWithPolymorphicFunctions$"),
1515
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.quoted.runtime.Patterns.higherOrderHoleWithTypes"),
16+
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.preview"),
1617
ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.packageObjectValues"),
1718
ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$packageObjectValues$"),
1819
),

tests/neg/preview-message.check

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- Error: tests/neg/preview-message.scala:15:2 -------------------------------------------------------------------------
2+
15 | f1() // error
3+
| ^^
4+
| method f1 is marked @preview
5+
|
6+
| Preview definition may only be used when compiling with the `-preview` compiler flag
7+
-- Error: tests/neg/preview-message.scala:16:2 -------------------------------------------------------------------------
8+
16 | f2() // error
9+
| ^^
10+
| method f2 is marked @preview
11+
|
12+
| Preview definition may only be used when compiling with the `-preview` compiler flag
13+
-- Error: tests/neg/preview-message.scala:17:2 -------------------------------------------------------------------------
14+
17 | f3() // error
15+
| ^^
16+
| method f3 is marked @preview: not yet stable
17+
|
18+
| Preview definition may only be used when compiling with the `-preview` compiler flag

tests/neg/preview-message.scala

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package scala // @preview is private[scala]
2+
3+
import scala.annotation.internal.preview
4+
5+
@preview
6+
def f1() = ???
7+
8+
@preview()
9+
def f2() = ???
10+
11+
@preview("not yet stable")
12+
def f3() = ???
13+
14+
def g() =
15+
f1() // error
16+
f2() // error
17+
f3() // error
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//> using options -preview
2+
package scala // @preview is private[scala]
3+
import scala.annotation.internal.preview
4+
5+
@preview def previewFeature = 42
6+
7+
def usePreviewFeature = previewFeature
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def usePreviewFeatureTransitively = scala.usePreviewFeature
2+
def usePreviewFeatureDirectly = scala.previewFeature // error

tests/neg/previewOverloads.scala

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package scala // @preview is private[scala]
2+
3+
import scala.annotation.internal.preview
4+
5+
trait A:
6+
def f: Int
7+
def g: Int = 3
8+
trait B extends A:
9+
@preview
10+
def f: Int = 4 // error
11+
12+
@preview
13+
override def g: Int = 5 // error

tests/neg/previewOverride.scala

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package scala // @preview is private[scala]
2+
3+
import scala.annotation.internal.preview
4+
5+
@preview
6+
class A:
7+
def f() = 1
8+
9+
@preview
10+
class B extends A:
11+
override def f() = 2
12+
13+
class C:
14+
@preview
15+
def f() = 1
16+
17+
class D extends C:
18+
override def f() = 2
19+
20+
trait A2:
21+
@preview
22+
def f(): Int
23+
24+
trait B2:
25+
def f(): Int
26+
27+
class C2 extends A2, B2:
28+
def f(): Int = 1
29+
30+
def test: Unit =
31+
val a: A = ??? // error
32+
val b: B = ??? // error
33+
val c: C = ???
34+
val d: D = ???
35+
val c2: C2 = ???
36+
a.f() // error
37+
b.f() // error
38+
c.f() // error
39+
d.f() // ok because D.f is a stable API
40+
c2.f() // ok because B2.f is a stable API
41+
()

tests/pos/preview-flag.scala

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//> using options -preview
2+
package scala // @preview is private[scala]
3+
import scala.annotation.internal.preview
4+
5+
@preview def previewDef: Int = 42
6+
7+
class Foo:
8+
def foo: Int = previewDef
9+
10+
class Bar:
11+
def bar: Int = previewDef
12+
object Bar:
13+
def bar: Int = previewDef
14+
15+
object Baz:
16+
def bar: Int = previewDef
17+
18+
def toplevelMethod: Int = previewDef

0 commit comments

Comments
 (0)