From b2d5d0961097ff1c517041d788461f7d94221ab6 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 10 Sep 2026 17:25:36 -0700 Subject: [PATCH 1/2] Document saturating floating-point conversions to small integral types Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/core/compatibility/11.md | 1 + .../jit/11/fp-to-small-integer.md | 111 ++++++++++++++++++ .../compatibility/jit/9.0/fp-to-integer.md | 3 + docs/core/compatibility/toc.yml | 2 + 4 files changed, 117 insertions(+) create mode 100644 docs/core/compatibility/jit/11/fp-to-small-integer.md diff --git a/docs/core/compatibility/11.md b/docs/core/compatibility/11.md index a1ef4f02af5fc..af10cbb9c4753 100644 --- a/docs/core/compatibility/11.md +++ b/docs/core/compatibility/11.md @@ -89,6 +89,7 @@ See [Breaking changes in EF Core 11](/ef/core/what-is-new/ef-core-11.0/breaking- | Title | Type of change | |-------------------------------------------------------------------|-------------------| +| [Floating-point conversions to small integral types are saturating](jit/11/fp-to-small-integer.md) | Behavioral change | | [Minimum hardware requirements updated](jit/11/minimum-hardware-requirements.md) | Behavioral change | ## Networking diff --git a/docs/core/compatibility/jit/11/fp-to-small-integer.md b/docs/core/compatibility/jit/11/fp-to-small-integer.md new file mode 100644 index 0000000000000..24740d3a32052 --- /dev/null +++ b/docs/core/compatibility/jit/11/fp-to-small-integer.md @@ -0,0 +1,111 @@ +--- +title: "Breaking change: Floating-point conversions to small integral types are saturating" +description: "Learn about the breaking change in .NET 11 where unchecked floating-point conversions to small integral types saturate at the destination type's bounds." +ms.date: 09/10/2026 +ai-usage: ai-assisted +ms.custom: https://github.com/dotnet/runtime/pull/128604 +--- + +# Floating-point conversions to small integral types are saturating + +In .NET 11, unchecked floating-point conversions to `sbyte`, `byte`, `short`, `ushort`, and `char` now have *saturating* behavior at the destination type's bounds. Values that are too small or too large are set to the destination type's minimum or maximum value, respectively. + +This change continues the [.NET 9 change to floating point-to-integer conversions](../9.0/fp-to-integer.md), which standardized conversions from `float` and `double` to `int`, `uint`, `long`, and `ulong`. .NET 11 extends saturation to 8- and 16-bit destinations. + +The change applies to CoreCLR, including its interpreter, and Native AOT. Mono is not included in this change. + +For more information, see [dotnet/runtime#128604](https://github.com/dotnet/runtime/pull/128604). + +## Version introduced + +.NET 11 Preview 7 + +## Previous behavior + +Previously, .NET did not guarantee the result of an unchecked floating-point to integral conversion when the value overflowed the destination type or was `NaN`. Results could differ between runtime implementations, such as CoreCLR and Mono, between architectures, such as x86, x64, Arm32, Arm64, and WebAssembly, and between hardware instruction sets within an architecture, such as x87, SSE2, AVX, and AVX-512. + +The .NET 9 breaking change specifically highlighted x86 and x64, where conversions commonly returned sentinel values on overflow. Arm64 already used saturating conversions by convention. The change standardized conversions to the wider integer types rather than establishing the old sentinel results as a contract. + +For small integral types, a common CoreCLR conversion sequence in .NET 9 and .NET 10 was a saturating conversion to `int`, followed by narrowing to the destination type by discarding the high bits. This sequence explains the behavior many applications experienced, but it was not a guaranteed contract for a direct floating-point to small-integral cast. + +The following table shows results from that two-step sequence for a runtime `float` or `double` value `x`. These inputs fit in `int`, so the examples isolate the effect of discarding all but the destination's low 8 or 16 bits. The retained bits are interpreted as signed for `sbyte` and `short`, and unsigned for `byte`, `ushort`, and `char`. Results for `char` are shown numerically. + +| Convert to | Value of `x` | Retained low bits | Example previous result | +| --- | --- | --- | --- | +| `sbyte` or `byte` | 298 | `0x2A` | 42 | +| `sbyte` | -298 | `0xD6` | -42 | +| `byte` | -42 | `0xD6` | 214 | +| `short`, `ushort`, or `char` | 65578 | `0x002A` | 42 | +| `short` | -65578 | `0xFFD6` | -42 | +| `ushort` or `char` | -42 | `0xFFD6` | 65494 | + +For example, the following code could return `42`: + +```csharp +static short ConvertValue(double value) +{ + return unchecked((short)value); +} + +short result = ConvertValue(65578.0); +``` + +The intermediate `int` is `65578` (`0x0001002A`). With only its low 16 bits retained, the result is `42` (`0x002A`), rather than saturation to `short.MaxValue`. + +## New behavior + +Starting in .NET 11, unchecked conversions saturate at the destination type's bounds. Finite values within the destination range continue to be rounded toward zero. `NaN` converts to zero. + +| Convert to | Below minimum, including negative infinity | Above maximum, including positive infinity | `NaN` | +| --- | --- | --- | --- | +| `sbyte` | -128 (`sbyte.MinValue`) | 127 (`sbyte.MaxValue`) | 0 | +| `byte` | 0 (`byte.MinValue`) | 255 (`byte.MaxValue`) | 0 | +| `short` | -32768 (`short.MinValue`) | 32767 (`short.MaxValue`) | 0 | +| `ushort` | 0 (`ushort.MinValue`) | 65535 (`ushort.MaxValue`) | 0 | +| `char` | 0 (`char.MinValue`) | 65535 (`char.MaxValue`) | 0 | + +The preceding example now returns `32767` (`short.MaxValue`) instead of `42`. Similarly, a conversion from `298` to `byte` now returns `255` instead of `42`, and a conversion from `-42` to `ushort` now returns `0` instead of `65494`. + +Because they convert through `float`, the corresponding unchecked conversions from also use the new behavior. and now correctly saturate for these small destination types. + +Checked conversions are unchanged and continue to throw when the conversion overflows. This change does not alter integer-to-integer narrowing conversions or the wider integer and vector conversions covered by the .NET 9 change. + +## Type of breaking change + +This change is a [behavioral change](../../categories.md#behavioral-change). + +## Reason for change + +The [.NET 9 change](../9.0/fp-to-integer.md) established saturating behavior for conversions to wider integer types, but conversions to 8- and 16-bit destinations still had hardware- and implementation-dependent behavior for out-of-range values and `NaN`. This change gives those conversions deterministic, saturating behavior and makes the JIT, CoreCLR interpreter, and Native AOT preinitialized values agree. + +## Recommended action + +If your code relies on previous results for out-of-range inputs, update it to expect saturation at the destination type's bounds where possible. + +If you need the platform-native behavior commonly used before these changes, the simplest workaround is or . For example, replace a direct `(ushort)x` cast with `double.ConvertToIntegerNative(x)` when `x` is a `double`, or `float.ConvertToIntegerNative(x)` when it is a `float`. + +You can also select the intermediate conversion explicitly. The following examples use a `double` input `x` and a `ushort` destination: + +| Required behavior | Conversion | +| --- | --- | +| Platform-native conversion to the destination type, which commonly recovers earlier behavior | `double.ConvertToIntegerNative(x)` | +| Saturation to `int`, then narrowing, matching the common .NET 9 and .NET 10 CoreCLR sequence | `unchecked((ushort)(int)x)` | +| Platform-native conversion to `int`, then narrowing, matching a common pre-.NET 9 sequence | `unchecked((ushort)double.ConvertToIntegerNative(x))` | + +Use `float.ConvertToIntegerNative` for `float` inputs and substitute the appropriate destination type for `sbyte`, `byte`, `short`, or `char`. + +As with the .NET 9 change, `ConvertToIntegerNative` is **not guaranteed to reproduce previous results** for out-of-range values or `NaN`. It selects behavior that is efficient for the current platform, which can change across runtimes, architectures, or hardware revisions. An explicit `(ushort)(int)x` instead selects a saturating conversion to `int` followed by integer narrowing; it does not restore every historical implementation's behavior. + +If the converted value is used as an array index, buffer offset, or length, validate that the resulting value is within the required bounds. A conversion that produces a usable value on one machine does not establish that platform-native conversion will do so on another. + +## Affected APIs + +- Unchecked explicit casts from or to , , , , or . +- unchecked explicit conversion operators: + - + - + - + - + - +- when `TInteger` is , , , , or . +- when `TInteger` is , , , , or . diff --git a/docs/core/compatibility/jit/9.0/fp-to-integer.md b/docs/core/compatibility/jit/9.0/fp-to-integer.md index a910081c486d4..6d7cfcbab94b3 100644 --- a/docs/core/compatibility/jit/9.0/fp-to-integer.md +++ b/docs/core/compatibility/jit/9.0/fp-to-integer.md @@ -2,11 +2,14 @@ title: "Floating point-to-integer conversions are saturating" description: Learn about the breaking change in .NET 9 where floating point-to-integer conversions have saturating behavior. ms.date: 09/03/2024 +ai-usage: ai-assisted --- # Floating point-to-integer conversions are saturating Floating point-to-integer conversions now have *saturating* behavior on x86 and x64 machines. Saturating behavior means that if the converted value is too small or large for the target type, the value is set to the minimum or maximum value, respectively, for that type. +In .NET 11, this behavior extends to unchecked conversions to `sbyte`, `byte`, `short`, `ushort`, and `char` on CoreCLR (including its interpreter) and Native AOT, but not Mono. For details, see [Floating-point conversions to small integral types are saturating](../11/fp-to-small-integer.md). + ## Previous behavior The following table shows the previous behavior when converting a `float` or `double` value. diff --git a/docs/core/compatibility/toc.yml b/docs/core/compatibility/toc.yml index e9e3443cd125e..0e59020c8d70b 100644 --- a/docs/core/compatibility/toc.yml +++ b/docs/core/compatibility/toc.yml @@ -84,6 +84,8 @@ items: href: interop/11/nativeaot-lib-prefix.md - name: JIT compiler items: + - name: Floating-point conversions to small integral types are saturating + href: jit/11/fp-to-small-integer.md - name: Minimum hardware requirements updated href: jit/11/minimum-hardware-requirements.md - name: Networking From 0ee8ec672ec2ec53a512ce72c32a6d7e14c7cd16 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Fri, 11 Sep 2026 16:00:05 -0700 Subject: [PATCH 2/2] Apply suggestion from @gewarren Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> --- docs/core/compatibility/jit/11/fp-to-small-integer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/compatibility/jit/11/fp-to-small-integer.md b/docs/core/compatibility/jit/11/fp-to-small-integer.md index 24740d3a32052..112dae954637b 100644 --- a/docs/core/compatibility/jit/11/fp-to-small-integer.md +++ b/docs/core/compatibility/jit/11/fp-to-small-integer.md @@ -76,7 +76,7 @@ This change is a [behavioral change](../../categories.md#behavioral-change). ## Reason for change -The [.NET 9 change](../9.0/fp-to-integer.md) established saturating behavior for conversions to wider integer types, but conversions to 8- and 16-bit destinations still had hardware- and implementation-dependent behavior for out-of-range values and `NaN`. This change gives those conversions deterministic, saturating behavior and makes the JIT, CoreCLR interpreter, and Native AOT preinitialized values agree. +The [.NET 9 change](../9.0/fp-to-integer.md) established saturating behavior for conversions to wider integer types, but conversions to 8-bit and 16-bit destinations still had hardware-dependent and implementation-dependent behavior for out-of-range values and `NaN`. This change gives those conversions deterministic, saturating behavior and makes the JIT, CoreCLR interpreter, and Native AOT preinitialized values agree. ## Recommended action