Skip to content

Commit b05c8a7

Browse files
committed
Add @scala.annotation.preview annotation and -preview flag.
1 parent 89c20f8 commit b05c8a7

File tree

20 files changed

+286
-17
lines changed

20 files changed

+286
-17
lines changed

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] =>
297297
case TypeDefs(_) => true
298298
case _ => false
299299

300-
private val languageSubCategories = Set(nme.experimental, nme.deprecated)
300+
private val languageSubCategories = Set(nme.experimental, nme.preview, nme.deprecated)
301301

302302
/** If `path` looks like a language import, `Some(name)` where name
303303
* is `experimental` if that sub-module is imported, and the empty

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ 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:
1718

1819
def experimental(str: PreName): TermName =
1920
QualifiedName(nme.experimental, str.toTermName)
21+
22+
def preview(str: PreName): TermName =
23+
QualifiedName(nme.preview, str.toTermName)
2024

2125
private def deprecated(str: PreName): TermName =
2226
QualifiedName(nme.deprecated, str.toTermName)
@@ -44,6 +48,10 @@ object Feature:
4448
defn.languageExperimentalFeatures
4549
.map(sym => experimental(sym.name))
4650
.filterNot(_ == captureChecking) // TODO is this correct?
51+
52+
def previewAutoEnableFeatures(using Context): List[TermName] =
53+
defn.languagePreviewFeatures
54+
.map(sym => preview(sym.name))
4755

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

225233
def isExperimentalEnabledByImport(using Context): Boolean =
226234
experimentalAutoEnableFeatures.exists(enabledByImport)
227-
235+
228236
/** Handle language import `import language.<prefix>.<imported>` if it is one
229237
* of the global imports `pureFunctions` or `captureChecking`. In this case
230238
* make the compilation unit's and current run's fields accordingly.
@@ -242,4 +250,35 @@ object Feature:
242250
true
243251
else
244252
false
253+
254+
def isPreviewEnabled(using Context): Boolean =
255+
ctx.settings.preview.value ||
256+
previewAutoEnableFeatures.exists(enabled)
257+
258+
def checkPreviewFeature(which: String, srcPos: SrcPos, note: => String = "")(using Context) =
259+
if !isPreviewEnabled then
260+
report.error(previewUseSite(which) + note, srcPos)
261+
262+
def checkPreviewDef(sym: Symbol, srcPos: SrcPos)(using Context) = if !isPreviewEnabled then
263+
val previewSym =
264+
if sym.hasAnnotation(defn.PreviewAnnot) then sym
265+
else if sym.owner.hasAnnotation(defn.PreviewAnnot) then sym.owner
266+
else NoSymbol
267+
val msg =
268+
previewSym.getAnnotation(defn.PreviewAnnot).collectFirst {
269+
case PreviewAnnotation(msg) if msg.nonEmpty => s": $msg"
270+
}.getOrElse("")
271+
val markedPreview =
272+
if previewSym.exists
273+
then i"$previewSym is marked @preview$msg"
274+
else i"$sym inherits @preview$msg"
275+
report.error(markedPreview + "\n\n" + previewUseSite("definition"), srcPos)
276+
277+
private def previewUseSite(which: String): String =
278+
s"""Preview $which may only be used under preview mode:
279+
| 1. in a definition marked as @preview, or
280+
| 2. a preview feature is imported at the package level, or
281+
| 3. compiling with the -preview compiler flag.
282+
|""".stripMargin
245283
end Feature
284+

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ 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-
119+
val preview: Setting[Boolean] = BooleanSetting(RootSetting, "preview", "Enable the use of preview features anywhere in the project.")
120+
120121
/* Coverage settings */
121122
val coverageOutputDir = PathSetting(RootSetting, "coverage-out", "Destination for coverage classfiles and instrumentation data.", "", aliases = List("--coverage-out"))
122123
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"))

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

Lines changed: 12 additions & 1 deletion
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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,7 @@ class Definitions {
808808
@tu lazy val LanguageModule: Symbol = requiredModule("scala.language")
809809
@tu lazy val LanguageModuleClass: Symbol = LanguageModule.moduleClass.asClass
810810
@tu lazy val LanguageExperimentalModule: Symbol = requiredModule("scala.language.experimental")
811+
@tu lazy val LanguagePreviewModule: Symbol = requiredModule("scala.language.preview")
811812
@tu lazy val LanguageDeprecatedModule: Symbol = requiredModule("scala.language.deprecated")
812813
@tu lazy val NonLocalReturnControlClass: ClassSymbol = requiredClass("scala.runtime.NonLocalReturnControl")
813814
@tu lazy val SelectableClass: ClassSymbol = requiredClass("scala.Selectable")
@@ -1053,6 +1054,7 @@ class Definitions {
10531054
@tu lazy val CompileTimeOnlyAnnot: ClassSymbol = requiredClass("scala.annotation.compileTimeOnly")
10541055
@tu lazy val SwitchAnnot: ClassSymbol = requiredClass("scala.annotation.switch")
10551056
@tu lazy val ExperimentalAnnot: ClassSymbol = requiredClass("scala.annotation.experimental")
1057+
@tu lazy val PreviewAnnot: ClassSymbol = requiredClass("scala.annotation.preview")
10561058
@tu lazy val ThrowsAnnot: ClassSymbol = requiredClass("scala.throws")
10571059
@tu lazy val TransientAnnot: ClassSymbol = requiredClass("scala.transient")
10581060
@tu lazy val UncheckedAnnot: ClassSymbol = requiredClass("scala.unchecked")
@@ -2079,6 +2081,10 @@ class Definitions {
20792081
@tu lazy val languageExperimentalFeatures: List[TermSymbol] =
20802082
LanguageExperimentalModule.moduleClass.info.decls.toList.filter(_.isAllOf(Lazy | Module)).map(_.asTerm)
20812083

2084+
/** Preview language features defined in `scala.runtime.stdLibPatches.language.preview` */
2085+
@tu lazy val languagePreviewFeatures: List[TermSymbol] =
2086+
LanguagePreviewModule.moduleClass.info.decls.toList.filter(_.isAllOf(Lazy | Module)).map(_.asTerm)
2087+
20822088
// ----- primitive value class machinery ------------------------------------------
20832089

20842090
class PerRun[T](generate: Context ?=> T) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,7 @@ object StdNames {
579579
val parts: N = "parts"
580580
val postfixOps: N = "postfixOps"
581581
val prefix : N = "prefix"
582+
val preview: N = "preview"
582583
val processEscapes: N = "processEscapes"
583584
val productArity: N = "productArity"
584585
val productElement: N = "productElement"

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

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -366,24 +366,31 @@ 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))
372-
373-
def isInExperimentalScope(using Context): Boolean =
374-
def isDefaultArgumentOfExperimentalMethod =
369+
def isExperimental(using Context): Boolean = isFeatureAnnotated(defn.ExperimentalAnnot)
370+
def isInExperimentalScope(using Context): Boolean = isInFeatureScope(defn.ExperimentalAnnot, _.isExperimental, _.isInExperimentalScope)
371+
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)
386-
390+
self.hasAnnotation(checkAnnotation)
391+
|| isDefaultArgumentOfCheckedMethod
392+
|| (!self.is(Package) && checkOwner(self.owner))
393+
387394
/** The declared self type of this class, as seen from `site`, stripping
388395
* all refinements for opaque types.
389396
*/

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,7 @@ trait Checking {
10791079
case Some(prefix) =>
10801080
val required =
10811081
if prefix == nme.experimental then defn.LanguageExperimentalModule
1082+
else if prefix == nme.preview then defn.LanguagePreviewModule
10821083
else if prefix == nme.deprecated then defn.LanguageDeprecatedModule
10831084
else defn.LanguageModule
10841085
if path.symbol != required then
@@ -1087,6 +1088,7 @@ trait Checking {
10871088
val foundClasses = path.tpe.classSymbols
10881089
if foundClasses.contains(defn.LanguageModule.moduleClass)
10891090
|| foundClasses.contains(defn.LanguageExperimentalModule.moduleClass)
1091+
|| foundClasses.contains(defn.LanguagePreviewModule.moduleClass)
10901092
then
10911093
report.error(em"no aliases can be used to refer to a language import", path.srcPos)
10921094

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

Lines changed: 11 additions & 1 deletion
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

Lines changed: 4 additions & 1 deletion
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.
@@ -643,8 +644,10 @@ object RefChecks {
643644
MigrationVersion.OverrideValParameter)
644645
else if !other.isExperimental && member.hasAnnotation(defn.ExperimentalAnnot) then // (1.13)
645646
overrideError("may not override non-experimental member")
646-
else if !member.hasAnnotation(defn.PublicInBinaryAnnot) && other.hasAnnotation(defn.PublicInBinaryAnnot) then // (1.14)
647+
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

0 commit comments

Comments
 (0)