Skip to content

Commit

Permalink
Update first-class Span speclet (#8221)
Browse files Browse the repository at this point in the history
* Update "better conversion target"

* Add betterness open question

* Avoid implicit span conversion for extension receiver in method group conversion

* Add extension receiver break open question

* Remove duplicate breaking change example

* Add conversion from type open question

* Improve wording
  • Loading branch information
jjonescz authored Jun 24, 2024
1 parent c181bab commit 19a63b2
Showing 1 changed file with 96 additions and 22 deletions.
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

0 comments on commit 19a63b2

Please sign in to comment.