Skip to content

Commit c3beaf8

Browse files
authored
Support consumption of instance increment operators (#77098)
1 parent 3d90c17 commit c3beaf8

File tree

9 files changed

+3096
-56
lines changed

9 files changed

+3096
-56
lines changed

src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11011,38 +11011,7 @@ private BoundConditionalAccess BindConditionalAccessExpression(ConditionalAccess
1101111011
// For improved diagnostics we detect the cases where the value will be used and produce a
1101211012
// more specific (though not technically correct) diagnostic here:
1101311013
// "Error CS0023: Operator '?' cannot be applied to operand of type 'T'"
11014-
bool resultIsUsed = true;
11015-
CSharpSyntaxNode parent = node.Parent;
11016-
11017-
if (parent != null)
11018-
{
11019-
switch (parent.Kind())
11020-
{
11021-
case SyntaxKind.ExpressionStatement:
11022-
resultIsUsed = ((ExpressionStatementSyntax)parent).Expression != node;
11023-
break;
11024-
11025-
case SyntaxKind.SimpleLambdaExpression:
11026-
resultIsUsed = (((SimpleLambdaExpressionSyntax)parent).Body != node) || MethodOrLambdaRequiresValue(ContainingMemberOrLambda, Compilation);
11027-
break;
11028-
11029-
case SyntaxKind.ParenthesizedLambdaExpression:
11030-
resultIsUsed = (((ParenthesizedLambdaExpressionSyntax)parent).Body != node) || MethodOrLambdaRequiresValue(ContainingMemberOrLambda, Compilation);
11031-
break;
11032-
11033-
case SyntaxKind.ArrowExpressionClause:
11034-
resultIsUsed = (((ArrowExpressionClauseSyntax)parent).Expression != node) || MethodOrLambdaRequiresValue(ContainingMemberOrLambda, Compilation);
11035-
break;
11036-
11037-
case SyntaxKind.ForStatement:
11038-
// Incrementors and Initializers doesn't have to produce a value
11039-
var loop = (ForStatementSyntax)parent;
11040-
resultIsUsed = !loop.Incrementors.Contains(node) && !loop.Initializers.Contains(node);
11041-
break;
11042-
}
11043-
}
11044-
11045-
if (resultIsUsed)
11014+
if (ResultIsUsed(node))
1104611015
{
1104711016
return GenerateBadConditionalAccessNodeError(node, receiver, access, diagnostics);
1104811017
}
@@ -11061,6 +11030,42 @@ private BoundConditionalAccess BindConditionalAccessExpression(ConditionalAccess
1106111030
return new BoundConditionalAccess(node, receiver, access, accessType);
1106211031
}
1106311032

11033+
private bool ResultIsUsed(ExpressionSyntax node)
11034+
{
11035+
bool resultIsUsed = true;
11036+
CSharpSyntaxNode parent = node.Parent;
11037+
11038+
if (parent != null)
11039+
{
11040+
switch (parent.Kind())
11041+
{
11042+
case SyntaxKind.ExpressionStatement:
11043+
resultIsUsed = ((ExpressionStatementSyntax)parent).Expression != node;
11044+
break;
11045+
11046+
case SyntaxKind.SimpleLambdaExpression:
11047+
resultIsUsed = (((SimpleLambdaExpressionSyntax)parent).Body != node) || MethodOrLambdaRequiresValue(ContainingMemberOrLambda, Compilation);
11048+
break;
11049+
11050+
case SyntaxKind.ParenthesizedLambdaExpression:
11051+
resultIsUsed = (((ParenthesizedLambdaExpressionSyntax)parent).Body != node) || MethodOrLambdaRequiresValue(ContainingMemberOrLambda, Compilation);
11052+
break;
11053+
11054+
case SyntaxKind.ArrowExpressionClause:
11055+
resultIsUsed = (((ArrowExpressionClauseSyntax)parent).Expression != node) || MethodOrLambdaRequiresValue(ContainingMemberOrLambda, Compilation);
11056+
break;
11057+
11058+
case SyntaxKind.ForStatement:
11059+
// Incrementors and Initializers doesn't have to produce a value
11060+
var loop = (ForStatementSyntax)parent;
11061+
resultIsUsed = !loop.Incrementors.Contains(node) && !loop.Initializers.Contains(node);
11062+
break;
11063+
}
11064+
}
11065+
11066+
return resultIsUsed;
11067+
}
11068+
1106411069
internal static bool MethodOrLambdaRequiresValue(Symbol symbol, CSharpCompilation compilation)
1106511070
{
1106611071
return symbol is MethodSymbol method &&

src/Compilers/CSharp/Portable/Binder/Binder_Lookup.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,10 @@ internal SingleLookupResult CheckViability(Symbol symbol, int arity, LookupOptio
13991399
{
14001400
return LookupResult.Empty();
14011401
}
1402+
else if ((options & LookupOptions.MustBeOperator) != 0 && unwrappedSymbol is not MethodSymbol { MethodKind: MethodKind.UserDefinedOperator })
1403+
{
1404+
return LookupResult.Empty();
1405+
}
14021406
else if (!IsInScopeOfAssociatedSyntaxTree(unwrappedSymbol))
14031407
{
14041408
return LookupResult.Empty();
@@ -1417,7 +1421,7 @@ internal SingleLookupResult CheckViability(Symbol symbol, int arity, LookupOptio
14171421
{
14181422
return LookupResult.WrongArity(symbol, diagInfo);
14191423
}
1420-
else if (!InCref && !unwrappedSymbol.CanBeReferencedByNameIgnoringIllegalCharacters)
1424+
else if (!InCref && !unwrappedSymbol.CanBeReferencedByNameIgnoringIllegalCharacters && (options & LookupOptions.MustBeOperator) == 0)
14211425
{
14221426
// Strictly speaking, this test should actually check CanBeReferencedByName.
14231427
// However, we don't want to pay that cost in cases where the lookup is based

src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs

Lines changed: 238 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Collections.Generic;
99
using System.Collections.Immutable;
1010
using System.Diagnostics;
11+
using System.Diagnostics.CodeAnalysis;
1112
using System.Linq;
1213
using Microsoft.CodeAnalysis.CSharp.Symbols;
1314
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -2262,7 +2263,9 @@ public static BinaryOperatorKind SyntaxKindToBinaryOperatorKind(SyntaxKind kind)
22622263
}
22632264
}
22642265

2265-
private BoundExpression BindIncrementOperator(CSharpSyntaxNode node, ExpressionSyntax operandSyntax, SyntaxToken operatorToken, BindingDiagnosticBag diagnostics)
2266+
#nullable enable
2267+
2268+
private BoundExpression BindIncrementOperator(ExpressionSyntax node, ExpressionSyntax operandSyntax, SyntaxToken operatorToken, BindingDiagnosticBag diagnostics)
22662269
{
22672270
operandSyntax.CheckDeconstructionCompatibleArgument(diagnostics);
22682271

@@ -2290,7 +2293,7 @@ private BoundExpression BindIncrementOperator(CSharpSyntaxNode node, ExpressionS
22902293

22912294
// The operand has to be a variable, property or indexer, so it must have a type.
22922295
var operandType = operand.Type;
2293-
Debug.Assert((object)operandType != null);
2296+
Debug.Assert(operandType is not null);
22942297

22952298
if (operandType.IsDynamic())
22962299
{
@@ -2310,6 +2313,13 @@ private BoundExpression BindIncrementOperator(CSharpSyntaxNode node, ExpressionS
23102313
hasErrors: false);
23112314
}
23122315

2316+
// Try an in-place user-defined operator
2317+
BoundIncrementOperator? inPlaceResult = tryApplyUserDefinedInstanceOperator(node, operatorToken, kind, operand, diagnostics);
2318+
if (inPlaceResult is not null)
2319+
{
2320+
return inPlaceResult;
2321+
}
2322+
23132323
LookupResultKind resultKind;
23142324
ImmutableArray<MethodSymbol> originalUserDefinedOperators;
23152325
var best = this.UnaryOperatorOverloadResolution(kind, operand, node, diagnostics, out resultKind, out originalUserDefinedOperators);
@@ -2339,7 +2349,7 @@ private BoundExpression BindIncrementOperator(CSharpSyntaxNode node, ExpressionS
23392349

23402350
var resultPlaceholder = new BoundValuePlaceholder(node, signature.ReturnType).MakeCompilerGenerated();
23412351

2342-
BoundExpression resultConversion = GenerateConversionForAssignment(operandType, resultPlaceholder, diagnostics, ConversionForAssignmentFlags.IncrementAssignment);
2352+
BoundExpression? resultConversion = GenerateConversionForAssignment(operandType, resultPlaceholder, diagnostics, ConversionForAssignmentFlags.IncrementAssignment);
23432353

23442354
bool hasErrors = resultConversion.HasErrors;
23452355

@@ -2376,6 +2386,231 @@ private BoundExpression BindIncrementOperator(CSharpSyntaxNode node, ExpressionS
23762386
originalUserDefinedOperators,
23772387
operandType,
23782388
hasErrors);
2389+
2390+
BoundIncrementOperator? tryApplyUserDefinedInstanceOperator(ExpressionSyntax node, SyntaxToken operatorToken, UnaryOperatorKind kind, BoundExpression operand, BindingDiagnosticBag diagnostics)
2391+
{
2392+
var operandType = operand.Type;
2393+
Debug.Assert(operandType is not null);
2394+
Debug.Assert(!operandType.IsDynamic());
2395+
2396+
if (kind is not (UnaryOperatorKind.PrefixIncrement or UnaryOperatorKind.PrefixDecrement or UnaryOperatorKind.PostfixIncrement or UnaryOperatorKind.PostfixDecrement) ||
2397+
operandType.SpecialType.IsNumericType() ||
2398+
!node.IsFeatureEnabled(MessageID.IDS_FeatureUserDefinedCompoundAssignmentOperators))
2399+
{
2400+
return null;
2401+
}
2402+
2403+
bool resultIsUsed = ResultIsUsed(node);
2404+
2405+
if ((kind is (UnaryOperatorKind.PostfixIncrement or UnaryOperatorKind.PostfixDecrement) && resultIsUsed) ||
2406+
!CheckValueKind(node, operand, BindValueKind.RefersToLocation | BindValueKind.Assignable, checkingReceiver: false, BindingDiagnosticBag.Discarded))
2407+
{
2408+
return null;
2409+
}
2410+
2411+
bool checkOverflowAtRuntime = CheckOverflowAtRuntime;
2412+
CompoundUseSiteInfo<AssemblySymbol> useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics);
2413+
2414+
ArrayBuilder<MethodSymbol>? methods = lookupUserDefinedInstanceOperators(
2415+
operandType,
2416+
checkedName: checkOverflowAtRuntime ?
2417+
(kind is UnaryOperatorKind.PrefixIncrement or UnaryOperatorKind.PostfixIncrement ?
2418+
WellKnownMemberNames.CheckedIncrementOperatorName :
2419+
WellKnownMemberNames.CheckedDecrementOperatorName) :
2420+
null,
2421+
ordinaryName: kind is UnaryOperatorKind.PrefixIncrement or UnaryOperatorKind.PostfixIncrement ?
2422+
WellKnownMemberNames.IncrementOperatorName :
2423+
WellKnownMemberNames.DecrementOperatorName,
2424+
ref useSiteInfo);
2425+
2426+
if (methods?.IsEmpty != false)
2427+
{
2428+
diagnostics.Add(node, useSiteInfo);
2429+
methods?.Free();
2430+
return null;
2431+
}
2432+
2433+
Debug.Assert(!methods.IsEmpty);
2434+
2435+
var overloadResolutionResult = OverloadResolutionResult<MethodSymbol>.GetInstance();
2436+
var typeArguments = ArrayBuilder<TypeWithAnnotations>.GetInstance();
2437+
var analyzedArguments = AnalyzedArguments.GetInstance();
2438+
2439+
OverloadResolution.MethodInvocationOverloadResolution(
2440+
methods,
2441+
typeArguments,
2442+
operand,
2443+
analyzedArguments,
2444+
overloadResolutionResult,
2445+
ref useSiteInfo,
2446+
OverloadResolution.Options.None);
2447+
2448+
typeArguments.Free();
2449+
diagnostics.Add(node, useSiteInfo);
2450+
2451+
BoundIncrementOperator? inPlaceResult;
2452+
2453+
if (overloadResolutionResult.Succeeded)
2454+
{
2455+
var method = overloadResolutionResult.ValidResult.Member;
2456+
2457+
ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver: false);
2458+
ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, method, node, isDelegateConversion: false);
2459+
2460+
inPlaceResult = new BoundIncrementOperator(
2461+
node,
2462+
(kind | UnaryOperatorKind.UserDefined).WithOverflowChecksIfApplicable(checkOverflowAtRuntime),
2463+
operand,
2464+
methodOpt: method,
2465+
constrainedToTypeOpt: null,
2466+
operandPlaceholder: null,
2467+
operandConversion: null,
2468+
resultPlaceholder: null,
2469+
resultConversion: null,
2470+
LookupResultKind.Viable,
2471+
ImmutableArray<MethodSymbol>.Empty,
2472+
resultIsUsed ? operandType : GetSpecialType(SpecialType.System_Void, diagnostics, node));
2473+
2474+
methods.Free();
2475+
}
2476+
else if (overloadResolutionResult.HasAnyApplicableMember)
2477+
{
2478+
ImmutableArray<MethodSymbol> methodsArray = methods.ToImmutableAndFree();
2479+
2480+
overloadResolutionResult.ReportDiagnostics(
2481+
binder: this, location: operatorToken.GetLocation(), nodeOpt: node, diagnostics: diagnostics, name: operatorToken.ValueText,
2482+
receiver: operand, invokedExpression: node, arguments: analyzedArguments, memberGroup: methodsArray,
2483+
typeContainingConstructor: null, delegateTypeBeingInvoked: null);
2484+
2485+
inPlaceResult = new BoundIncrementOperator(
2486+
node,
2487+
(kind | UnaryOperatorKind.UserDefined).WithOverflowChecksIfApplicable(checkOverflowAtRuntime),
2488+
operand,
2489+
methodOpt: null,
2490+
constrainedToTypeOpt: null,
2491+
operandPlaceholder: null,
2492+
operandConversion: null,
2493+
resultPlaceholder: null,
2494+
resultConversion: null,
2495+
LookupResultKind.OverloadResolutionFailure,
2496+
methodsArray,
2497+
resultIsUsed ? operandType : GetSpecialType(SpecialType.System_Void, diagnostics, node));
2498+
}
2499+
else
2500+
{
2501+
inPlaceResult = null;
2502+
methods.Free();
2503+
}
2504+
2505+
analyzedArguments.Free();
2506+
overloadResolutionResult.Free();
2507+
2508+
return inPlaceResult;
2509+
}
2510+
2511+
ArrayBuilder<MethodSymbol>? lookupUserDefinedInstanceOperators(TypeSymbol lookupInType, string? checkedName, string ordinaryName, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
2512+
{
2513+
var lookupResult = LookupResult.GetInstance();
2514+
ArrayBuilder<MethodSymbol>? methods = null;
2515+
if (checkedName is not null)
2516+
{
2517+
this.LookupMembersWithFallback(lookupResult, lookupInType, name: checkedName, arity: 0, ref useSiteInfo, basesBeingResolved: null, options: LookupOptions.MustBeInstance | LookupOptions.MustBeOperator);
2518+
2519+
if (lookupResult.IsMultiViable)
2520+
{
2521+
methods = ArrayBuilder<MethodSymbol>.GetInstance(lookupResult.Symbols.Count);
2522+
appendViableMethods(lookupResult, methods);
2523+
}
2524+
2525+
lookupResult.Clear();
2526+
}
2527+
2528+
this.LookupMembersWithFallback(lookupResult, lookupInType, name: ordinaryName, arity: 0, ref useSiteInfo, basesBeingResolved: null, options: LookupOptions.MustBeInstance | LookupOptions.MustBeOperator);
2529+
2530+
if (lookupResult.IsMultiViable)
2531+
{
2532+
if (methods is null)
2533+
{
2534+
methods = ArrayBuilder<MethodSymbol>.GetInstance(lookupResult.Symbols.Count);
2535+
appendViableMethods(lookupResult, methods);
2536+
}
2537+
else
2538+
{
2539+
var existing = new HashSet<MethodSymbol>(PairedOperatorComparer.Instance);
2540+
2541+
foreach (var method in methods)
2542+
{
2543+
existing.Add(method.GetLeastOverriddenMethod(ContainingType));
2544+
}
2545+
2546+
foreach (MethodSymbol method in lookupResult.Symbols)
2547+
{
2548+
if (isViable(method) && !existing.Contains(method.GetLeastOverriddenMethod(ContainingType)))
2549+
{
2550+
methods.Add(method);
2551+
}
2552+
}
2553+
}
2554+
}
2555+
2556+
lookupResult.Free();
2557+
2558+
return methods;
2559+
2560+
static void appendViableMethods(LookupResult lookupResult, ArrayBuilder<MethodSymbol> methods)
2561+
{
2562+
foreach (MethodSymbol method in lookupResult.Symbols)
2563+
{
2564+
if (isViable(method))
2565+
{
2566+
methods.Add(method);
2567+
}
2568+
}
2569+
}
2570+
2571+
static bool isViable(MethodSymbol method)
2572+
{
2573+
return method.ParameterCount == 0;
2574+
}
2575+
}
2576+
}
2577+
2578+
#nullable disable
2579+
2580+
private class PairedOperatorComparer : IEqualityComparer<MethodSymbol>
2581+
{
2582+
public static readonly PairedOperatorComparer Instance = new PairedOperatorComparer();
2583+
2584+
private PairedOperatorComparer() { }
2585+
2586+
public bool Equals(MethodSymbol x, MethodSymbol y)
2587+
{
2588+
Debug.Assert(!x.IsOverride);
2589+
Debug.Assert(!x.IsStatic);
2590+
2591+
Debug.Assert(!y.IsOverride);
2592+
Debug.Assert(!y.IsStatic);
2593+
2594+
var typeComparer = Symbols.SymbolEqualityComparer.AllIgnoreOptions;
2595+
return typeComparer.Equals(x.ContainingType, y.ContainingType) &&
2596+
SourceMemberContainerTypeSymbol.DoOperatorsPair(x, y);
2597+
}
2598+
2599+
public int GetHashCode([DisallowNull] MethodSymbol method)
2600+
{
2601+
Debug.Assert(!method.IsOverride);
2602+
Debug.Assert(!method.IsStatic);
2603+
2604+
var typeComparer = Symbols.SymbolEqualityComparer.AllIgnoreOptions;
2605+
int result = typeComparer.GetHashCode(method.ContainingType);
2606+
2607+
if (method.ParameterTypesWithAnnotations is [var typeWithAnnotations, ..])
2608+
{
2609+
result = Hash.Combine(result, typeComparer.GetHashCode(typeWithAnnotations.Type));
2610+
}
2611+
2612+
return result;
2613+
}
23792614
}
23802615

23812616
#nullable enable

src/Compilers/CSharp/Portable/Binder/LookupOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ internal enum LookupOptions
110110
/// Do not consider symbols that are parameters.
111111
/// </summary>
112112
MustNotBeParameter = 1 << 16,
113+
114+
/// <summary>
115+
/// Consider only symbols that are user-defined operators.
116+
/// </summary>
117+
MustBeOperator = 1 << 17,
113118
}
114119

115120
internal static class LookupOptionExtensions

0 commit comments

Comments
 (0)