Skip to content

Commit 7af42a7

Browse files
committed
add clarification of scala 2 differences
1 parent bd67982 commit 7af42a7

File tree

1 file changed

+84
-0
lines changed

1 file changed

+84
-0
lines changed

content/replace-nonsensical-unchecked-annotation.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ title: SIP-57 - Replace non-sensical @unchecked annotations
1414
| Date | Version |
1515
|---------------|--------------------|
1616
| Dec 8th 2023 | Initial Draft |
17+
| Jan 19th 2024 | Clarification about current @unchecked behavior |
1718

1819
## Summary
1920

@@ -57,6 +58,7 @@ Having to wrap the `@unchecked` in parentheses requires editing in two places, a
5758
Nominally, the purpose of the annotation is to silence warnings (_from the [API docs](https://www.scala-lang.org/api/3.3.1/scala/unchecked.html#)_):
5859
> An annotation to designate that the annotated entity should not be considered for additional compiler checks.
5960
61+
_The exact meaning of this description is open to interpretation, leading to differences between Scala 2.13 and Scala 3.x. See the [misinterpretation](#misinterpretation-of-unchecked) annex for more._
6062

6163

6264
In the following code however, the word `unchecked` is a misnomer, so could be confused for another meaning by an inexperienced user:
@@ -159,6 +161,88 @@ The new method elaborates to an annotated expression before the associated patte
159161

160162
3) Should the `RuntimeCheck` annotation be in the `scala.annotation.internal` package?
161163

164+
### Misinterpretation of unchecked
165+
166+
We would further like to highlight that the `unchecked` annotation is unspecified except for its imprecise API documentation. This leads to a crucial difference in its behavior between Scala 2.13 and the latest Scala 3.3.1 release.
167+
168+
#### Scala 3 semantics
169+
170+
Say you have the following:
171+
```scala
172+
val xs = List(1: Any)
173+
```
174+
175+
The following expression in Scala 3.3.1 yields two warnings:
176+
```scala
177+
xs match {
178+
case is: ::[Int] => is.head
179+
}
180+
```
181+
182+
```scala
183+
2 warnings found
184+
-- [E029] Pattern Match Exhaustivity Warning: ----------------------------------
185+
1 |xs match {
186+
|^^
187+
|match may not be exhaustive.
188+
|
189+
|It would fail on pattern case: List(_, _*), Nil
190+
|
191+
| longer explanation available when compiling with `-explain`
192+
val res0: Int = 1
193+
-- Unchecked Warning: ----------------------------------------------------------
194+
2 | case is: ::[Int] => is.head
195+
| ^
196+
|the type test for ::[Int] cannot be checked at runtime because its type arguments can't be determined from List[Any]
197+
```
198+
199+
using `@unchecked` on `xs` has the effect of silencing any warnings that depend on checking `xs`, so no warnings will be emitted for the following change:
200+
201+
```scala
202+
(xs: @unchecked) match {
203+
case is: ::[Int] => is.head
204+
}
205+
```
206+
207+
#### Scala 2.13 semantics
208+
209+
However, in Scala 2.13, this will only silence the `match may not be exhaustive` warning, and the user will still see the `type test for ::[Int] cannot be checked at runtime` warning:
210+
211+
```scala
212+
scala> (xs: @unchecked) match {
213+
| case is: ::[Int] => is.head
214+
| } ^
215+
On line 2: warning: non-variable type argument Int in type pattern scala.collection.immutable.::[Int] (the underlying of ::[Int]) is unchecked since it is eliminated by erasure
216+
val res2: Int = 1
217+
```
218+
219+
#### Aligning to Scala 2.13 semantics with runtimeCheck
220+
221+
with `xs.runtimeCheck` we should still produce an unchecked warning for `case is: ::[Int] =>`
222+
```scala
223+
scala> xs.runtimeChecked match {
224+
| case is: ::[Int] => is.head
225+
| }
226+
1 warning found
227+
-- Unchecked Warning: ----------------------------------------------------------
228+
2 | case is: ::[Int] => is.head
229+
| ^
230+
|the type test for ::[Int] cannot be checked at runtime because its type arguments can't be determined from List[Any]
231+
val res13: Int = 1
232+
```
233+
This is because `xs.runtimeChecked` means trust the user as long as the pattern can be checked at runtime.
234+
235+
To fully avoid warnings, the `@unchecked` will be put on the type argument:
236+
```scala
237+
scala> xs.runtimeChecked match {
238+
| case is: ::[Int @unchecked] => is.head
239+
| }
240+
val res14: Int = 1
241+
```
242+
This has a small extra migration cost because if the scrutinee changes from `(xs: @unchecked)` to `xs.runtimeCheck` now some individual cases might need to add `@unchecked` on type arguments to avoid creating new warnings - however this cost is offset by perhaps revealing unsafe patterns previously unaccounted for.
243+
244+
Once again `@nowarn` can be used to fully restore any old behavior
245+
162246
## Alternatives
163247

164248
1) make `runtimeCheck` a method on `Any` that returns the receiver (not inline). The compiler would check for presence of a call to this method when deciding to perform static checking of pattern exhaustivity. This idea was criticised for being brittle with respect to refactoring, or automatic code transformations via macro.

0 commit comments

Comments
 (0)