Skip to content
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

Merged
merged 15 commits into from
Jul 2, 2021

Conversation

tannergooding
Copy link
Member

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 on release/dev16.10-vs-deps, while main-vs-deps will integrate into some release/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.

@dotnet-issue-labeler
Copy link

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, 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.

@dotnet-issue-labeler
Copy link

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.

@tannergooding
Copy link
Member Author

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.

@ghost
Copy link

ghost commented Jun 23, 2021

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

Issue Details

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 on release/dev16.10-vs-deps, while main-vs-deps will integrate into some release/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.

Author: tannergooding
Assignees: -
Labels:

area-System.Numerics, new-api-needs-documentation

Milestone: -

@davidwrighton
Copy link
Member

@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.

@davidwrighton
Copy link
Member

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));
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member Author

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).

Copy link
Member

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.

Copy link
Member

@stephentoub stephentoub Jul 1, 2021

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]
Copy link
Member

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

Copy link
Contributor

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.

Copy link
Member

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?

Copy link
Contributor

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);
Copy link
Member

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?

Copy link
Contributor

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

Copy link
Member

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.

Copy link
Member Author

@tannergooding tannergooding Jun 24, 2021

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

Copy link
Contributor

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.

Copy link
Member

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)
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member Author

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.

@ViktorHofer
Copy link
Member

CC. @ViktorHofer, @ericstj for the pinning of the Roslyn version.

That change is right 👍 Thanks


[RequiresPreviewFeatures]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool INumber<char>.TryCreate<TOther>(TOther value, out char result)
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member Author

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 🤔

Copy link
Member

@stephentoub stephentoub Jun 24, 2021

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.

Copy link
Member Author

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.

Copy link
Member Author

@tannergooding tannergooding Jun 28, 2021

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

Copy link
Member

@stephentoub stephentoub Jun 30, 2021

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?

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member Author

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>
Copy link
Member

@stephentoub stephentoub Jun 24, 2021

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?

Copy link
Member Author

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.

Copy link
Member

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.

@stephentoub
Copy link
Member

Are we planning to add any APIs that use these interfaces?

@tannergooding
Copy link
Member Author

Disable the tests on Mono AOT.

@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.

@lambdageek
Copy link
Member

Disable the tests on Mono AOT.

@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 :-(
Sorry I didn't appreciate that.

@vargaz can we avoid the assert in cases where the static virtual interface methods aren't actually called?

@tannergooding
Copy link
Member Author

@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.

@vargaz
Copy link
Contributor

vargaz commented Jun 30, 2021

#54981 will hopefully fix the assert.

@tannergooding
Copy link
Member Author

#54981 will hopefully fix the assert.

Thanks! I've temporarily cherry-picked it to get some early validation and confirm that everything will pass once merged.

@@ -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>
Copy link
Member

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)
Copy link
Member

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?

Copy link
Member Author

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));
}
}
}
Copy link
Member

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());
Copy link
Contributor

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>
Copy link
Contributor

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 :/

Copy link
Member Author

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.

Copy link
Contributor

@pgovind pgovind left a 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?

@vargaz
Copy link
Contributor

vargaz commented Jul 1, 2021

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.

Copy link
Member

@lambdageek lambdageek left a 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) <[email protected]>
Copy link
Member

@jeffhandley jeffhandley left a 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!

@tannergooding
Copy link
Member Author

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.

@tannergooding tannergooding merged commit fe9a54d into dotnet:main Jul 2, 2021
@EgorBo
Copy link
Member

EgorBo commented Jul 5, 2021

Looks like this PR increased System.Private.CoreLib.dll size from 9.66 to 9.94 Mb (Windows.x64.Release)

@ghost ghost locked as resolved and limited conversation to collaborators Aug 4, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.