Implement Decimal32/64/128 arithmetic and comparison operators#130508
Implement Decimal32/64/128 arithmetic and comparison operators#130508tannergooding wants to merge 17 commits into
Conversation
Adds the IUnaryPlusOperators and IUnaryNegationOperators operators to Decimal32, Decimal64, and Decimal128 as part of dotnet#81376. Unary plus returns the value unchanged; unary negation toggles the sign bit, matching the IEEE 754 negate operation for finite values, infinities, and NaN. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add binary operator + and operator - for the IEEE 754 decimal types via a shared generic Number.AddDecimalIeee754 helper. The helper aligns operands to the smaller (preferred) exponent using exact base-10 digit arithmetic, folds truly-dropped low-order digits into a sticky flag, and feeds the exact sum into the existing NumberToDecimalIeee754Bits round-nearest-ties-even pipeline. Subtraction negates the right operand's sign bit and adds. Infinity results are canonicalized. Adds exact-bit test vectors validated against an independent oracle. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add IEEE 754 == and != operators to Decimal32, Decimal64, and Decimal128. A new shared Number.EqualsDecimalIeee754<TDecimal,TValue> helper returns false when either operand is NaN (NaN is unordered and never equal, even to itself) and otherwise defers to the existing CompareDecimalIeee754 for value-based equality, so the two zeros compare equal and different members of a cohort compare equal. operator != is defined as !(left == right). The ref assembly also declares the Equals(object)/GetHashCode overrides that the concrete types already provide (required once == / != are present). Tests use bit-exact MemberData vectors produced by an independent Python decimal oracle, covering NaN, signed zero, cohort equality, and non-canonical infinity inputs, plus random equal-cohort and unequal pairs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds IEEE 754 relational operators (<, >, <=, >=) to Decimal32, Decimal64, and Decimal128. NaN is unordered, so any comparison involving NaN returns false; non-NaN operands use the existing CompareDecimalIeee754 value ordering (+0 == -0, cohort members equal, -Inf < finite < +Inf). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds `operator *` for the IEEE 754 decimal types. Coefficients are multiplied exactly in base 10 (a product can span up to twice the format precision, exceeding UInt128 for Decimal128, so digit spans are used rather than the underlying integer width). The product exponent is the sum of the operand exponents, which is also the IEEE 754 preferred exponent since the product's trailing zeros are retained; the exact digit string is then rounded through the shared NumberToDecimalIeee754Bits pipeline. Also fixes two latent issues in the shared encoding pipeline that only a multiply product can reach: * NumberToDecimalIeee754Bits underflow branch assumed at most Precision significant digits. A wide product whose quantum falls just below the minimum adjusted exponent can carry more, so the digit count is now capped to Precision (a single correct rounding of the full product; a no-op for <=Precision inputs from parse/add/sub). * DecimalIeee754FiniteNumberBinaryEncoding clamped a zero coefficient's exponent to MaxExponent instead of MaxAdjustedExponent. A zero result with a preferred exponent above the maximum quantum (e.g. zero times a large-exponent value) overflowed the biased exponent field. Add/sub never reach this because their zero-path exponents are bounded by the operand quanta. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds the op_Increment (++) and op_Decrement (--) operators for the IEEE 754 decimal types. These are implemented as value + 1E0 / value - 1E0 by reusing the existing Number.AddDecimalIeee754 helper, matching the binary +/- operators. A OneValue constant (canonical 1E0) is added to each type. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The internal Add/Multiply/Divide cores for the IEEE 754 decimal types previously serialized significands into per-digit ASCII byte spans and performed base-10 "string" arithmetic, which made the operators far too slow to be useful. Rewrite the arithmetic to operate directly on the native limb type (uint/ulong/UInt128): - Multiply computes the full double-width product via WideMultiply and rounds through the shared word-level encoder. - Add aligns operands using single-limb scaling into a (Hi,Lo) pair, then performs a wide add (same sign) or compare-and-subtract (opposite sign) with correct sticky-bit propagation. - Divide uses single-limb decimal long division, producing the quotient one digit at a time with an exact-result trailing-zero strip toward the preferred exponent. All three feed NumberToDecimalIeee754BitsFromWide, which mirrors the existing NumberToDecimalIeee754Bits rounding (round-half-even, subnormal capping to avoid double rounding, overflow-to-infinity, exact preferred exponent). The now-dead Big* digit-span helper cluster is removed. Validated against the Intel BID reference oracle with large randomized batches (39235 vectors across all three ops and formats) plus the existing committed test suite, all passing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Decimal32/64/128 operator declarations were previously hand-added to the reference assembly in a non-conformant style (non-partial, unqualified names, hand-ordered members, missing the implicit IParsable interface). Per docs/coding-guidelines/updating-ref-source.md, regenerate these three types with the GenAPI tool so the reference source matches tool output (fully-qualified names, partial structs, canonical member ordering), while filtering out the unrelated GenAPI churn to the rest of the file. No public API surface changes; this only normalizes the reference source. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ByTen The Intel reference implementation divides by powers of ten using precomputed reciprocals; this helper uses direct division for now. Add a remark documenting the potential future optimization. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…OTICES The Decimal32/64/128 arithmetic is based on Intel's BSD 3-Clause licensed reference implementation. Reproduce the copyright and license text as required by the license. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Tagging subscribers to this area: @dotnet/area-system-numerics |
There was a problem hiding this comment.
Pull request overview
Adds IEEE 754 decimal operator support for System.Numerics.Decimal32/Decimal64/Decimal128, implemented via shared BID-style arithmetic helpers in System.Number, with corresponding ref-surface updates and tests.
Changes:
- Added arithmetic/comparison operator implementations to the three decimal IEEE 754 types.
- Introduced/extended shared
Number.DecimalIeee754helpers for equality and relational comparisons, plus add/mul/div integer-significand arithmetic. - Updated
System.Runtimeref surface and added test vectors for the new operators; added the Intel Decimal FP Math Library notice.
Show a summary per file
| File | Description |
|---|---|
| THIRD-PARTY-NOTICES.TXT | Adds BSD 3-Clause notice for Intel Decimal Floating-Point Math Library. |
| src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal32Tests.cs | Adds operator test vectors (unary, add/sub, comparisons, mul/div, ++/--). |
| src/libraries/System.Runtime/ref/System.Runtime.cs | Updates public ref surface for Decimal32/64/128 to include new operators. |
| src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs | Adds operator implementations forwarding to Number.DecimalIeee754 helpers. |
| src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs | Adds operator implementations forwarding to Number.DecimalIeee754 helpers. |
| src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs | Adds operator implementations forwarding to Number.DecimalIeee754 helpers. |
| src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs | Adds IEEE 754 equality/relational helpers and BID-style add/mul/div implementations plus wide rounding support. |
Copilot's findings
- Files reviewed: 8/9 changed files
- Comments generated: 6
…-ieee754-operators # Conflicts: # src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs # src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs # src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs # src/libraries/System.Runtime/ref/System.Runtime.cs # src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal128Tests.cs # src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal32Tests.cs # src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal64Tests.cs
IEEE 754 BID specifies that a finite encoding whose significand exceeds the maximum representable coefficient (10^p - 1) is non-canonical and represents zero. The generic UnpackDecimalIeee754 helper decoded the raw significand without applying this rule, so arithmetic and comparisons on such bit patterns could misbehave (e.g. finite/nc not treated as division by zero). Add a single clamp inside UnpackDecimalIeee754, matching unpack_BID32/64/128 in the Intel Decimal Floating-Point Math Library, so every downstream consumer (Add/Subtract/Multiply/Divide, Compare, GetHashCode) handles all finite bit patterns correctly. Also fix copy/pasted addition comments in op_Subtraction_TestData across all three test files so they describe subtraction operands and outcomes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
In NumberToDecimalIeee754BitsFromWide the expensive WideDigitCount was computed before the exponent > MaxExponent early return, so the cost was paid even when immediately returning +/-Infinity. Move the overflow check ahead of the digit count. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…e754 The addition alignment scale factor 10^effectiveDifference was always computed via a multiply-by-ten loop. Reuse the format's existing Power10 lookup table: exponents in range (0..Precision-1) index it directly, and the larger alignment exponents (Precision..Precision+2, which the table does not cover) reduce by the largest table entry each iteration and finish with a single lookup for the remainder. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The op_Increment_TestData and op_Decrement_TestData comments were copy/pasted, so several described the wrong operator (e.g. decrement cases annotated with '++ -> ...'). Rewrite each dataset's comments to describe only its own operator's semantics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The op_Equality_TestData 'all-nines' case described the result with '!=' even though the table asserts '=='; correct it to match the operator under test. Also replace the ignored 'expected' parameter in Decimal128.op_UnaryPlus (which reuses UnaryNegation_TestData because UInt128 cannot be an InlineData constant) with a discard parameter, removing the redundant assignment. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
| int bits = TValue.Zero.GetByteCount() * 8; | ||
| int half = bits / 2; | ||
| TValue lowMask = (TValue.One << half) - TValue.One; | ||
| TValue baseValue = TValue.One << half; |
| { | ||
| roundDigit = 0; | ||
|
|
||
| for (int i = 0; i < dropCount; i++) |
There was a problem hiding this comment.
Without the full reciprocal tables, you could still do a simple optimization for Decimal32 and Decimal64 by widening them to C# types. If TValue is UInt64 then create a UInt128 and divide by 10^dropCount. That should already be in the power of 10 table and if not it can be calculated by multiplying large enough powers of 10.
| /// direct integer division for simplicity; adopting the reciprocal-multiply tables is a possible future | ||
| /// performance optimization. | ||
| /// </remarks> | ||
| private static int WideDivideByTen<TValue>(ref TValue high, ref TValue low) |
There was a problem hiding this comment.
Same comment here about widening to C# types for when TValue is uint and ulong.
There was a problem hiding this comment.
The Intel implementation has a lot of tests as well. It would probably be worth adding those if not already added. They have different rounding modes, so filtering to just round to even and the specific operation we want to test would give us a good subset of tests.
Implements the operator surface for
Decimal32/Decimal64/Decimal128inSystem.Numerics, continuing the work in #81376. The types previously provided only layout plus parsing/formatting; this adds the arithmetic and comparison operators, built up incrementally (one commit per operator group).Operators added
+/-==/!=)<,<=,>,>=)++/--)Implementation notes
bidNN_add/bidNN_mul/bidNN_div). Each such method carries an attribution header pointing atTHIRD-PARTY-NOTICES.TXT.WideDivideByTen) uses direct division rather than Intel's precomputed reciprocal tables. This is noted in-source as a potential future optimization.Licensing
THIRD-PARTY-NOTICES.TXT.Testing
Decimal{32,64,128}Tests.cs.decimalmodule,ROUND_HALF_EVEN,clamp=1) with 0 failures; the committed tests exercise the smaller in-repo data sets.Ref assembly
System.Runtimeref regenerated via GenAPI perdocs/coding-guidelines/updating-ref-source.md(scoped to the Decimal types).Note
This PR description was drafted with GitHub Copilot.