Skip to content

Add Decimal32, Decimal64, Decimal128#100729

Open
RaymondHuy wants to merge 98 commits into
dotnet:mainfrom
RaymondHuy:issue-81376
Open

Add Decimal32, Decimal64, Decimal128#100729
RaymondHuy wants to merge 98 commits into
dotnet:mainfrom
RaymondHuy:issue-81376

Conversation

@RaymondHuy
Copy link
Copy Markdown
Contributor

Resolve #81376

@ghost
Copy link
Copy Markdown

ghost commented Apr 6, 2024

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@RaymondHuy RaymondHuy marked this pull request as draft April 6, 2024 17:29
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Apr 6, 2024
Comment thread src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs Outdated
@tannergooding
Copy link
Copy Markdown
Member

This is on my radar to take a look at; just noting it might be a bit delayed due to other priorities for the .NET 9 release.

CC. @jeffhandley

@RaymondHuy RaymondHuy marked this pull request as ready for review April 9, 2024 17:21
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

Copilot reviewed 16 out of 16 changed files in this pull request and generated 9 comments.

Comments suppressed due to low confidence (2)

src/libraries/System.Runtime/ref/System.Runtime.cs:1

  • The reference assembly declarations for Decimal32/Decimal64/Decimal128 appear to be missing public members that exist in the implementations (notably public override bool Equals(object? obj) and public override int GetHashCode()). Ref/impl mismatches will break API surface validation and can cause binding inconsistencies. Add these overrides (and any other public members present in the CoreLib implementations) to the ref file so the public contract matches.
    src/libraries/System.Runtime/ref/System.Runtime.cs:1
  • The Decimal32 implementation is annotated with [StructLayout(LayoutKind.Sequential)], but the ref assembly declaration does not include this attribute. Since layout is part of the public interop contract, the ref should mirror it (or the implementation should drop it if not required).

Comment thread src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs Outdated
Comment thread src/libraries/Common/src/System/Number.NumberBuffer.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs Outdated
Comment thread src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal64Tests.cs Outdated
Copilot AI review requested due to automatic review settings May 5, 2026 17:08
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

Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.

Comment thread src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs Outdated
Copilot AI review requested due to automatic review settings May 5, 2026 18:01
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

Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.

Comment thread src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs Outdated
Comment thread src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs
Copilot AI review requested due to automatic review settings May 6, 2026 14:20
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

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/libraries/System.Runtime/ref/System.Runtime.cs:1

  • These new numeric types expose ToString(string?, IFormatProvider?) overloads but do not implement IFormattable. As a result, composite formatting / interpolated string format specifiers (e.g., string.Format(\"{0:N}\", value) or $\"{value:N}\") may ignore the format string and fall back to parameterless ToString(). Consider adding System.IFormattable to Decimal32/Decimal64/Decimal128 (and updating the CoreLib implementations accordingly); optionally also implement ISpanFormattable to match other numerics.

Comment thread src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs Outdated
Comment thread src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Decimal64Tests.cs Outdated
@RaymondHuy
Copy link
Copy Markdown
Contributor Author

Hi @tannergooding I have resolved all Copilot comments. You can re-review it when you have time ;)

private const uint NaNMask = 0x7C00_0000;
private const uint InfinityMask = 0x7800_0000;
private const uint MaxSignificand = 9_999_999;
private const uint MaxInternalValue = 0x77F8_967F; // 9,999,999 x 10^90
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The comment here is potentially confusing as it mismatches the standard value documented, it'd help to have both specified.

In particular you have significand * 10^(exponent-bias), which allows treating significand as a positive integer. But more typically its represented in standard scientific notation and so you get 9.999999 instead of 9999999, this shifts the bias by p-1, where p is 7/16/34 for decimal32/64/128

In this case, that changes it from 9_999_999 * 10^90 to 9.999_999 * 10^96.

I'd recommend just doc'ing this as something like // +9.999_999 * 10^96; aka +9_999_999 * 10^90

public Decimal32(int significand, int exponent)
{
bool isNegative = significand < 0;
uint unsignedSignificand = isNegative ? (uint)(-(long)significand) : (uint)significand;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: This can just be (uint)(-significand). Every negative int except int.MinValue becomes the appropriate positive int when negated. MinValue stays the same, but then when cast to uint becomes the right positive value anyways.

Comment on lines +30 to +36
private static readonly UInt128 PositiveInfinityValue = new UInt128(upper: 0x7800_0000_0000_0000, lower: 0);
private static readonly UInt128 NegativeInfinityValue = new UInt128(upper: 0xf800_0000_0000_0000, lower: 0);
private static readonly UInt128 ZeroValue = new UInt128(0, 0);
private static readonly UInt128 NegativeZeroValue = new UInt128(0x8000_0000_0000_0000, 0);
private static readonly UInt128 QuietNaNValue = new UInt128(0xFC00_0000_0000_0000, 0);
private static readonly UInt128 MaxInternalValue = new UInt128(upper: 0x5FFF_ED09_BEAD_87C0, lower: 0x378D_8E63_FFFF_FFFF);
private static readonly UInt128 MinInternalValue = new UInt128(upper: 0xDFFF_ED09_BEAD_87C0, lower: 0x378D_8E63_FFFF_FFFF);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This had been commented on before and I think got handled, but looks like it was since lost.

The UInt128 cases need to be properties, not static readonly, it will help improve codegen and make it easier for the JIT to see its just constant

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It may even be better to just have the internal Decimal128 constructor take ulong upperBits, ulong lowerBits instead of UInt128 and then you can just have:

public static Decimal128 PositiveInfinity => new Decimal128(0x7800_0000_0000_0000, 0x0000_0000_0000_0000);

1000000,
];

public Decimal32(int significand, int exponent)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This still has the pending comment that it shouldn't be public as I'm not convinced its the right shape and UX. Please make it and the corresponding constructor on the other types internal

Comment on lines +339 to +340
uint comb = decimalBits & NaNMask;
return comb != NaNMask && comb != InfinityMask;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this can do (value & signMask) > Infinity to avoid multiple comparisons or extracting small parts

return ClampExponentOverflow(signed, significand, exponent);
}

