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

Implement remaining span conversions #74420

Merged
Show file tree
Hide file tree
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
193 changes: 178 additions & 15 deletions src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -487,51 +487,214 @@ void checkConstraintLanguageVersionAndRuntimeSupportForConversion(SyntaxNode syn
}
else if (conversion.IsSpan)
{
Debug.Assert(destination.OriginalDefinition.IsSpan() || destination.OriginalDefinition.IsReadOnlySpan());
Debug.Assert(source.Type is not null);
Debug.Assert(destination.IsSpan() || destination.IsReadOnlySpan());

CheckFeatureAvailability(syntax, MessageID.IDS_FeatureFirstClassSpan, diagnostics);

// PROTOTYPE: Check runtime APIs used for other span conversions once they are implemented.
// NOTE: We cannot use well-known members because per the spec
// the Span types involved in the Span conversions can be any that match the type name.
if (TryFindImplicitOperatorFromArray(destination.OriginalDefinition) is { } method)

// Span<T>.op_Implicit(T[]) or ReadOnlySpan<T>.op_Implicit(T[])
if (source.Type is ArrayTypeSymbol)
{
diagnostics.ReportUseSite(method, syntax);
reportUseSiteOrMissing(
TryFindImplicitOperatorFromArray(destination.OriginalDefinition),
destination.OriginalDefinition,
WellKnownMemberNames.ImplicitConversionName,
syntax,
diagnostics);
}
else

// ReadOnlySpan<T> Span<T>.op_Implicit(Span<T>)
if (source.Type.IsSpan())
{
Error(diagnostics,
ErrorCode.ERR_MissingPredefinedMember,
Debug.Assert(destination.IsReadOnlySpan());
reportUseSiteOrMissing(
TryFindImplicitOperatorFromSpan(source.Type.OriginalDefinition, destination.OriginalDefinition),
source.Type.OriginalDefinition,
WellKnownMemberNames.ImplicitConversionName,
syntax,
destination.OriginalDefinition,
WellKnownMemberNames.ImplicitConversionName);
diagnostics);
}

// ReadOnlySpan<T> ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
if (source.Type.IsSpan() || source.Type.IsReadOnlySpan())
{
Debug.Assert(destination.IsReadOnlySpan());
if (NeedsSpanCastUp(source.Type, destination))
{
// If converting Span<TDerived> -> ROS<TDerived> -> ROS<T>,
// the source of the CastUp is the return type of the op_Implicit (i.e., the ROS<TDerived>)
// which has the same original definition as the destination ROS<T>.
TypeSymbol sourceForCastUp = source.Type.IsSpan()
? destination.OriginalDefinition
: source.Type.OriginalDefinition;

MethodSymbol? castUpMethod = TryFindCastUpMethod(sourceForCastUp, destination.OriginalDefinition);
reportUseSiteOrMissing(
castUpMethod,
destination.OriginalDefinition,
WellKnownMemberNames.CastUpMethodName,
syntax,
diagnostics);
castUpMethod?
.AsMember((NamedTypeSymbol)destination)
.Construct([((NamedTypeSymbol)source.Type).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0]])
.CheckConstraints(new ConstraintsHelper.CheckConstraintsArgs(Compilation, Conversions, includeNullability: false, syntax.Location, diagnostics));
}
}

// ReadOnlySpan<char> MemoryExtensions.AsSpan(string)
if (source.Type.IsStringType())
{
reportUseSiteOrMissing(
TryFindAsSpanCharMethod(Compilation, destination),
WellKnownMemberNames.MemoryExtensionsTypeFullName,
WellKnownMemberNames.AsSpanMethodName,
syntax,
diagnostics);
}
}
}

static void reportUseSiteOrMissing(MethodSymbol? method, object containingType, string methodName, SyntaxNode syntax, BindingDiagnosticBag diagnostics)
{
if (method is not null)
{
diagnostics.ReportUseSite(method, syntax);
}
else
{
Error(diagnostics,
ErrorCode.ERR_MissingPredefinedMember,
syntax,
containingType,
methodName);
}
}
}

// {type}.op_Implicit(T[])
internal static MethodSymbol? TryFindImplicitOperatorFromArray(TypeSymbol type)
{
Debug.Assert(type.IsSpan() || type.IsReadOnlySpan());
Debug.Assert(type.IsDefinition);

return TryFindImplicitOperator(type, 0, static (_, method) =>
method.Parameters[0].Type is ArrayTypeSymbol { IsSZArray: true, ElementType: TypeParameterSymbol });
}

// ReadOnlySpan<T> Span<T>.op_Implicit(Span<T>)
internal static MethodSymbol? TryFindImplicitOperatorFromSpan(TypeSymbol spanType, TypeSymbol readonlySpanType)
{
Debug.Assert(spanType.IsSpan() && readonlySpanType.IsReadOnlySpan());
Debug.Assert(spanType.IsDefinition && readonlySpanType.IsDefinition);

return TryFindImplicitOperator(spanType, readonlySpanType,
static (readonlySpanType, method) => method.Parameters[0].Type.IsSpan() &&
readonlySpanType.Equals(method.ReturnType.OriginalDefinition, TypeCompareKind.ConsiderEverything));
}

return TryFindSingleMember(type, WellKnownMemberNames.ImplicitConversionName,
static (method) => method is
private static MethodSymbol? TryFindImplicitOperator<TArg>(TypeSymbol type, TArg arg,
Func<TArg, MethodSymbol, bool> predicate)
{
return TryFindSingleMethod(type, WellKnownMemberNames.ImplicitConversionName, (predicate, arg),
static (arg, method) => method is
{
ParameterCount: 1,
Arity: 0,
IsStatic: true,
DeclaredAccessibility: Accessibility.Public,
Parameters: [{ Type: ArrayTypeSymbol { IsSZArray: true, ElementType: TypeParameterSymbol } }]
});
} && arg.predicate(arg.arg, method));
}

internal static bool NeedsSpanCastUp(TypeSymbol source, TypeSymbol destination)
{
Debug.Assert(source.IsSpan() || source.IsReadOnlySpan());
Debug.Assert(destination.IsReadOnlySpan());
Debug.Assert(!source.IsDefinition && !destination.IsDefinition);

var sourceElementType = ((NamedTypeSymbol)source).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0].Type;
var destinationElementType = ((NamedTypeSymbol)destination).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0].Type;

var sameElementTypes = sourceElementType.Equals(destinationElementType, TypeCompareKind.AllIgnoreOptions);

Debug.Assert(!source.IsReadOnlySpan() || !sameElementTypes);

return !sameElementTypes;
}

// ReadOnlySpan<T> ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>) where TDerived : class
internal static MethodSymbol? TryFindCastUpMethod(TypeSymbol source, TypeSymbol destination)
{
Debug.Assert(source.IsReadOnlySpan() && destination.IsReadOnlySpan());
Debug.Assert(source.IsDefinition && destination.IsDefinition);

return TryFindSingleMethod(destination, WellKnownMemberNames.CastUpMethodName, (source, destination),
static (arg, method) => method is
{
ParameterCount: 1,
Arity: 1,
IsStatic: true,
DeclaredAccessibility: Accessibility.Public,
Parameters: [{ } parameter],
TypeArgumentsWithAnnotations: [{ } typeArgument],
} &&
// parameter type is the source ReadOnlySpan<>
arg.source.Equals(parameter.Type.OriginalDefinition, TypeCompareKind.ConsiderEverything) &&
// return type is the destination ReadOnlySpan<>
arg.destination.Equals(method.ReturnType.OriginalDefinition, TypeCompareKind.ConsiderEverything) &&
jjonescz marked this conversation as resolved.
Show resolved Hide resolved
// TDerived : class
typeArgument.Type.IsReferenceType &&
// parameter type argument is TDerived
((NamedTypeSymbol)parameter.Type).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0].Type.Equals(typeArgument.Type, TypeCompareKind.ConsiderEverything) &&
// return type argument is T
((NamedTypeSymbol)method.ReturnType).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0].Type.Equals(((NamedTypeSymbol)arg.destination).TypeParameters[0], TypeCompareKind.ConsiderEverything));
}

// ReadOnlySpan<char> MemoryExtensions.AsSpan(string)
internal static MethodSymbol? TryFindAsSpanCharMethod(CSharpCompilation compilation, TypeSymbol readOnlySpanType)
{
Debug.Assert(readOnlySpanType.IsReadOnlySpan());
Debug.Assert(!readOnlySpanType.IsDefinition);
Debug.Assert(((NamedTypeSymbol)readOnlySpanType).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0].SpecialType is SpecialType.System_Char);

MethodSymbol? result = null;
foreach (var memoryExtensionsType in compilation.GetTypesByMetadataName(WellKnownMemberNames.MemoryExtensionsTypeFullName))
{
if (memoryExtensionsType.DeclaredAccessibility == Accessibility.Public &&
TryFindSingleMethod(memoryExtensionsType.GetSymbol<NamedTypeSymbol>(), WellKnownMemberNames.AsSpanMethodName, 0,
static (_, method) => method is
{
ParameterCount: 1,
Arity: 0,
IsStatic: true,
DeclaredAccessibility: Accessibility.Public,
Parameters: [{ Type.SpecialType: SpecialType.System_String }]
}) is { } method &&
method.ReturnType.Equals(readOnlySpanType, TypeCompareKind.ConsiderEverything))
{
if (result is not null)
{
// Ambiguous member found.
jjonescz marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

result = method;
}
}

return result;
}

private static MethodSymbol? TryFindSingleMember(TypeSymbol type, string name, Func<MethodSymbol, bool> predicate)
private static MethodSymbol? TryFindSingleMethod<TArg>(TypeSymbol type, string name, TArg arg, Func<TArg, MethodSymbol, bool> predicate)
{
var members = type.GetMembers(name);
MethodSymbol? result = null;
foreach (var member in members)
{
if (member is MethodSymbol method && predicate(method))
if (member is MethodSymbol method && predicate(arg, method))
{
if (result is not null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3953,7 +3953,7 @@ private bool IsFeatureFirstClassSpanEnabled

private bool HasImplicitSpanConversion(TypeSymbol? source, TypeSymbol destination, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
{
if (!IsFeatureFirstClassSpanEnabled)
if (source is null || !IsFeatureFirstClassSpanEnabled)
{
return false;
}
Expand All @@ -3962,19 +3962,39 @@ private bool HasImplicitSpanConversion(TypeSymbol? source, TypeSymbol destinatio
if (source is ArrayTypeSymbol { IsSZArray: true, ElementTypeWithAnnotations: { } elementType })
{
// SPEC: ...to `System.Span<Ei>`.
if (destination.OriginalDefinition.IsSpan())
if (destination.IsSpan())
{
var spanElementType = ((NamedTypeSymbol)destination).TypeArgumentsWithDefinitionUseSiteDiagnostics(ref useSiteInfo)[0];
return hasIdentityConversion(elementType, spanElementType);
}

// SPEC: ...to `System.ReadOnlySpan<Ui>`, provided that `Ei` is covariance-convertible to `Ui`.
if (destination.OriginalDefinition.IsReadOnlySpan())
if (destination.IsReadOnlySpan())
{
var spanElementType = ((NamedTypeSymbol)destination).TypeArgumentsWithDefinitionUseSiteDiagnostics(ref useSiteInfo)[0];
return hasCovariantConversion(elementType, spanElementType, ref useSiteInfo);
}
}
// SPEC: From `System.Span<Ti>` to `System.ReadOnlySpan<Ui>`, provided that `Ti` is covariance-convertible to `Ui`.
// SPEC: From `System.ReadOnlySpan<Ti>` to `System.ReadOnlySpan<Ui>`, provided that `Ti` is covariance-convertible to `Ui`.
else if (source.IsSpan() || source.IsReadOnlySpan())
{
if (destination.IsReadOnlySpan())
{
var sourceElementType = ((NamedTypeSymbol)source).TypeArgumentsWithDefinitionUseSiteDiagnostics(ref useSiteInfo)[0];
var destinationElementType = ((NamedTypeSymbol)destination).TypeArgumentsWithDefinitionUseSiteDiagnostics(ref useSiteInfo)[0];
return hasCovariantConversion(sourceElementType, destinationElementType, ref useSiteInfo);
}
}
// SPEC: From `string` to `System.ReadOnlySpan<char>`.
else if (source.IsStringType())
{
if (destination.IsReadOnlySpan())
{
var spanElementType = ((NamedTypeSymbol)destination).TypeArgumentsWithDefinitionUseSiteDiagnostics(ref useSiteInfo)[0];
return spanElementType.SpecialType is SpecialType.System_Char;
}
}

return false;

Expand Down Expand Up @@ -4005,7 +4025,7 @@ private bool HasExplicitSpanConversion(TypeSymbol? source, TypeSymbol destinatio
// to `System.Span<Ui>` or `System.ReadOnlySpan<Ui>`
// provided an explicit reference conversion exists from `Ti` to `Ui`.
if (source is ArrayTypeSymbol { IsSZArray: true, ElementTypeWithAnnotations: { } elementType } &&
(destination.OriginalDefinition.IsSpan() || destination.OriginalDefinition.IsReadOnlySpan()))
(destination.IsSpan() || destination.IsReadOnlySpan()))
{
var spanElementType = ((NamedTypeSymbol)destination).TypeArgumentsWithDefinitionUseSiteDiagnostics(ref useSiteInfo)[0];
return HasIdentityOrReferenceConversion(elementType.Type, spanElementType.Type, ref useSiteInfo) &&
Expand All @@ -4017,26 +4037,12 @@ private bool HasExplicitSpanConversion(TypeSymbol? source, TypeSymbol destinatio

private bool IgnoreUserDefinedSpanConversions(TypeSymbol? source, TypeSymbol? target)
{
// SPEC: User-defined conversions are not considered when converting between types
// for which an implicit or an explicit span conversion exists.
var discarded = CompoundUseSiteInfo<AssemblySymbol>.Discarded;
return source is not null && target is not null &&
IsFeatureFirstClassSpanEnabled &&
(ignoreUserDefinedSpanConversionsInOneDirection(source, target) ||
ignoreUserDefinedSpanConversionsInOneDirection(target, source));

static bool ignoreUserDefinedSpanConversionsInOneDirection(TypeSymbol a, TypeSymbol b)
{
// SPEC: User-defined conversions are not considered when converting between
// SPEC: - any single-dimensional `array_type` and `System.Span<T>`/`System.ReadOnlySpan<T>`
if (a is ArrayTypeSymbol { IsSZArray: true } &&
(b.OriginalDefinition.IsSpan() || b.OriginalDefinition.IsReadOnlySpan()))
{
return true;
}

// PROTOTYPE: - any combination of `System.Span<T>`/`System.ReadOnlySpan<T>`
// PROTOTYPE: - `string` and `System.ReadOnlySpan<char>`

return false;
}
(HasImplicitSpanConversion(source, target, ref discarded) ||
HasExplicitSpanConversion(source, target, ref discarded));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8904,8 +8904,8 @@ private TypeWithState VisitConversion(
{
var previousKind = conversion.Kind;
conversion = GenerateConversion(_conversions, conversionOperand, operandType.Type, targetType, fromExplicitCast, extensionMethodThisArgument, isChecked: conversionOpt?.Checked ?? false);
Debug.Assert(!conversion.Exists || conversion.Kind == previousKind);
canConvertNestedNullability = conversion.Exists;
// We do not want user-defined conversions to relax nullability, so we consider only span conversions.
canConvertNestedNullability = conversion.Exists && conversion.IsSpan;
}
break;

Expand Down
Loading