Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions compiler/rustc_codegen_llvm/src/intrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,26 @@ impl<'ll, 'tcx> IntrinsicCallBuilderMethods<'tcx> for Builder<'_, 'll, 'tcx> {
)
}

sym::scalbn => {
let ty = args[0].layout.ty;
let ty::Float(f) = ty.kind() else {
span_bug!(
span,
"the `{}` intrinsic requires a floating-point argument, got {:?}",
name,
ty
);
};
let llty = self.type_float_from_ty(*f);

// For our floats (that use a radix of 2), scalbn corresponds to ldexp.
self.call_intrinsic(
"llvm.ldexp",
&[llty, self.type_i32()],
&args.iter().map(|arg| arg.immediate()).collect::<Vec<_>>(),
)
}

sym::raw_eq => {
use BackendRepr::*;
let tp_ty = fn_args.type_at(0);
Expand Down
42 changes: 40 additions & 2 deletions compiler/rustc_const_eval/src/interpret/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
sym::copysignf64 => self.float_copysign_intrinsic::<Double>(args, dest)?,
sym::copysignf128 => self.float_copysign_intrinsic::<Quad>(args, dest)?,

sym::fabs => {
sym::fabs | sym::ilogb => {
let arg = self.read_immediate(&args[0])?;
let ty::Float(float_ty) = arg.layout.ty.kind() else {
span_bug!(
Expand All @@ -636,6 +636,27 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
self.write_scalar(out_val, dest)?;
}

sym::scalbn => {
let arg = self.read_immediate(&args[0])?;
let exp = self.read_immediate(&args[1])?;

let ty::Float(float_ty) = arg.layout.ty.kind() else {
span_bug!(
self.cur_span(),
"non-float type for float intrinsic: {}",
arg.layout.ty,
);
};

let res = match float_ty {
FloatTy::F16 => self.scalbn_intrinsic::<Half>(arg, exp)?,
FloatTy::F32 => self.scalbn_intrinsic::<Single>(arg, exp)?,
FloatTy::F64 => self.scalbn_intrinsic::<Double>(arg, exp)?,
FloatTy::F128 => self.scalbn_intrinsic::<Quad>(arg, exp)?,
};
self.write_scalar(res, dest)?;
}

sym::floorf16 => self.float_round_intrinsic::<Half>(
args,
dest,
Expand Down Expand Up @@ -1219,12 +1240,29 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
{
let x: F = arg.to_scalar().to_float()?;
match name {
// bitwise, no NaN adjustments
// fabs and ilogb are both bitwise, no NaN adjustments.
sym::fabs => interp_ok(x.abs().into()),
sym::ilogb => interp_ok(Scalar::from_i32(x.ilogb())),
_ => bug!("not a unary float intrinsic: {}", name),
}
}

fn scalbn_intrinsic<F>(
&self,
arg: ImmTy<'tcx, M::Provenance>,
exp: ImmTy<'tcx, M::Provenance>,
) -> InterpResult<'tcx, Scalar<M::Provenance>>
where
F: rustc_apfloat::Float + rustc_apfloat::FloatConvert<F> + Into<Scalar<M::Provenance>>,
{
let arg: F = arg.to_scalar().to_float()?;
let exp: i32 = exp.to_scalar().to_i32()?;

let res = arg.scalbn(exp);
let res = self.adjust_nan(res, &[arg]);
interp_ok(res.into())
Comment on lines +1261 to +1263

@RalfJung RalfJung Sep 25, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This isn't doing precision non-determinism. That seems wrong?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is also a bitwise operation, so the result is exact.

@RalfJung RalfJung Sep 25, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There are three kinds of float operations:

  • bitwise ops like fabs, fully deterministic
  • core arithmetic operations like add, mul, div, sqrt; non-deterministic NaNs but otherwise deterministic, precise results
  • imprecise arithmetic operations like sin, exp

What does scalbn guarantee about NaNs? If it's bitwise, it should preserve the exact NaN payload and sign. Your implementation calls adjust_nan indicating that it is not bitwise.

What's the best authoritative spec for scalbn (and ilogb)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I haven’t read what exactly the C spec does but most implementations prepare a scale based on n then multiply x by it. So possible results are the same as multiplication between the input and a real, at least in practice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not exactly authoritative but a data point

https://en.cppreference.com/c/numeric/math/scalbn

Return value

If no errors occur, [ ... ]

If a range error due to overflow occurs, ±HUGE_VAL, ±HUGE_VALF, or ±HUGE_VALL is returned.

If a range error due to underflow occurs, the correct result (after rounding) is returned.

My interpretation of that is that it's in this category

core arithmetic operations like add, mul, div, sqrt; non-deterministic NaNs but otherwise deterministic, precise results

Nothing suggests it's in the sin etc category. And modulo underflow it is an exact operation, except that it's not guaranteed that a NaN is returned unchanged, hence the adjust_nan.

}

fn float_minmax<F>(
&self,
a: Scalar<M::Provenance>,
Expand Down
5 changes: 5 additions & 0 deletions compiler/rustc_hir_analysis/src/check/intrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ fn intrinsic_operation_unsafety(tcx: TyCtxt<'_>, intrinsic_id: LocalDefId) -> hi
| sym::frem_algebraic
| sym::fsub_algebraic
| sym::gpu_launch_sized_workgroup_mem
| sym::ilogb
| sym::integer_max
| sym::integer_min
| sym::is_val_statically_known
Expand Down Expand Up @@ -185,6 +186,7 @@ fn intrinsic_operation_unsafety(tcx: TyCtxt<'_>, intrinsic_id: LocalDefId) -> hi
| sym::rustc_peek
| sym::saturating_add
| sym::saturating_sub
| sym::scalbn
| sym::select_unpredictable
| sym::sin
| sym::size_of
Expand Down Expand Up @@ -469,6 +471,9 @@ pub(crate) fn check_intrinsic_type(
| sym::sin
| sym::cos => (1, 0, vec![param(0)], param(0)),

sym::scalbn => (1, 0, vec![param(0), tcx.types.i32], param(0)),
sym::ilogb => (1, 0, vec![param(0)], tcx.types.i32),

sym::minimum_number_nsz_f16 => (0, 0, vec![tcx.types.f16, tcx.types.f16], tcx.types.f16),
sym::minimum_number_nsz_f32 => (0, 0, vec![tcx.types.f32, tcx.types.f32], tcx.types.f32),
sym::minimum_number_nsz_f64 => (0, 0, vec![tcx.types.f64, tcx.types.f64], tcx.types.f64),
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,7 @@ symbols! {
if_let_rescope,
if_while_or_patterns,
ignore,
ilogb,
immediate_abort: "immediate-abort",
impl_header_lifetime_elision,
impl_lint_pass,
Expand Down Expand Up @@ -1922,6 +1923,7 @@ symbols! {
sanitizer_runtime,
saturating_add,
saturating_sub,
scalbn,
sdylib,
search_unbox,
section,
Expand Down
8 changes: 8 additions & 0 deletions library/compiler-builtins/compiler-builtins/src/math/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ pub mod full_availability {
fn fminimum_numf16(x: f16, y: f16) -> f16;
fn fminimumf16(x: f16, y: f16) -> f16;
fn fmodf16(x: f16, y: f16) -> f16;
fn ilogbf16(x: f16) -> i32;
fn rintf16(x: f16) -> f16;
fn roundevenf16(x: f16) -> f16;
fn roundf16(x: f16) -> f16;
fn scalbnf16(x: f16, n: i32) -> f16;
fn sqrtf16(x: f16) -> f16;
fn truncf16(x: f16) -> f16;
}
Expand All @@ -58,8 +60,10 @@ pub mod full_availability {
fn fmaxf(x: f32, y: f32) -> f32;
fn fminf(x: f32, y: f32) -> f32;
fn fmodf(x: f32, y: f32) -> f32;
fn ilogbf(x: f32) -> i32;
fn rintf(x: f32) -> f32;
fn roundf(x: f32) -> f32;
fn scalbnf(x: f32, n: i32) -> f32;
fn sqrtf(x: f32) -> f32;
fn truncf(x: f32) -> f32;

Expand All @@ -74,8 +78,10 @@ pub mod full_availability {
fn fmax(x: f64, y: f64) -> f64;
fn fmin(x: f64, y: f64) -> f64;
fn fmod(x: f64, y: f64) -> f64;
fn ilogb(x: f64) -> i32;
fn rint(x: f64) -> f64;
fn round(x: f64) -> f64;
fn scalbn(x: f64, n: i32) -> f64;
fn sqrt(x: f64) -> f64;
fn trunc(x: f64) -> f64;
}
Expand Down Expand Up @@ -110,9 +116,11 @@ pub mod full_availability {
fn fminimum_numf128(x: f128, y: f128) -> f128;
fn fminimumf128(x: f128, y: f128) -> f128;
fn fmodf128(x: f128, y: f128) -> f128;
fn ilogbf128(x: f128) -> i32;
fn rintf128(x: f128) -> f128;
fn roundevenf128(x: f128) -> f128;
fn roundf128(x: f128) -> f128;
fn scalbnf128(x: f128, n: i32) -> f128;
fn sqrtf128(x: f128) -> f128;
fn truncf128(x: f128) -> f128;
}
Expand Down
30 changes: 29 additions & 1 deletion library/core/src/intrinsics/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,34 @@ macro_rules! intrinsic_dispatch_on_type {
$($arg:ident: $arg_ty:ty),* $(,)?
) -> $ret_ty:ty;
$($concrete:ty => $body:block)*
) => {
intrinsic_dispatch_on_type! {
@declare [] $(#[$attr])*
$vis fn $name<$generic: $bound>($($arg: $arg_ty),*) -> $ret_ty;
$($concrete => $body)*
}
};

(
$(#[$attr:meta])*
$vis:vis const fn $name:ident<$generic:ident: $bound:path>(
$($arg:ident: $arg_ty:ty),* $(,)?
) -> $ret_ty:ty;
$($concrete:ty => $body:block)*
) => {
intrinsic_dispatch_on_type! {
@declare [const] $(#[$attr])*
$vis fn $name<$generic: $bound>($($arg: $arg_ty),*) -> $ret_ty;
$($concrete => $body)*
}
};

(
@declare [$($constness:tt)?] $(#[$attr:meta])*
$vis:vis fn $name:ident<$generic:ident: $bound:path>(
$($arg:ident: $arg_ty:ty),*
) -> $ret_ty:ty;
$($concrete:ty => $body:block)*
) => {
mod $name {
use super::*;
Expand All @@ -39,7 +67,7 @@ macro_rules! intrinsic_dispatch_on_type {
}

$(#[$attr])*
$vis fn $name<$generic: $name::Dispatch>($($arg: $arg_ty),*) -> $ret_ty {
$vis $($constness)? fn $name<$generic: $name::Dispatch>($($arg: $arg_ty),*) -> $ret_ty {
<$generic as $name::Dispatch>::dispatch($($arg),*)
}
};
Expand Down
39 changes: 39 additions & 0 deletions library/core/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,45 @@ intrinsic_dispatch_on_type! {
f128 => { libm::maybe_available::log2f128(x) }
}

intrinsic_dispatch_on_type! {
/// Calculates `x * 2.0^exp` efficiently.
///
/// This intrinsic does not have a stable counterpart.
#[rustc_nounwind]
#[inline]
#[rustc_intrinsic]
// The fallback bodies call into `libm`, which is not available at compile time. This is fine
// because const-eval implements this intrinsic itself and never runs the fallback body.
#[rustc_do_not_const_check]
pub const fn scalbn<T: bounds::FloatPrimitive>(x: T, exp: i32) -> T;

f16 => { libm::scalbnf16(x, exp) }
f32 => { libm::scalbnf(x, exp) }
f64 => { libm::scalbn(x, exp) }
f128 => { libm::scalbnf128(x, exp) }
}

intrinsic_dispatch_on_type! {
/// Returns the order of magnitude of `x` (in base 2).

@clarfonthey clarfonthey Sep 23, 2026 •

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.

Should probably clarify that this means it ignores the sign, even though it's technically correct.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do you have a concrete suggestion for how to rephrase this? When I try it just gets more confusing, leaning on the examples here seems fine to me.

///
/// When `x` is NaN or `0.0`, the return value is implementation-defined, and may be different
/// between const eval and runtime. When `x` is an infinity, the return value is [`i32::MAX`].
///
/// This intrinsic does not have a stable counterpart.
Comment thread
folkertdev marked this conversation as resolved.
#[rustc_nounwind]
#[inline]
#[rustc_intrinsic]
// The fallback bodies call into `libm`, which is not available at compile time. This is fine
// because const-eval implements this intrinsic itself and never runs the fallback body.
#[rustc_do_not_const_check]
pub const fn ilogb<T: bounds::FloatPrimitive>(x: T) -> i32;

f16 => { libm::ilogbf16(x) }
f32 => { libm::ilogbf(x) }
f64 => { libm::ilogb(x) }
f128 => { libm::ilogbf128(x) }
}

/// Returns `a * b + c` without rounding the intermediate result for `f16` values.
///
/// The stabilized version of this intrinsic is
Expand Down
24 changes: 24 additions & 0 deletions library/core/src/num/f128.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1682,6 +1682,30 @@ impl f128 {
intrinsics::copysignf128(self, sign)
}

/// Placeholder.
#[inline]
#[unstable(feature = "float_exponent_methods", issue = "163184")]
#[rustc_const_unstable(feature = "float_exponent_methods", issue = "163184")]
#[must_use = "method returns a new number and does not mutate the original value"]
pub const fn scalbn(self, exponent: i32) -> f128 {
intrinsics::scalbn(self, exponent)
}

/// Placeholder.
Comment on lines +1685 to +1694

@clarfonthey clarfonthey Sep 23, 2026 •

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.

Placeholder docs. (I hate that we can't just use macros to define the float types.)

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What could be nice nowadays is putting the docs into one of the macro_rules! attribute macros so you could do something like #[float_docs(ilogb, f128, constant_a = 1234, constant_b = 3.141)]. So the docs can get reused but "go to source" works correctly and the implementations can deviate where necessary.

#[inline]
#[unstable(feature = "float_exponent_methods", issue = "163184")]
#[rustc_const_unstable(feature = "float_exponent_methods", issue = "163184")]
#[must_use = "this returns the result of the operation, without modifying the original"]
pub const fn ilogb(self) -> Option<i32> {
if self.is_nan() {
None
} else if self == 0.0 {
Some(i32::MIN)
} else {
Some(intrinsics::ilogb(self))
}
}

/// Float addition that allows optimizations based on algebraic rules.
///
/// See [algebraic operators](primitive@f32#algebraic-operators) for more info.
Expand Down
24 changes: 24 additions & 0 deletions library/core/src/num/f16.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1668,6 +1668,30 @@ impl f16 {
intrinsics::copysignf16(self, sign)
}

/// Placeholder.
#[inline]
#[unstable(feature = "float_exponent_methods", issue = "163184")]
#[rustc_const_unstable(feature = "float_exponent_methods", issue = "163184")]
#[must_use = "method returns a new number and does not mutate the original value"]
pub const fn scalbn(self, exponent: i32) -> f16 {
intrinsics::scalbn(self, exponent)
}

/// Placeholder.
Comment on lines +1671 to +1680

@clarfonthey clarfonthey Sep 23, 2026 •

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.

Placeholder docs. (I hate that we can't just use macros to define the float types.)

View changes since the review

#[inline]
#[unstable(feature = "float_exponent_methods", issue = "163184")]
#[rustc_const_unstable(feature = "float_exponent_methods", issue = "163184")]
#[must_use = "this returns the result of the operation, without modifying the original"]
pub const fn ilogb(self) -> Option<i32> {
if self.is_nan() {
None
} else if self == 0.0 {
Some(i32::MIN)
} else {
Some(intrinsics::ilogb(self))
}
}

/// Float addition that allows optimizations based on algebraic rules.
///
/// See [algebraic operators](primitive@f32#algebraic-operators) for more info.
Expand Down
Loading
Loading