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

Error in derived PartialOrd for structs based on types with incomparable elements. #49650

Closed
0ndorio opened this issue Apr 4, 2018 · 14 comments · Fixed by #49881
Closed

Error in derived PartialOrd for structs based on types with incomparable elements. #49650

0ndorio opened this issue Apr 4, 2018 · 14 comments · Fixed by #49881
Assignees
Labels
C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@0ndorio
Copy link

0ndorio commented Apr 4, 2018

If a type deriving PartialOrd wraps another type which contains incomparable elements - and therefore only defines an implementation of PartialOrd and not a Ord - the derived implementations of the le and ge functions are flawed.

The leads to a state where the internal type returns false as result of the expressions a <= b and b >= a for two incomparable elements a and b while the wrapper returns true. [playground]

If we look into the attached output of cargo-expand it becomes clear that both implementations ignore the the underlying type only implements PartialOrd and may contain incomparable elements which can't be checked by (a < b) || !(b < a) or (a > b) || !(a > b).

Expanded: Less or Equal

    #[inline]
    fn le(&self, __arg_0: &StructWithoutOrd) -> bool {
        match *__arg_0 {
            StructWithoutOrd(ref __self_1_0) => match *self {
                StructWithoutOrd(ref __self_0_0) => {
                    (*__self_0_0) < (*__self_1_0) || !((*__self_1_0) < (*__self_0_0)) && true
                }   
            },  
        }   
    }

Expanded: Greater or Equal

    #[inline]
    fn ge(&self, __arg_0: &StructWithoutOrd) -> bool {
        match *__arg_0 {
            StructWithoutOrd(ref __self_1_0) => match *self {
                StructWithoutOrd(ref __self_0_0) => {
                    (*__self_0_0) > (*__self_1_0) || !((*__self_1_0) > (*__self_0_0)) && true
                }   
            },  
        }   
    }
@pietroalbini pietroalbini added T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. C-bug Category: This is a bug. labels Apr 5, 2018
@leoyvens
Copy link
Contributor

leoyvens commented Apr 5, 2018

This is very weird because it's inconsistent with the behavior of PartialOrd for floats. This simplified case will print false :

#[derive(PartialOrd, PartialEq)]
struct MyFloat(f64);
fn main() {
    println!("{}", (0.0 / 0.0 >= 0.0) == (MyFloat(0.0 / 0.0) >= MyFloat(0.0)))
}

Which is highly inconsistent. This would be a difficult breaking change to make, perhaps not even worth fixing.

Edit: I asked on IRC and it seems this issue is somewhat known, but I don't see any other bugs tracking it.

@eddyb
Copy link
Member

eddyb commented Apr 7, 2018

Why are comparison operators used more than once? This could cause exponential cost, at worst (in the depth of the value), especially for recursive structures.

cc @Manishearth @alexcrichton

@Manishearth
Copy link
Member

We should just forward to .le(), yes?

This isn't a breaking change, seems like a bug and it doesn't break compilation.

@nox
Copy link
Contributor

nox commented Apr 9, 2018

It's a change of behaviour that people may be relying on. Forwarding to .le doesn't help, given you need to do self.0 < other.0 || self.0 == other.0 && (self.1 < other.1 || …).

@0ndorio
Copy link
Author

0ndorio commented Apr 9, 2018

I can understand that a fix in such deep integrated code almost automatically involves a breaking change but I would still argue that its actually logically wrong and therefore should be fixed. A potential alternative way could be to add a new lint check which warns about using <= and => on types which only implement PartialOrd as the result of such a check may not lead to the expected result.

(A similar procedure was proposed by @oli-obk in rust-lang/rust-clippy#2626 for a related issue.)

@nox
Copy link
Contributor

nox commented Apr 9, 2018

@0ndorio Oh yeah definitely, I do want to fix this issue and consider the current behaviour a bug too, I'm just pointing out that it is observable and thus people may be relying on it. I'm certainly not hoping that people are relying on it.

@varkor
Copy link
Member

varkor commented Apr 11, 2018

@eddyb: you need both comparison operations per pair of values because there are three conditions (<, > or unrelated); as far as I can tell, you can't reuse any of the operations.

@eddyb
Copy link
Member

eddyb commented Apr 11, 2018

@varkor Can't you use partial_ord recursively?

@varkor
Copy link
Member

varkor commented Apr 11, 2018

@eddyb: Oh, that's a good point. I'll update the PR to use partial_cmp instead.

Actually, this is going to end up generating a ton of nested if expressions to use partial_cmp instead of the other operations, to make sure we only generate results lazily. It might still be a win for performance, but the generated code is going to be much messier.

@eddyb
Copy link
Member

eddyb commented Apr 11, 2018

@varkor What do you mean, can't we rely on the Ordering chaining functionality?

bors added a commit that referenced this issue Apr 15, 2018
Fix derive(PartialOrd) and optimise final field operation

```rust
// Before (`lt` on 2-field struct)
self.f1 < other.f1 || (!(other.f1 < self.f1) &&
(self.f2 < other.f2 || (!(other.f2 < self.f2) &&
(false)
))
)

// After
self.f1 < other.f1 || (!(other.f1 < self.f1) &&
self.f2 < other.f2
)

// Before (`le` on 2-field struct)
self.f1 < other.f1 || (!(other.f1 < self.f1) &&
(self.f2 < other.f2 || (!(other.f2 < self.f2) &&
(true)
))
)

// After
self.f1 < other.f1 || (self.f1 == other.f1 &&
self.f2 <= other.f2
)
```

(The big diff is mainly because of a past faulty rustfmt application that I corrected 😒)

Fixes #49650 and fixes #49505.
@varkor
Copy link
Member

varkor commented Apr 16, 2018

#49650 (comment) is addressed by #50011.

bors added a commit that referenced this issue May 15, 2018
Ensure derive(PartialOrd) is no longer accidentally exponential

Previously, two comparison operations would be generated for each field, each of which could delegate to another derived PartialOrd. Now we use ordering and optional chaining to ensure each pair of fields is only compared once, addressing #49650 (comment).

Closes #49505.

r? @Manishearth (sorry for changing it again so soon!)

Close #50755
@leoyvens
Copy link
Contributor

This issue is not totally fixed, only the performance issues were (thanks, @varkor!). Could someone reopen?

@varkor
Copy link
Member

varkor commented May 16, 2018

@leodasvacas: #49881 fixed this originally, and then #50011 followed up on it to fix the inefficiency.

@leoyvens
Copy link
Contributor

@varkor Ah great, nevermind me then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants