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

Casting u128::MAX to f32 is undefined #41799

Closed
est31 opened this issue May 7, 2017 · 22 comments · Fixed by #45900
Closed

Casting u128::MAX to f32 is undefined #41799

est31 opened this issue May 7, 2017 · 22 comments · Fixed by #45900
Labels
B-unstable Blocker: Implemented in the nightly compiler and unstable. C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness P-medium Medium priority T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@est31
Copy link
Member

est31 commented May 7, 2017

Given this code (playpen):

#![feature(i128_type,i128)]

fn cast(src: f32) -> Result<u128, &'static str> {
    use std::{u128, f32};

    Err(if src != src {
        "NaN"
    } else if src < u128::MIN as f32 {
        "Underflow"
    } else if src > u128::MAX as f32 {
        "Overflow"
    } else {
        return Ok(src as u128);
    })
}
fn main() {
    let f = 2742605.0f32;
    println!("{:?}", cast(f));
}

It should print Ok(2742605). In release mode, that happens, but in debug mode, it prints Err("Overflow").

The value is mostly irrelevant, it would also work for 42.0.

cc @nagisa
cc #35118 tracking issue

@est31 est31 changed the title i128 casting issue i128 casting issue on debug May 7, 2017
@nagisa
Copy link
Member

nagisa commented May 7, 2017

u128::MAX as f32 is undefined. It ends up being as float undef in LLVM. This issue is in a similar vein as the float to integer casts. All we really need is a decision on how to handle this. For u128->f32 it seems pretty obvious to simply produce an infinity.

@ollie27
Copy link
Member

ollie27 commented May 7, 2017

If it is currently UB then maybe #10185 needs to be reopened.

@nagisa
Copy link
Member

nagisa commented May 7, 2017

u64::MAX to f32 converts just fine.

@ghost
Copy link

ghost commented May 7, 2017

Yes, because the maximum value of a u64 fits in a f32 fine.

@nagisa nagisa changed the title i128 casting issue on debug Casting i128 to f32 is undefined May 8, 2017
@nagisa nagisa added I-wrong T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels May 8, 2017
@nagisa nagisa changed the title Casting i128 to f32 is undefined Casting u128::MAX to f32 is undefined May 8, 2017
@nagisa nagisa added the T-lang Relevant to the language team, which will review and decide on the PR/issue. label May 8, 2017
@nagisa
Copy link
Member

nagisa commented May 8, 2017

Cross-linking #10184.

@ollie27
Copy link
Member

ollie27 commented May 8, 2017

Don't forget #15536.

@isislovecruft
Copy link

Hi! Sorry if this is a stupid suggestion… but as someone who uses u128 (in several cryptographic libraries) and doesn't use floats, I wonder how many u128 users actually need safe casting to floats? Is this something one would do in graphics development? If there isn't much use for it, perhaps a simple fix would be to disallow 42u128 as f32 casts, i.e. they should have to first cast to u64.

@nodakai
Copy link
Contributor

nodakai commented May 13, 2017

If a new as-cast rule is going to be introduced, please make it generic so that it can be naturally extensible to something like mini-floats or GMP types

@nagisa
Copy link
Member

nagisa commented May 13, 2017

@isislovecruft Rust is a general purpose programming language and it already has a syntax for doing conversions that one could expect to work on all the primitive types, so it not supporting as-casts for only this particular conversion would make the language inconsistent.

Furthermore, consider a code like this:

macro_rules! foo {
    ($x: ident) = {
        /* some stuff */
        $x as f32 
        /* rest of the stuff */
     }
}

Now this macro works just fine for every single integer and float combo. Forbidding it for i/u128 would extend the discrepancy to macros as well. I hope that’s enough of demonstration of a need to keep functionality consistent.

@isislovecruft
Copy link

@nagisa The consistency issue, esp. w.r.t. macros, makes perfect sense. Thanks for taking the time to point that out.

@kennytm
Copy link
Member

kennytm commented May 15, 2017

u128 as f32 currently generates this LLVM IR:

%1 = uitofp i128 %0 to float

which, on x86_64, i686 and aarch64, becomes a call to the compiler_rt function __floatuntisf

call	__floatuntisf@PLT

Directly calling the function, instead of generating the uitofp instruction, seems to be able to hide the UB?

The current implementation casts u128::MAX to +∞.

@nagisa
Copy link
Member

nagisa commented May 15, 2017

It makes no sense to do that over just adding the relevant branches into the IR, because it inhibits pretty much every optimisation with the values involved.

@kennytm
Copy link
Member

kennytm commented May 15, 2017

@nagisa Yes we could do this

    if a >= 0xffffff80000000000000000000000000_u128 {
        f32::INFINITY
    } else {
        a as f32
    }

producing

  %1 = icmp ugt i128 %0, -10141204801825835211973625643009
  %2 = uitofp i128 %0 to float
  %_0.0 = select i1 %1, float 0x7FF0000000000000, float %2

producing (unfortunately the function will always be called, and the cold branch check remains)

	mov	rbx, rsi
	shr	rbx, 39
	call	__floatuntisf@PLT
	cmp	rbx, 33554430       ; 0x1fffffe
	jbe	.LBB0_2
	movss	xmm0, dword ptr [rip + .LCPI0_0]
.LBB0_2:
	ret

.LCPI0_0:
	.long	2139095040     ; 0x7f800000

@kennytm
Copy link
Member

kennytm commented May 15, 2017

u128::MAX as f32 gives undef during LLVM-level constant-folding with LLVMConstUIToFP. Could the range check placed there as a first step, to prevent a naked undef being generated?


Or just add a lint/error if MirConstContext::trans() produces an undef Const? Or wait for miri?

@nagisa
Copy link
Member

nagisa commented May 15, 2017

Could the range check placed there as a first step, to prevent a naked undef being generated?

LLVM will want some solution that’s more general and we want to avoid having custom patches in our LLVM, so I do not think that would work.

Or just add a lint/error if MirConstContext::trans() produces an undef Const?

Interesting option, but does not help with code like

target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

define internal i128 @foo() unnamed_addr {
start:
  ret i128 -1
}

define float @bar() unnamed_addr {
start:
  %0 = call i128 @foo()
  %1 = uitofp i128 %0 to float
  ret float %1
}

which is still very UB, even if it does not produce a literal undef at the moment after the optimisations.

@sunfishcode
Copy link
Member

Ignoring LLVM and C/C++ for the moment, IEEE 754 section 5.4.1 "Arithmetic operations" defines convertFromInt and says:

If the converted value is not exactly representable in the destination format, the result is determined according to the applicable rounding-direction attribute [...].

Integer to floating-point conversions typically use "to nearest" rounding. IEEE 754 section 4.3.1 "Rounding-direction attributes to nearest" defines "no nearest" and says:

[...] an infinitely precise result with magnitude at least bemax(b − ½b1−p) shall round to ∞ with no change in sign [...].

u128::MAX fits that condition when converted to f32. So by IEEE 754 rules, for what that's worth, the result is defined and has the value ∞.

@bstrie bstrie added I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness I-nominated labels Jul 26, 2017
@Mark-Simulacrum Mark-Simulacrum added the C-bug Category: This is a bug. label Jul 27, 2017
@aturon aturon added P-medium Medium priority and removed I-nominated labels Aug 3, 2017
@aturon
Copy link
Member

aturon commented Aug 3, 2017

Marking P-medium to match other similar bugs.

alexcrichton added a commit to alexcrichton/rust that referenced this issue Sep 2, 2017
These don't appear to have a stable ABI as noted in rust-lang#41799 and the work in
compiler-builtins definitely seems to be confirming it!
bors added a commit that referenced this issue Sep 3, 2017
rustc: Flag {i,u}128 as unsafe for FFI

These don't appear to have a stable ABI as noted in #41799 and the work in
compiler-builtins definitely seems to be confirming it!
@bstrie bstrie added the B-unstable Blocker: Implemented in the nightly compiler and unstable. label Sep 20, 2017
bors added a commit that referenced this issue Nov 8, 2017
Saturating casts between integers and floats

