Skip to content

Commit

Permalink
Collection expressions: collection builder methods (dotnet#68930)
Browse files Browse the repository at this point in the history
  • Loading branch information
cston authored Jul 24, 2023
1 parent 7db1e56 commit e40e44e
Show file tree
Hide file tree
Showing 44 changed files with 4,263 additions and 73 deletions.
222 changes: 194 additions & 28 deletions src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,8 @@ private BoundExpression ConvertCollectionExpression(
case CollectionExpressionTypeKind.Span:
case CollectionExpressionTypeKind.ReadOnlySpan:
return BindArrayOrSpanCollectionExpression(node, targetType, wasCompilerGenerated: wasCompilerGenerated, collectionTypeKind, elementType!, diagnostics);
case CollectionExpressionTypeKind.CollectionBuilder:
return BindCollectionBuilderCollectionExpression(node, (NamedTypeSymbol)targetType, wasCompilerGenerated: wasCompilerGenerated, diagnostics);
case CollectionExpressionTypeKind.ListInterface:
return BindListInterfaceCollectionExpression(node, targetType, wasCompilerGenerated: wasCompilerGenerated, elementType!, diagnostics);
case CollectionExpressionTypeKind.None:
Expand All @@ -551,6 +553,19 @@ private BoundExpression ConvertCollectionExpression(
}
}

internal bool TryGetCollectionIterationType(ExpressionSyntax syntax, TypeSymbol collectionType, out TypeWithAnnotations iterationType)
{
BoundExpression collectionExpr = new BoundValuePlaceholder(syntax, collectionType);
return GetEnumeratorInfoAndInferCollectionElementType(
syntax,
syntax,
ref collectionExpr,
isAsync: false,
BindingDiagnosticBag.Discarded,
out iterationType,
builder: out _);
}

private BoundCollectionExpression BindArrayOrSpanCollectionExpression(
BoundUnconvertedCollectionExpression node,
TypeSymbol targetType,
Expand Down Expand Up @@ -583,49 +598,50 @@ private BoundCollectionExpression BindArrayOrSpanCollectionExpression(
GetWellKnownType(WellKnownType.System_Collections_Generic_List_T, diagnostics, syntax).Construct(elementType),
wasCompilerGenerated: wasCompilerGenerated,
diagnostics);
return result.Update(result.CollectionTypeKind, result.Placeholder, result.CollectionCreation, result.Elements, targetType);
return result.Update(result.CollectionTypeKind, result.Placeholder, result.CollectionCreation, result.CollectionBuilderMethod, result.Elements, targetType);
}

var implicitReceiver = new BoundObjectOrCollectionValuePlaceholder(syntax, isNewInstance: true, targetType) { WasCompilerGenerated = true };
var builder = ArrayBuilder<BoundExpression>.GetInstance(elements.Length);
foreach (var element in elements)
{
builder.Add(convertArrayElement(element, elementType, diagnostics));
builder.Add(ConvertCollectionExpressionArrayElement(element, elementType, diagnostics));
}
return new BoundCollectionExpression(
syntax,
collectionTypeKind,
implicitReceiver,
collectionCreation: null,
collectionBuilderMethod: null,
builder.ToImmutableAndFree(),
targetType)
{ WasCompilerGenerated = wasCompilerGenerated };
}

BoundExpression convertArrayElement(BoundExpression element, TypeSymbol elementType, BindingDiagnosticBag diagnostics)
{
var useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics);
var conversion = Conversions.ClassifyImplicitConversionFromExpression(element, elementType, ref useSiteInfo);
diagnostics.Add(element.Syntax, useSiteInfo);
bool hasErrors = !conversion.IsValid;
if (hasErrors)
{
GenerateImplicitConversionError(diagnostics, element.Syntax, conversion, element, elementType);
// Suppress any additional diagnostics
diagnostics = BindingDiagnosticBag.Discarded;
}
var result = CreateConversion(
element.Syntax,
element,
conversion,
isCast: false,
conversionGroupOpt: null,
wasCompilerGenerated: true,
destination: elementType,
diagnostics,
hasErrors: hasErrors);
result.WasCompilerGenerated = true;
return result;
}
private BoundExpression ConvertCollectionExpressionArrayElement(BoundExpression element, TypeSymbol elementType, BindingDiagnosticBag diagnostics)
{
var useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics);
var conversion = Conversions.ClassifyImplicitConversionFromExpression(element, elementType, ref useSiteInfo);
diagnostics.Add(element.Syntax, useSiteInfo);
bool hasErrors = !conversion.IsValid;
if (hasErrors)
{
GenerateImplicitConversionError(diagnostics, element.Syntax, conversion, element, elementType);
// Suppress any additional diagnostics
diagnostics = BindingDiagnosticBag.Discarded;
}
var result = CreateConversion(
element.Syntax,
element,
conversion,
isCast: false,
conversionGroupOpt: null,
wasCompilerGenerated: true,
destination: elementType,
diagnostics,
hasErrors: hasErrors);
result.WasCompilerGenerated = true;
return result;
}