if (exponent < TDecimal.MinAdjustedExponent)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think this is correct. For underflow for decimal 32, for example, it's ±1.0 ×10^−101 for subnormals, in conrast to ±1.000000 * 10^−95 for normals (noting this is the x.yyyyy form where its got exactly 1 integral digit)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In particular while while MinAdjustedExponent is correct given TSelf.MinExponent - TSelf.Precision + 1; which is -95 - 7 + 1, so -102 + 1, or `-101, the issue is that you have to account for midpoint rounding

This is simple to explain with float where float.MaxValue + 50 doesn't become infinity, it rather stays float.MaxValue. You have to add at least half the delta to move it "closer" to infinity and make it round over.

Likewise float.Epsilon / float.BitDecrement(2) stays float.Epsilon, it's only for float.Epislon / 2 or greater that it becomes zero.

So I similarly believe that ±0.500000.......1 * 10^−96 becomes epsilon. -- This is notably a tricky edge case for binary floats too and is why we have to track up to 768 digits for double there and have to parse the whole string at least tracking "any non-zero digits", because we have to handle the case where its exactly a midpoint +/- 1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @tannergooding Correct me if I am wrong. Epsilon is the smallest positive value that is greater than zero. In this Decimal32 case I think it should be 1 * 10^-101.
Therefore "So I similarly believe that ±0.500000.......1 * 10^−96 becomes epsilon" I don't think it will become epsilon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Epsilon is the smallest positive value that is greater than zero.

Close. It is rather the smallest representable value that compares greater than zero. The representable here is key because values smaller than it may still round to it.

This can be seen with this trivial program, which showcases that float.Epsilon / 2 produces 0, but float.Epsilon / LargestValueThatComparesLessThanTwo still produces float.Epsilon (despite it clearly having an infinitely precise value that must be less than float.Epsilon):

Console.WriteLine($"{float.Epsilon:G99}");
Console.WriteLine($"{float.Epsilon / float.BitDecrement(2.0f):G99}");
Console.WriteLine($"{float.Epsilon / 2.0f:G99}");

which outputs:

1.40129846432481707092372958328991613128026194187651577175706828388979108268586060148663818836212158E-45
1.40129846432481707092372958328991613128026194187651577175706828388979108268586060148663818836212158E-45
0

You can also see the same thing in the inverse direction as well:

var delta = float.MaxValue - float.BitDecrement(float.MaxValue);
Console.WriteLine($"{float.MaxValue:G99}");
Console.WriteLine($"{float.MaxValue + (delta / 2):G99}");
Console.WriteLine($"{float.MaxValue + (delta / float.BitIncrement(2)):G99}");

which outputs:

340282346638528859811704183484516925440340282346638528859811704183484516925440

This is because MaxValue + (delta / 2) puts it at exactly the midpoint value, so it rounds "up" to infinity. While the value just below that puts it below the midpoint, so despite being greater than MaxValue it rounds back down to MaxValue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

decimal32/64/128 must handle these same edges correctly and so we need to account for the fact that any value > (Epsilon / 2) becomes Epsilon

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @tannergooding, thanks for the detailed explanation.

1 * 10^-101 is representable in Decimal32. Would it be correct to conclude that this should be Epsilon? Its encoded value is:

0_0000_0000_000_0000_0000_0000_0000_0001

For your example, ±0.500000.......1 * 10^-96, I don't think it would round to ±Epsilon if Epsilon means 1 * 10^-101, since this value is much larger than 1 * 10^-101 and should still be representable as a Decimal32 subnormal value.

Copy link
Copy Markdown
Member

@tannergooding tannergooding May 15, 2026

Choose a reason for hiding this comment

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

Would it be correct to conclude that this should be Epsilon

Yes

For your example, ±0.500000.......1 * 10^-96, I don't think it would round to ±Epsilon

Correct, I was commenting in part from memory and used the small normal value by accident

It would be 5.00000.....1 * 10^-102 as the actual boundary, where values less than this become 0 and values greater than or equal to this, but less than Epsilon, round to Epsilon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Numerics community-contribution Indicates that the PR has been added by a community member new-api-needs-documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Add Decimal32, Decimal64, and Decimal128 from the IEEE 754-2019 standard.

9 participants