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

Avoid incorrect simplifications when updating bounds in the constraint #16410

Merged
merged 1 commit into from
Dec 1, 2022

Conversation

smarter
Copy link
Member

@smarter smarter commented Nov 24, 2022

When combining an old and a new bound, we use Type#&/Type#| which perform simplifications. This is usually fine, but if the new bounds refer to the parameter currently being updated, we can run into cyclic reasoning issues which make the simplifications invalid after the update.

We already have logic for handling self-references in parameter bounds: updateEntry calls ensureNonCyclic which sanitizes the type, but at this point the simplifications have already occured. This commit simply moves the logic out of updateEntry so that we can sanitize the new bounds before simplification.

More precisely, we rename ensureNonCyclic to validBoundsFor which calls validBoundFor (singular). Both are used to sanitize bounds where needed in addOneBound and unify.

Since all calls to updateEntry now have sanitized bounds, we no longer need to sanitize them in updateEntry itself, we document this change by adding a pre-condition to updateEntry.

For the record, here's how ConstraintsTest#validBoundsInit used to fail. It defines a method:

def foo[S >: T <: T | Int, T <: String]: Any

Before this commit, when foo was added to the current constraints, the constraint S <: T | Int was propagated to the lower bound T of S. The updated upper bound of T was thus set to:

String & (T | Int)

But because Type#& performs simplifications, this became

T | (String & Int)

by relying on the fact that at this point, T <: String. But in fact this simplified bound no longer ensures that T <: String! The self-reference was then replaced by Any in OrderingConstraint#ensureNonCyclic. After this commit, the problematic simplification no longer occurs since the new T | Int is sanitized to Any before being intersected with the old bound.

@dwijnand
Copy link
Member

The updated upper bound of T was thus set to String & (T | Int). But because Type#& performs simplifications, this became T | (String & Int) by relying on the fact that at this point, T <: String. But in fact this simplified bound no longer ensures that T <: String!

That's brilliant. 😄 Amazing sleuthing.

Copy link
Member

@dwijnand dwijnand left a comment

Choose a reason for hiding this comment

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

LGTM. Took me too long to see the difference between validBoundFor and validBoundsFor, probably a "skill issue" on my side.

@@ -88,6 +88,8 @@ abstract class Constraint extends Showable {
* - Another type, indicating a solution for the parameter
*
* @pre `this contains param`.
* @pre `tp` does not contain top-level references to `param`
* (see `ensureNonCyclic`)
Copy link
Contributor

Choose a reason for hiding this comment

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

ensureNonCyclic is no longer there.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should probably be validBounds. Also: Should there be a way to test the precondition if a Config flag is set? Or can you argue statically that the precondition is always true? If yes, maybe add a note to this effect.

Copy link
Member Author

Choose a reason for hiding this comment

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

Should there be a way to test the precondition if a Config flag is set?

I believe this is already checked a posteriori by the call to checkWellFormed() when Config.checkConstraintsNonCyclic is set.

@odersky odersky assigned smarter and unassigned odersky Nov 28, 2022
@odersky
Copy link
Contributor

odersky commented Nov 28, 2022

Otherwise LGTM

When combining an old and a new bound, we use `Type#&`/`Type#|` which perform
simplifications. This is usually fine, but if the new bounds refer to the
parameter currently being updated, we can run into cyclic reasoning issues which
make the simplifications invalid after the update.

We already have logic for handling self-references in parameter bounds:
`updateEntry` calls `ensureNonCyclic` which sanitizes the type, but at this point
the simplifications have already occured. This commit simply moves the logic out
of `updateEntry` so that we can sanitize the new bounds before simplification.

More precisely, we rename `ensureNonCyclic` to `validBoundsFor` which calls
`validBoundFor`. Both are used to sanitize bounds where needed in `addOneBound`
and `unify`.

Since all calls to `updateEntry` now have sanitized bounds, we no longer need to
sanitize them in `updateEntry` itself, we document this change by adding a
pre-condition to `updateEntry`.

For the record, here's how `ConstraintsTest#validBoundsInit` used to fail.
It defines a method:

    def foo[S >: T <: T | Int, T <: String]: Any

Before this commit, when `foo` was added to the current constraints, the
constraint `S <: T | Int` was propagated to the lower bound `T` of `S`. The
updated upper bound of `T` was thus set to:

    String & (T | Int)

But because `Type#&` performs simplifications, this became

    T | (String & Int)

by relying on the fact that at this point, `T <: String`. But in fact this
simplified bound no longer ensures that `T <: String`! The self-reference was
then replaced by `Any` in `OrderingConstraint#ensureNonCyclic`.
After this commit, the problematic simplification no longer occurs since the
new `T | Int` is sanitized to `Any` before being intersected with the old bound.
@smarter smarter merged commit e842810 into scala:main Dec 1, 2022
@smarter smarter deleted the safe-add-bound-3 branch December 1, 2022 08:49
@Kordyjan Kordyjan added this to the 3.3.0 milestone Aug 1, 2023
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.

4 participants