Skip to content

Commit

Permalink
Support declaring and binding to methods with params Span<T>
Browse files Browse the repository at this point in the history
  • Loading branch information
cston committed Jan 27, 2022
1 parent 6d5668e commit aa2eae4
Show file tree
Hide file tree
Showing 28 changed files with 1,492 additions and 40 deletions.
12 changes: 11 additions & 1 deletion src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2989,11 +2989,21 @@ internal static bool CheckValEscape(SyntaxNode node, BoundExpression expr, uint
{
case BoundKind.DefaultLiteral:
case BoundKind.DefaultExpression:
case BoundKind.Parameter:
case BoundKind.ThisReference:
// always returnable
return true;

case BoundKind.Parameter:
var parameter = ((BoundParameter)expr).ParameterSymbol;
// params Span<T> value cannot be returned since that would
// prevent sharing repeated allocations at the call-site.
if (parameter.IsParams && !parameter.Type.IsSZArray())
{
Error(diagnostics, ErrorCode.ERR_EscapeParamsSpan, node, expr.Syntax);
return false;
}
return true;

case BoundKind.TupleLiteral:
case BoundKind.ConvertedTupleLiteral:
var tupleLiteral = (BoundTupleExpression)expr;
Expand Down
2 changes: 1 addition & 1 deletion src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3079,7 +3079,7 @@ private static TypeWithAnnotations GetCorrespondingParameterTypeWithAnnotations(

if (paramNum == parameters.Length - 1 && result.Kind == MemberResolutionKind.ApplicableInExpandedForm)
{
type = ((ArrayTypeSymbol)type.Type).ElementTypeWithAnnotations;
type = type.Type.GetParamsElementType();
}

return type;
Expand Down
10 changes: 10 additions & 0 deletions src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,16 @@ private BoundCall BindInvocationExpressionContinued(
gotError = ReportUnsafeIfNotAllowed(node, diagnostics) || gotError;
}

if (expanded)
{
var parameter = method.Parameters[^1];
Debug.Assert(parameter.IsParams);
if (!parameter.Type.IsSZArray() && !CheckFeatureAvailability(node, MessageID.IDS_FeatureParamsSpan, diagnostics))
{
gotError = true;
}
}

bool hasBaseReceiver = receiver != null && receiver.Kind == BoundKind.BaseReference;

ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ private static bool PreferExpandedFormOverNormalForm(MemberAnalysisResult normal
// We need to know if this is a valid formal parameter list with a parameter array
// as the final formal parameter. We might be in an error recovery scenario
// where the params array is not an array type.
public static bool IsValidParams(Symbol member)
public bool IsValidParams(Symbol member)
{
// A varargs method is never a valid params method.
if (member.GetIsVararg())
Expand All @@ -1026,10 +1026,10 @@ public static bool IsValidParams(Symbol member)
return IsValidParamsParameter(final);
}

public static bool IsValidParamsParameter(ParameterSymbol final)
public bool IsValidParamsParameter(ParameterSymbol final)
{
Debug.Assert((object)final == final.ContainingSymbol.GetParameters().Last());
return final.IsParams && ((ParameterSymbol)final.OriginalDefinition).Type.IsSZArray();
return final.IsParams && final.Type.IsParamsType(Compilation);
}

/// <summary>
Expand Down Expand Up @@ -1638,9 +1638,9 @@ private TypeSymbol GetParameterType(ParameterSymbol parameter, MemberAnalysisRes
{
var type = parameter.Type;
if (result.Kind == MemberResolutionKind.ApplicableInExpandedForm &&
parameter.IsParams && type.IsSZArray())
parameter.IsParams && type.IsParamsType(Compilation))
{
return ((ArrayTypeSymbol)type).ElementType;
return type.GetParamsElementType().Type;
}
else
{
Expand Down Expand Up @@ -2036,11 +2036,29 @@ private BetterResult BetterFunctionMember<TMember>(

// NB: OriginalDefinition, not ConstructedFrom. Substitutions into containing symbols
// must also be ignored for this tie-breaker.
var m1Original = m1.LeastOverriddenMember.OriginalDefinition.GetParameters();
var m2Original = m2.LeastOverriddenMember.OriginalDefinition.GetParameters();

// Prefer params Span<T> or ReadOnlySpan<T> over params T[].
if (m1.Result.Kind == MemberResolutionKind.ApplicableInExpandedForm && m2.Result.Kind == MemberResolutionKind.ApplicableInExpandedForm)
{
var parameter1 = m1Original.Last();
var parameter2 = m2Original.Last();

Debug.Assert(parameter1.IsParams);
Debug.Assert(parameter2.IsParams);

switch (parameter1.Type.IsSZArray(), parameter2.Type.IsSZArray())
{
case (false, true):
return BetterResult.Left;
case (true, false):
return BetterResult.Right;
}
}

var uninst1 = ArrayBuilder<TypeSymbol>.GetInstance();
var uninst2 = ArrayBuilder<TypeSymbol>.GetInstance();
var m1Original = m1.LeastOverriddenMember.OriginalDefinition.GetParameters();
var m2Original = m2.LeastOverriddenMember.OriginalDefinition.GetParameters();
for (i = 0; i < arguments.Count; ++i)
{
// If these are both applicable varargs methods and we're looking at the __arglist argument
Expand Down Expand Up @@ -3236,7 +3254,7 @@ private static EffectiveParameters GetEffectiveParametersInExpandedForm<TMember>
var parameter = parameters[parm];
var type = parameter.TypeWithAnnotations;

types.Add(parm == parameters.Length - 1 ? ((ArrayTypeSymbol)type.Type).ElementTypeWithAnnotations : type);
types.Add(parm == parameters.Length - 1 ? type.Type.GetParamsElementType() : type);

var argRefKind = hasAnyRefArg ? argumentRefKinds[arg] : RefKind.None;
var paramRefKind = GetEffectiveParameterRefKind(parameter, argRefKind, isMethodGroupConversion, allowRefOmittedArguments, binder, ref hasAnyRefOmittedArgument);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public ImmutableArray<int> ToImmutableArray()
}
}

private static ArgumentAnalysisResult AnalyzeArguments(
private ArgumentAnalysisResult AnalyzeArguments(
Symbol symbol,
AnalyzedArguments arguments,
bool isMethodGroupConversion,
Expand Down
7 changes: 7 additions & 0 deletions src/Compilers/CSharp/Portable/CSharpResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -5107,6 +5107,9 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="ERR_EscapeStackAlloc" xml:space="preserve">
<value>A result of a stackalloc expression of type '{0}' cannot be used in this context because it may be exposed outside of the containing method</value>
</data>
<data name="ERR_EscapeParamsSpan" xml:space="preserve">
<value>Cannot use params '{0}' in this context because it may prevent reuse at the call-site</value>
</data>
<data name="ERR_InitializeByValueVariableWithReference" xml:space="preserve">
<value>Cannot initialize a by-value variable with a reference</value>
</data>
Expand Down Expand Up @@ -6642,6 +6645,10 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="IDS_FeatureVarianceSafetyForStaticInterfaceMembers" xml:space="preserve">
<value>variance safety for static interface members</value>
</data>
<data name="IDS_FeatureParamsSpan" xml:space="preserve">
<value>params Span&lt;T&gt;</value>
<comment>params Span&lt;T&gt; is not localizable</comment>
</data>
<data name="ERR_EqualityContractRequiresGetter" xml:space="preserve">
<value>Record equality contract property '{0}' must have a get accessor.</value>
</data>
Expand Down
2 changes: 2 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2027,6 +2027,8 @@ internal enum ErrorCode
ERR_NullCheckingOnOutParameter = 8994,
WRN_NullCheckingOnNullableType = 8995,

ERR_EscapeParamsSpan = 8999,

#endregion

// Note: you will need to re-generate compiler code after adding warnings (eng\generate-compiler-code.cmd)
Expand Down
2 changes: 2 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/MessageID.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ internal enum MessageID
IDS_ParameterNullChecking = MessageBase + 12815,

IDS_FeatureCacheStaticMethodGroupConversion = MessageBase + 12816,
IDS_FeatureParamsSpan = MessageBase + 12817,
}

// Message IDs may refer to strings that need to be localized.
Expand Down Expand Up @@ -358,6 +359,7 @@ internal static LanguageVersion RequiredVersion(this MessageID feature)
case MessageID.IDS_FeatureListPattern: // semantic check
case MessageID.IDS_FeatureCacheStaticMethodGroupConversion: // lowering check
case MessageID.IDS_ParameterNullChecking: // syntax check
case MessageID.IDS_FeatureParamsSpan: // semantic check
return LanguageVersion.Preview;

// C# 10.0 features.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ private ImmutableArray<BoundExpression> MakeArguments(
// Step two: If we have a params array, build the array and fill in the argument.
if (expanded)
{
actualArguments[actualArguments.Length - 1] = BuildParamsArray(syntax, methodOrIndexer, argsToParamsOpt, rewrittenArguments, parameters, actualArguments[actualArguments.Length - 1]);
actualArguments[actualArguments.Length - 1] = BuildParamsArray(syntax, argsToParamsOpt, rewrittenArguments, parameters, actualArguments[actualArguments.Length - 1]);
}

if (isComReceiver)
Expand Down Expand Up @@ -1027,7 +1027,6 @@ private static bool IsBeginningOfParamArray(

private BoundExpression BuildParamsArray(
SyntaxNode syntax,
Symbol methodOrIndexer,
ImmutableArray<int> argsToParamsOpt,
ImmutableArray<BoundExpression> rewrittenArguments,
ImmutableArray<ParameterSymbol> parameters,
Expand Down Expand Up @@ -1092,7 +1091,12 @@ private BoundExpression BuildParamsArray(
}
}

return CreateParamArrayArgument(syntax, paramArrayType, arrayArgs, _compilation, this);
if (paramArrayType.IsSZArray())
{
return CreateParamArrayArgument(syntax, paramArrayType, arrayArgs, _compilation, this);
}

return CreateParamsSpan(syntax, paramArrayType, arrayArgs);
}

private static BoundExpression CreateParamArrayArgument(SyntaxNode syntax,
Expand All @@ -1101,7 +1105,6 @@ private static BoundExpression CreateParamArrayArgument(SyntaxNode syntax,
CSharpCompilation compilation,
LocalRewriter? localRewriter)
{

TypeSymbol int32Type = compilation.GetSpecialType(SpecialType.System_Int32);
BoundExpression arraySize = MakeLiteral(syntax, ConstantValue.Create(arrayArgs.Length), int32Type, localRewriter);

Expand All @@ -1113,6 +1116,92 @@ private static BoundExpression CreateParamArrayArgument(SyntaxNode syntax,
{ WasCompilerGenerated = true };
}

private BoundExpression CreateParamsSpan(
SyntaxNode syntax,
TypeSymbol paramArrayType,
ImmutableArray<BoundExpression> arrayArgs)
{
var elementType = paramArrayType.GetParamsElementType().Type;
int length = arrayArgs.Length;

// PROTOTYPE: If we actually allocate from the stack, it may be necessary to
// clear the execution stack before allocation. (See corresponding requirement
// for 'stackalloc' in LocalRewriter.VisitStackAllocArrayCreationBase().)

// For now, simply allocate the array on the heap: new Span<T>(new T[length])
var intType = _compilation.GetSpecialType(SpecialType.System_Int32);
var array = new BoundArrayCreation(
syntax,
ImmutableArray.Create(MakeLiteral(syntax, ConstantValue.Create(length), intType, this)),
initializerOpt: null,
ArrayTypeSymbol.CreateSZArray(_compilation.SourceAssembly, TypeWithAnnotations.Create(elementType)))
{ WasCompilerGenerated = true };

MethodSymbol spanConstructor;
MethodSymbol spanGetItem;
MethodSymbol? spanToReadOnlySpanOperator = null;
if (!TryGetWellKnownTypeMember(syntax, WellKnownMember.System_Span_T__ctorArray, out spanConstructor) ||
!TryGetWellKnownTypeMember(syntax, WellKnownMember.System_Span_T__get_Item, out spanGetItem) ||
// PROTOTYPE: To convert from Span<T> to ReadOnlySpan<T>, we're currently looking for
// 'static implicit operator ReadOnlySpan<T>(Span<T> span)' explicitly. Instead, should
// we use ConversionsBase.ClassifyImplicitConversionFromType() to find the conversion?
(paramArrayType.OriginalDefinition.Equals(_compilation.GetWellKnownType(WellKnownType.System_ReadOnlySpan_T), TypeCompareKind.AllIgnoreOptions) &&
!TryGetWellKnownTypeMember<MethodSymbol>(syntax, WellKnownMember.System_Span_T__op_Implicit_SpanReadOnlySpan, out spanToReadOnlySpanOperator)))
{
return BadExpression(syntax, paramArrayType, ImmutableArray<BoundExpression>.Empty);
}

var spanType = spanConstructor.ContainingType.Construct(elementType);
spanConstructor = spanConstructor.AsMember(spanType);
spanGetItem = spanGetItem.AsMember(spanType);
spanToReadOnlySpanOperator = spanToReadOnlySpanOperator?.AsMember(spanType);

var sideEffects = ArrayBuilder<BoundExpression>.GetInstance();
var span = _factory.New(spanConstructor, array);
var temp = _factory.StoreToTemp(span, out var assignment);
sideEffects.Add(assignment);

// temp[0] = arrayArgs[0];
// ...
// temp[n - 1] = arrayArgs[n - 1];
// temp
for (int i = 0; i < arrayArgs.Length; i++)
{
assignment = _factory.AssignmentExpression(
_factory.Call(temp, spanGetItem, MakeLiteral(syntax, ConstantValue.Create(i), intType, this)),
_factory.Convert(elementType, arrayArgs[i]));
sideEffects.Add(assignment);
}

BoundExpression expr = temp;
if (spanToReadOnlySpanOperator is { })
{
// Convert Span<T> to ReadOnlySpan<T>.
expr = new BoundCall(
syntax,
receiverOpt: null,
method: spanToReadOnlySpanOperator,
arguments: ImmutableArray.Create(expr),
argumentNamesOpt: default,
argumentRefKindsOpt: default,
isDelegateCall: false,
expanded: false,
invokedAsExtensionMethod: false,
argsToParamsOpt: default,
defaultArguments: default,
resultKind: LookupResultKind.Viable,
type: spanToReadOnlySpanOperator.ReturnType);
}

Debug.Assert(expr.Type is { });
return new BoundSequence(
syntax,
locals: ImmutableArray.Create(temp.LocalSymbol),
sideEffects.ToImmutableAndFree(),
expr,
expr.Type);
}

/// <summary>
/// To create literal expression for IOperation, set localRewriter to null.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ private BoundIndexerAccess TransformIndexerAccess(BoundIndexerAccess indexerAcce
// Step two: If we have a params array, build the array and fill in the argument.
if (expanded)
{
BoundExpression array = BuildParamsArray(syntax, indexer, argsToParamsOpt, rewrittenArguments, parameters, actualArguments[actualArguments.Length - 1]);
BoundExpression array = BuildParamsArray(syntax, argsToParamsOpt, rewrittenArguments, parameters, actualArguments[actualArguments.Length - 1]);
BoundAssignmentOperator storeToTemp;
var boundTemp = _factory.StoreToTemp(array, out storeToTemp);
stores.Add(storeToTemp);
Expand Down
Loading

0 comments on commit aa2eae4

Please sign in to comment.