Skip to content

Commit

Permalink
Allow and emit params arrays in lambdas (#64861)
Browse files Browse the repository at this point in the history
* Allow `params` arrays in lambdas

* Disallow `params` in anonymous method expressions

* Add delegate type tests

* Fix `AnonymousTypeField.IsParams` propagation

* Test method groups

* Set `hasParamsArray` only for the last parameter

* Use named arguments for `bool`s

* Mark `IsParams` as `sealed`

* Check lambda types for equality

* Use `ref` to get synthesized delegate

* Test `null` params array

* Support `params` on any lambda parameter symbol

* Add a prototype comment

* Name tuple arguments

* Use `IsKind`

* Make lambda / anonymous method parameters exclusive
  • Loading branch information
jjonescz authored Oct 25, 2022
1 parent 68c56b3 commit 3711c43
Show file tree
Hide file tree
Showing 18 changed files with 294 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public override bool HasExplicitReturnType(out RefKind refKind, out TypeWithAnno
public override int ParameterCount { get { return _parameters.Length; } }
public override bool IsAsync { get { return false; } }
public override bool IsStatic => false;
public override bool HasParamsArray => false;
public override RefKind RefKind(int index) { return Microsoft.CodeAnalysis.RefKind.None; }
public override DeclarationScope DeclaredScope(int index) => DeclarationScope.Unscoped;
public override MessageID MessageID { get { return MessageID.IDS_FeatureQueryExpression; } } // TODO: what is the correct ID here?
Expand Down
13 changes: 9 additions & 4 deletions src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8921,8 +8921,9 @@ private MethodGroupResolution ResolveDefaultMethodGroup(
var parameterDefaultValues = parameters.Any(p => p.HasExplicitDefaultValue) ?
parameters.SelectAsArray(p => p.ExplicitDefaultConstantValue) :
default;
var hasParamsArray = !parameters.IsEmpty && parameters[^1].IsParams;

return GetMethodGroupOrLambdaDelegateType(node.Syntax, method.RefKind, method.ReturnTypeWithAnnotations, method.ParameterRefKinds, parameterScopes, method.ParameterTypesWithAnnotations, parameterDefaultValues: parameterDefaultValues);
return GetMethodGroupOrLambdaDelegateType(node.Syntax, method.RefKind, method.ReturnTypeWithAnnotations, method.ParameterRefKinds, parameterScopes, method.ParameterTypesWithAnnotations, parameterDefaultValues, hasParamsArray);
}

/// <summary>
Expand Down Expand Up @@ -9009,12 +9010,14 @@ static bool isCandidateUnique(ref MethodSymbol? method, MethodSymbol candidate)
ImmutableArray<RefKind> parameterRefKinds,
ImmutableArray<DeclarationScope> parameterScopes,
ImmutableArray<TypeWithAnnotations> parameterTypes,
ImmutableArray<ConstantValue?> parameterDefaultValues)
ImmutableArray<ConstantValue?> parameterDefaultValues,
bool hasParamsArray)
{
Debug.Assert(ContainingMemberOrLambda is { });
Debug.Assert(parameterRefKinds.IsDefault || parameterRefKinds.Length == parameterTypes.Length);
Debug.Assert(parameterDefaultValues.IsDefault || parameterDefaultValues.Length == parameterTypes.Length);
Debug.Assert(returnType.Type is { }); // Expecting System.Void rather than null return type.
Debug.Assert(!hasParamsArray || parameterTypes.Length != 0);

bool returnsVoid = returnType.Type.IsVoidType();
var typeArguments = returnsVoid ? parameterTypes : parameterTypes.Add(returnType);
Expand All @@ -9032,7 +9035,8 @@ static bool isCandidateUnique(ref MethodSymbol? method, MethodSymbol candidate)
}

// Use System.Action<...> or System.Func<...> if possible.
if (returnRefKind == RefKind.None &&
if (!hasParamsArray &&
returnRefKind == RefKind.None &&
parameterDefaultValues.IsDefault &&
(parameterRefKinds.IsDefault || parameterRefKinds.All(refKind => refKind == RefKind.None)) &&
(parameterScopes.IsDefault || parameterScopes.All(scope => scope == DeclarationScope.Unscoped)))
Expand Down Expand Up @@ -9069,7 +9073,8 @@ static bool isCandidateUnique(ref MethodSymbol? method, MethodSymbol candidate)
parameterTypes[i],
parameterRefKinds.IsDefault ? RefKind.None : parameterRefKinds[i],
parameterScopes.IsDefault ? DeclarationScope.Unscoped : parameterScopes[i],
parameterDefaultValues.IsDefault ? null : parameterDefaultValues[i])
parameterDefaultValues.IsDefault ? null : parameterDefaultValues[i],
isParams: hasParamsArray && i == parameterTypes.Length - 1)
);
}
fieldsBuilder.Add(new AnonymousTypeField(name: "", location, returnType, returnRefKind, DeclarationScope.Unscoped));
Expand Down
20 changes: 17 additions & 3 deletions src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ private UnboundLambda AnalyzeAnonymousFunction(

var isAsync = syntax.Modifiers.Any(SyntaxKind.AsyncKeyword);
var isStatic = syntax.Modifiers.Any(SyntaxKind.StaticKeyword);
var hasParamsArray = false;

if (parameterSyntaxList != null)
{
Expand All @@ -111,9 +112,12 @@ private UnboundLambda AnalyzeAnonymousFunction(
// However, we still want to give errors on every bad type in the list, even if one
// is missing.

int parameterCount = 0;
int underscoresCount = 0;
foreach (var p in parameterSyntaxList.Value)
{
parameterCount++;

if (p.Identifier.IsUnderscoreToken())
{
underscoresCount++;
Expand All @@ -139,8 +143,17 @@ private UnboundLambda AnalyzeAnonymousFunction(
else
{
type = BindType(typeSyntax, diagnostics);
ParameterHelpers.CheckParameterModifiers(p, diagnostics, parsingFunctionPointerParams: false, parsingLambdaParams: true);
refKind = ParameterHelpers.GetModifiers(p.Modifiers, out _, out _, out _, out scope);
var isAnonymousMethod = syntax.IsKind(SyntaxKind.AnonymousMethodExpression);
ParameterHelpers.CheckParameterModifiers(p, diagnostics, parsingFunctionPointerParams: false,
parsingLambdaParams: !isAnonymousMethod,
parsingAnonymousMethodParams: isAnonymousMethod);
refKind = ParameterHelpers.GetModifiers(p.Modifiers, out _, out var paramsKeyword, out _, out scope);

var isLastParameter = parameterCount == parameterSyntaxList.Value.Count;
if (isLastParameter && paramsKeyword.Kind() != SyntaxKind.None)
{
hasParamsArray = true;
}
}

namesBuilder.Add(p.Identifier.ValueText);
Expand Down Expand Up @@ -192,7 +205,7 @@ private UnboundLambda AnalyzeAnonymousFunction(

namesBuilder.Free();

return UnboundLambda.Create(syntax, this, diagnostics.AccumulatesDependencies, returnRefKind, returnType, parameterAttributes, refKinds, scopes, types, names, discardsOpt, parameterSyntaxList, defaultValues, isAsync, isStatic);
return UnboundLambda.Create(syntax, this, diagnostics.AccumulatesDependencies, returnRefKind, returnType, parameterAttributes, refKinds, scopes, types, names, discardsOpt, parameterSyntaxList, defaultValues, isAsync: isAsync, isStatic: isStatic, hasParamsArray: hasParamsArray);

static ImmutableArray<bool> computeDiscards(SeparatedSyntaxList<ParameterSyntax> parameters, int underscoresCount)
{
Expand Down Expand Up @@ -315,6 +328,7 @@ private UnboundLambda BindAnonymousFunction(AnonymousFunctionExpressionSyntax sy
}

// UNDONE: Where do we report improper use of pointer types?
// PROTOTYPE: Set `isParams` to report errors about them.
ParameterHelpers.ReportParameterErrors(owner: null, paramSyntax, ordinal: i, isParams: false, lambda.ParameterTypeWithAnnotations(i),
lambda.RefKind(i), lambda.DeclaredScope(i), containingSymbol: null, thisKeyword: default, paramsKeyword: default, firstDefault, diagnostics);
}
Expand Down
17 changes: 13 additions & 4 deletions src/Compilers/CSharp/Portable/BoundTree/UnboundLambda.cs
Original file line number Diff line number Diff line change
Expand Up @@ -390,14 +390,15 @@ public static UnboundLambda Create(
SeparatedSyntaxList<ParameterSyntax>? syntaxList,
ImmutableArray<EqualsValueClauseSyntax?> defaultValues,
bool isAsync,
bool isStatic)
bool isStatic,
bool hasParamsArray)
{
Debug.Assert(binder != null);
Debug.Assert(syntax.IsAnonymousFunction());
bool hasErrors = !types.IsDefault && types.Any(static t => t.Type?.Kind == SymbolKind.ErrorType);

var functionType = FunctionTypeSymbol.CreateIfFeatureEnabled(syntax, binder, static (binder, expr) => ((UnboundLambda)expr).Data.InferDelegateType());
var data = new PlainUnboundLambdaState(binder, returnRefKind, returnType, parameterAttributes, names, discardsOpt, types, refKinds, declaredScopes, defaultValues, syntaxList, isAsync, isStatic, includeCache: true);
var data = new PlainUnboundLambdaState(binder, returnRefKind, returnType, parameterAttributes, names, discardsOpt, types, refKinds, declaredScopes, defaultValues, syntaxList, isAsync: isAsync, isStatic: isStatic, hasParamsArray: hasParamsArray, includeCache: true);
var lambda = new UnboundLambda(syntax, data, functionType, withDependencies, hasErrors: hasErrors);
data.SetUnboundLambda(lambda);
functionType?.SetExpression(lambda.WithNoCache());
Expand Down Expand Up @@ -462,6 +463,7 @@ public TypeWithAnnotations InferReturnType(ConversionsBase conversions, NamedTyp
public bool GenerateSummaryErrors(BindingDiagnosticBag diagnostics) { return Data.GenerateSummaryErrors(diagnostics); }
public bool IsAsync { get { return Data.IsAsync; } }
public bool IsStatic => Data.IsStatic;
public bool HasParamsArray => Data.HasParamsArray;
public SyntaxList<AttributeListSyntax> ParameterAttributes(int index) { return Data.ParameterAttributes(index); }
public TypeWithAnnotations ParameterTypeWithAnnotations(int index) { return Data.ParameterTypeWithAnnotations(index); }
public TypeSymbol ParameterType(int index) { return ParameterTypeWithAnnotations(index).Type; }
Expand Down Expand Up @@ -535,6 +537,7 @@ internal UnboundLambdaState WithCaching(bool includeCache)
public abstract int ParameterCount { get; }
public abstract bool IsAsync { get; }
public abstract bool IsStatic { get; }
public abstract bool HasParamsArray { get; }
public abstract Location ParameterLocation(int index);
public abstract TypeWithAnnotations ParameterTypeWithAnnotations(int index);
public abstract RefKind RefKind(int index);
Expand Down Expand Up @@ -729,7 +732,8 @@ private static TypeWithAnnotations DelegateReturnTypeWithAnnotations(MethodSymbo
parameterRefKinds,
parameterEffectiveScopesBuilder.ToImmutableAndFree(),
parameterTypes,
parameterDefaultValues);
parameterDefaultValues,
_unboundLambda.HasParamsArray);

LambdaSymbol createLambdaSymbol()
{
Expand Down Expand Up @@ -1485,6 +1489,7 @@ internal sealed class PlainUnboundLambdaState : UnboundLambdaState
private readonly SeparatedSyntaxList<ParameterSyntax>? _parameterSyntaxList;
private readonly bool _isAsync;
private readonly bool _isStatic;
private readonly bool _hasParamsArray;

internal PlainUnboundLambdaState(
Binder binder,
Expand All @@ -1500,6 +1505,7 @@ internal PlainUnboundLambdaState(
SeparatedSyntaxList<ParameterSyntax>? parameterSyntaxList,
bool isAsync,
bool isStatic,
bool hasParamsArray,
bool includeCache)
: base(binder, includeCache)
{
Expand All @@ -1515,6 +1521,7 @@ internal PlainUnboundLambdaState(
_parameterSyntaxList = parameterSyntaxList;
_isAsync = isAsync;
_isStatic = isStatic;
_hasParamsArray = hasParamsArray;
}

public override bool HasSignature { get { return !_parameterNames.IsDefault; } }
Expand All @@ -1534,6 +1541,8 @@ public override bool HasExplicitReturnType(out RefKind refKind, out TypeWithAnno

public override bool IsStatic => _isStatic;

public override bool HasParamsArray => _hasParamsArray;

public override MessageID MessageID { get { return this.UnboundLambda.Syntax.Kind() == SyntaxKind.AnonymousMethodExpression ? MessageID.IDS_AnonMethod : MessageID.IDS_Lambda; } }

private CSharpSyntaxNode Body
Expand Down Expand Up @@ -1606,7 +1615,7 @@ public override TypeWithAnnotations ParameterTypeWithAnnotations(int index)

protected override UnboundLambdaState WithCachingCore(bool includeCache)
{
return new PlainUnboundLambdaState(Binder, _returnRefKind, _returnType, _parameterAttributes, _parameterNames, _parameterIsDiscardOpt, _parameterTypesWithAnnotations, _parameterRefKinds, _parameterDeclaredScopes, _defaultValues, _parameterSyntaxList, _isAsync, _isStatic, includeCache);
return new PlainUnboundLambdaState(Binder, _returnRefKind, _returnType, _parameterAttributes, _parameterNames, _parameterIsDiscardOpt, _parameterTypesWithAnnotations, _parameterRefKinds, _parameterDeclaredScopes, _defaultValues, _parameterSyntaxList, isAsync: _isAsync, isStatic: _isStatic, hasParamsArray: _hasParamsArray, includeCache: includeCache);
}

protected override BoundExpression? GetLambdaExpressionBody(BoundBlock body)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ internal bool Equals(AnonymousTypeDescriptor other, TypeCompareKind comparison)
return Fields.SequenceEqual(
other.Fields,
comparison,
static (x, y, comparison) => x.TypeWithAnnotations.Equals(y.TypeWithAnnotations, comparison) && x.RefKind == y.RefKind && x.Scope == y.Scope && x.DefaultValue == y.DefaultValue);
static (x, y, comparison) => x.TypeWithAnnotations.Equals(y.TypeWithAnnotations, comparison) && x.RefKind == y.RefKind && x.Scope == y.Scope && x.DefaultValue == y.DefaultValue && x.IsParams == y.IsParams);
}

/// <summary>
Expand All @@ -103,7 +103,7 @@ internal AnonymousTypeDescriptor WithNewFieldsTypes(ImmutableArray<TypeWithAnnot
Debug.Assert(!newFieldTypes.IsDefault);
Debug.Assert(newFieldTypes.Length == this.Fields.Length);

var newFields = Fields.ZipAsArray(newFieldTypes, static (field, type) => new AnonymousTypeField(field.Name, field.Location, type, field.RefKind, field.Scope, field.DefaultValue));
var newFields = Fields.ZipAsArray(newFieldTypes, static (field, type) => new AnonymousTypeField(field.Name, field.Location, type, field.RefKind, field.Scope, field.DefaultValue, field.IsParams));
return new AnonymousTypeDescriptor(newFields, this.Location);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,21 @@ internal readonly struct AnonymousTypeField

public readonly ConstantValue? DefaultValue;

public readonly bool IsParams;

/// <summary>Anonymous type field type</summary>
public TypeSymbol Type => TypeWithAnnotations.Type;

// PROTOTYPE: Sync with IDE about the addition of DefaultValue to AnonymousTypeField to see how it will affect their usage of anonymous type symbols
public AnonymousTypeField(string name, Location location, TypeWithAnnotations typeWithAnnotations, RefKind refKind, DeclarationScope scope, ConstantValue? defaultValue = null)
// PROTOTYPE: Sync with IDE about the addition of DefaultValue and IsParams to AnonymousTypeField to see how it will affect their usage of anonymous type symbols
public AnonymousTypeField(string name, Location location, TypeWithAnnotations typeWithAnnotations, RefKind refKind, DeclarationScope scope, ConstantValue? defaultValue = null, bool isParams = false)
{
this.Name = name;
this.Location = location;
this.TypeWithAnnotations = typeWithAnnotations;
this.RefKind = refKind;
this.Scope = scope;
this.DefaultValue = defaultValue;
this.IsParams = isParams;
}

[Conditional("DEBUG")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ static bool hasDefaultScope(bool useUpdatedEscapeRules, AnonymousTypeField field

static bool isValidTypeArgument(bool useUpdatedEscapeRules, AnonymousTypeField field)
{
return hasDefaultScope(useUpdatedEscapeRules, field) &&
return !field.IsParams &&
hasDefaultScope(useUpdatedEscapeRules, field) &&
field.DefaultValue is null &&
field.Type is { } type &&
!type.IsPointerOrFunctionPointer() &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ private ImmutableArray<Symbol> CreateMembers()
var constructor = new SynthesizedDelegateConstructor(this, Manager.System_Object, Manager.System_IntPtr);
var fields = TypeDescriptor.Fields;
int parameterCount = fields.Length - 1;
var parameters = ArrayBuilder<(TypeWithAnnotations, RefKind, DeclarationScope, ConstantValue?)>.GetInstance(parameterCount);
var parameters = ArrayBuilder<(TypeWithAnnotations, RefKind, DeclarationScope, ConstantValue?, bool)>.GetInstance(parameterCount);
for (int i = 0; i < parameterCount; i++)
{
var field = fields[i];
parameters.Add((field.TypeWithAnnotations, field.RefKind, field.Scope, field.DefaultValue));
parameters.Add((field.TypeWithAnnotations, field.RefKind, field.Scope, field.DefaultValue, field.IsParams));
}
var returnField = fields.Last();
var invokeMethod = new SynthesizedDelegateInvokeMethod(this, parameters, returnField.TypeWithAnnotations, returnField.RefKind);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ static SynthesizedDelegateInvokeMethod createInvokeMethod(AnonymousDelegateTempl
var typeParams = containingType.TypeParameters;

int parameterCount = typeParams.Length - (voidReturnTypeOpt is null ? 1 : 0);
var parameters = ArrayBuilder<(TypeWithAnnotations, RefKind, DeclarationScope, ConstantValue?)>.GetInstance(parameterCount);
var parameters = ArrayBuilder<(TypeWithAnnotations Type, RefKind RefKind, DeclarationScope Scope, ConstantValue? DefaultValue, bool IsParams)>.GetInstance(parameterCount);
for (int i = 0; i < parameterCount; i++)
{
parameters.Add((TypeWithAnnotations.Create(typeParams[i]), refKinds.IsNull ? RefKind.None : refKinds[i], DeclarationScope.Unscoped, null));
parameters.Add((TypeWithAnnotations.Create(typeParams[i]), refKinds.IsNull ? RefKind.None : refKinds[i], DeclarationScope.Unscoped, DefaultValue: null, IsParams: false));
}

// if we are given Void type the method returns Void, otherwise its return type is the last type parameter of the delegate:
Expand Down Expand Up @@ -127,11 +127,11 @@ static SynthesizedDelegateInvokeMethod createInvokeMethod(
TypeMap typeMap)
{
var parameterCount = fields.Length - 1;
var parameters = ArrayBuilder<(TypeWithAnnotations, RefKind, DeclarationScope, ConstantValue?)>.GetInstance(parameterCount);
var parameters = ArrayBuilder<(TypeWithAnnotations, RefKind, DeclarationScope, ConstantValue?, bool)>.GetInstance(parameterCount);
for (int i = 0; i < parameterCount; i++)
{
var field = fields[i];
parameters.Add((typeMap.SubstituteType(field.Type), field.RefKind, field.Scope, field.DefaultValue));
parameters.Add((typeMap.SubstituteType(field.Type), field.RefKind, field.Scope, field.DefaultValue, field.IsParams));
}

var returnParameter = fields[^1];
Expand Down
Loading

0 comments on commit 3711c43

Please sign in to comment.