Introduces a new flag, `-Z saturating-float-casts`, which makes code generation for int->float and float->int casts safe (`undef`-free), implementing [the saturating semantics laid out by](#10184 (comment)) @jorendorff for float->int casts and overflowing to infinity for `u128::MAX` -> `f32`.
Constant evaluation in trans was changed to behave like HIR const eval already did, i.e., saturate for u128->f32 and report an error for problematic float->int casts.

Many thanks to @eddyb, whose APFloat port simplified many parts of this patch, and made HIR constant evaluation recognize dangerous float casts as mentioned above.
Also thanks to @ActuallyaDeviloper whose branchless implementation served as inspiration for this implementation.

cc #10184 #41799
fixes #45134
@hanna-kruppe
Copy link
Contributor

hanna-kruppe commented Nov 8, 2017

#45205 has been merged, so there's now an opt-in solution to this issue (-Z saturating-float-casts). Presumably we'll want to make this behavior the default at some point in the future – does anyone have opinions on when that should be and what prerequisites there are (if any)?

@est31
Copy link
Member Author

est31 commented Nov 8, 2017

@rkruppe I'd say we should do a public call, asking people to test the feature and report back. Then if there are no major issues after idk, 2-3 weeks (some people only read the weekly newsletter), we could flip the defaults and allow an opt out.

It could stay this way on the nightly channel and if there continue to be no issues, we can let it ride the trains and include it in the next beta to be added to the next stable (without an opt out, as it would require a -C option and you won't be able to remove the opt-out again). If we hear about issues while the feature is in the beta channel, we can disable it on the beta channel as well.

@hanna-kruppe
Copy link
Contributor

@est31 Huh, that's a bit more scrutiny than I would have expected. To be clear, in the context of this issue I am only talking about u128 -> f32 casts, the float -> int direction covered by #10184 can and should be rolled out independently. With that in mind, do you think the change to u128 -> f32 casts is more likely to cause regressions than the average bug fix?

@hanna-kruppe
Copy link
Contributor

I've file a PR (#45900) to just make the u128->f32 behavior the default, refocusing the -Z flag to just cover float->int casts. As with any other change, we'll have up to six weeks in nightly and six weeks of beta to find and fix any regressions there may be.

hanna-kruppe pushed a commit to hanna-kruppe/rust that referenced this issue Nov 10, 2017
... rather than being gated by -Z saturating-float-casts.
There are several reasons for this:

1. Const eval already implements this behavior.
2. Unlike with float->int casts, this behavior is uncontroversially the
right behavior and it is not as performance critical. Thus there is no
particular need to make the bug fix for u128->f32 casts opt-in.
3. Having two orthogonal features under one flag is silly, and never
should have happened in the first place.
4. Benchmarking float->int casts with the -Z flag should not pick up
performance changes due to the u128->f32 casts (assuming there are any).

Fixes rust-lang#41799
bors added a commit that referenced this issue Nov 12, 2017
…lexcrichton

Make saturating u128 -> f32 casts the default behavior

... rather than being gated by `-Z saturating-float-casts`. There are several reasons for this:

1. Const eval already implements this behavior.
2. Unlike with float->int casts, this behavior is uncontroversially the right behavior and it is not as performance critical. Thus there is no particular need to make the bug fix for u128->f32 casts opt-in.
3. Having two orthogonal features under one flag is silly, and never should have happened in the first place.
4. Benchmarking float->int casts with the -Z flag should not pick up performance changes due to the u128->f32 casts (assuming there are any).

Fixes #41799
@nikic
Copy link
Contributor

nikic commented Jun 28, 2018

FYI overflowing s/uitofp casts are no longer considered undefined as of https://reviews.llvm.org/D47807. So we should be able to drop the extra range checking code somewhere in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
B-unstable Blocker: Implemented in the nightly compiler and unstable. C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness P-medium Medium priority T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.