Skip to content

Fix BigInteger.LeadingZeroCount, PopCount, TrailingZeroCount, RotateLeft, and RotateRight for platform-independent 32-bit word semantics#126259

Merged
tannergooding merged 22 commits intomainfrom
copilot/fix-leading-zero-count-implementation
Apr 25, 2026
Merged

Fix BigInteger.LeadingZeroCount, PopCount, TrailingZeroCount, RotateLeft, and RotateRight for platform-independent 32-bit word semantics#126259
tannergooding merged 22 commits intomainfrom
copilot/fix-leading-zero-count-implementation

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 28, 2026

After the _bits migration from uint[] to nuint[] (#125799), several IBinaryInteger<BigInteger> methods returned platform-dependent results. This PR restores consistent 32-bit word semantics for all five affected methods, including both the _sign path and the _bits path.

Description

Changes

LeadingZeroCount:

  • _sign path: nint.LeadingZeroCountuint.LeadingZeroCount((uint)value._sign)
  • _bits path: BitOperations.LeadingZeroCount(value._bits[^1]) & 31 to map to 32-bit word semantics

PopCount:

  • _sign path: nint.PopCount(value._sign)int.PopCount(value._sign)
    • Fixes PopCount(-1) returning 64 on 64-bit vs 32 on 32-bit
  • _bits negative path: Uses standard nuint two's complement with MSL (most-significant limb) upper-32-bit correction. On 64-bit, the MSL's upper 32 bits may contain sign-extension 1s that don't correspond to real 32-bit words, so the correction subtracts BitOperations.PopCount(bits[^1] >> BitsPerUInt32) * (BitsPerLimb - BitsPerUInt32) to account for those phantom bits. Uses IndexOfAnyExcept((nuint)0) for efficient first non-zero limb detection.

TrailingZeroCount:

  • _sign path: nint.TrailingZeroCount(value._sign)int.TrailingZeroCount(value._sign)
    • Fixes TrailingZeroCount(BigInteger.Zero) returning 64 on 64-bit vs 32 on 32-bit

RotateLeft:

  • _sign path: BitOperations.RotateLeft((nuint)value._sign, rotateAmount)uint.RotateLeft((uint)value._sign, rotateAmount) with new BigInteger((int)rs) / new BigInteger(rs) result construction
    • Fixes RotateLeft(BigInteger.One, 32) returning 2^32 on 64-bit vs 1 on 32-bit
  • _bits path: The rotation operates directly on nuint limbs using the original SIMD-accelerated algorithm. On 64-bit, a last-index fixup handles the edge case where the upper 32 bits of the final limb are zero (positive values) or the upper 33 bits are all ones (negative values after two's complement), indicating those bits are "extra" compared to uint[] storage. When the effective 32-bit word count is odd, SwapUpperAndLower uses MemoryMarshal.Cast<nuint, uint> for the digit swap at the mid-nuint boundary, while all shift operations use the existing SIMD-accelerated nuint overloads directly. On 32-bit, nuint == uint so the algorithm is unchanged.

RotateRight:

  • _sign path: BitOperations.RotateRight((nuint)value._sign, rotateAmount)uint.RotateRight((uint)value._sign, rotateAmount) with new BigInteger((int)rs) / new BigInteger(rs) result construction
    • Fixes RotateRight(BigInteger.One, 1) returning 2^63 on 64-bit vs 2^31 on 32-bit
  • _bits path: Same Rotate() approach as RotateLeft (shared helper)

Performance: The rotation _bits path operates directly on nuint limbs, preserving full 64-bit throughput with SIMD-accelerated shifts. Only the SwapUpperAndLower digit-swap step uses a Span<uint> view (via MemoryMarshal.Cast) when the swap boundary falls mid-nuint on 64-bit with an odd word count. The Rotate() helper works directly in the final result nuint[] buffer allocated via RentedBuffer, avoiding any temporary array allocation.

Endianness safety: On big-endian 64-bit platforms, the SwapUpperAndLower helper swaps the two 32-bit halves within each nuint limb before and after performing uint-level digit swapping, ensuring correct word order regardless of endianness. On 32-bit platforms, nuint and uint are the same size so no special handling is needed.

BigIntegerCalculator helpers:

  • The existing nuint RotateLeft / RotateRight / SwapUpperAndLower / LeftShiftSelf / RightShiftSelf overloads are used as the primary rotation API, with full Vector128/256/512 SIMD acceleration for shifts
  • SwapUpperAndLower uses MemoryMarshal.Cast<nuint, uint> for digit swapping when the swap boundary falls mid-nuint on 64-bit with an odd word count
  • SwapUpperAndLower uses RentedBuffer for temporary storage, matching the pattern used elsewhere in BigInteger

Tests: Comprehensive platform-independent test cases added to existing BigIntegerTests.GenericMath.cs for all five methods, covering _sign path values, _bits path boundary values (32-bit word boundaries), and negative multiword values including mixed-bit cases exercising the two's complement PopCount formula. All pre-existing Rotate.cs tests updated from platform-dependent nint.Size * 8 to fixed 32-bit semantics.


⌨️ Start Copilot coding agent tasks without leaving your editor — available in VS Code, Visual Studio, JetBrains IDEs and Eclipse.

@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

Comment thread src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Outdated
Comment thread src/libraries/System.Runtime.Numerics/tests/BigInteger/LeadingZeroCountTests.cs Outdated
@stephentoub stephentoub marked this pull request as ready for review March 28, 2026 18:10
Copilot AI review requested due to automatic review settings March 28, 2026 18:10
@stephentoub
Copy link
Copy Markdown
Member

@tannergooding, is the .NET 10 behavior that uses the upper 32-bit word what we want?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR restores platform-independent, 32-bit word–based semantics for BigInteger.LeadingZeroCount after the internal _bits storage migrated from uint[] to nuint[], and adds/updates tests to lock in the intended behavior.

Changes:

  • Update BigInteger.LeadingZeroCount to always compute LZC using 32-bit word semantics (including on 64-bit platforms).
  • Fix existing generic-math test expectations to be platform-independent (32/31 for Zero/One).
  • Add a dedicated LeadingZeroCountTests suite and include it in the Numerics test project.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Adjusts LeadingZeroCount to use 32-bit word semantics for both _sign and _bits representations.
src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs Updates assertions to expect platform-independent results for LeadingZeroCount(Zero/One).
src/libraries/System.Runtime.Numerics/tests/BigInteger/LeadingZeroCountTests.cs Adds coverage for boundary and platform-independence scenarios (small/large, positive/negative).
src/libraries/System.Runtime.Numerics/tests/System.Runtime.Numerics.Tests.csproj Includes the newly added LeadingZeroCountTests.cs in the test build.
Comments suppressed due to low confidence (1)

src/libraries/System.Runtime.Numerics/tests/BigInteger/LeadingZeroCountTests.cs:67

  • Same as above: this parse call would be clearer and more consistent with the surrounding test suite if it used using System.Globalization; + NumberStyles.HexNumber instead of the Globalization. qualifier.
            // Parse the magnitude as a positive hex value (leading zero keeps high bit clear),
            // then negate it so the result is negative and stored in _bits.
            BigInteger magnitude = BigInteger.Parse(hexMagnitude, Globalization.NumberStyles.HexNumber);
            BigInteger value = -magnitude;

Comment thread src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Outdated
Comment thread src/libraries/System.Runtime.Numerics/tests/BigInteger/LeadingZeroCountTests.cs Outdated
@github-actions

This comment has been minimized.

Copilot AI and others added 3 commits March 28, 2026 15:23
…into GenericMath

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/82e43e2a-a6a2-4c40-ad57-ffaa52c2cc18

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
…rd semantics

- PopCount _sign path: nint.PopCount -> int.PopCount (fixes platform-dependent
  results for small negative values like -1 returning 64 on 64-bit vs 32 on 32-bit)
- PopCount _bits negative path: replace inline two's complement with formula
  PopCount(2^W - m) = W - PopCount(m) - TZC(m) + 1 using 32-bit word width W
  (fixes ~nuint filling upper 32 bits with 1s on 64-bit)
- TrailingZeroCount _sign path: nint.TrailingZeroCount -> int.TrailingZeroCount
  (fixes TZC(0) returning 64 on 64-bit vs 32 on 32-bit)
- LeadingZeroCount: replace magic constant 32 with BitsPerUInt32
- Add comprehensive tests for all three methods covering _sign path, _bits path,
  large positive/negative values, and platform-independence invariants

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@stephentoub stephentoub changed the title Fix BigInteger.LeadingZeroCount to use 32-bit word semantics after nuint[] migration Fix BigInteger.LeadingZeroCount, PopCount, and TrailingZeroCount for platform-independent 32-bit word semantics Mar 28, 2026
Comment thread src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Outdated
Replace the Environment.Is64BitProcess branch with the simpler
BitOperations.LeadingZeroCount(value._bits[^1]) & 31 expression.
The & 31 maps 64-bit LZC to 32-bit word semantics: when the upper
half is zero, LZC is 32 + uint_lzc, and (32 + x) & 31 == x.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@stephentoub stephentoub force-pushed the copilot/fix-leading-zero-count-implementation branch from b0ca7bb to d9c38b9 Compare March 28, 2026 23:02
@github-actions

This comment has been minimized.

@stephentoub
Copy link
Copy Markdown
Member

@copilot also address RotateLeft/RotateRight and any other similarly affected methods on BigInteger. Ensure comprehensive tests are added/updated in a manner consistent with the other APIs in this PR.

…pdate tests

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/0c384115-0987-41ef-b9d1-34ac2bb67e7d

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 29, 2026

@copilot also address RotateLeft/RotateRight and any other similarly affected methods on BigInteger. Ensure comprehensive tests are added/updated in a manner consistent with the other ...

Fixed RotateLeft and RotateRight _sign paths in d9749d7 — replaced BitOperations.RotateLeft/Right((nuint)value._sign, ...) with uint.RotateLeft/Right((uint)value._sign, ...) and updated result construction to use (int)rs/(uint)rs. Updated all platform-dependent test assertions (nint.Size * 8 expressions → fixed 32-bit values) and added comprehensive tests for _sign path values.

I also verified that Log2 is NOT affected — the platform dependencies in BitsPerLimb and BitOperations.LeadingZeroCount cancel each other out, producing correct results on both platforms.

Note: The pre-existing Rotate.cs tests (RunSmallTests, RunRotateTests) fail because the _bits path Rotate() helper also has platform-dependent behavior when operating on nuint[] limbs (e.g., two's complement conversion sign-extends differently on 64-bit). This is a broader issue separate from the _sign path fixes in this PR.

Copilot AI changed the title Fix BigInteger.LeadingZeroCount, PopCount, and TrailingZeroCount for platform-independent 32-bit word semantics Fix BigInteger.LeadingZeroCount, PopCount, TrailingZeroCount, RotateLeft, and RotateRight for platform-independent 32-bit word semantics Mar 29, 2026
@stephentoub
Copy link
Copy Markdown
Member

Note: The pre-existing Rotate.cs tests (RunSmallTests, RunRotateTests) fail because the _bits path Rotate() helper also has platform-dependent behavior when operating on nuint[] limbs (e.g., two's complement conversion sign-extends differently on 64-bit). This is a broader issue separate from the _sign path fixes in this PR.

@copilot, can you elaborate? If it wasn't platform-dependent before #125799, then I need you to fix it in this PR.

Remove the "32" suffix from RotateLeft32, RotateRight, SwapUpperAndLower32,
LeftShiftSelf32, and RightShiftSelf32 since the nuint-based versions no
longer exist. Add clearer documentation explaining that the rotation
operates on a Span<uint> view of the nuint[] buffer, with shift operations
delegating to the SIMD-accelerated nuint overloads while carry extraction
and digit swapping operate at 32-bit word granularity.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/f2568909-19c7-4a74-9b63-a849eee3dce8

Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 23, 2026 18:25
…r, 32-bit uses nuint path, 64-bit uses uint view with inline rotation

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/fc887d25-0f15-446b-b265-1ff699c595ee

Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 23, 2026 21:15
…ke nuint RotateLeft/Right overloads private

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/fc887d25-0f15-446b-b265-1ff699c595ee

Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com>
…wer feedback

On 64-bit with even word count: standard nuint rotation, absorbing odd
32-bit digit shifts into the nuint small shift (0..63).

On 64-bit with odd word count (half-used last limb): nuint SIMD shift
with carry fixup, uint-level digit swap only where the swap boundary
may fall mid-nuint.

Two's complement conversion uses NumericsHelpers.DangerousMakeTwosComplement
at nuint level with half-limb clearing.

Removed LeftShiftSelf(Span<uint>) and RightShiftSelf(Span<uint>) since
the bit-shift now operates at nuint level for all cases.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/e72bd9b4-df13-46c5-8180-72d9cc2022f2

Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 23, 2026 22:49
@tannergooding
Copy link
Copy Markdown
Member

This is still not as small of a diff as it should be, but I'm tired of fighting with copilot on the simpler fix for Rotate. I'll follow up to remove the unnecessary code duplication in a separate PR.

@tannergooding
Copy link
Copy Markdown
Member

/ba-g unrelated mono ios timeout

@tannergooding tannergooding merged commit 409b346 into main Apr 25, 2026
92 of 94 checks passed
@tannergooding tannergooding deleted the copilot/fix-leading-zero-count-implementation branch April 25, 2026 00:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants