Rust 1.45 defines out-of-range float casts by saturation
Safe float-to-integer casts no longer inherit undefined behavior from LLVM's conversion instruction. Saturation supplies defined results while an unsafe method retains explicit preconditions.
A safe numeric cast must have a defined result even when its input is inconvenient for the processor instruction used to implement it. Before Rust 1.45, converting a floating-point value outside an integer type's range could violate that expectation. The compiler lowered the operation to an LLVM instruction whose result was not defined for every possible input. Ordinary safe Rust could consequently encounter undefined behavior through a cast.
The Rust 1.45 announcement traces the issue to a report opened in October 2013. The resolution gives as saturating behavior for float-to-integer conversion. Values beyond the destination's range reach its endpoint, and NaN becomes zero. Code can express the unchecked operation separately, with an unsafe contract.
I find this correction central to Rust's appeal as an alternative to C for low-level work. A safe expression needs a meaning that survives optimization. Understanding an instruction's usual output on one processor cannot substitute for the language contract.
The backend instruction had a narrower domain
The announcement shows a function converting f32 to u8. Earlier compilers emitted LLVM's fptoui, the floating-point-to-unsigned-integer instruction. That operation truncates toward zero when conversion is representable. For an out-of-range input, its poison result cannot be treated as an ordinary integer with a merely surprising value.
The difference matters because optimizers reason from the semantics of their intermediate representation. If the compiler tells LLVM that an operation satisfies a restricted contract, LLVM can transform surrounding code using that assumption. Observing a particular printed result does not establish that another build, optimization setting, or caller will preserve it.
Rust's source-level operation did not require an unsafe block. The mismatch therefore violated the expectation that safe code avoids undefined behavior. Fixing the issue required choosing a meaning for every floating-point input that could reach the cast, then implementing that meaning without exposing the backend instruction's invalid cases.
One possible response would have been to make callers prove every value in range. That would change the role of an ordinary cast. The chosen design instead defines the safe conversion and retains a separate unchecked operation for code that can establish its requirements.
Saturation describes the endpoints explicitly
For an unsigned byte, the maximum is 255 and the minimum is zero. A value too large converts to the maximum, while one too small converts to zero. NaN also converts to zero. The release demonstrates these cases directly.
fn main() {
assert_eq!(300.0_f32 as u8, 255);
assert_eq!((-100.0_f32) as u8, 0);
assert_eq!(f32::NAN as u8, 0);
assert_eq!(12.75_f32 as u8, 12);
}
The in-range example still truncates toward zero. Saturation concerns what happens beyond the destination range; it does not turn the conversion into rounding to the nearest integer. A program that needs a different rounding rule must express that rule separately.
The result is also not an error report. Converting an excessively large value to the maximum loses information about how far outside the range it was. That can be appropriate for a bounded output representation, but it cannot tell a parser that its input violated an application limit. Defined behavior and input acceptance are different policy questions.
For signed destinations, the relevant endpoints belong to the signed type. An explanation focused on u8 should not imply that every negative input maps to zero regardless of the destination. The general rule is saturation at the integer type's limits, with zero for NaN.
The cast also preserves a numeric interpretation rather than the floating-point bit pattern. Turning a float's encoding into an integer representation is a different operation. Treating a saturating conversion as bit reinterpretation would give a program the wrong data even though the operation is now well defined.
An unchecked method makes the proof obligation visible
The tagged f32 implementation documents to_int_unchecked. Its safety conditions require a value that is not NaN or infinite and whose truncated result is representable in the selected integer type. The caller must establish those facts before invoking it.
let value = 4.6_f32;
let integer: u16 = unsafe { value.to_int_unchecked() };
assert_eq!(integer, 4);
This constant example meets the preconditions. Replacing it with unchecked external input would not preserve that proof. The unsafe block records that the caller owns the requirements; it does not make the requirements true.
The documentation's wording about truncation is important near an endpoint. Representability is assessed after removing the fractional part. A review that applies a different rounding operation or tests only a loosely related bound may fail to establish the method's actual contract.
The method's stability annotation in the tagged source records availability since 1.44.0. The soundness change discussed in this release is the saturating behavior of the safe cast. Availability of the explicit unsafe alternative and the release that changes safe semantics should not be conflated.
The release compares the design with checked array indexing and get_unchecked. A safe operation establishes its required boundary behavior. The unsafe alternative permits omission of that handling when the caller can prove the relevant condition. This comparison describes responsibility, not a promise that all numeric and indexing implementations have identical cost.
The optimization question follows the contract
Extra handling can cost instructions when the compiler cannot prove the input range. The announcement notes that checks can disappear when the optimizer can prove them unnecessary. That leaves a useful distinction between an operation whose semantics are defined and a particular generated implementation of those semantics.
Removing undefined behavior does not require the compiler to retain redundant tests forever. It requires every optimization to preserve the defined result for inputs the program can supply. When an input range is known, the safe conversion and an unchecked conversion may produce equivalent code. Without that proof, the safe operation retains responsibility for the exceptional inputs.
I prefer that division because it keeps the normal source expression reliable. Code requiring a stronger optimization claim must expose the assumptions that justify it. A benchmark result alone cannot prove that NaN, infinity, or an out-of-range value is impossible.
The long history of this issue illustrates a design choice beyond a one-line code-generation repair. The project needed an answer for input values that the old lowering did not handle soundly. Saturation supplies that answer without requiring every application author to learn LLVM's poison semantics before using as.
Application validation remains a separate operation
Consider a field that represents an integer quantity but arrives through a floating-point computation. Saturation means the conversion produces a valid destination integer even if the calculation exceeded its range. It does not establish that the resulting endpoint is an acceptable value in the application's domain. An application that needs to distinguish overflow from an intentional maximum must retain that distinction before conversion discards it.
NaN makes the information loss especially clear. A NaN and an ordinary zero both become zero when converted to an unsigned integer. Afterward, the integer alone cannot identify which source category produced it. This is a direct consequence of the specified mapping, so code that cares about that difference needs a floating-point check before the cast.
Tests for a numerical boundary should consequently state which behavior they are checking. A cast test can verify the release's endpoint and NaN mappings. A domain test must separately establish whether those inputs should have been accepted at all. The latter should not disappear because a formerly unsound operation now returns a defined integer. Saturation solves the language-level conversion problem while leaving the meaning of excessive or nonnumeric input with its consumer.
Procedural macros gain more positions
Rust 1.45 also stabilizes function-like procedural macros in expression, pattern, and statement positions. Item-position use was already stable. A macro can now participate directly in more of the contexts where a library's syntax is useful, provided its expansion suits the surrounding position.
The announcement identifies this as the final nightly-only language requirement for the upcoming stable-capable Rocket release. It does not say that the new Rocket version has already shipped. That distinction separates compiler capability from the release state of a consuming framework.
These macro changes expand where generated syntax can appear. They do not establish that any arbitrary token stream is a valid expression or pattern. The compiler still checks the expansion in its context. The benefit comes from permitting the procedural implementation at those sites while preserving the rules of the surrounding language.
The numeric change makes a different boundary precise. Safe float casts now define what happens when a value exceeds the integer representation. An explicit unsafe method carries the narrower preconditions for callers that can prove them. Generated machine code can then optimize within a contract that ordinary source code is entitled to rely on.