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

Update first-class Span speclet #8221

Merged
merged 7 commits into from
Jun 24, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 96 additions & 22 deletions proposals/first-class-span-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ We also add _implicit span conversion_ to the list of acceptable implicit conver
> - The name of `Mₑ` is *identifier*
> - `Mₑ` is accessible and applicable when applied to the arguments as a static method as shown above
> - An implicit identity, reference ~~or boxing~~ **, boxing, or span** conversion exists from *expr* to the type of the first parameter of `Mₑ`.
> **Span conversion is not considered when overload resolution is performed for a method group conversion.**

Note that implicit span conversion is not considered for extension receiver in method group conversions
which makes the following code continue working as opposed to resulting in a compile-time error
`CS1113: Extension method 'E.M<int>(Span<int>, int)' defined on value type 'Span<int>' cannot be used to create delegates`:

```cs
using System;
using System.Collections.Generic;
Action<int> a = new int[0].M; // binds to M<int>(IEnumerable<int>, int)
static class E
{
public static void M<T>(this Span<T> s, T x) => Console.Write(1);
public static void M<T>(this IEnumerable<T> e, T x) => Console.Write(2);
}
```

There's [an open question](#delegate-extension-receiver-break) whether this break should be avoided or not.

#### Variance

Expand Down Expand Up @@ -144,7 +162,8 @@ The compiler expects to use the following helpers or equivalents to implement th
| ReadOnlySpan to ReadOnlySpan | `static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)` |
| string to ReadOnlySpan | `static ReadOnlySpan<char> MemoryExtensions.AsSpan(string)` |

#### Overload resolution
#### Better conversion from expression
[betterness-rule]: #better-conversion-from-expression

*Better conversion from expression* ([§12.6.4.5][better-conversion-from-expression]) is updated to prefer implicit span conversions.
This is based on [collection expressions overload resolution changes][ce-or].
Expand Down Expand Up @@ -205,6 +224,34 @@ static class C
> For example, if .NET 9 BCL introduces such overloads, users that upgrade to `net9.0` TFM but stay on lower LangVersion
> will get ambiguity errors for existing code, unless BCL also applies
> [the new `OverloadResolutionPriorityAttribute`][overload-resolution-priority].
> See also [an open question](#unrestricted-betterness-rule) below.

#### Better conversion target

*Better conversion target* ([§12.6.4.7][better-conversion-target]) is updated to consider implicit span conversions.

> Given two types `T₁` and `T₂`, `T₁` is a *better conversion target* than `T₂` if one of the following holds:
>
> - An implicit conversion from `T₁` to `T₂` exists and no implicit conversion from `T₂` to `T₁` exists
> **(the implicit span conversion is considered in this bullet point, even though it's a conversion from expression)**
> - [...]

This change is needed to avoid ambiguities in existing code that would arise
because we are now ignoring user-defined Span conversions which were considered in the "better conversion target" rule,
whereas "implicit Span conversion" would not be considered as it is a "conversion from expression", not a "conversion from type".

```cs
using System;
C.M(null); // used to print 1, would be ambiguous without this rule
static class C
{
public static void M(object[] x) => Console.Write(1);
public static void M(ReadOnlySpan<object> x) => Console.Write(2);
}
```

This rule is not needed if the span conversion is a conversion from type.
See [an open question][conversion-from-type] below.

### Type inference

Expand Down Expand Up @@ -360,27 +407,6 @@ namespace N2
}
```

#### Extension invocations as delegates

In the following code snippet, `Enumerable.Contains` was chosen previously,
but `MemoryExtensions.Contains` will be chosen with this feature
(thanks to the betterness rule, otherwise this would become an ambiguity).
However, it will result in this error:

```
error CS1113: Extension method 'MemoryExtensions.Contains<int>(Span<int>, int)' defined on value type 'Span<int>' cannot be used to create delegates
```

```cs
using System;
using System.Collections.Generic;
using System.Linq;

var a = new[] { 1, 2 };
var l = new List<int> { 1, 2, 3, 4 };
l.RemoveAll(a.Contains); // works today, error tomorrow
```

## Open questions

### Delegate signature matching (answered)
Expand Down Expand Up @@ -418,12 +444,60 @@ without needing to create wrappers. We don't have precedent in the language for

We will not allow variance in delegate conversions here. `D1 d1 = M1;` and `D2 d2 = M2;` will not compile. We could reconsider at a later point if use cases are discovered.

### Unrestricted betterness rule

Should we make [the betterness rule][betterness-rule] unconditional on LangVersion?
That would allow API authors to add new Span APIs where IEnumerable equivalents exist
without breaking users on older LangVersions and without needing to use the `OverloadResolutionPriorityAttribute`.
However, that would mean users could get different behavior after updating the toolset (without changing LangVersion or TargetFramework):
- Compiler could choose different overloads (technically a breaking change, but hopefully those overloads would have equivalent behavior).
- Other breaks could arise, unknown at this time.

### Delegate extension receiver break

Should we break existing code like the following (real code found in runtime)?
LDM recently allowed breaks related to new Span overloads (https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-06-17.md#params-span-breaks).

```cs
using System;
using System.Collections.Generic;
using System.Linq;

var list = new List<int> { 1, 2, 3, 4 };
var toRemove = new int[] { 2, 3 };
list.RemoveAll(toRemove.Contains); // error CS1113: Extension method 'MemoryExtensions.Contains<int>(Span<int>, int)' defined on value type 'Span<int>' cannot be used to create delegates
```

### Conversion from type vs. from expression
[conversion-from-type]: #conversion-from-type-vs-from-expression

We now think it would be better if the span conversion would be a conversion from type:

- Nothing about span conversions cares what form the expression takes; it can be a local variable, a `new[]`, a collection expression, a field, a `stackalloc`, etc.
- We have to go through everywhere in the spec that doesn't accept conversions from expression and check if they need updating to accept span conversions.
Like [better conversion target](#better-conversion-target) above.

Note that it is not possible to define a user-defined operator between types for which a non-user-defined conversion exists ([§10.5.2 Permitted user-defined conversions][permitted-udcs]).
Hence, we would need to make an exception, so BCL can keep defining the existing Span conversion operators even when they switch to C# 13
(to avoid binary breaking changes and also because we use these operators in codegen of the new standard span conversion).

In Roslyn, type conversions do not have access to Compilation which is needed to access well-known type Span.
We see a couple of ways of solving this concern:

1. We make the new conversions only applicable to the case where Span comes from the corelib, which would significantly simplify this space for us.
This would mean the rules don't exist downlevel; on the one hand, they already partly have issues downlevel
(covariance won't exist downlevel because the helper API doesn't exist).
On the other hand, that could affect partners abilities to take them up.
2. We couple type conversions to a Compilation or look at other ways of providing the well-known type to it. This will take a bit of investigation.

## Alternatives

Keep things as they are.

[standard-explicit-conversions]: https://github.com/dotnet/csharpstandard/blob/8c5e008e2fd6057e1bbe802a99f6ce93e5c29f64/standard/conversions.md#1043-standard-explicit-conversions
[permitted-udcs]: https://github.com/dotnet/csharpstandard/blob/8c5e008e2fd6057e1bbe802a99f6ce93e5c29f64/standard/conversions.md#1052-permitted-user-defined-conversions
[better-conversion-from-expression]: https://github.com/dotnet/csharpstandard/blob/8c5e008e2fd6057e1bbe802a99f6ce93e5c29f64/standard/expressions.md#12645-better-conversion-from-expression
[better-conversion-target]: https://github.com/dotnet/csharpstandard/blob/8c5e008e2fd6057e1bbe802a99f6ce93e5c29f64/standard/expressions.md#12647-better-conversion-target
[is-type-operator]: https://github.com/dotnet/csharpstandard/blob/8c5e008e2fd6057e1bbe802a99f6ce93e5c29f64/standard/expressions.md#1212121-the-is-type-operator

[ce-or]: https://github.com/dotnet/csharplang/blob/566a4812682ccece4ae4483d640a489287fa9c76/proposals/csharp-12.0/collection-expressions.md#overload-resolution
Expand Down