JIT, AOT preinit, and CoreLib: Saturate float/double conversions to small integral types#128604
Merged
Merged
Conversation
Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com>
Contributor
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
…ll casts Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Fix invalid result for short double 32768
JIT: Saturate float/double conversions to small integral types
May 26, 2026
…ing casts Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com>
This was referenced May 27, 2026
Open
Member
|
This should be ready for review now. Handles the scenario for everything except Mono, which requires a significantly larger and more complex PR covering their multiple backends. |
This was referenced Jun 10, 2026
Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com>
This was referenced Jun 24, 2026
jkotas
reviewed
Jul 13, 2026
jkotas
reviewed
Jul 13, 2026
Member
|
Anything left here? It looks good to me overall, the JIT changes can use a review from somebody on codegen. |
Member
It should be ready and was just pending review. CC. @dotnet/jit-contrib for review |
…oongArch64 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…E_HW_INTRINSICS gtNewSimdMinMaxNativeNode is NYI for WASM, so route the float-domain clamp through the scalar GT_INTRINSIC MaxNative/MinNative nodes (which have real codegen) instead of asserting during Morph. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Member
|
CI is passing, so this is just pending sign-off to be merged now. |
JulieLeeMSFT
approved these changes
Jul 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Unchecked
float/double→ small integral type (sbyte,byte,short,ushort,char) conversions did not saturate at the small-type boundary. For example,(short)(double)32768.000000000007produced-32768instead of32767, diverging from the saturatingR → intcontract introduced in .NET 9.The JIT expands
R → smallTasR → int → smallT. The innerR → intsaturates, but the outerint → smallTonly truncates low bits. The CoreCLR interpreter VM helper and the ILC type preinitializer had analogous bugs and were updated for parity with runtime behavior. In addition, CoreLibTryConvertpaths forfloat/doubleto small integral types were updated to use direct casts on CoreCLR (while preserving existing MONO behavior under#if MONO).Summary
Fixes saturating semantics for unchecked
float/double→ small integral type casts across all supported JIT targets (xarch, x86, arm64, RISC-V64, LoongArch64, ARM32, and WASM), the CoreCLR interpreter VM helper, and the NativeAOT type preinitializer, and applies corresponding CoreCLR-sideTryConvertsimplifications that rely on the corrected cast semantics. Mono runtime backends still need analogous updates and are tracked separately; the JIT regression test is[SkipOnMono]in the meantime.Changes Made
src/coreclr/jit/morph.cpp—fgMorphExpandCast: Added target-specific handling soR -> smallsaturates at destination bounds across supported architectures:FEATURE_HW_INTRINSICStargets use float-domain min/max clamping before existing cast expansion.GT_INTRINSIC.R -> int32followed byNI_PRIMITIVE_SaturateTo{Int8,Int16,UInt8,UInt16}), preserving NaN→0 semantics from the saturatingR → intcast.NI_PRIMITIVE_SaturateTo{Int8,Int16,UInt8,UInt16}intrinsics and the WASM binaryGT_INTRINSIC(MaxNative/MinNative)produced by morph:gentree.cpp: added the fourSaturateTo*cases to the unaryGT_INTRINSICcost switch ingtSetEvalOrder, added aTARGET_WASMblock to the binaryGT_INTRINSICcost switch forMaxNative/MinNative, and added dump labels for the fourSaturateTo*IDs (fixes the'Unknown binary GT_INTRINSIC operator'assert seen on e.g.System.Half:op_Explicit(System.Half):byte).importercalls.cppIsTargetIntrinsic: added the fourSaturateTo*IDs to the ARM, RISCV64, and LoongArch64 switches sorationalize.cpp'sIsTargetIntrinsicassertion is satisfied.importercalls.cppimpPrimitiveNamedIntrinsic: split the small-target-type path soNI_PRIMITIVE_ConvertToIntegeremits a single saturatingR -> smallcast whileNI_PRIMITIVE_ConvertToIntegerNativekeeps the original two-cast (saturatingR -> intfollowed by truncatingint -> small) pattern that preserves native truncating semantics.valuenum.cppfgValueNumberIntrinsic: added an explicit branch for theSaturateTo*IDs that gives the node an opaque unary VN, backed by newVNF_SaturateToInt8/Int16/UInt8/UInt16entries invaluenumfuncs.h, so VN no longer hits theassert(NI_System_Object_GetType)for these intrinsics.valuenum.cppEvalMathFuncBinary: extended theMaxNative/MinNativeconstant-folding (TYP_DOUBLE / TYP_FLOAT) and VNFunc selection (VNF_MaxNumber/VNF_MinNumber) cases to also apply on WASM, not just RISC-V64, so the binaryGT_INTRINSICproduced by morph on WASM no longer hitsunreached()here.assertionprop.cppIntegralRange::ForNode: taught range analysis the exact result range for eachSaturateTo*intrinsic ([ByteMin, ByteMax],[ShortMin, ShortMax],[0, UByteMax],[0, UShortMax]).SaturateTo*: newINS_ssat/INS_usatThumb-2 instructions ininstrsarm.h, encoding/disassembly support inemitarm.cpp, LSRA/codegen wiring inlsraarm.cppandcodegenarmarch.cpp.SaturateTo*: branch-based clamp incodegenriscv64.cpp/codegenloongarch64.cpp, with LSRA support inlsrariscv64.cpp/lsraloongarch64.cpp.src/coreclr/vm/interpexec.cpp—ConvFpHelper: clamps tonumeric_limits<TResult>(the destination type) instead ofTIntermediate, so floating-point → small-integral conversions now saturate at the destination type's range.src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypePreinit.cs: float-domainconv_i1/conv_i2/conv_u1/conv_u2cases now explicitly saturate viaMath.Clamp(NaN → 0), so AOT-baked statics match runtime behavior regardless of which host JIT runs ILC. Original simple-cast version is kept commented out with a TODO to restore once the fix has propagated through the toolchain.TryConvertsimplifications (Double.cs,Single.cs,SByte.cs,Int16.cs):float/double→ small-integralTryConvertTo*/TryConvertFrom*paths now use a direct cast on CoreCLR, with the existing manual clamp preserved under#if MONO.DoubleTests.GenericMath.ConvertToIntegerTest/SingleTests.GenericMath.ConvertToIntegerTest(issue (short)(double)32768.000000000007 produces invalid result #116823 suppressions removed).[SkipOnMono]pending Mono backend updates).DoubleTests.GenericMath.ConvertToIntegerNativeTest/SingleTests.GenericMath.ConvertToIntegerNativeTestto validate only in-range inputs and removed the[ActiveIssue("#124344", ... IsAppleMobile && IsX64Process && IsCoreCLR)]suppression, sinceConvertToIntegerNativeis explicitly allowed to use platform-native behavior for out-of-range inputs (which can also evolve over time, e.g. as AVX10.2 saturating conversions get exposed on xarch).Note: the previously included change to
src/coreclr/interpreter/compiler.cpp(adding a two-step expansion forNI_PRIMITIVE_ConvertToIntegerNativeon small target types) was reverted. The saturating behavior is allowed as the native interpreter behavior, and when R2R is present the R2R version of the intrinsic is used, so consistency with R2R is achieved that way.