private BoundCollectionExpression BindCollectionInitializerCollectionExpression(
Expand Down Expand Up @@ -688,6 +704,7 @@ private BoundCollectionExpression BindCollectionInitializerCollectionExpression(
collectionTypeKind,
implicitReceiver,
collectionCreation,
collectionBuilderMethod: null,
builder.ToImmutableAndFree(),
targetType,
hasErrors)
Expand All @@ -708,7 +725,7 @@ private BoundCollectionExpression BindListInterfaceCollectionExpression(
GetWellKnownType(WellKnownType.System_Collections_Generic_List_T, diagnostics, node.Syntax).Construct(elementType),
wasCompilerGenerated: wasCompilerGenerated,
diagnostics);
return result.Update(result.CollectionTypeKind, result.Placeholder, result.CollectionCreation, result.Elements, targetType);
return result.Update(result.CollectionTypeKind, result.Placeholder, result.CollectionCreation, result.CollectionBuilderMethod, result.Elements, targetType);
}

private BoundCollectionExpression BindCollectionExpressionForErrorRecovery(
Expand All @@ -727,11 +744,160 @@ private BoundCollectionExpression BindCollectionExpressionForErrorRecovery(
collectionTypeKind: CollectionExpressionTypeKind.None,
placeholder: null,
collectionCreation: null,
collectionBuilderMethod: null,
elements: builder.ToImmutableAndFree(),
targetType,
hasErrors: true);
}

private BoundCollectionExpression BindCollectionBuilderCollectionExpression(
BoundUnconvertedCollectionExpression node,
NamedTypeSymbol targetType,
bool wasCompilerGenerated,
BindingDiagnosticBag diagnostics)
{
var syntax = (ExpressionSyntax)node.Syntax;

bool hasAttribute = targetType.HasCollectionBuilderAttribute(out TypeSymbol? builderType, out string? methodName);
Debug.Assert(hasAttribute);

var targetTypeOriginalDefinition = targetType.OriginalDefinition;
TryGetCollectionIterationType(syntax, targetTypeOriginalDefinition, out TypeWithAnnotations elementTypeOriginalDefinition);
if (!elementTypeOriginalDefinition.HasType)
{
diagnostics.Add(ErrorCode.ERR_CollectionBuilderNoElementType, syntax, targetType);
return BindCollectionExpressionForErrorRecovery(node, targetType, diagnostics);
}

var constructMethod = GetCollectionBuilderMethod(syntax, targetType, elementTypeOriginalDefinition.Type, builderType, methodName, diagnostics);
if (constructMethod is null)
{
diagnostics.Add(ErrorCode.ERR_CollectionBuilderAttributeMethodNotFound, syntax, methodName ?? "", elementTypeOriginalDefinition, targetTypeOriginalDefinition);
return BindCollectionExpressionForErrorRecovery(node, targetType, diagnostics);
}

constructMethod.CheckConstraints(
new ConstraintsHelper.CheckConstraintsArgs(Compilation, Conversions, syntax.Location, diagnostics));

ReportDiagnosticsIfObsolete(diagnostics, constructMethod.ContainingType, syntax, hasBaseReceiver: false);
ReportDiagnosticsIfObsolete(diagnostics, constructMethod, syntax, hasBaseReceiver: false);
ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, constructMethod, syntax, isDelegateConversion: false);

var readOnlySpanType = (NamedTypeSymbol)constructMethod.Parameters[0].Type;
Debug.Assert(readOnlySpanType.OriginalDefinition.Equals(Compilation.GetWellKnownType(WellKnownType.System_ReadOnlySpan_T), TypeCompareKind.AllIgnoreOptions));

var elementType = readOnlySpanType.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0].Type;

var elements = node.Elements;
if (elements.Any(e => e is BoundCollectionExpressionSpreadElement))
{
// The array initializer includes at least one spread element, so we'll create an intermediate List<T> instance.
// https://github.com/dotnet/roslyn/issues/68785: Avoid intermediate List<T> if all spread elements have Length property.
// https://github.com/dotnet/roslyn/issues/68785: Use CollectionsMarshal.AsSpan<T>(List<T>) to create a span
// from the list, to avoid creating an additional intermediate array.
_ = GetWellKnownTypeMember(WellKnownMember.System_Collections_Generic_List_T__ToArray, diagnostics, syntax: syntax);
var result = BindCollectionInitializerCollectionExpression(
node,
CollectionExpressionTypeKind.CollectionBuilder,
GetWellKnownType(WellKnownType.System_Collections_Generic_List_T, diagnostics, syntax).Construct(elementType),
wasCompilerGenerated: wasCompilerGenerated,
diagnostics);
return result.Update(result.CollectionTypeKind, result.Placeholder, result.CollectionCreation, constructMethod, result.Elements, targetType);
}

var implicitReceiver = new BoundObjectOrCollectionValuePlaceholder(syntax, isNewInstance: true, readOnlySpanType) { WasCompilerGenerated = true };
var builder = ArrayBuilder<BoundExpression>.GetInstance(elements.Length);
foreach (var element in elements)
{
builder.Add(ConvertCollectionExpressionArrayElement(element, elementType, diagnostics));
}
return new BoundCollectionExpression(
syntax,
CollectionExpressionTypeKind.CollectionBuilder,
implicitReceiver,
collectionCreation: null,
collectionBuilderMethod: constructMethod,
builder.ToImmutableAndFree(),
targetType)
{ WasCompilerGenerated = wasCompilerGenerated };
}

private MethodSymbol? GetCollectionBuilderMethod(
ExpressionSyntax syntax,
NamedTypeSymbol targetType,
TypeSymbol elementTypeOriginalDefinition,
TypeSymbol? builderType,
string? methodName,
BindingDiagnosticBag diagnostics)
{
if (!SourceNamedTypeSymbol.IsValidCollectionBuilderType(builderType))
{
return null;
}

if (string.IsNullOrEmpty(methodName))
{
return null;
}

var readOnlySpanType = Compilation.GetWellKnownType(WellKnownType.System_ReadOnlySpan_T);

foreach (var candidate in builderType.GetMembers(methodName))
{
if (candidate is not MethodSymbol { IsStatic: true } method)
{
continue;
}

var useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics);
if (!IsAccessible(method, ref useSiteInfo))
{
continue;
}

var builder = ArrayBuilder<TypeWithAnnotations>.GetInstance();
targetType.GetAllTypeArgumentsNoUseSiteDiagnostics(builder);
var allTypeArguments = builder.ToImmutableAndFree();

if (method.Arity != allTypeArguments.Length)
{
continue;
}

if (method.Parameters is not [{ RefKind: RefKind.None, Type: var parameterType }]
|| !readOnlySpanType.Equals(parameterType.OriginalDefinition, TypeCompareKind.AllIgnoreOptions))
{
continue;
}

if (allTypeArguments.Length > 0)
{
var allTypeParameters = TypeMap.TypeParametersAsTypeSymbolsWithAnnotations(targetType.OriginalDefinition.GetAllTypeParameters());
var mDef = method.OriginalDefinition.Construct(allTypeParameters);
var spanElementType = ((NamedTypeSymbol)mDef.Parameters[0].Type).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0].Type;
if (!elementTypeOriginalDefinition.Equals(spanElementType, TypeCompareKind.AllIgnoreOptions))
{
continue;
}
if (!targetType.OriginalDefinition.Equals(mDef.ReturnType, TypeCompareKind.AllIgnoreOptions))
{
continue;
}
method = method.Construct(allTypeArguments);
}

if (!targetType.Equals(method.ReturnType, TypeCompareKind.AllIgnoreOptions))
{
continue;
}

diagnostics.Add(syntax, useSiteInfo);
return method;
}

return null;
}

