Skip to content

Commit d5d9af3

Browse files
bishaboshaodersky
authored andcommitted
Add proposal for runtimeChecked method
1 parent 19915b8 commit d5d9af3

File tree

1 file changed

+176
-0
lines changed

1 file changed

+176
-0
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
---
2+
layout: sip
3+
permalink: /sips/:title.html
4+
stage: implementation
5+
status: waiting-for-implementation
6+
presip-thread: https://contributors.scala-lang.org/t/pre-sip-replace-non-sensical-unchecked-annotations/6342
7+
title: SIP-NN - Replace non-sensical @unchecked annotations
8+
---
9+
10+
**By: Martin Odersky and Jamie Thompson**
11+
12+
## History
13+
14+
| Date | Version |
15+
|---------------|--------------------|
16+
| Dec 8th 2023 | Initial Draft |
17+
18+
## Summary
19+
20+
We propose to replace the mechanism to silence warnings for "unchecked" patterns, in the cases where silencing the warning will still result in the pattern being checked at runtime.
21+
22+
Currently, a user can silence warnings that a scrutinee may not be matched by a pattern, by annotating the scrutinee with the `@unchecked` annotation. This SIP proposes to use a new annotation `@RuntimeCheck` to replace `@unchecked` for this purpose. For convenience, an extension method will be added to `Predef` that marks the receiver with the annotation (used as follows: `foo.runtimeCheck`). Functionally it behaves the same as the old annotation, but improves readability at the callsite.
23+
24+
## Motivation
25+
26+
As described in [Scala 3 Reference: Pattern Bindings](https://docs.scala-lang.org/scala3/reference/changed-features/pattern-bindings.html), under `-source:future` it is an error for a pattern definition to be refutable. For instance, consider:
27+
```scala
28+
def xs: List[Any] = ???
29+
val y :: ys = xs
30+
```
31+
32+
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.
33+
As an escape hatch we recommend to use `@unchecked`:
34+
```
35+
-- Warning: ../../new/test.scala:6:16 ------------------------------------------
36+
6 | val y :: ys = xs
37+
| ^^
38+
|pattern's type ::[Any] is more specialized than the right hand side expression's type List[Any]
39+
|
40+
|If the narrowing is intentional, this can be communicated by adding `: @unchecked` after the expression,
41+
|which may result in a MatchError at runtime.
42+
```
43+
Similarly for non-exhaustive `match` expressions, where we also recommend to put `@unchecked` on the scrutinee.
44+
45+
But `@unchecked` has several problems. First, it is ergonomically bad. For instance to fix the exhaustivity warning in
46+
```scala
47+
xs match
48+
case y :: ys => ...
49+
```
50+
we'd have to write
51+
```
52+
(xs: @unchecked) match
53+
case y :: ys => ...
54+
```
55+
Having to wrap the `@unchecked` in parentheses requires editing in two places, and arguably harms readability: both due to the churn in extra symbols, and because in this use case the `@unchecked` annotation poorly communicates intent.
56+
57+
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#)_):
58+
> An annotation to designate that the annotated entity should not be considered for additional compiler checks.
59+
60+
61+
62+
In the following code however, the word `unchecked` is a misnomer, so could be confused for another meaning by an inexperienced user:
63+
64+
```scala
65+
def xs: List[Any] = ???
66+
val y :: ys = xs: @unchecked
67+
```
68+
After all, the pattern `y :: ys` _is_ checked, but it is done at runtime (by looking at the runtime class), rather than statically.
69+
70+
As a direct contradiction, in the following usage of `unchecked`, the meaning is the opposite:
71+
```scala
72+
xs match
73+
case ints: List[Int @unchecked] =>
74+
```
75+
Here, `@unchecked` means that the `Int` parameter will _not_ be checked at runtime: The compiler instead trusts the user that `ints` is a `List[Int]`. This could lead to a `ClassCastException` in an unrelated piece of code that uses `ints`, possibly without leaving a clear breadcrumb trail of where the faulty cast originally occurred.
76+
77+
## Proposed solution
78+
79+
### High-level overview
80+
81+
This SIP proposes to fix the ergnomics and readability of `@unchecked` in the usage where it means "checked at runtime", by instead adding a new annotation `scala.internal.RuntimeCheck`.
82+
83+
```scala
84+
package scala.annotation.internal
85+
86+
final class RuntimeCheck extends Annotation
87+
```
88+
89+
In all usages where the compiler looks for `@unchecked` for this purpose, we instead change to look for `@RuntimeCheck`.
90+
91+
By placing the annotation in the `internal` package, we communicate that the user is not meant to directly use the annotation.
92+
93+
Instead, for convenience, we provide an extension method `Predef.runtimeCheck`, which can be applied to any expression.
94+
95+
The new usage to assert that a pattern is checked at runtime then becomes as follows:
96+
```scala
97+
def xs: List[Any] = ???
98+
val y :: ys = xs.runtimeCheck
99+
```
100+
101+
We also make `runtimeCheck` a transparent inline method. This ensures that the elaboration of the method defines its semantics. (i.e. `runtimeCheck` is not meaningful because it is immediately inlined at type-checking).
102+
103+
### Specification
104+
105+
The addition of a new `scala.Predef` method:
106+
107+
```scala
108+
package scala
109+
110+
import scala.annotation.internal.RuntimeCheck
111+
112+
object Predef:
113+
extension [T](x: T)
114+
transparent inline def runtimeCheck: x.type =
115+
x: @RuntimeCheck
116+
```
117+
118+
### Compatibility
119+
120+
This change carries the usual backward binary and TASTy compatibility concerns as any other standard library addition to the Scala 3 only library.
121+
122+
Considering backwards source compatibility, the following situation will change:
123+
124+
```scala
125+
// source A.scala
126+
package example
127+
128+
extension (predef: scala.Predef.type)
129+
transparent inline def runtimeCheck[T](x: T): x.type =
130+
println("fake runtimeCheck")
131+
x
132+
```
133+
```scala
134+
// source B.scala
135+
package example
136+
137+
@main def Test =
138+
val xs = List[Any](1,2,3)
139+
val y :: ys = Predef.runtimeCheck(xs)
140+
assert(ys == List(2, 3))
141+
```
142+
143+
Previously this code would print `fake runtimeCheck`, however with the proposed change then recompiling this code will _succeed_ and no longer will print.
144+
145+
Potentially we could mitigate this if necessary with a migration warning when the new method is resolved (`@experimental` annotation would be a start)
146+
147+
148+
In general however, the new `runtimeCheck` method will not change any previously linking method without causing an ambiguity compilation error.
149+
150+
### Other concerns
151+
152+
In 3.3 we already require the user to put `@unchecked` to avoid warnings, there is likely a significant amount of existing code that will need to migrate to the new mechanism. (We can leverage already exisiting mechanisms help migrate code automatically).
153+
154+
### Open questions
155+
156+
1) A large question was should the method or annotation carry semantic weight in the language. In this proposal we weigh towards the annotation being the significant element.
157+
The new method elaborates to an annotated expression before the associated pattern exhaustivity checks occur.
158+
2) Another point, where should the helper method go? In Predef it requires no import, but another possible location was the `compiletime` package. Requiring the extra import could discourage usage without consideration - however if the method remains in `Predef` the name itself (and documentation) should signal danger, like with `asInstanceOf`.
159+
160+
3) Should the `RuntimeCheck` annotation be in the `scala.annotation.internal` package?
161+
162+
## Alternatives
163+
164+
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.
165+
166+
2) `runtimeCheck` should elaborate to code that matches the expected type, e.g. to heal `t: Any` to `Int` when the expected type is `Int`. The problem is that this is not useful for patterns that can not be runtime checked by type alone. Also, it implies a greater change to the spec, because now `runtimeCheck` would have to be specially treated.
167+
168+
## Related work
169+
170+
- [Pre SIP thread](https://contributors.scala-lang.org/t/pre-sip-replace-non-sensical-unchecked-annotations/6342)
171+
- [Scala 3 Reference: Pattern Bindings](https://docs.scala-lang.org/scala3/reference/changed-features/pattern-bindings.html),
172+
- None of OCaml, Rust, Swift, or Java offer explicit escape hatches for non-exhaustive pattern matches (Haskell does not even warn by default). Instead the user must add a default case, (making it exhaustive) or use the equivalent of `@nowarn` when they exist.
173+
174+
## FAQ
175+
176+
N/A so far.

0 commit comments

Comments
 (0)