Skip to content

change lower bound of Option.orNull to nullable#25733

Merged
sjrd merged 4 commits into
scala:mainfrom
dotty-staging:option-ornull
Apr 29, 2026
Merged

change lower bound of Option.orNull to nullable#25733
sjrd merged 4 commits into
scala:mainfrom
dotty-staging:option-ornull

Conversation

@olhotak
Copy link
Copy Markdown
Contributor

@olhotak olhotak commented Apr 8, 2026

This PR changes the signature of Option.orNull from

  @inline final def orNull[A1 >: A](implicit ev: Null <:< A1): A1 = this getOrElse ev(null)

to

  @inline final def orNull: A | Null = this getOrElse ev(null)

Under -Yexplicit-nulls, type inference sometimes infers A1 to be a non-null type, leading to a failure to find the implicit ev. The new result type is more straightforward for type inference.

Without -Yexplicit-nulls, A | Null is also correct. When A is a nullable type, A | Null is equivalent to A.

How much have you relied on LLM-based tools in this contribution?

No.

How was the solution tested?

Added test option-ornull minimized from akka-management.

Additional notes

This may have to wait until a version when we can start making changes to standard library signatures.

@olhotak olhotak requested a review from a team as a code owner April 8, 2026 14:29
@SethTisue
Copy link
Copy Markdown
Member

SethTisue commented Apr 17, 2026

This may have to wait until a version when we can start making changes to standard library signatures

MiMa says the change is binary compatible. Do you see any source compatibility concerns here?

We're not forbidden from breaking source compatibility, especially under a -Y flag, though we shouldn't do it without thought/consideration, either. Is there any possibility of this breaking user code we should hesitate to break?

@SethTisue SethTisue added this to the 3.9.0 milestone Apr 17, 2026
@SethTisue
Copy link
Copy Markdown
Member

SethTisue commented Apr 17, 2026

I have taken the liberty of milestoning this for 3.9. My reasoning is that offhand, this looks to me like a pretty basic usability issue for -Yexplicit-nulls, and if we have an opportunity to make -Yexplicit-nulls more usable in an LTS release (that library authors will be stuck on for some years), we should consider taking it.

@olhotak
Copy link
Copy Markdown
Contributor Author

olhotak commented Apr 18, 2026

I don't see any source compatibility concerns here. If any source instantiates A1 with a type that is not a supertype of Null, it would not be able to supply the ev parameter. I don't see a problem, though perhaps somebody else does? @noti0na1 ?

On the contrary, this fixes source compatibility for several of the Open Community Build projects with #25461 (which would turn -Yexplicit-nulls on by default). That PR was the motivation for this PR.

@sjrd
Copy link
Copy Markdown
Member

sjrd commented Apr 18, 2026

I just realized this is not a backward TASTy compatible change. I thought the Null <:< A1 evidence was enough to guarantee that any valid instantiation of A1 must also be a supertype of Null. But that is not true if that evidence is forwarded from elsewhere, rather than synthesized on the spot. If one has previously written

def foo[A, B >: A](x: Option[A])(using Null <:< B): B =
  x.orNull

the call was previously well typed as x.orNull[B](summon[Null <:< B]). But now, it is not valid anymore because B >: A | Null is not true.

@sjrd
Copy link
Copy Markdown
Member

sjrd commented Apr 18, 2026

I think an alternative would be to hide this one as protected, and introduce a new one with a simpler signature:

@inline final def orNull: A | Null = this.getOrElse(null)

// binary and TASTy compat
@inline protected final def orNull[A1 >: A](implicit ev: Null <:< A1): A1 = this getOrElse ev(null)

@noti0na1
Copy link
Copy Markdown
Member

I think an alternative would be to hide this one as protected, and introduce a new one with a simpler signature:

Why does adding a protected version not break the compatibility? The new orNull will have a different signature.

@sjrd
Copy link
Copy Markdown
Member

sjrd commented Apr 18, 2026

Why does adding a protected version not break the compatibility?

