Skip to content

Commit d29ec53

Browse files
committed
add method, annotation and test cases
1 parent 594306d commit d29ec53

File tree

19 files changed

+237
-7
lines changed

19 files changed

+237
-7
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,7 @@ class Definitions {
10341034
@tu lazy val TransparentTraitAnnot: ClassSymbol = requiredClass("scala.annotation.transparentTrait")
10351035
@tu lazy val NativeAnnot: ClassSymbol = requiredClass("scala.native")
10361036
@tu lazy val RepeatedAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Repeated")
1037+
@tu lazy val RuntimeCheckedAnnot: ClassSymbol = requiredClass("scala.annotation.internal.RuntimeChecked")
10371038
@tu lazy val SourceFileAnnot: ClassSymbol = requiredClass("scala.annotation.internal.SourceFile")
10381039
@tu lazy val ScalaSignatureAnnot: ClassSymbol = requiredClass("scala.reflect.ScalaSignature")
10391040
@tu lazy val ScalaLongSignatureAnnot: ClassSymbol = requiredClass("scala.reflect.ScalaLongSignature")

compiler/src/dotty/tools/dotc/semanticdb/ExtractSemanticDB.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -458,14 +458,15 @@ object ExtractSemanticDB:
458458
def unapply(tree: ValDef)(using Context): Option[(Tree, Tree)] = tree.rhs match
459459

460460
case Match(Typed(selected: Tree, tpt: TypeTree), CaseDef(pat: Tree, _, _) :: Nil)
461-
if tpt.span.exists && !tpt.span.hasLength && tpt.tpe.isAnnotatedByUnchecked =>
461+
if tpt.span.exists && !tpt.span.hasLength && tpt.tpe.isAnnotatedByUncheckedOrRuntimeChecked =>
462462
Some((pat, selected))
463463

464464
case _ => None
465465

466466
extension (tpe: Types.Type)
467-
private inline def isAnnotatedByUnchecked(using Context) = tpe match
468-
case Types.AnnotatedType(_, annot) => annot.symbol == defn.UncheckedAnnot
467+
private inline def isAnnotatedByUncheckedOrRuntimeChecked(using Context) = tpe match
468+
case Types.AnnotatedType(_, annot) =>
469+
annot.symbol == defn.UncheckedAnnot || annot.symbol == defn.RuntimeCheckedAnnot
469470
case _ => false
470471

471472
def collectPats(pat: Tree): List[Tree] =

compiler/src/dotty/tools/dotc/transform/patmat/Space.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,7 @@ object SpaceEngine {
794794
}
795795

796796
!sel.tpe.hasAnnotation(defn.UncheckedAnnot)
797+
&& !sel.tpe.hasAnnotation(defn.RuntimeCheckedAnnot)
797798
&& {
798799
ctx.settings.YcheckAllPatmat.value
799800
|| isCheckable(sel.tpe)
@@ -903,7 +904,7 @@ object SpaceEngine {
903904
def checkMatch(m: Match)(using Context): Unit =
904905
checkMatchExhaustivityOnly(m)
905906
if reachabilityCheckable(m.selector) then checkReachability(m)
906-
907+
907908
def checkMatchExhaustivityOnly(m: Match)(using Context): Unit =
908909
if exhaustivityCheckable(m.selector) then checkExhaustivity(m)
909910
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,7 @@ trait Checking {
981981
def recur(pat: Tree, pt: Type): Boolean =
982982
!sourceVersion.isAtLeast(`3.2`)
983983
|| pt.hasAnnotation(defn.UncheckedAnnot)
984+
|| pt.hasAnnotation(defn.RuntimeCheckedAnnot)
984985
|| {
985986
patmatch.println(i"check irrefutable $pat: ${pat.tpe} against $pt")
986987
pat match

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2067,7 +2067,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
20672067
result match {
20682068
case result @ Match(sel, CaseDef(pat, _, _) :: _) =>
20692069
tree.selector.removeAttachment(desugar.CheckIrrefutable) match {
2070-
case Some(checkMode) if !sel.tpe.hasAnnotation(defn.UncheckedAnnot) =>
2070+
case Some(checkMode) if !(sel.tpe.hasAnnotation(defn.UncheckedAnnot) || sel.tpe.hasAnnotation(defn.RuntimeCheckedAnnot)) =>
20712071
val isPatDef = checkMode == desugar.MatchCheck.IrrefutablePatDef
20722072
if !checkIrrefutable(sel, pat, isPatDef)
20732073
&& sourceVersion.isAtLeast(`3.2`)

compiler/test/dotty/tools/repl/TabcompleteTests.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import org.junit.Test
99
class TabcompleteTests extends ReplTest {
1010

1111
@Test def tabCompleteList = initially {
12-
val comp = tabComplete("List.r")
12+
val comp = tabComplete("List.ra")
1313
assertEquals(List("range"), comp.distinct)
1414
}
1515

@@ -112,7 +112,7 @@ class TabcompleteTests extends ReplTest {
112112
val comp = tabComplete("(null: AnyRef).")
113113
assertEquals(
114114
List("!=", "##", "->", "==", "asInstanceOf", "ensuring", "eq", "equals", "formatted",
115-
"getClass", "hashCode", "isInstanceOf", "ne", "nn", "notify", "notifyAll", "synchronized", "toString", "wait", ""),
115+
"getClass", "hashCode", "isInstanceOf", "ne", "nn", "notify", "notifyAll", "runtimeChecked", "synchronized", "toString", "wait", ""),
116116
comp.distinct.sorted)
117117
}
118118

@@ -163,6 +163,7 @@ class TabcompleteTests extends ReplTest {
163163
"nn",
164164
"notify",
165165
"notifyAll",
166+
"runtimeChecked",
166167
"synchronized",
167168
"toString",
168169
"valueOf",
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
layout: doc-page
3+
title: "The runtimeChecked method"
4+
nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/runtimeChecked.html
5+
---
6+
7+
The `runtimeChecked` method is an extension method, defined in `scala.Predef`. It can be called on any expression. An expression marked as `runtimeChecked` is exempt from certain static checks in the compiler, for example pattern match exhaustivity. It is intended to replace `: @unchecked` type ascription in these cases.
8+
9+
## Example
10+
11+
A common use case for `runtimeChecked` is to assert that a pattern will always match, either for convenience, or because there is a known invariant that the types can not express.
12+
13+
e.g. looking up an expected entry in a dynamically loaded dictionary-like structure
14+
```scala
15+
// example 1
16+
trait AppConfig:
17+
def get(key: String): Option[String]
18+
19+
val config: AppConfig = ???
20+
21+
val Some(appVersion) = config.get("appVersion").runtimeChecked
22+
```
23+
24+
or to assert that a value can only match some specific patterns:
25+
```scala
26+
// example 2
27+
enum Day:
28+
case Mon, Tue, Wed, Thu, Fri, Sat, Sun
29+
30+
val weekDay: Option[Day] = ???
31+
32+
weekDay.runtimeChecked match
33+
case Some(Mon | Tue | Wed | Thu | Fri) => println("got weekday")
34+
// case Some(Sat | Sun) => // weekend should not appear
35+
case None =>
36+
```
37+
38+
In both of these cases, without `runtimeChecked` then there would either be an error (example 1), or a warning (example 2), because statically, the compiler knows that there could be other cases at runtime - so is right to caution the programmer.
39+
40+
```scala
41+
// warning in example 2 when we don't add `.runtimeChecked`.
42+
-- [E029] Pattern Match Exhaustivity Warning: ----------------------------------
43+
6 |weekDay match
44+
|^^^^^^^
45+
|match may not be exhaustive.
46+
|
47+
|It would fail on pattern case: Some(Sat), Some(Sun)
48+
```
49+
50+
## Safety
51+
52+
The `runtimeChecked` method only turns off static checks that can be soundly performed at runtime. This means that patterns with unchecked type-tests will still generate warnings. For example:
53+
```scala
54+
scala> val xs = List(1: Any)
55+
| xs.runtimeChecked match {
56+
| case is: ::[Int] => is.head
57+
| }
58+
1 warning found
59+
-- Unchecked Warning: ---------------------------------------
60+
3 | case is: ::[Int] => is.head
61+
| ^
62+
|the type test for ::[Int] cannot be checked at runtime
63+
|because its type arguments can't be determined from List[Any]
64+
val res0: Int = 1
65+
```
66+
As the warning hints, the type `::[Int]` can not be tested at runtime on a value of type `List[Any]`, so using `runtimeChecked` still protects the user against assertions that can not be validated.
67+
68+
To fully avoid warnings, as with previous Scala versions, `@unchecked` should be put on the type argument:
69+
```scala
70+
scala> xs.runtimeChecked match {
71+
| case is: ::[Int @unchecked] => is.head
72+
| }
73+
val res1: Int = 1
74+
```
75+
76+
77+
## Specification
78+
79+
We add a new annotation `scala.internal.RuntimeChecked`, this is part of the standard Scala 3 library. A programmer is not expected to use this annotation directly.
80+
81+
```scala
82+
package scala.annotation.internal
83+
84+
final class RuntimeChecked extends Annotation
85+
```
86+
87+
Any term that is the scrutinee of a pattern match, that has a type annotated with `RuntimeChecked`, is exempt from pattern match exhaustivity checking.
88+
89+
90+
The user facing API is provided by a new extension method `scala.Predef.runtimeChecked`, qualified for any value:
91+
```scala
92+
package scala
93+
94+
import scala.annotation.internal.RuntimeChecked
95+
96+
object Predef:
97+
...
98+
extension [T](x: T)
99+
inline def runtimeChecked: x.type @RuntimeChecked =
100+
x: @RuntimeChecked
101+
```
102+
103+
The `runtimeChecked` method returns its argument, refining its type with the `RuntimeChecked` annotation.
104+
105+
## Motivation
106+
107+
As described in [Pattern Bindings](../changed-features/pattern-bindings.md), under `-source:future` it is an error for a pattern definition to be refutable. For instance, consider:
108+
```scala
109+
def xs: List[Any] = ???
110+
val y :: ys = xs
111+
```
112+
113+
This compiled without warning in 3.0, became a warning in 3.2, and we would like to make it an error by default in a future 3.x version.
114+
As an escape hatch in 3.2 we recommended to use a type ascription of `: @unchecked`:
115+
```
116+
-- Warning: ../../new/test.scala:6:16 -----------------------
117+
6 | val y :: ys = xs
118+
| ^^
119+
|pattern's type ::[Any] is more specialized than the right
120+
|hand side expression's type List[Any]
121+
|
122+
|If the narrowing is intentional, this can be communicated
123+
|by adding `: @unchecked` after the expression,
124+
|which may result in a MatchError at runtime.
125+
```
126+
127+
We suggest that `: @unchecked` is syntactically awkward, and also a misnomer - in fact in this case the the pattern is fully checked, but the necessary checks occur at runtime. The `runtimeChecked` method is then a successor to `@unchecked` for this purpose.
128+
129+
We propose that `@unchecked` will still be necessary for silencing warnings on unsound type tests.
130+
131+
### Restoring Scala 2.13 semantics with runtimeChecked
132+
133+
In Scala 3, the `: @unchecked` type ascription has the effect of turning off all pattern-match warnings on the match scrutinee - this differs from 2.13 in which it strictly turns off only pattern exhaustivity checking. `runtimeChecked` restores the semantics of Scala 2.13.

docs/sidebar.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ subsection:
157157
- page: reference/experimental/named-tuples.md
158158
- page: reference/experimental/modularity.md
159159
- page: reference/experimental/typeclasses.md
160+
- page: reference/experimental/runtimeChecked.md
160161
- page: reference/syntax.md
161162
- title: Language Versions
162163
index: reference/language-versions/language-versions.md

language-server/test/dotty/tools/languageserver/CompletionTest.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,7 @@ class CompletionTest {
10281028
("ensuring", Method, "(cond: Boolean): Foo.Bar.type"),
10291029
("##", Method, "=> Int"),
10301030
("nn", Method, "=> Foo.Bar.type"),
1031+
("runtimeChecked", Method, "=> Foo.Bar.type"),
10311032
("==", Method, "(x$0: Any): Boolean"),
10321033
("ensuring", Method, "(cond: Boolean, msg: => Any): Foo.Bar.type"),
10331034
("ne", Method, "(x$0: Object): Boolean"),
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package scala.annotation.internal
2+
3+
import scala.annotation.Annotation
4+
import scala.annotation.experimental
5+
6+
/**An annotation marking an intention that all checks on a value can be reliably performed at runtime.
7+
*
8+
* The compiler will remove certain static checks except those that can't be performed at runtime.
9+
*/
10+
@experimental
11+
final class RuntimeChecked() extends Annotation

library/src/scala/runtime/stdLibPatches/Predef.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package scala.runtime.stdLibPatches
22

33
import scala.annotation.experimental
4+
import scala.annotation.internal.RuntimeChecked
45

56
object Predef:
67
import compiletime.summonFrom
@@ -80,4 +81,19 @@ object Predef:
8081
@experimental
8182
infix type is[A <: AnyKind, B <: Any{type Self <: AnyKind}] = B { type Self = A }
8283

84+
extension [T](x: T)
85+
/**Asserts that a term should be exempt from static checks that can be reliably checked at runtime.
86+
* @example {{{
87+
* val xs: Option[Int] = Option(1)
88+
* xs.runtimeChecked match
89+
* case Some(x) => x // `Some(_)` can be checked at runtime, so no warning
90+
* }}}
91+
* @example {{{
92+
* val xs: List[Int] = List(1,2,3)
93+
* val y :: ys = xs.runtimeChecked // `_ :: _` can be checked at runtime, so no warning
94+
* }}}
95+
*/
96+
@experimental
97+
inline def runtimeChecked: x.type @RuntimeChecked = x: @RuntimeChecked
98+
8399
end Predef

presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class CompletionSuite extends BaseCompletionSuite:
117117
|fromSpecific(from: Any)(it: IterableOnce[Nothing]): List[Nothing]
118118
|fromSpecific(it: IterableOnce[Nothing]): List[Nothing]
119119
|nn: List.type & List.type
120+
|runtimeChecked scala.collection.immutable
120121
|toFactory(from: Any): Factory[Nothing, List[Nothing]]
121122
|formatted(fmtstr: String): String
122123
|→[B](y: B): (List.type, B)

tests/neg/runtimeChecked-2.check

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- [E030] Match case Unreachable Warning: tests/neg/runtimeChecked-2.scala:10:11 ---------------------------------------
2+
10 | case is: Some[t] => ??? // unreachable
3+
| ^^^^^^^^^^^
4+
| Unreachable case
5+
No warnings can be incurred under -Werror (or -Xfatal-warnings)

tests/neg/runtimeChecked-2.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//> using options -Werror -source:future -experimental
2+
3+
object Foo {
4+
5+
val xs: Option[Int] = Some(1)
6+
7+
def test: Int =
8+
xs.runtimeChecked match { // this test asserts that reachability is not avoided by runtimeChecked
9+
case is: Some[t] => is.get
10+
case is: Some[t] => ??? // unreachable
11+
}
12+
}
13+
// nopos-error: No warnings can be incurred under -Werror (or -Xfatal-warnings)

tests/neg/runtimeChecked.check

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- [E092] Pattern Match Unchecked Warning: tests/neg/runtimeChecked.scala:11:11 ----------------------------------------
2+
11 | case is: ::[Int/* can not be checked so still err */] => is.head
3+
| ^
4+
|the type test for ::[Int] cannot be checked at runtime because its type arguments can't be determined from List[Any]
5+
|
6+
| longer explanation available when compiling with `-explain`
7+
No warnings can be incurred under -Werror (or -Xfatal-warnings)

tests/neg/runtimeChecked.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//> using options -Werror -source:future -experimental
2+
3+
object Foo {
4+
5+
val xs: List[Any] = List(1: Any)
6+
7+
def test: Int =
8+
xs.runtimeChecked match { // this test asserts that unsound type tests still require @unchecked
9+
// tests/run/runtimeChecked.scala adds @unchecked to the
10+
// unsound type test to avoid the warning.
11+
case is: ::[Int/* can not be checked so still err */] => is.head
12+
}
13+
}
14+
// nopos-error: No warnings can be incurred under -Werror (or -Xfatal-warnings)

tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ val experimentalDefinitionInLibrary = Set(
8585
"scala.annotation.internal.WitnessNames",
8686
"scala.compiletime.package$package$.deferred",
8787
"scala.runtime.stdLibPatches.Predef$.is",
88+
89+
// New feature: SIP 57 - runtimeChecked replacement of @unchecked
90+
"scala.Predef$.runtimeChecked", "scala.annotation.internal.RuntimeChecked"
8891
)
8992

9093

tests/run/runtimeChecked-2.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//> using options -Werror -source:future -experimental
2+
3+
val xs: List[Any] = List(1: Any)
4+
5+
@main
6+
def Test: Unit =
7+
val head :: _ = xs.runtimeChecked
8+
assert(head == 1)

tests/run/runtimeChecked.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//> using options -Werror -source:future -experimental
2+
3+
val xs: List[Any] = List(1: Any)
4+
5+
@main
6+
def Test: Unit =
7+
val head = xs.runtimeChecked match {
8+
// tests/neg/runtimeChecked.scala asserts that @unchecked is
9+
// still needed for unsound type tests.
10+
case is: ::[Int @unchecked] => is.head
11+
}
12+
assert(head == 1)

0 commit comments

Comments
 (0)