-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Explicitly null check the stdlib #23566
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
@noti0na1 As agreed to, please push the changes to explicitly null check the stdlib here. |
2fe2627
to
2d178a7
Compare
@@ -313,7 +313,7 @@ object ArraySeq extends StrictOptimizedClassTagSeqFactory[ArraySeq] { self => | |||
* `ArraySeq.unsafeWrapArray(a.asInstanceOf[Array[Int]])` does not work, it throws a | |||
* `ClassCastException` at runtime. | |||
*/ | |||
def unsafeWrapArray[T](x: Array[T]): ArraySeq[T] = ((x: @unchecked) match { | |||
def unsafeWrapArray[T](x: Array[T] | Null): ArraySeq[T] | Null = ((x: @unchecked) match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could pull something off with a universal match type and its mapNull
helper.
scala> type MapNull[X, Y] <: Y | Null = X match { case Null => Null; case _ => Y }
scala> inline def mapNull[A, B](a: A | Null, inline f: A => B): MapNull[a.type, B] = (if a == null then null else f(a)).asInstanceOf[MapNull[a.type, B]]
def mapNull[A, B](a: A | Null, f: A => B): MapNull[a.type, B]
scala> def foo(x: String | Null): MapNull[x.type, Array[Char]] = mapNull(x, _.toCharArray())
def foo(x: String | Null): MapNull[x.type, Array[Char]]
scala> foo("hello"): Array[Char]
val res7: Array[Char] = Array(h, e, l, l, o)
scala> foo(null): Null
val res8: Null = null
scala> var s: String | Null = "hello"
var s: String | Null = hello
scala> foo(s): Array[Char]
-- [E007] Type Mismatch Error: -------------------------------------------------
1 |foo(s): Array[Char]
|^^^^^^
|Found: MapNull[(?1 : String | Null), Array[Char]]
|Required: Array[Char]
|
|where: ?1 is an unknown value of type String | Null
|
|
|Note: a match type could not be fully reduced:
|
| trying to reduce MapNull[(?1 : String | Null), Array[Char]]
| failed since selector (?1 : String | Null)
| does not match case Null => Null
| and cannot be shown to be disjoint from it either.
| Therefore, reduction cannot advance to the remaining case
|
| case _ => Array[Char]
|
| longer explanation available when compiling with `-explain`
1 error found
scala> foo(s): Array[Char] | Null
val res9: Array[Char] | Null = Array(h, e, l, l, o)
Note that the signature of foo
is specially crafted so that its TASTy Signature
and its binary descriptor are the same as if it were defined def foo(s: String | Null): Array[Char] | Null
.
It's not bullet-proof. Sometimes, use site inference gets in the way, as in:
scala> foo(s): Array[Char] | Null
val res9: Array[Char] | Null = Array(h, e, l, l, o)
scala> Array(foo(s))
-- [E172] Type Error: ----------------------------------------------------------
1 |Array(foo(s))
| ^
| No ClassTag available for MapNull[(?1 : String | Null), Array[Char]]
|
| where: ?1 is an unknown value of type String | Null
|
|
| Note: a match type could not be fully reduced:
|
| trying to reduce MapNull[(?1 : String | Null), Array[Char]]
| failed since selector (?1 : String | Null)
| does not match case Null => Null
| and cannot be shown to be disjoint from it either.
| Therefore, reduction cannot advance to the remaining case
|
| case _ => Array[Char]
1 error found
scala> Array(foo("hello"))
val res10: Array[Array[Char]] = Array(Array(h, e, l, l, o))
However, that's only for the nullable argument case, and in some use site scenarios. It might be worth exploring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an interesting idea. I will try this and see how it looks.
cc174ce
to
99df51b
Compare
@hamzaremmal Do you know how the stdlib is compiled at the first step? When I do |
When we compute `afterPatternContext`, the wrong `ctx` is passed to the function. ```scala def f(s: AnyRef | Null) = s match case null => 0 case s: String => s.length case _ => val a: AnyRef = s a.toString.length ``` Will be useful for #23566
} | ||
|
||
private[scala] abstract class LowPriorityImplicits2 { | ||
@deprecated("implicit conversions from Array to immutable.IndexedSeq are implemented by copying; use `toIndexedSeq` explicitly if you want to copy, or use the more efficient non-copying ArraySeq.unsafeWrapArray", since="2.13.0") | ||
implicit def copyArrayToImmutableIndexedSeq[T](xs: Array[T]): IndexedSeq[T] = | ||
if (xs eq null) null | ||
if (xs eq null) null.asInstanceOf[IndexedSeq[T]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this not use mapNull
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can move the mapNull
definition to Predef
then I can use it.
@@ -41,10 +41,10 @@ trait AsScalaConverters { | |||
* @param i The Java `Iterator` to be converted. | |||
* @return A Scala `Iterator` view of the argument. | |||
*/ | |||
def asScala[A](i: ju.Iterator[A]): Iterator[A] = i match { | |||
def asScala[A](i: ju.Iterator[A] | Null): Iterator[A] | Null = i match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could do some kind of trick like mapNull
, with match types, to give asScala
a null-polymorphic signature, so that it returns a non-null when you pass it a non-null. That would reduce the need for many .nn
s not only within the library itself, but probably also in code that uses the library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to add a match type as shown in @sjrd 's comment. But this means the new type would show up at the type signature, and adding a new public definition to stdlib is impossible at this point...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I should just forbid null
like the wrappers wrapRefArray
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forbidding null
s would break the current contract. That's a big no-no.
I also agree that we shouldn't introduce the match type I suggested at this time. We can reconsider in a future release, as we get closer to general adoption of explicit nulls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would argue for non-explicit-nulls users, forbidding nulls will not change any contract. It is just a more strict type for explicit-nulls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would make the body inconsistent with its typing. Constant-folding in the compiler could for example mis-"optimize" x == null
as false
because the type of x
is not nullable. So even non-explicit-nulls users could get affected, indirectly.
In general I don't think the types under explicit-nulls should be any more restrictive than the existing (sometimes tacit 🤷♂️) contract. Typing should better describe the contract; not make the contract stricter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we have to make a decision here. Of course we will ensure the behaviour of the body will not change, no matter passing null or not. Take asScala
as an example:
- Keep the original type:
def asScala[A](i: ju.Iterator[A]): Iterator[A]
. No effect for non-explicit-nulls in terms of typing. Not able to passnull
in explicit nulls, more convenient to have a chain of collection operations. - Change the type to:
def asScala[A](i: ju.Iterator[A] | Null): Iterator[A] | Null
. Still no effect for non-explicit-nulls. More precise to the original behaviour and document. Have to add.nn
at more places in explicit-nulls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After experimenting with the wrapper, I personally think 1 is batter choice for the library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we disagree, I added this question as an agenda item for tomorrow's core meeting.
@@ -305,7 +305,7 @@ object BigDecimal { | |||
implicit def double2bigDecimal(d: Double): BigDecimal = decimal(d) | |||
|
|||
/** Implicit conversion from `java.math.BigDecimal` to `scala.BigDecimal`. */ | |||
implicit def javaBigDecimal2bigDecimal(x: BigDec): BigDecimal = if (x == null) null else apply(x) | |||
implicit def javaBigDecimal2bigDecimal(x: BigDec | Null): BigDecimal | Null = if (x == null) null else apply(x) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above for asScala
.
216877c
to
2a854a1
Compare
5b251c0
to
747eb65
Compare
Why does the MiMa check a java class?
|
Why should it not? Java classes are also part of the ABI of our artifacts. Even tasty-mima checks Java classes! |
Oh, I thought we only check scala code. |
I added So I guess at least the stdlib itself is good? |
a22fa25
to
582c5c5
Compare
Strange tailrac errors only during [info] compiling 650 Scala sources and 55 Java sources to /Users/Work/dotty/library-js/target/scala-library/classes ...
[error] -- Error: /Users/Work/dotty/library/src/scala/collection/concurrent/TrieMap.scala:60:26
[error] 60 | else GCAS_Complete(/*READ*/mainnode, ct)
[error] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error] | Cannot rewrite recursive call: it is not in tail position
[error] -- Error: /Users/Work/dotty/library/src/scala/collection/concurrent/TrieMap.scala:73:28
[error] 73 | else GCAS_Complete(m, ct)
[error] | ^^^^^^^^^^^^^^^^^^^^
[error] | Cannot rewrite recursive call: it is not in tail position
[error] -- Error: /Users/Work/dotty/library/src/scala/collection/concurrent/TrieMap.scala:77:23
[error] 77 | GCAS_Complete(/*READ*/mainnode, ct)
[error] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error] | Cannot rewrite recursive call: it is not in tail position
[error] -- Error: /Users/Work/dotty/library/src/scala/collection/concurrent/TrieMap.scala:179:53
[error] 179 | if (startgen eq in.gen) in.rec_insertif(k, v, hc, cond, fullEquals, lev + 5, this, startgen, ct)
[error] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error] | Cannot rewrite recursive call: it is not in tail position
[error] -- Error: /Users/Work/dotty/library/src/scala/collection/concurrent/TrieMap.scala:181:72
[error] 181 | if (GCAS(cn, cn.renewed(startgen, ct), ct)) rec_insertif(k, v, hc, cond, fullEquals, lev, parent, startgen, ct)
[error] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error] | Cannot rewrite recursive call: it is not in tail position
[error] -- Error: /Users/Work/dotty/library/src/scala/runtime/MethodCache.scala:73:33
[error] 73 | case x: PolyMethodCache => x findInternal forReceiver
[error] | ^^^^^^^^^^^^^^^^^^^^^^^^^^
[error] | Cannot rewrite recursive call: it is not in tail position The same files was fine when only compiling the stdlib. |
It seems there are extra |
No description provided.