Because it's public in bytecode, and TASTy does not check accessibility.

@olhotak
Copy link
Copy Markdown
Contributor Author

olhotak commented Apr 18, 2026

Error:  scala-library-bootstrapped: Failed binary compatibility check against org.scala-lang:scala-library:3.8.0! Found 3 potential problems (filtered 1734)
Error:   * static method orNull(scala.<:<)java.lang.Object in class scala.None does not have a correspondent in current version
Error:     filter with: ProblemFilters.exclude[DirectMissingMethodProblem]("scala.None.orNull")
Error:   * static method orNull()java.lang.Object in class scala.None does not have a correspondent in other version
Error:     filter with: ProblemFilters.exclude[DirectMissingMethodProblem]("scala.None.orNull")
Error:   * method orNull()java.lang.Object in class scala.Option does not have a correspondent in other version
Error:     filter with: ProblemFilters.exclude[DirectMissingMethodProblem]("scala.Option.orNull")

@olhotak
Copy link
Copy Markdown
Contributor Author

olhotak commented Apr 25, 2026

The existing public orNull(ev: <:<) generates a static forwarder in object None. When I change it to final protected, that forwarder goes away, failing binary compatibility (and Mima).

I've changed it to non-final protected and added an extra explicit orNull(ev: <:<) method to object None. Now I think it's binary compatible.

@sjrd could you please confirm that this is OK and review the PR in general?

@olhotak olhotak requested a review from sjrd April 25, 2026 00:29
Comment thread library/src/scala/Option.scala Outdated
def get: Nothing = throw new NoSuchElementException("None.get")

// for binary and TASTy backwards compatibility
@deprecated @inline override def orNull[A1](implicit ev: Null <:< A1): A1 = null.asInstanceOf[A1]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not good. It has at least two issues:

  • It creates two visible overloads of orNull on None, which are only distinguished by an implicit parameter list.
  • It turns dispatch on Option.orNull into a bimorphic call, where it was monomorphic before.

I don't think this is an acceptable change.

The static forwarder for None.orNull has approximately 0% chance of being used in the wild. It would have to be called from Java, explicitly passing the evidence of <:<, all to get null. I think we can destroy it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back to the version that adds the backwards-compatibility method only to Option, not to None. I also put in an override for the Mima error on None to get the CI to pass. Is that OK?

@olhotak olhotak requested a review from sjrd April 26, 2026 20:36
Copy link
Copy Markdown
Member

@sjrd sjrd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM from me, other than the misleading placement of the MiMa filter.

I will put it up for discussion at the core meeting tomorrow, to be sure.

Comment thread project/MiMaFilters.scala Outdated
@SethTisue
Copy link
Copy Markdown
Member

SethTisue commented Apr 29, 2026

No objections at core meeting today.

(I can imagine that in the future, some similar case might motivate us to build a mechanism to emit a legacy static forwarder when one is needed. But this case doesn't seem nearly important enough to motivate us to build that mechanism now.)

@sjrd sjrd merged commit a24622b into scala:main Apr 29, 2026
45 checks passed
@sjrd sjrd deleted the option-ornull branch April 29, 2026 16:51
mbovel pushed a commit to mbovel/dotty that referenced this pull request May 4, 2026
This PR changes the signature of `Option.orNull` from
```scala
  @inline final def orNull[A1 >: A](implicit ev: Null <:< A1): A1 = this getOrElse ev(null)
```
to
```scala
  @inline final def orNull: A | Null = this getOrElse ev(null)
```
Under `-Yexplicit-nulls`, type inference sometimes infers `A1` to be a
non-null type, leading to a failure to find the implicit `ev`. The new
result type is more straightforward for type inference.

Without `-Yexplicit-nulls`, `A | Null` is also correct. When `A` is a
nullable type, `A | Null` is equivalent to `A`.
@soronpo
Copy link
Copy Markdown
Contributor

soronpo commented May 9, 2026

FYI, this causes a regression for ~10 projects in the OpenCB: #26026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants