Skip to content

Add integer to f16 conversions - #1261

Open
npmccallum wants to merge 3 commits into
rust-lang:mainfrom
npmccallum:f16-int-conv-pr
Open

Add integer to f16 conversions#1261
npmccallum wants to merge 3 commits into
rust-lang:mainfrom
npmccallum:f16-int-conv-pr

Conversation

@npmccallum

@npmccallum npmccallum commented Aug 8, 2026

Copy link
Copy Markdown

Add integer to f16 conversions

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 (they are not in LLVM's compiler-rt either). Code doing
int as f16 therefore fails to link on any target without another provider of
these 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/rust subtree.

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 that
question, 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:

  1. test: exercise 128-bit integer to f16 link paths — adds the six
    integer-to-f16 cases to builtins-test-intrinsics, which links every
    intrinsic 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.

  2. feat(float): add integer to f16 conversions — implements all six
    routines 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.

  3. test(float): validate int-to-f16 rounding via apfloat — exercises the
    new routines and fixes the rounding oracle so f16 can 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-intrinsics is 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_f rounding check reconstructed an error bracket by casting the float
result and its neighbours back to integers. That 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 every correctly-overflowing conversion was
flagged 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 — 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. This replaces the bracket for all
widths, not just f16, and is the minimal change that lets the f16 tests be
correct rather than a special-case skip.

Verification

  • Commit 1 alone fails to link (undefined symbol: __floatuntihf,
    __floattihf); commit 2 links cleanly on both glibc and musl.
  • All 41 conv tests pass, including the six new f16 cases; f32/f64/f128 are
    unchanged.
  • The old bracket oracle, applied to the new f16 tests, fails all six with
    the saturation signature (infu32::MAX); the apfloat oracle passes them.
  • rustfmt and clippy clean under -Dwarnings.
  • The f16 <-> int system routines are gated off on targets that lack them
    (apple, windows), matching the existing no-sys-* handling.

The ti variants carry #[cfg_attr(target_os = "uefi", unadjusted_on_win64)]
to match their sf/df siblings.

`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.
@npmccallum
npmccallum force-pushed the f16-int-conv-pr branch 2 times, most recently from 2cc0b35 to 366b97e Compare August 8, 2026 18:50

@tgross35 tgross35 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

View changes since this review

Comment on lines +44 to 70
// 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,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@quaternic would you mind reviewing this portion?

tgross35 and others added 2 commits August 10, 2026 08:52
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>
@npmccallum

Copy link
Copy Markdown
Author

Thanks @tgross35, and thanks for splitting off df3c10a — I did try to take it directly. The snag is that it's the Nov-2024 version, so it conflicts against current main in conv.rs, and its test block uses the old feature = "no-sys-f16-int-convert" cfg (now no_sys_f16_int_convert via build.rs) plus the original bracket oracle — so taking it verbatim would either reintroduce the conflict or leave an intermediate commit that's red on f16, which I wanted to avoid for a clean history.

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 df3c10a are dropping the unrelated reordering churn and adding #[cfg_attr(target_os = "uefi", unadjusted_on_win64)] on the ti→hf variants for parity with their sf/df siblings. The rounding oracle follows Juho's diagnosis — apfloat directly instead of the f→i bracket.

Happy to adjust the attribution however you'd prefer. Thanks again for the original work here.

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.

2 participants