Skip to content

Commit

Permalink
Support modifiers with simple lambda parameters. (#75400)
Browse files Browse the repository at this point in the history
Co-authored-by: Jan Jones <[email protected]>
  • Loading branch information
CyrusNajmabadi and jjonescz authored Jan 16, 2025
1 parent 81afe92 commit 8c05cc8
Show file tree
Hide file tree
Showing 37 changed files with 3,160 additions and 527 deletions.
25 changes: 25 additions & 0 deletions docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

This document lists known breaking changes in Roslyn after .NET 9 general release (.NET SDK version 9.0.100) through .NET 10 general release (.NET SDK version 10.0.100).

## `scoped` in a lambda parameter list is now always a modifier.

***Introduced in Visual Studio 2022 version 17.13***

C# 14 introduces the ability to write a lambda with parameter modifiers, without having to specify a parameter type:
https://github.com/dotnet/csharplang/blob/main/proposals/simple-lambda-parameters-with-modifiers.md

As part of this work, a breaking change was accepted where `scoped` will always be treated as a modifier
in a lambda parameter, even where it might have been accepted as a type name in the past. For example:

```c#
var v = (scoped scoped s) => { ... };

ref struct @scoped { }
```

In C# 14 this will be an error as both `scoped` tokens are treated as modifiers. The workaround is to
use `@` in the type name position like so:

```c#
var v = (scoped @scoped s) => { ... };

ref struct @scoped { }
```

## `Span<T>` and `ReadOnlySpan<T>` overloads are applicable in more scenarios in C# 14 and newer

***Introduced in Visual Studio 2022 version 17.13***
Expand Down
143 changes: 66 additions & 77 deletions src/Compilers/CSharp/Portable/Binder/Binder_Lambda.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
Expand All @@ -20,6 +20,7 @@ internal partial class Binder
// delegate (int x) { } (typed parameter list)
// x => ... (type-inferred parameter list)
// (x) => ... (type-inferred parameter list)
// (ref x) => ... (type-inferred parameter list with modifiers)
// (x, y) => ... (type-inferred parameter list)
// ( ) => ... (typed parameter list)
// (ref int x) => ... (typed parameter list)
Expand Down Expand Up @@ -52,7 +53,7 @@ private UnboundLambda AnalyzeAnonymousFunction(

var namesBuilder = ArrayBuilder<string>.GetInstance();
ImmutableArray<bool> discardsOpt = default;
SeparatedSyntaxList<ParameterSyntax>? parameterSyntaxList = null;
SeparatedSyntaxList<ParameterSyntax>? parameterSyntaxListOpt = null;
bool hasSignature;

if (syntax is LambdaExpressionSyntax lambdaSyntax)
Expand Down Expand Up @@ -80,8 +81,9 @@ private UnboundLambda AnalyzeAnonymousFunction(
{
(returnRefKind, returnType) = BindExplicitLambdaReturnType(returnTypeSyntax, diagnostics);
}
parameterSyntaxList = paren.ParameterList.Parameters;
CheckParenthesizedLambdaParameters(parameterSyntaxList.Value, diagnostics);

parameterSyntaxListOpt = paren.ParameterList.Parameters;
CheckParenthesizedLambdaParameters(parameterSyntaxListOpt.Value, diagnostics);
break;
case SyntaxKind.AnonymousMethodExpression:
// delegate (int x) { }
Expand All @@ -92,7 +94,7 @@ private UnboundLambda AnalyzeAnonymousFunction(
hasSignature = anon.ParameterList != null;
if (hasSignature)
{
parameterSyntaxList = anon.ParameterList!.Parameters;
parameterSyntaxListOpt = anon.ParameterList!.Parameters;
}

break;
Expand All @@ -115,9 +117,10 @@ private UnboundLambda AnalyzeAnonymousFunction(
}
}

if (parameterSyntaxList != null)
if (parameterSyntaxListOpt is { } parameterSyntaxList)
{
var hasExplicitlyTypedParameterList = true;
var isAnonymousMethod = syntax.IsKind(SyntaxKind.AnonymousMethodExpression);
var hasExplicitlyTypedParameterList = parameterSyntaxList.All(static p => p.Type != null);

var typesBuilder = ArrayBuilder<TypeWithAnnotations>.GetInstance();
var refKindsBuilder = ArrayBuilder<RefKind>.GetInstance();
Expand All @@ -135,20 +138,26 @@ private UnboundLambda AnalyzeAnonymousFunction(

int parameterCount = 0;
int underscoresCount = 0;
foreach (var p in parameterSyntaxList.Value)
int firstDefault = -1;
for (int i = 0, n = parameterSyntaxList.Count; i < n; i++)
{
parameterCount++;

var p = parameterSyntaxList[i];
if (p.Identifier.IsUnderscoreToken())
{
underscoresCount++;
}

checkAttributes(syntax, p.AttributeLists, diagnostics);

var isAnonymousMethod = syntax.IsKind(SyntaxKind.AnonymousMethodExpression);
if (p.Default != null)
{
if (firstDefault == -1)
{
firstDefault = i;
}

if (isAnonymousMethod)
{
Error(diagnostics, ErrorCode.ERR_DefaultValueNotAllowed, p.Default.EqualsToken);
Expand All @@ -165,43 +174,43 @@ private UnboundLambda AnalyzeAnonymousFunction(
continue;
}

var typeSyntax = p.Type;
TypeWithAnnotations type = default;
var refKind = RefKind.None;
var scope = ScopedKind.None;
var typeOpt = p.Type is not null ? BindType(p.Type, diagnostics) : default;

if (typeSyntax == null)
{
hasExplicitlyTypedParameterList = false;
}
else
var refKind = ParameterHelpers.GetModifiers(p.Modifiers, out _, out var paramsKeyword, out _, out var scope);
var isParams = paramsKeyword.Kind() != SyntaxKind.None;

ParameterHelpers.CheckParameterModifiers(p, diagnostics, parsingFunctionPointerParams: false,
parsingLambdaParams: !isAnonymousMethod,
parsingAnonymousMethodParams: isAnonymousMethod);

ParameterHelpers.ReportParameterErrors(
owner: null, p, ordinal: i, lastParameterIndex: n - 1, isParams: isParams, typeOpt,
refKind, containingSymbol: null, thisKeyword: default, paramsKeyword: paramsKeyword, firstDefault, diagnostics);

if (parameterCount == parameterSyntaxList.Count &&
paramsKeyword.Kind() != SyntaxKind.None &&
!typeOpt.IsDefault &&
typeOpt.IsSZArray())
{
type = BindType(typeSyntax, diagnostics);
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 && type.IsSZArray())
{
ReportUseSiteDiagnosticForSynthesizedAttribute(Compilation,
WellKnownMember.System_ParamArrayAttribute__ctor,
diagnostics,
paramsKeyword.GetLocation());
}
ReportUseSiteDiagnosticForSynthesizedAttribute(Compilation,
WellKnownMember.System_ParamArrayAttribute__ctor,
diagnostics,
paramsKeyword.GetLocation());
}

namesBuilder.Add(p.Identifier.ValueText);
typesBuilder.Add(type);
typesBuilder.Add(typeOpt);
refKindsBuilder.Add(refKind);
scopesBuilder.Add(scope);
attributesBuilder.Add(syntax.Kind() == SyntaxKind.ParenthesizedLambdaExpression ? p.AttributeLists : default);
defaultValueBuilder.Add(p.Default);
}

discardsOpt = computeDiscards(parameterSyntaxList.Value, underscoresCount);
discardsOpt = computeDiscards(parameterSyntaxListOpt.Value, underscoresCount);

// Only include the types if *all* the parameters had types. Otherwise, if there were no parameter
// types (or a mix of typed and untyped parameters) include no types. Note, in the latter case we will
// have already reported an error about this issue.
if (hasExplicitlyTypedParameterList)
{
types = typesBuilder.ToImmutable();
Expand Down Expand Up @@ -241,7 +250,7 @@ private UnboundLambda AnalyzeAnonymousFunction(

namesBuilder.Free();

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

static ImmutableArray<bool> computeDiscards(SeparatedSyntaxList<ParameterSyntax> parameters, int underscoresCount)
{
Expand Down Expand Up @@ -308,39 +317,40 @@ private static void CheckParenthesizedLambdaParameters(
{
if (parameterSyntaxList.Count > 0)
{
var hasTypes = parameterSyntaxList[0].Type != null;
// If one parameter has a type, then all parameters must have a type.
var requiresTypes = parameterSyntaxList.Any(static p => p.Type != null);

checkForImplicitDefault(hasTypes, parameterSyntaxList[0], diagnostics);

for (int i = 1, n = parameterSyntaxList.Count; i < n; i++)
foreach (var parameter in parameterSyntaxList)
{
var parameter = parameterSyntaxList[i];

// Ignore parameters with missing names. We'll have already reported an error
// about them in the parser.
if (!parameter.Identifier.IsMissing)
{
var thisParameterHasType = parameter.Type != null;

if (hasTypes != thisParameterHasType)
if (requiresTypes)
{
diagnostics.Add(ErrorCode.ERR_InconsistentLambdaParameterUsage,
parameter.Type?.GetLocation() ?? parameter.Identifier.GetLocation());
if (parameter.Type is null)
{
diagnostics.Add(ErrorCode.ERR_InconsistentLambdaParameterUsage,
parameter.Identifier.GetLocation());
}
}
else
{
if (parameter.Default != null)
{
diagnostics.Add(ErrorCode.ERR_ImplicitlyTypedDefaultParameter,
parameter.Identifier.GetLocation(), parameter.Identifier.Text);
}
}

checkForImplicitDefault(thisParameterHasType, parameter, diagnostics);
// Check if `(ref i) => ...` is supported by this language version.
if (parameter.Modifiers.Count > 0 && parameter.Type is null)
{
CheckFeatureAvailability(parameter, MessageID.IDS_FeatureSimpleLambdaParameterModifiers, diagnostics);
}
}
}
}

static void checkForImplicitDefault(bool hasType, ParameterSyntax param, BindingDiagnosticBag diagnostics)
{
if (!hasType && param.Default != null)
{
diagnostics.Add(ErrorCode.ERR_ImplicitlyTypedDefaultParameter,
param.Identifier.GetLocation(), param.Identifier.Text);
}
}
}

private UnboundLambda BindAnonymousFunction(AnonymousFunctionExpressionSyntax syntax, BindingDiagnosticBag diagnostics)
Expand All @@ -350,27 +360,6 @@ private UnboundLambda BindAnonymousFunction(AnonymousFunctionExpressionSyntax sy

var lambda = AnalyzeAnonymousFunction(syntax, diagnostics);
var data = lambda.Data;
if (data.HasExplicitlyTypedParameterList)
{
int firstDefault = -1;
for (int i = 0; i < lambda.ParameterCount; i++)
{
// paramSyntax should not be null here; we should always be operating on an anonymous function which will have parameter information
var paramSyntax = lambda.ParameterSyntax(i);
Debug.Assert(paramSyntax is { });
if (paramSyntax.Default != null && firstDefault == -1)
{
firstDefault = i;
}

ParameterHelpers.GetModifiers(paramSyntax.Modifiers, refnessKeyword: out _, out var paramsKeyword, thisKeyword: out _, scope: out _);
var isParams = paramsKeyword.Kind() != SyntaxKind.None;

// UNDONE: Where do we report improper use of pointer types?
ParameterHelpers.ReportParameterErrors(owner: null, paramSyntax, ordinal: i, lastParameterIndex: lambda.ParameterCount - 1, isParams: isParams, lambda.ParameterTypeWithAnnotations(i),
lambda.RefKind(i), containingSymbol: null, thisKeyword: default, paramsKeyword: paramsKeyword, firstDefault, diagnostics);
}
}

// Parser will only have accepted static/async as allowed modifiers on this construct.
// However, it may have accepted duplicates of those modifiers. Ensure that any dupes
Expand Down
48 changes: 26 additions & 22 deletions src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2148,18 +2148,33 @@ internal void GenerateAnonymousFunctionConversionError(BindingDiagnosticBag diag
// The simplest possible case is (x, y, z)=>whatever where the target type has a ref or out parameter.

var delegateParameters = delegateType.DelegateParameters();
if (reason == LambdaConversionResult.RefInImplicitlyTypedLambda)
if (reason == LambdaConversionResult.MismatchedParameterRefKind)
{
for (int i = 0; i < anonymousFunction.ParameterCount; ++i)
{
var delegateRefKind = delegateParameters[i].RefKind;
if (delegateRefKind != RefKind.None)
var lambdaRefKind = anonymousFunction.RefKind(i);

if (!OverloadResolution.AreRefsCompatibleForMethodConversion(
candidateMethodParameterRefKind: lambdaRefKind,
delegateParameterRefKind: delegateRefKind,
this.Compilation))
{
// Parameter {0} must be declared with the '{1}' keyword
Error(diagnostics, ErrorCode.ERR_BadParamRef, anonymousFunction.ParameterLocation(i),
i + 1, delegateRefKind.ToParameterDisplayString());
var lambdaParameterLocation = anonymousFunction.ParameterLocation(i);
if (delegateRefKind == RefKind.None)
{
// Parameter {0} should not be declared with the '{1}' keyword
Error(diagnostics, ErrorCode.ERR_BadParamExtraRef, lambdaParameterLocation, i + 1, lambdaRefKind.ToParameterDisplayString());
}
else
{
// Parameter {0} must be declared with the '{1}' keyword
Error(diagnostics, ErrorCode.ERR_BadParamRef, lambdaParameterLocation, i + 1, delegateRefKind.ToParameterDisplayString());
}
}
}

Debug.Assert(diagnostics.DiagnosticBag?.Count is not 0);
return;
}

Expand All @@ -2181,16 +2196,18 @@ internal void GenerateAnonymousFunctionConversionError(BindingDiagnosticBag diag

if (reason == LambdaConversionResult.MismatchedParameterType)
{
// This is checked in the code that returns LambdaConversionResult.MismatchedParameterType
Debug.Assert(anonymousFunction.HasExplicitlyTypedParameterList);

// Cannot convert {0} to type '{1}' because the parameter types do not match the delegate parameter types
conversionError(diagnostics, ErrorCode.ERR_CantConvAnonMethParams, id, targetType);
Debug.Assert(anonymousFunction.ParameterCount == delegateParameters.Length);
for (int i = 0; i < anonymousFunction.ParameterCount; ++i)
{
var lambdaParameterType = anonymousFunction.ParameterType(i);
if (lambdaParameterType.IsErrorType())
{
continue;
}

// Can't be an error type. This was already checked in a loop above this one.
Debug.Assert(!lambdaParameterType.IsErrorType());

var lambdaParameterLocation = anonymousFunction.ParameterLocation(i);
var lambdaRefKind = anonymousFunction.RefKind(i);
Expand All @@ -2205,19 +2222,6 @@ internal void GenerateAnonymousFunctionConversionError(BindingDiagnosticBag diag
Error(diagnostics, ErrorCode.ERR_BadParamType, lambdaParameterLocation,
i + 1, lambdaRefKind.ToParameterPrefix(), distinguisher.First, delegateRefKind.ToParameterPrefix(), distinguisher.Second);
}
else if (lambdaRefKind != delegateRefKind)
{
if (delegateRefKind == RefKind.None)
{
// Parameter {0} should not be declared with the '{1}' keyword
Error(diagnostics, ErrorCode.ERR_BadParamExtraRef, lambdaParameterLocation, i + 1, lambdaRefKind.ToParameterDisplayString());
}
else
{
// Parameter {0} must be declared with the '{1}' keyword
Error(diagnostics, ErrorCode.ERR_BadParamRef, lambdaParameterLocation, i + 1, delegateRefKind.ToParameterDisplayString());
}
}
}
return;
}
Expand Down
Loading

0 comments on commit 8c05cc8

Please sign in to comment.