-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expose and implement generic math interfaces on core types #54650
Conversation
…atic Abstracts in Interfaces
- Disable constraint checking during EEInit - Disable il linker running on CoreLib - Fixup generic math tests to actually build
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to 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. |
I couldn't figure out the best area label to add to this PR. If you have write-permissions please help me learn by adding exactly one area label. |
CC. @ViktorHofer, @ericstj for the pinning of the Roslyn version. CC. @jeffhandley, @pgovind, @stephentoub for the library side changes CC. @MadsTorgersen, @davidwrighton, @terrajobst as an FYI that this part is up for review. |
Tagging subscribers to this area: @dotnet/area-system-numerics Issue DetailsThis exposes the library side of the Static Abstracts in Interfaces feature as per dotnet/designs#205. This requires pinning the Roslyn version as This does not currently include anything more than the basic tests. I have more tests locally, but they are not ready to be pushed quite yet.
|
@tannergooding I was looking at the failures in the test results, and it looks like there is a bug with static virtual method implementations existing on a type exposed to COM. I'll take a look and see what I can do about that. |
classcompat loader failures are fixed by #54658 I'll get that pushed through tomorrow. |
|
||
// [RequiresPreviewFeatures] | ||
// static checked byte IAdditionOperators<byte, byte, byte>.operator +(byte left, byte right) | ||
// => checked((byte)(left + right)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the functions commented out? What's the plan for them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are dependent on one or more of the proposed language features (such as checked operators
) being approved and implemented. None of these were on the table for .NET 6.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about adding this to the top of the comment block then:
// TODO - Uncomment once checked operator support is added to C#
// https://github.com/dotnet/csharplang/issues/4665
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's going to be a lot of additional commentation for likely little benefit.
For .NET 7, this will all be touched anyways to remove the [RequiresPreviewFeatures]
annotations and making bits public, at which point these will either be uncommented (if they make it in) or removed (if LDM rejects the features).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough. Thanks for considering it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My preference is to delete such code until such time that the language feature is agreed upon and exists and we decide to and are able to add such features to the interfaces. It's otherwise confusing to have such code in but commented out, especially without an explanation.
// IAdditionOperators | ||
// | ||
|
||
[RequiresPreviewFeatures] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Annotating the interface itself as preview isn't sufficient? We also need to annotate all explicit implementations of that interface? cc: @pgovind
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missed this notification in my email. We don't need to. We can also just suppress the analyzer here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to. We can also just suppress the analyzer here.
Does this mean the analyzer currently requires it but you agree it shouldn't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean the analyzer currently requires it
Yup. The analyzer does single level lookups right now and tags methods/properties etc when it find the attribute on them (or the class/interface). It works well for implicit implementations. For explicit implementations I need to think about it some more. If we had a 3rd party library author implement IAdditionOperators
explicitly, I think having a diagnostic at the implementation is good. That way the library author knows that they are implementing a preview interface. I'm hoping to get dotnet/roslyn-analyzers#5155 in soon though and we can iterate on the design more concretely once we start consuming it in the runtime.
} | ||
else if (typeof(TOther) == typeof(char)) | ||
{ | ||
return checked((byte)(char)(object)value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are all of these checked?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unchecked appears to be the CreateTruncated instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should revisit these names then. In general unchecked is the default for C#, and you have to opt in to checked. It's strange then for the APIs to invert that by having the "Create" name be checked and having to append a suffix on it to effectively opt-in to unchecked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unchecked
is the default for C# yes, unless you are dealing with literals or many other forms of creation.
I believe it will ultimately be a pit of failure for a user to write T x = T.Create(<TooBigLiteral>)
and for it to not fail by default. This is why the compiler emits CS0221 (Constant value '###' cannot be converted to a 'T' (use 'unchecked' syntax to override
) for such a scenario
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It certainly threw me off, that normal Create was checked. Most important is both are available. But agree naming seems a bit off then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it will ultimately be a pit of failure for a user to write T x = T.Create() and for it to not fail by default.
I similarly think it'll be a pit of failure for a user to write T x = T.Create(variable)
and have that use checked arithmetic and throw overflow exceptions and the like. We can't emulate the language's compile-time validation, but we can map to its run-time defaults.
// | ||
|
||
[RequiresPreviewFeatures] | ||
static byte ISpanParseable<byte>.Parse(ReadOnlySpan<char> s, IFormatProvider? provider) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we'd discussed just combining ISpanParseable and IParseable. Did we decide not to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We hadn't formalized this in API review and there were still open questions here about F# and VB to my understanding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When are we going to decide on it then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sometime before this ships as stable, so possibly after .NET 6.
That change is right 👍 Thanks |
|
||
[RequiresPreviewFeatures] | ||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
static bool INumber<char>.TryCreate<TOther>(TOther value, out char result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still question whether this is the right creation direction. This requires a type T to know how to create itself from another type TOther, which means T needs to know about TOther. That prevents, for example, Int64 being able to create itself from BigInteger. If it were the other way around, BigInteger could know how to create an instance of any type it can see, so it could implement the cast to Int64.
Is that direction covered elsewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You result in the same fundamental issue in either direction. For an arbitrary, unknown U
, you might not be able to create or convert to a given T
.
There are some directions where going from U
to T
is easier and some where going from T
to U
will be easier. Right now the interfaces are only exposing "constructor" equivalents (create a T
from a U
, rather than convert a U
to a T
) because exposing both raises complications in determining the right way/order to cover both scenarios and avoid infinite recursion issues.
That prevents, for example, Int64 being able to create itself from BigInteger
This is going to be solved by IBinaryInteger.TryWriteBytes
and related methods. At some point, you have to be able to fallback to something "generic enough" so you don't need to codify "every possible type".
Having something like IConvertibleTo<TOther>
would help, but its also not something that the core types can reasonably implement due to additional startup cost and number of conversions between all 13 primitive types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, TryWriteBytes
isn't in this PR, that isn't right 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For an arbitrary, unknown U, you might not be able to create or convert to a given T.
No, but you have a much higher chance of doing so when the thing with the data to be converted is able to reference the thing to be created.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, but you have a much higher chance of doing so when the thing with the data to be converted is able to reference the thing to be created.
Can you elaborate, preferably with examples, of why ConvertTo
has a higher chance of success than CreateFrom
? My expectation is that you ultimately need both and that they should be roughly equivalent given internal implementation details.
The biggest problem is the pit of failure for having some Create
method, without DIMs, that will correctly try both (to give the highest chance of success) without risking any kind of infinite recursion (or a potentially unbounded number of interfaces to check), etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This all comes down to: "We need both in practice". There are flaws and limitations to just one or the other and so we ultimately need both CreateFrom
and ConvertTo
style APIs to ensure the relevant scenarios are covered.
@stephentoub and I did discuss this a bit last week and it will likely come down to some pattern where we tell users that you shouldn't have CreateFrom
call ConvertTo
or vice-versa, to avoid the issue with potential infinite recursion (imagine CreateFrom<TOther>
falls back to calling value.ConvertTo<TSelf>
for an unrecognized TOther
, and value.ConvertTo<TOther>
falls back to TOther.CreateFrom<TSelf>
).
For implementors, there should likely be some recommended pattern to ensure that both functions cover all the relevant items. My initial thought is that we have:
static TSelf Create<TOther>(TOther value)
static bool TryCreate<TOther>(TOther value, out TSelf result)
static bool TryConvertFrom<TOther>(TOther value, out TSelf result)
static bool TryConvertTo<TOther>(TSelf value, out TOther result)
Where the standard pattern is something like:
public static TSelf Create<TOther>(TOther value)
{
if (!TryCreate(value, out TSelf result))
{
throw new NotSupportedException();
}
return result;
}
public static bool TryCreate<TOther>(TOther value, out TSelf result)
{
return TryConvertFrom<TOther>(value, out result)
|| TOther.TryConvertTo<TSelf>(value, out result);
}
public static bool TryConvertFrom<TOther>(TOther value, out TSelf result)
{
if (typeof(TOther) == typeof(...))
{
// ..., any TOther where ConvertFrom is most efficient
}
else
{
return false;
}
}
public static bool TryConvertTo<TOther>(TSelf value, out TOther result)
{
if (typeof(TOther) == typeof(...))
{
// ..., any TOther where ConvertTo is most efficient
}
else
{
return false;
}
}
This would extend to each Create*
pattern and so would include "exact", "saturating", "truncating", etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need both Create and TryCreate variants, or could we just have the TryCreate? Could Create be a DIM if/when we decide we need it? For that matter could TryCreate be a DIM and you only implemenet TryConvertFrom/To?
Also, you talked about the fallback of being able to convert to/from bytes. Where does that fit into this pattern?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need both Create and TryCreate variants, or could we just have the TryCreate
We could probably have just TryCreate
but that might make it less idiomatic to use in expressions. I don't have a strong preference here.
For that matter could TryCreate be a DIM and you only implemenet TryConvertFrom/To?
That would be ideal, but is out of scope for .NET 6. It would be addressed before we ship for real based on whether or not DIMs are in the official release
Also, you talked about the fallback of being able to convert to/from bytes. Where does that fit into this pattern?
You would handle it in one or both of the TryConvert*
APIs as one of the typeof() == typeof()
checks. Basically:
- Handle any concrete type
- Handle any abstract type via things like IBinaryInteger where you can get the raw 2s complement representation as bytes
return false
This, for value type TOther
, will compile down very efficiently. For reference types there will be casts and checks but still not bad overall.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could probably have just TryCreate but that might make it less idiomatic to use in expressions
Fair enough.
What's your plan for factoring this all in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to get this PR merged so we have the base bits in and ready. Then to follow up with a PR that adds in TryConvert*
, ideally before Preview 7, but possibly for RC1.
IMinMaxValue<DateTimeOffset>, | ||
ISpanParseable<DateTimeOffset>, | ||
ISubtractionOperators<DateTimeOffset, TimeSpan, DateTimeOffset>, | ||
ISubtractionOperators<DateTimeOffset, DateTimeOffset, TimeSpan> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In practice, with such a specific combination of generic type arguments, how will this be used in practice? i.e. what's an example of a method that would successfully operate over this interface and not be tied directly to DateTimeOffset?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
APIs like a generic Sum
or Clamp
can still take advantage. Other APIs may indeed not be as usable for cases like DateTimeOffset
or may only be usable if you can customize more than just one input type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm particularly fond of Clamp
in this scenario.
Are we planning to add any APIs that use these interfaces? |
@lambdageek, I'm not sure disabling the tests is going to be feasible. This impacts 75 test assemblies and since its resulting in a crash an unknown number of tests. |
Oh this is impacting other testsuites, not just the new ones in this PR :-( @vargaz can we avoid the assert in cases where the static virtual interface methods aren't actually called? |
@lambdageek, @vargaz. Just pinging on this as we'd ideally get this in sooner to get the extra validation on any startup or size regressions and general E2E integration. |
#54981 will hopefully fix the assert. |
5dff01c
to
aadd7fd
Compare
Thanks! I've temporarily cherry-picked it to get some early validation and confirm that everything will pass once merged. |
aadd7fd
to
5f60387
Compare
@@ -17,6 +17,8 @@ | |||
<DotNetFinalVersionKind Condition="'$(StabilizePackageVersion)' == 'true'">release</DotNetFinalVersionKind> | |||
<!-- Opt-in/out repo features --> | |||
<UsingToolMicrosoftNetCompilers>true</UsingToolMicrosoftNetCompilers> | |||
<!-- TODO: Upgrade compiler version to enable Static Abstracts in Interfaces; remove this once the employed SDK uses a new enough version. --> | |||
<MicrosoftNetCompilersToolsetVersion>4.0.0-2.21323.11</MicrosoftNetCompilersToolsetVersion> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're going to push any more changes, might be worth bumping this up to the latest available at the time.
|
||
[RequiresPreviewFeatures] | ||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
static byte INumber<byte>.CreateSaturating<TOther>(TOther value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is "Saturating" the right name here? Don't we use "Clamp" elsewhere for the same concept?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends on the API I think. We use Saturating
in a few places, such as in the hardware intrinsics and its the "correct" term.
I think clamping
is generally for if the user can specify the boundary values.
Assert.Equal(expected: 536854528, actual: GenericMath.Sum<int, int>(values)); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd feel a whole lot more comfortable having 15000 lines of product code merged if there were more than 110 lines of test code.
where TResult : INumber<TResult> | ||
{ | ||
TResult sum = Sum<TSelf, TResult>(values); | ||
return TResult.Create(sum) / TResult.Create(values.Count()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just adding a note that once we start consuming the DetectPreviewFeaturesAnalyzer, we'd need to opt into preview features in this csproj.
|
||
namespace System.Tests | ||
{ | ||
public sealed class Int32GenericMathTests : GenericMathTests<int> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to write a text template file to generate tests? This is a huge PR, it's not easy to see how much code coverage we have here :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In answer to both yours and @stephentoub comment: #54650 (comment)
I've done a lot of testing locally, but a lot of this was "ad-hoc" and "generated" to validate various scenarios. I'm still working on porting these tests to be xUnit based and that's the main reason they aren't in this PR. I had, ideally, wanted to get this in so it makes the preview 7 cutoff and to try and get the tests up early next week.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Library changes look good to me. We should consider having a text template file generate the tests though?
There is a failure in the wasm aot tests, which is expected, since there is little support for generic sharing + static virtual methods right now on aot. I'd suggest disabling those tests for wasm+aot. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests should work on the mono interpreter, just no (yet) on mono AOT
Co-authored-by: Aleksey Kliger (λgeek) <akliger@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
After discussing with @jeffhandley, we're going to merge this now to ensure it can flow early and isn't going into Preview 7 last minute. The test PR will be going up early to mid next week after I finish porting them over to use xUnit. |
Looks like this PR increased System.Private.CoreLib.dll size from 9.66 to 9.94 Mb (Windows.x64.Release) |
This exposes the library side of the Static Abstracts in Interfaces feature as per dotnet/designs#205.
This requires pinning the Roslyn version as
main-vs-deps
won't integrate into another branch until sometime next week and then that will need to flow through Arcade (which is currently onrelease/dev16.10-vs-deps
, whilemain-vs-deps
will integrate into somerelease/dev17-*-vs-deps
branch).This does not currently include anything more than the basic tests. I have more tests locally, but they are not ready to be pushed quite yet.