Skip to content

JIT: Math.Clamp throws spurious ArgumentException when min is computed from (~Compare) * const #129104

@AndyAyersMS

Description

@AndyAyersMS

Note

This issue body is AI-generated (GitHub Copilot CLI). The investigation, reduction, and analysis are checked by me.

Description

Math.Clamp(int, int, int) throws ArgumentException from JIT-optimized code when called with a min argument computed from (~c) * constant, where c is a Compare result ({0, 1}). The actual value of min is correct in the exception message (e.g. -672479), but the JIT's min > max check still fires even though the comparison is false under signed arithmetic.

Tier0/MinOpts works correctly. FullOpts, Tier1, and Tier1+PGO all fail.

Repro

using System;
using System.Runtime.CompilerServices;

public class Program
{
    private static volatile int P1 = 0;
    private static volatile int Sink;

    [MethodImpl(MethodImplOptions.NoInlining)]
    public static int Bug(int p1)
    {
        // c ∈ {0, 1} from a Compare. With P1 = 0, c = 0.
        int c = (p1 < 0) ? 1 : 0;

        // The OUTER Math.Clamp is required to trigger the bug.
        int _ = Math.Clamp(c, 0, p1);

        // v6 = (~0) * 672479 = -672479.
        int v6 = (~c) * 672479;

        // INNER Clamp: should clamp 0x5756E (357230) into [-672479, 100] → 100.
        return Math.Clamp(0x0005756E, v6, 100);
    }

    public static int Main()
    {
        int r = 0;
        for (int i = 0; i < 400; i++) { try { Sink = Bug(P1); } catch { } }
        System.Threading.Thread.Sleep(150);
        for (int i = 0; i < 400; i++) { try { Sink = Bug(P1); } catch { } }
        System.Threading.Thread.Sleep(200);
        try
        {
            r = Bug(P1);
            Console.WriteLine($"OK: 0x{r:X8}");
            return 0;
        }
        catch (ArgumentException e)
        {
            Console.WriteLine($"FAIL: {e.Message}");
            return 1;
        }
    }
}

Observed (checked CoreCLR at a8b2c92ce21, osx-arm64)

Config Output
DOTNET_TieredCompilation=1 DOTNET_TC_CallCountThreshold=999999 (Tier0/MinOpts) OK: 0x00000064
DOTNET_TieredCompilation=0 (FullOpts) FAIL: '-672479' cannot be greater than 100.
DOTNET_TieredCompilation=1 + warmup (Tier1) FAIL: '-672479' cannot be greater than 100.
DOTNET_TieredPGO=1 (Tier1+PGO) FAIL: '-672479' cannot be greater than 100.

-672479 > 100 is false under signed arithmetic, so Math.Clamp should not throw. Note that the value -672479 in the exception message is correct — the JIT computed v6 correctly. The bug is in the min > max comparison itself.

Regression

  • 11.0.100-preview.3.26207.106 (shipping): OK — does not throw.
  • HEAD (a8b2c92ce21, May 2026): FAIL.

Same regression window as #129076 and #129099.

Hypothesis

The JIT appears to be doing an unsigned comparison for min > max when one operand has a range deduced from a (~Compare) * const chain.

  • c ∈ {0, 1} from the ternary
  • ~c ∈ {-1, -2} signed, or {0xFFFFFFFF, 0xFFFFFFFE} unsigned
  • (~c) * 672479 is computed correctly as -672479 in twos-complement
  • But range analysis may treat the operand as unsigned, deducing v6 ∈ [huge positive, ...]
  • Then the constant-folded v6 > 100 check yields true under unsigned compare

That hypothesis fits all the variants I tried:

  • ~c - 672478 → no bug (no multiplication)
  • (~c) * 672479 → bug
  • 0xA42DF / ~c → bug (exception says min = 0; division produces a different range issue)

Notes

  • Triggered by ReifyCs (sibling repo), seed 22004304, profile stage2_intrinsic. The original program also exercised Stage 2 multi-function composition, but the bug reproduces in a single function with no helpers (shown above).
  • Both Math.Clamp calls are needed; removing the outer one suppresses the bug.
  • (~c) is required; substituting a literal -1 or (c - 1) does not trigger.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions