Skip to content
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

Fix #17467: Limit isNullable widening to stable TermRefs; remove under explicit nulls. #17470

Merged
merged 2 commits into from
Aug 4, 2023

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented May 11, 2023

The Scala language specification has a peculiar clause about the nullness of singleton types of the form path.type. It says that Null <:< path.type if the underlying type U of path is nullable itself.

The previous implementation of that rule was overly broad, as it indiscrimately widened all types. This resulted in problematic subtyping relationships like Null <:< "foo".

We do not widen anymore. Instead, we specifically handle TermRefs of stable members, which are how dotc represents singleton types. We also have a rule for Null <:< null, which is necessary for pattern matching exhaustivity to keep working in the presence of nulls.


Under explicit nulls, remove the rule Null <:< x.type.

The specified rule that Null <:< x.type when the underlying type U of x is nullable is dubious to begin with. Under explicit nulls, it becomes decidedly out of place. We now disable that rule under -Yexplicit-nulls.

@sjrd sjrd added the needs-minor-release This PR cannot be merged until the next minor release label May 11, 2023
@sjrd sjrd requested review from odersky and noti0na1 May 11, 2023 14:31
Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

Nicely confined changes!

@odersky odersky removed their assignment May 12, 2023
Copy link
Member

@noti0na1 noti0na1 left a comment

Choose a reason for hiding this comment

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

I agree this is definitely a more refined rule for singleton type!
BTW, is there any specific reason why

val c1: Null = null 
val c2: c1.type = null

is disallowed under explicit nulls?


val b1: String | Null = "foo"
val b2: b1.type = null // error
summon[Null, b1.type] // error
Copy link
Member

Choose a reason for hiding this comment

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

summon[Null <:< b1.type]

tests/explicit-nulls/neg/i17467.scala Show resolved Hide resolved
@sjrd
Copy link
Member Author

sjrd commented May 17, 2023

BTW, is there any specific reason why

val c1: Null = null 
val c2: c1.type = null

is disallowed under explicit nulls?

I added a comment with the test you suggested. The reason is that allowing this under explicit nulls would require a dedicated subtyping rule (both in spec and in implementation), and would provide marginal/debatable value. So IMO it is better to leave it out.

@sjrd sjrd requested a review from noti0na1 May 17, 2023 08:08
@sjrd sjrd assigned noti0na1 and unassigned sjrd May 17, 2023
@dwijnand dwijnand added this to the 3.4.0 milestone May 17, 2023
Copy link
Member

@noti0na1 noti0na1 left a comment

Choose a reason for hiding this comment

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

LGTM

The Scala language specification has a peculiar clause about the
nullness of singleton types of the form `path.type`. It says that
`Null <:< path.type` if the *underlying* type `U` of `path` is
nullable itself.

The previous implementation of that rule was overly broad, as it
indiscrimately widened all types. This resulted in problematic
subtyping relationships like `Null <:< "foo"`.

We do not widen anymore. Instead, we specifically handle `TermRef`s
of stable members, which are how dotc represents singleton types.
We also have a rule for `Null <:< null`, which is necessary for
pattern matching exhaustivity to keep working in the presence of
nulls.
The specified rule that `Null <:< x.type` when the underlying type
`U` of `x` is nullable is dubious to begin with. Under explicit
nulls, it becomes decidedly out of place. We now disable that rule
under `-Yexplicit-nulls`.
@sjrd
Copy link
Member Author

sjrd commented Aug 2, 2023

Rebased to be on top of #18284, since this was waiting for a minor release.

@Kordyjan Could you confirm that main is for 3.4.x now, and that therefore we can merge things that need a minor release?

@sjrd sjrd removed their assignment Aug 2, 2023
@Kordyjan
Copy link
Contributor

Kordyjan commented Aug 4, 2023

Yes. The Main is now 3.4. It can be safely merged.

@Kordyjan Kordyjan merged commit 73967d5 into scala:main Aug 4, 2023
17 checks passed
@Kordyjan Kordyjan deleted the nullable-singletons branch August 4, 2023 11:26
@WojciechMazur
Copy link
Contributor

WojciechMazur commented Aug 14, 2023

We've found 1 project (slick/slick) which failed in Open CB after merging this change. The failing code can be minimized to:

class Foo:
  def foo(flag: Boolean): this.type = if(flag) this else null 

Failing with:

-- [E007] Type Mismatch Error: /Users/wmazur/projects/dotty/bisect/main.scala:4:49 
4 |  def foo(): this.type = if(flag) this else null 
  |                                            ^^^^
  |Found:    Null
  |Required: (Foo.this : Foo)

The snippet above makes no sense to me so I don't treat it as a regression.

@sjrd
Copy link
Member Author

sjrd commented Aug 14, 2023

Indeed, that makes absolutely no sense, and is precisely the kind of soundness holes that this PR is meant to fix.

@WojciechMazur
Copy link
Contributor

WojciechMazur commented Aug 14, 2023

I've found 1 more project failing due this change simy4/coregex, which also should not be a regression:

type Matching[A >: Null <: String, Regex >: Null <: String with Singleton] <: A
def uuid: Matching[String, "[0-9a-f]"] = ??? // error Type argument ("[0-9a-f]" : String) does not conform to lower bound Null

The obious workaround it removing the null bound which allows to compile it:

type Matching[A <: String, Regex <: String with Singleton] <: A
def uuid: Matching[String, "[0-9a-f]"] = ???

@sjrd
Copy link
Member Author

sjrd commented Aug 14, 2023

We've found 1 project (slick/slick) which failed in Open CB after merging this change. The failing code can be minimized to:

Actually, after thinking about this more, I think that particular case, with the this.type, is in fact an unintended regression. this.type is a singleton type in spec language, but not a TermRef in the implementation (it's a ThisType instead), so I think we need to handle ThisType in the new logic.

mergify bot added a commit to slick/slick that referenced this pull request Aug 14, 2023
…4.x (#2801)

After merging scala/scala3#17470 in 3.4.x and
running Open Community Build we found out it is no longer compiling -
[build
logs](https://github.com/VirtusLab/community-build3/actions/runs/5836591202/job/15831389412).
However, the compiler team has decided that that reason for that is not
a new regression, but rather unsafe usage of `null` for method returning
`this.type` which can lead to unsoundness. Without further inspection of
code it's hard to decide if returning to null is unsafe for this use
case.

To allow for further testing this project in the Open Community Build I
propose a temporary workaround which explcitly cast null to `this.type`
to allow to pass compilation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:nullability needs-minor-release This PR cannot be merged until the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Null <:< "foo" and other problems with Null subtyping (also in -Yexplicit-nulls)
6 participants