/// <summary>
/// Rewrite the subexpressions in a conditional expression to convert the whole thing to the destination type.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ internal enum CollectionExpressionTypeKind
Array,
Span,
ReadOnlySpan,
CollectionBuilder,
CollectionInitializer,
ListInterface,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,10 @@ internal static CollectionExpressionTypeKind GetCollectionExpressionTypeKind(CSh
{
return CollectionExpressionTypeKind.ReadOnlySpan;
}
else if ((destination as NamedTypeSymbol)?.HasCollectionBuilderAttribute(out _, out _) == true)
{
return CollectionExpressionTypeKind.CollectionBuilder;
}
else if (implementsIEnumerable(compilation, destination))
{
elementType = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ private void MakeCollectionExpressionTypeInferences(
return;
}

if (!TryGetCollectionIterationType(binder, (ExpressionSyntax)argument.Syntax, target.Type, out TypeWithAnnotations targetElementType))
if (!binder.TryGetCollectionIterationType((ExpressionSyntax)argument.Syntax, target.Type, out TypeWithAnnotations targetElementType))
{
return;
}
Expand All @@ -659,19 +659,6 @@ private void MakeCollectionExpressionTypeInferences(
MakeExplicitParameterTypeInferences(binder, element, targetElementType, kind, ref useSiteInfo);
}
}

private static bool TryGetCollectionIterationType(Binder binder, ExpressionSyntax syntax, TypeSymbol collectionType, out TypeWithAnnotations iterationType)
{
BoundExpression collectionExpr = new BoundValuePlaceholder(syntax, collectionType);
return binder.GetEnumeratorInfoAndInferCollectionElementType(
syntax,
syntax,
ref collectionExpr,
isAsync: false,
BindingDiagnosticBag.Discarded,
out iterationType,
builder: out _);
}
#nullable disable

private bool MakeExplicitParameterTypeInferences(Binder binder, BoundTupleLiteral argument, TypeWithAnnotations target, ExactOrBoundsKind kind, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
Expand Down Expand Up @@ -860,7 +847,7 @@ private void MakeOutputTypeInferences(Binder binder, BoundUnconvertedCollectionE
return;
}

if (!TryGetCollectionIterationType(binder, (ExpressionSyntax)argument.Syntax, formalType.Type, out TypeWithAnnotations targetElementType))
if (!binder.TryGetCollectionIterationType((ExpressionSyntax)argument.Syntax, formalType.Type, out TypeWithAnnotations targetElementType))
{
return;
}
Expand Down
2 changes: 2 additions & 0 deletions src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1888,6 +1888,8 @@
<Field Name="Placeholder" Type="BoundObjectOrCollectionValuePlaceholder?" Null="allow" SkipInVisitor="true"/>
<!-- Constructor call to create the empty collection type. -->
<Field Name="CollectionCreation" Type="BoundExpression?" Null="allow" SkipInVisitor="true"/>
<!-- Construct method for collection types with [CollectionBuilder]. -->
<Field Name="CollectionBuilderMethod" Type="MethodSymbol?" Null="allow"/>
<!-- Collection expression elements. -->
<Field Name="Elements" Type="ImmutableArray&lt;BoundExpression&gt;"/>
</Node>
Expand Down
12 changes: 12 additions & 0 deletions src/Compilers/CSharp/Portable/CSharpResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -6801,6 +6801,18 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="ERR_CollectionExpressionNoTargetType" xml:space="preserve">
<value>There is no target type for the collection expression.</value>
</data>
<data name="ERR_CollectionBuilderAttributeMethodNotFound" xml:space="preserve">
<value>Could not find an accessible '{0}' method with the expected signature: a static method with a single parameter of type 'ReadOnlySpan&lt;{1}&gt;' and return type '{2}'.</value>
</data>
<data name="ERR_CollectionBuilderNoElementType" xml:space="preserve">
<value>'{0}' has a CollectionBuilderAttribute but no element type.</value>
</data>
<data name="ERR_CollectionBuilderAttributeInvalidType" xml:space="preserve">
<value>The CollectionBuilderAttribute builder type must be a non-generic class or struct.</value>
</data>
<data name="ERR_CollectionBuilderAttributeInvalidMethodName" xml:space="preserve">
<value>The CollectionBuilderAttribute method name is invalid.</value>
</data>
<data name="ERR_EqualityContractRequiresGetter" xml:space="preserve">
<value>Record equality contract property '{0}' must have a get accessor.</value>
</data>
Expand Down
4 changes: 4 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2247,6 +2247,10 @@ internal enum ErrorCode
WRN_InlineArraySliceNotUsed = 9182,
WRN_InlineArrayConversionOperatorNotUsed = 9183,
WRN_InlineArrayNotSupportedByLanguage = 9184,
ERR_CollectionBuilderAttributeInvalidType = 9185,
ERR_CollectionBuilderAttributeInvalidMethodName = 9186,
ERR_CollectionBuilderAttributeMethodNotFound = 9187,
ERR_CollectionBuilderNoElementType = 9188,

#endregion

Expand Down
Loading

0 comments on commit e40e44e

Please sign in to comment.