Add integer to f16 conversions - #1261
Conversation
`builtins-test-intrinsics` links every intrinsic LLVM may emit, so a missing symbol surfaces as a link failure rather than silently. It did not cover integer-to-`f16` conversions, which LLVM emits for 128-bit operands: `x as f16` where `x` is a `u128`/`i128` requires `__floatuntihf` / `__floattihf`, and compiler-builtins does not define them. Add all six integer-to-`f16` cases (`i32`/`i64`/`i128` and unsigned). This intentionally fails to link on targets without a fallback provider (e.g. musl, macos) until the following commit adds the routines, matching the failure real user code already hits. On glibc the link only succeeds because libgcc happens to supply the symbols.
2cc0b35 to
366b97e
Compare
There was a problem hiding this comment.
Thank you for picking this up! I think the test bits are fine but Juho understood the issue better than me.
Totally fine to supersede my PR but mind picking df3c10a directly? I split off the conflict-y bit.
| // Check rounding against `rustc_apfloat`, a correctly-rounded oracle, | ||
| // by comparing bits directly. The previous heuristic instead cast the | ||
| // float result and its neighbours back to integers and compared error | ||
| // brackets; that saturates `inf` to `iN::MAX` and so misfires for `f16`, | ||
| // where any overflowing conversion correctly yields `inf` (for example | ||
| // `2147483648 as f16`) yet the round-trip flagged it as mis-rounded. | ||
| // | ||
| // Gated on the system routine being available; otherwise `f0` is already | ||
| // the apfloat result and the native comparison below covers it. | ||
| #[cfg($sys_available)] { | ||
| // This makes sure that the conversion produced the best rounding possible, and does | ||
| // this independent of `x as $into` rounding correctly. | ||
| // This assumes that float to integer conversion is correct. | ||
| let y_minus_ulp = <$f_ty>::from_bits(f1.to_bits().wrapping_sub(1)) as $i_ty; | ||
| let y = f1 as $i_ty; | ||
| let y_plus_ulp = <$f_ty>::from_bits(f1.to_bits().wrapping_add(1)) as $i_ty; | ||
| let error_minus = <$i_ty as Int>::abs_diff(y_minus_ulp, x); | ||
| let error = <$i_ty as Int>::abs_diff(y, x); | ||
| let error_plus = <$i_ty as Int>::abs_diff(y_plus_ulp, x); | ||
|
|
||
| // The first two conditions check that none of the two closest float values are | ||
| // strictly closer in representation to `x`. The second makes sure that rounding is | ||
| // towards even significand if two float values are equally close to the integer. | ||
| if error_minus < error | ||
| || error_plus < error | ||
| || ((error_minus == error || error_plus == error) | ||
| && ((f0.to_bits() & 1) != 0)) | ||
| { | ||
| type ApFloat = rustc_apfloat::ieee::$apfloat_ty; | ||
|
|
||
| let expected = if <$i_ty>::SIGNED { | ||
| ApFloat::from_i128(x.try_into().unwrap()).value | ||
| } else { | ||
| ApFloat::from_u128(x.try_into().unwrap()).value | ||
| } | ||
| .to_bits(); | ||
|
|
||
| if u128::from(f1.to_bits()) != expected { | ||
| panic!( | ||
| "incorrect rounding by {}({}): {}, ({}, {}, {}), errors ({}, {}, {})", | ||
| "incorrect conversion by {}({}): apfloat {:#x}, builtins {:#x}", | ||
| stringify!($fn), | ||
| x, | ||
| expected, | ||
| f1.to_bits(), | ||
| y_minus_ulp, | ||
| y, | ||
| y_plus_ulp, | ||
| error_minus, | ||
| error, | ||
| error_plus, | ||
| ); |
There was a problem hiding this comment.
@quaternic would you mind reviewing this portion?
LLVM emits `__float{,un}sihf`, `__float{,un}dihf` and `__float{,un}tihf`
when converting 32-, 64- and 128-bit integers to `f16`, but
compiler-builtins never provided them. Code performing `int as f16` fails
to link on any target without another provider of these symbols (for
example musl or macos); glibc targets only link because libgcc supplies
them. These routines are not in LLVM's compiler-rt either.
Implement all six by reusing the existing left-align / round-to-even
machinery. Unlike wider float types, any integer can overflow `f16`'s
exponent range, so each routine clamps to infinity once the rounded
exponent saturates.
Co-authored-by: Nathaniel McCallum <nathaniel.mccallum@amd.com>
Exercise the new integer-to-`f16` routines and check their rounding, but first replace the rounding oracle so `f16` can be checked at all. The `i_to_f` check reconstructed an error bracket by casting the float result and its neighbours back to integers. Casting a float back to an integer saturates `inf` to `iN::MAX`, so for `f16` the bracket is meaningless: `f16::MAX` (65504) is far closer to 0 than any `iN::MAX` for N >= 32, and any overflowing conversion (correctly producing `inf`) was flagged as mis-rounded. No wider float type hits this, which is why the bracket went unchallenged. Compare the builtin's result directly against `rustc_apfloat`, a correctly-rounded oracle already used on the fallback path. Overflow to infinity falls out naturally: apfloat rounds with an unbounded exponent and applies the overflow threshold afterwards, so the expected bit pattern is `inf` exactly when the builtin must return it. The check stays gated on the system routine being available. Co-authored-by: Trevor Gross <tmgross@umich.edu>
366b97e to
b8d7db5
Compare
|
Thanks @tgross35, and thanks for splitting off What I've done instead: kept the equivalent implementation but set you as the author of the conversions commit (I'm listed as co-author), so the primary credit is yours. It's the same six routines; the only deltas from Happy to adjust the attribution however you'd prefer. Thanks again for the original work here. |
Add integer to
f16conversionsLLVM emits
__float{,un}sihf,__float{,un}dihfand__float{,un}tihfwhen converting 32-, 64- and 128-bit integers to
f16, but compiler-builtinsnever provided them (they are not in LLVM's compiler-rt either). Code doing
int as f16therefore fails to link on any target without another provider ofthese symbols — for example musl or macos. glibc targets only link because
libgcc happens to supply them. This is the compiler-builtins side of
rust-lang/rust#132614 (part of the tracking issue rust-lang/rust#116909); it
will resolve that issue once synced into the
rust-lang/rustsubtree.Credit
The conversion routines here are Trevor Gross's work from #729, which has been
open since 2024-11. That PR was complete on the implementation side but stalled
on a single question about its test oracle. This PR carries his implementation
forward with him listed as
Co-authored-by:, proposes an answer to thatquestion, and adds the missing link coverage. Thanks @tgross35 for the original
implementation — this is intended to supersede #729, and I'm happy to fold it
back into that PR instead if you'd prefer.
Approach
Three commits, each doing one thing:
test: exercise 128-bit integer to f16 link paths— adds the sixinteger-to-
f16cases tobuiltins-test-intrinsics, which links everyintrinsic LLVM may emit. This commit intentionally fails to link (the
symbols don't exist yet), reproducing the exact failure real user code hits
on musl/macos.
feat(float): add integer to f16 conversions— implements all sixroutines by reusing the existing left-align / round-to-even machinery.
Unlike wider float types, any integer can overflow
f16's exponent range,so each routine clamps to infinity once the rounded exponent saturates.
This turns commit 1 green.
test(float): validate int-to-f16 rounding via apfloat— exercises thenew routines and fixes the rounding oracle so
f16can be checked at all.Commit 1 intentionally fails to link on its own. I've kept it that way because
it documents the defect as a bisectable red→green and mirrors what user code
currently hits — but
builtins-test-intrinsicsis excluded from the workspace,so the library and default members still build at that commit. If the project
would rather avoid a non-building commit in history, I'm happy to reorder so the
implementation lands first, keeping the link test as its own commit but green.
The oracle change (the question #729 stalled on)
The
i_to_frounding check reconstructed an error bracket by casting the floatresult and its neighbours back to integers. That saturates
inftoiN::MAX,so for
f16the bracket is meaningless:f16::MAX(65504) is far closer to 0than any
iN::MAXfor N ≥ 32, and every correctly-overflowing conversion wasflagged as mis-rounded. No wider float type hits this, which is why the bracket
went unchallenged.
This compares the builtin's result directly against
rustc_apfloat— acorrectly-rounded oracle already used on the fallback path. Overflow to
infinity falls out naturally: apfloat rounds with an unbounded exponent and
applies the overflow threshold afterwards, so the expected bit pattern is
infexactly when the builtin must return it. This replaces the bracket for all
widths, not just
f16, and is the minimal change that lets thef16tests becorrect rather than a special-case skip.
Verification
undefined symbol: __floatuntihf,__floattihf); commit 2 links cleanly on both glibc and musl.convtests pass, including the six newf16cases; f32/f64/f128 areunchanged.
f16tests, fails all six withthe saturation signature (
inf→u32::MAX); the apfloat oracle passes them.rustfmtandclippyclean under-Dwarnings.f16 <-> intsystem routines are gated off on targets that lack them(apple, windows), matching the existing
no-sys-*handling.The
tivariants carry#[cfg_attr(target_os = "uefi", unadjusted_on_win64)]to match their
sf/dfsiblings.