Fix Mono LLVM lowering of MinNumber/MaxNumber to preserve the sign of zero - #131172
Conversation
… zero double.MinNumber/MaxNumber (IEEE 754-2019 minimumNumber/maximumNumber) treat -0 as less than +0, but the scalar lowering used llvm.minnum/maxnum, which leave the sign of zero unspecified -- e.g. minnum(+0, -0) returns +0 on x86. Lower to llvm.minimumnum/maximumnum instead, which are NaN-suppressing and sign-of-zero aware. Min/Max already use llvm.minimum/maximum and are correct. Un-skips Runtime_130831 on Mono now that the divergence is fixed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 4 pipeline(s). 12 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @steveisok, @vitek-karas |
There was a problem hiding this comment.
Pull request overview
This PR updates Mono’s LLVM backend to correctly implement IEEE 754-2019 minimumNumber / maximumNumber semantics for MinNumber / MaxNumber, specifically preserving the sign of zero (e.g., min(+0, -0) => -0) on LLVM AOT. It also re-enables a previously skipped regression test on Mono now that the behavior should match the BCL contract.
Changes:
- Switch Mono LLVM scalar lowering of
MinNumber/MaxNumberfromllvm.minnum/llvm.maxnumtollvm.minimumnum/llvm.maximumnum. - Update the intrinsic ID list to use the new LLVM intrinsic names.
- Unskip
Runtime_130831on Mono to validate signed-zero behavior.
Show a summary per file
| File | Description |
|---|---|
| src/mono/mono/mini/mini-llvm.c | Routes OP_*MINNUM/OP_*MAXNUM to the new MINIMUMNUM/MAXIMUMNUM intrinsics during LLVM emission. |
| src/mono/mono/mini/llvm-intrinsics.h | Replaces MINNUM/MAXNUM intrinsic definitions with MINIMUMNUM/MAXIMUMNUM to match the intended semantics. |
| src/mono/mono/mini/intrinsics.c | Updates commentary/documentation for MinNumber/MaxNumber lowering to reflect the new LLVM intrinsics and semantics. |
| src/tests/JIT/Regression/JitBlue/Runtime_130831/Runtime_130831.cs | Removes [SkipOnMono] so the signed-zero regression test runs on Mono again. |
Copilot's findings
Comments suppressed due to low confidence (1)
src/tests/JIT/Regression/JitBlue/Runtime_130831/Runtime_130831.cs:22
- The test now runs on Mono and validates the signed-zero behavior for Min/MinNumber, but this PR also changes the LLVM lowering for MaxNumber. It would be good to add Max/MaxNumber signed-zero assertions here as well so the new MAXIMUMNUM lowering is covered and a future regression is caught.
[Fact]
public static void TestEntryPoint()
{
Assert.Equal(BitConverter.DoubleToInt64Bits(-0.0), BitConverter.DoubleToInt64Bits(MinNegZeroConst(+0.0)));
Assert.Equal(BitConverter.DoubleToInt64Bits(-0.0), BitConverter.DoubleToInt64Bits(MinNumberZeroConst(-0.0)));
Assert.Equal(BitConverter.SingleToInt32Bits(-0.0f), BitConverter.SingleToInt32Bits(MinNegZeroConst(+0.0f)));
Assert.Equal(BitConverter.SingleToInt32Bits(-0.0f), BitConverter.SingleToInt32Bits(MinNumberZeroConst(-0.0f)));
}
- Files reviewed: 4/4 changed files
- Comments generated: 0
…scompiled minimumnum/maximumnum llvm.minimumnum/maximumnum have the right IEEE 754-2019 semantics but are miscompiled by the x86 backend in the LLVM version Mono builds against, regressing Runtime_98068.TestMinNumber on the Mono LLVM AOT leg. Lower MinNumber/MaxNumber by composing llvm.minimum/maximum (sign-of-zero aware, already used for Math.Min/Max) with an explicit NaN fixup instead. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
The Fixed by composing Note This comment was drafted by Copilot. |
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (2)
src/tests/JIT/Regression/JitBlue/Runtime_130831/Runtime_130831.cs:21
- This test now runs on Mono LLVM AOT, but it only validates the Min/MinNumber signed-zero behavior. The PR also changes the MaxNumber lowering (OP_*MAXNUM), so adding symmetric assertions for Max/MaxNumber would help prevent regressions in that path as well.
Assert.Equal(BitConverter.DoubleToInt64Bits(-0.0), BitConverter.DoubleToInt64Bits(MinNegZeroConst(+0.0)));
Assert.Equal(BitConverter.DoubleToInt64Bits(-0.0), BitConverter.DoubleToInt64Bits(MinNumberZeroConst(-0.0)));
Assert.Equal(BitConverter.SingleToInt32Bits(-0.0f), BitConverter.SingleToInt32Bits(MinNegZeroConst(+0.0f)));
Assert.Equal(BitConverter.SingleToInt32Bits(-0.0f), BitConverter.SingleToInt32Bits(MinNumberZeroConst(-0.0f)));
src/mono/mono/mini/mini-llvm.c:7639
- The PR description says OP_*MINNUM/OP_*MAXNUM are lowered to llvm.minimumnum/maximumnum, but this implementation instead composes llvm.minimum/maximum with explicit NaN fixups and even states minimumnum/maximumnum are miscompiled on x86. Please reconcile (either switch to the new intrinsics as described, or update the PR description / add an LLVM issue reference + clarify the expected codegen impact, especially on AArch64).
* We compose this from llvm.minimum/maximum (sign-of-zero aware but
* NaN-propagating) plus an explicit NaN fixup, rather than lowering
* directly to an intrinsic: llvm.minnum/maxnum leave the sign of zero
* unspecified (minnum(+0, -0) may return +0 on x86, see dotnet/runtime
* #131130) and llvm.minimumnum/maximumnum are miscompiled by the x86
- Files reviewed: 4/4 changed files
- Comments generated: 1
should we file an llvm bug for this? |
Was already working on that. I've logged a few issues against Clang/LLVM in the past few days 😅 |
|
@akoeplinger, looks like this is already fixed upstream Copilot reports back, and then I verified after: LLVM
|
| Revision | minimumnum(NaN, 1.0) |
|---|---|
| LLVM 22.1.0 release | ✅ correct |
upstream main @ 2026-06-13 (a72232481fe7, the pin) |
❌ -1.0 (per Mono CI) |
| current trunk | ✅ correct |
Trunk-only regression: introduced after the 22.x branch, already fixed on trunk. The dnceng snapshot landed inside the window. Bug is asymmetric — minimumnum broken, maximumnum fine.
Supposed fix (not yet in the fork)
dotnet/main last merged upstream at c9811b58 (2026-06-13), so the fork lacks these later X86 changes:
961ab7a3(2026-07-08) — [SDAG][X86] Uplift pseudo fmin/fmax from X86 (#188489) — leading suspect; removes X86's own 177-line pseudo fmin/fmax lowering (the exact path the pin still uses) and re-homes it in SDAG.8a3b7152(2026-07-01) — [X86] Fold FP UNORD/ORD compare against known-non-NaN to self-compare (#206943) — touches thecmpordsdNaN logic; lower confidence.
Caveat: couldn't build llc at a72232481fe7 to execute + git-bisect the exact fix, so #188489 is the leading suspect by code path, not proven. The "not a Mono patch" conclusion is proven via blob comparison.
Fix path
Fresh merge-upstream-main into dotnet/llvm-project past 2026-07-08 (pulls in #188489), then bump the snapshot in eng/Version.Details.xml.
| Item | Value |
|---|---|
| Mono LLVM pin | 23.1.0-alpha.1.26357.1 → dotnet/llvm-project 04bf14b2 |
| Effective upstream base | a72232481fe7 (main @ 2026-06-13) |
| Suspected upstream fix | #188489 961ab7a3 (2026-07-08) |
|
ok. I see commit 961ab7a3 made it into the 23.x release branch which we're planning to sync to in a bit. |
@akoeplinger, if you can ping me when this happens I can either sign-off on or remove the extra workaround here |
Fixes #131130.
double.MinNumber/MaxNumber(and thefloat/Halfvariants) implement IEEE 754-2019minimumNumber/maximumNumber, which are NaN-suppressing and treat-0as less than+0. The Mono LLVM scalar lowering usedllvm.minnum/maxnum(IEEE 754-2008minNum/maxNum), which leave the sign of zero unspecified -- e.g.minnum(+0, -0)returns+0on x86, sodouble.MinNumber(+0.0, -0.0)returned+0.0instead of-0.0on the LLVM AOT configuration.Lower to
llvm.minimumnum/maximumnuminstead (available since LLVM 20; Mono is on LLVM 23). These are the exact 2019minimumNumber/maximumNumberoperations -- NaN-suppressing and sign-of-zero aware -- and on AArch64 still lower to a singlefminnm/fmaxnm.Min/Maxalready usellvm.minimum/maximumand are correct, so they are unchanged.I confirmed the divergence and the fix by JIT-executing each intrinsic on x86-64:
minnum(+0, -0)returns+0(wrong), whileminimumnum(+0, -0)returns-0(correct) and still suppresses NaN.Un-skips the
Runtime_130831regression test (added in #130832) on Mono, which covers theMin/MinNumbersigned-zero cases directly and runs on theAllSubsets_Mono_LLVMAot_RuntimeTestsleg.Note
This change was authored with the assistance of GitHub Copilot.