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 {f16,f32,f64,f128}::div_euclid #133755

Closed
wants to merge 1 commit into from

Conversation

traviscross
Copy link
Contributor

@traviscross traviscross commented Dec 2, 2024

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Dec 2, 2024
@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

Comment on lines +319 to +326
if r < 0.0 {
return if rhs > 0.0 { r + rhs } else { r - rhs };
}
r
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: is there a reason this has a return instead of

Suggested change
if r < 0.0 {
return if rhs > 0.0 { r + rhs } else { r - rhs };
}
r
if r < 0.0 {
if rhs > 0.0 { r + rhs } else { r - rhs }
} else {
r
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For consistency (both stylistic and as it makes the mathematically analogy more clear), I followed the style of the existing div_euclid implementation:

    pub fn div_euclid(self, rhs: f64) -> f64 {
        let q = (self / rhs).trunc();
        if self % rhs < 0.0 {
            return if rhs > 0.0 { q - 1.0 } else { q + 1.0 };
        }
        q
    }

@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

The current implementation of `rem_euclid` for floating point numbers
violates the invariant, stated in the documentation, that:

```rust
a.rem_euclid(b) ~= a - b * a.div_euclid(b)
```

In a 2001 paper[^1], Daan Leijen (who notably later created the Koka
programming language) provides the correct formulation of this (and
of `div_euclid`) in "Algorithm E":

    q_E = q_T - I
    r_E = r_T + I * b
    where
        I = if r_T >= 0 then 0 else if b > 0 then 1 else -1
        q_T = trunc(a / b)
        r_T = a - b * q_T
        a is a dividend, a real number
        b is a divisor, a real number

In section 1.5 of the paper, he gives a proof of correctness.

To encode this in Rust, we might think to use `a % b` for
`r_T` (remainder of truncated division).  After all, we document[^2]
that `a % b` is computed as...

```rust
a - b * (a / b).trunc()
```

However, as it turns out, we do not currently compute `Rem` in this
way, as can be seen trivially with:

```rust
let (x, y) = (11f64, 1.1f64);
assert_eq!(x - (x / y).trunc() * y, x % y); //~ PANIC
```

Therefore, we've encoded `r_T` in the literal way.

As we know the maxim, from Knuth, to...

> Beware of bugs in the above code; I have only proved it correct, not
> tried it.

...we have additionally subjected our encoding of this formulation to
fuzzing.  It seems to hold up against the desired invariants.

This is of course a breaking change.  But the current implementation
is broken, and libs-api has signaled openness to fixing it.

[^1]: "Division and Modulus for Computer Scientists",
    Daan Leijen, 2001,
    <https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/divmodnote-letter.pdf>

[^2]: https://doc.rust-lang.org/1.83.0/std/ops/trait.Rem.html#impl-Rem-for-f64
@traviscross
Copy link
Contributor Author

In talking with @ehuss, he pointed out RFC 2169 where div_euclid and rem_euclid were proposed.

In looking at that, interestingly, the definition given for div_euclid matches exactly what's in the standard library today. And more interestingly, the definition that it gives for rem_euclid...

fn mod_euc(self, rhs: f64) -> f64 {
    let r = self % rhs;
    if r < 0.0 {
        return if rhs > 0.0 { r + rhs } else { r - rhs }
    }
    r
}

...matches exactly the original implementation in this PR (which, due to #133758, is now being adjusted slightly).

I wonder what the history was in moving from the definition in the RFC, which seems more obvious and is in line with the 2001 Leijen paper, to the one now in the standard library.

@traviscross
Copy link
Contributor Author

traviscross commented Dec 3, 2024

Looking at the git history, the current implementation goes all the way back to the original PR:

Looking through the discussion there, and also in the RFC thread and the tracking issue...

...I don't immediately see any discussion about the mathematics to justify the change from the RFC specification to the current implementation, which is:

pub fn rem_euclid(self, rhs: f64) -> f64 {
    let r = self % rhs;
    if r < 0.0 { r + rhs.abs() } else { r }
}

@quaternic
Copy link

Because,

  • r - rhs is equivalent to r + (-rhs)
  • rhs.abs() is equivalent to if rhs.is_sign_positive() { rhs } else { -rhs }

So the changed expression would only differ when rhs is +NaN or 0.0, which aren't possible within the r < 0.0 case. That is, the two implementations are exactly equivalent, I believe.

@tczajka
Copy link

tczajka commented Dec 8, 2024

This PR should be rejected. rem_euclid is correct, it's div_euclid that is incorrect, as discussed in #107904.

@traviscross
Copy link
Contributor Author

traviscross commented Dec 8, 2024

It's a draft, so there's nothing to reject at this point. And yes, having analyzed it, I agree that rem_euclid is correct.

@traviscross traviscross changed the title Fix {f16,f32,f64,f128}::rem_euclid Fix {f16,f32,f64,f128}::div_euclid Dec 8, 2024
@traviscross
Copy link
Contributor Author

traviscross commented Dec 8, 2024

(The state of this draft PR lags the current state of my analysis. I have a hypothesis for a fix for div_euclid that I'm currently testing.)

@traviscross
Copy link
Contributor Author

Closing in favor of:

(...so as to fix the branch name.)

@traviscross traviscross closed this Dec 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants