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

Mitigate first-class Span breaks #108789

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

jjonescz
Copy link
Member

No description provided.

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Oct 11, 2024
@jjonescz jjonescz added area-codeflow for labeling automated codeflow and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Oct 11, 2024
@@ -305,7 +305,7 @@ public void PeekRef_Empty()
protected override IEnumerable<T> GetEnumerableOf<T>(params T[] contents)
{
ImmutableStack<T> stack = ImmutableStack<T>.Empty;
foreach (T value in contents.Reverse())
foreach (T value in Enumerable.Reverse(contents))
Copy link
Member Author

Choose a reason for hiding this comment

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

This mitigation is only needed in runtime, user code shouldn't break (unless they write code into System namespace).

The break is because MemoryExtensions.Reverse<T>(this Span<T>) is now applicable for array.Reverse(). Because Span is better than IEnumerable by overload resolution rules, code like array.Reverse() would break when going from C# 13 to C# 14. So we added Enumerable.Reverse<T>(this T[]) overload (#107723) which is preferred in normal scenarios for arrays. But it actually doesn't have an effect here since we are in the System namespace, so System.MemoryExtensions is found and then the search stops and doesn't reach System.Linq.Enumerable.

@@ -41,7 +41,7 @@ public static void TryCopyToArraySegmentImplicit()
Memory<int> srcMemory = src;
bool success = srcMemory.TryCopyTo(segment);
Assert.True(success);
Assert.Equal(src, segment);
Assert.Equal(src.AsSpan(), segment);
Copy link
Member Author

Choose a reason for hiding this comment

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

There are some ambiguities in xunit that should be resolved by updating (xunit/xunit#3021) but that seemed like a complex task (to update xunit in dotnet/runtime)

@@ -1165,7 +1165,7 @@ internal static void GetStoreValuesAsStringOrStringArray(HeaderDescriptor descri
else
{
Debug.Assert(length > 1, "The header should have been removed when it became empty");
values = multiValue = new string[length];
values = (multiValue = new string[length])!;
Copy link
Member Author

Choose a reason for hiding this comment

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

Nullability changed a bit where previously user-defined conversions between Spans and arrays were involved and now the built-in implicit span conversions are used. The rules should be actually "better", for example this code was assigning string[] to Span<string?> without nullability warnings.

@@ -13,6 +13,7 @@
<ProjectReference Include="$(CoreLibProject)" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime\src\System.Runtime.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Collections\src\System.Collections.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Memory\src\System.Memory.csproj" />
Copy link
Member Author

@jjonescz jjonescz Oct 11, 2024

Choose a reason for hiding this comment

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

Without this the build would fail with:

error CS0656: Missing compiler required member 'System.MemoryExtensions.AsSpan'

That's because that helper is now used for codegen of the built-in span conversions from string to ReadOnlySpan. Previously the user-defined conversion on string was used, but that's not available on .NET Framework. In this case the conversion is part of interpolated string conversion, so this worked previously because a different interpolated string builder overload was chosen that didn't need the string->span conversion (the compiler doesn't gate the overload resolution by whether the codegen helpers exist).

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 not understanding this one. For .NET Framework, Span itself is part of the System.Memory package. How could you be in a situation where you're using Span on netfx but don't have the System.Memory reference in your transitive closure?

@@ -2,6 +2,7 @@

<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum)</TargetFrameworks>
<LangVersion>latest</LangVersion>
Copy link
Member Author

Choose a reason for hiding this comment

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

This project was building against net8.0, net9.0 with C# 14 (preview). That means it gets the first-class Span feature but not the Enumerable.Reverse(this T[]) API. I changed it to build with langversion=latest instead of preview. I could alternatively change the specific Reverse() usage, but in general building older TFMs with newer langversions is unsupported, so this seemed better.

Copy link
Member

Choose a reason for hiding this comment

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

What will that mean when C# 14 is latest? We'll still be building this against net8.0 at that time.

Copy link
Member Author

@jjonescz jjonescz Oct 11, 2024

Choose a reason for hiding this comment

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

But when you are targeting net8.0, then you are getting LangVersion=13 as latest, I think. But yeah, there was just one break here I think, so I could just explicitly use Enumerable.Range as well.

Copy link
Member

Choose a reason for hiding this comment

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

That's not the case. latest does not take TFM into account, e.g. if I create a .NET Framework 4.8 app, it defaults to using C# 7.3, but if I change LangVer to latest, I get the most recent version my compiler supports.

@jaredpar
Copy link
Member

@stephentoub FYI

@@ -137,7 +137,7 @@ public static void Set_CustomComparerTest()

int visited = s_dictionaryData.Length;

Assert.All(s_dictionaryData.Reverse(), element =>
Assert.All(Enumerable.Reverse(s_dictionaryData), element =>
Copy link
Member

Choose a reason for hiding this comment

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

Why does this one need to change? We're only building here for netcoreappcurrent, which should be able to see the array-based overload, right?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, are these all in the same boat as your comment on the immutable tests, that they're not seeing the new Enumerable overload because the tests themselves are in the system namespace?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, these are all in the same boat; they are all in System namespace.

@@ -181,7 +181,6 @@ public void Queue_Generic_ToArray_NonWrappedQueue(int count)
Queue<T> collection = new Queue<T>(count + 1);
AddToCollection(collection, count);
T[] elements = collection.ToArray();
elements.Reverse();
Copy link
Member

Choose a reason for hiding this comment

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

:)

@@ -80,7 +80,7 @@ public void Stack_Generic_Constructor_IEnumerable(EnumerableType enumerableType,
_ = numberOfMatchingElements;
IEnumerable<T> enumerable = CreateEnumerable(enumerableType, null, enumerableLength, 0, numberOfDuplicateElements);
Stack<T> stack = new Stack<T>(enumerable);
Assert.Equal(enumerable.ToArray().Reverse(), stack.ToArray());
Assert.Equal(Enumerable.Reverse(enumerable.ToArray()), stack.ToArray());
Copy link
Member

Choose a reason for hiding this comment

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

Same for many of these... I'm not clear on why changes here are necessary

@@ -2658,7 +2658,7 @@ public static Tensor<T> PermuteDimensions<T>(this Tensor<T> tensor, params ReadO

if (dimensions.IsEmpty)
{
lengths = tensor._lengths.Reverse().ToArray();
lengths = Enumerable.Reverse(tensor._lengths).ToArray();
Copy link
Member

Choose a reason for hiding this comment

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

@michaelgsharp, subsequently we should avoid using LINQ in these cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-codeflow for labeling automated codeflow
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants