/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Compilers/CSharp/Portable/Binder/Binder_Deconstruct.cs
967 строк
49 KB
AlekseyTs
Assert the shape of bound tree created for user defined conversions (#81896)
08 янв 2026, 00:55
Не верифицирован
08 янв 2026, 00:55
b5872bb
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Immutable; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp { /// <summary> /// This portion of the binder converts deconstruction-assignment syntax (AssignmentExpressionSyntax nodes with the left /// being a tuple expression or declaration expression) into a BoundDeconstructionAssignmentOperator (or bad node). /// The BoundDeconstructionAssignmentOperator will have: /// - a BoundTupleLiteral as its Left, /// - a BoundConversion as its Right, holding: /// - a tree of Conversion objects with Kind=Deconstruction, information about a Deconstruct method (optional) and /// an array of nested Conversions (like a tuple conversion), /// - a BoundExpression as its Operand. /// </summary> internal partial class Binder { internal BoundExpression BindDeconstruction(AssignmentExpressionSyntax node, BindingDiagnosticBag diagnostics, bool resultIsUsedOverride = false) { var left = node.Left; var right = node.Right; DeclarationExpressionSyntax? declaration = null; ExpressionSyntax? expression = null; var result = BindDeconstruction(node, left, right, diagnostics, ref declaration, ref expression, resultIsUsedOverride); if (declaration != null) { // only allowed at the top level, or in a for loop switch (node.Parent?.Kind()) { case null: case SyntaxKind.ExpressionStatement: if (expression != null) { MessageID.IDS_FeatureMixedDeclarationsAndExpressionsInDeconstruction .CheckFeatureAvailability(diagnostics, Compilation, node.Location); } break; case SyntaxKind.ForStatement: if (((ForStatementSyntax)node.Parent).Initializers.Contains(node)) { if (expression != null) { MessageID.IDS_FeatureMixedDeclarationsAndExpressionsInDeconstruction .CheckFeatureAvailability(diagnostics, Compilation, node.Location); } } else { Error(diagnostics, ErrorCode.ERR_DeclarationExpressionNotPermitted, declaration); } break; default: Error(diagnostics, ErrorCode.ERR_DeclarationExpressionNotPermitted, declaration); break; } } return result; } /// <summary> /// Bind a deconstruction assignment. /// </summary> /// <param name="deconstruction">The deconstruction operation</param> /// <param name="left">The left (tuple) operand</param> /// <param name="right">The right (deconstructable) operand</param> /// <param name="diagnostics">Where to report diagnostics</param> /// <param name="declaration">A variable set to the first variable declaration found in the left</param> /// <param name="expression">A variable set to the first expression in the left that isn't a declaration or discard</param> /// <param name="resultIsUsedOverride">The expression evaluator needs to bind deconstructions (both assignments and declarations) as expression-statements /// and still access the returned value</param> /// <param name="rightPlaceholder"></param> /// <returns></returns> internal BoundDeconstructionAssignmentOperator BindDeconstruction( CSharpSyntaxNode deconstruction, ExpressionSyntax left, ExpressionSyntax right, BindingDiagnosticBag diagnostics, ref DeclarationExpressionSyntax? declaration, ref ExpressionSyntax? expression, bool resultIsUsedOverride = false, BoundDeconstructValuePlaceholder? rightPlaceholder = null) { DeconstructionVariable locals = BindDeconstructionVariables(left, diagnostics, ref declaration, ref expression); Debug.Assert(locals.NestedVariables is object); var deconstructionDiagnostics = BindingDiagnosticBag.GetInstance(withDiagnostics: true, withDependencies: diagnostics.AccumulatesDependencies); BoundExpression boundRight = rightPlaceholder ?? BindValue(right, deconstructionDiagnostics, BindValueKind.RValue); boundRight = FixTupleLiteral(locals.NestedVariables, boundRight, deconstruction, deconstructionDiagnostics); boundRight = BindToNaturalType(boundRight, diagnostics); bool resultIsUsed = resultIsUsedOverride || IsDeconstructionResultUsed(left); var assignment = BindDeconstructionAssignment(deconstruction, left, boundRight, locals.NestedVariables, resultIsUsed, deconstructionDiagnostics); DeconstructionVariable.FreeDeconstructionVariables(locals.NestedVariables); diagnostics.AddRangeAndFree(deconstructionDiagnostics); return assignment; } private BoundDeconstructionAssignmentOperator BindDeconstructionAssignment( CSharpSyntaxNode node, ExpressionSyntax left, BoundExpression boundRHS, ArrayBuilder<DeconstructionVariable> checkedVariables, bool resultIsUsed, BindingDiagnosticBag diagnostics) { Debug.Assert(diagnostics.DiagnosticBag is object); if ((object?)boundRHS.Type == null || boundRHS.Type.IsErrorType()) { // we could still not infer a type for the RHS FailRemainingInferences(checkedVariables, diagnostics); var voidType = GetSpecialType(SpecialType.System_Void, diagnostics, node); var type = boundRHS.Type ?? voidType; return new BoundDeconstructionAssignmentOperator( node, DeconstructionVariablesAsTuple(left, checkedVariables, diagnostics, ignoreDiagnosticsFromTuple: true), new BoundConversion(boundRHS.Syntax, boundRHS, Conversion.Deconstruction, @checked: false, explicitCastInCode: false, conversionGroupOpt: null, InConversionGroupFlags.Unspecified, constantValueOpt: null, type: type, hasErrors: true), resultIsUsed, voidType, hasErrors: true); } Conversion conversion; // Among other things, MakeDeconstructionConversion() will handle // value escape analysis for the Deconstruct() method group. bool hasErrors = !MakeDeconstructionConversion( boundRHS.Type, node, boundRHS.Syntax, diagnostics, checkedVariables, out conversion); if (conversion.Method != null) { CheckImplicitThisCopyInReadOnlyMember(boundRHS, conversion.Method, diagnostics); } FailRemainingInferences(checkedVariables, diagnostics); var lhsTuple = DeconstructionVariablesAsTuple(left, checkedVariables, diagnostics, ignoreDiagnosticsFromTuple: diagnostics.HasAnyErrors() || !resultIsUsed); Debug.Assert(hasErrors || lhsTuple.Type is object); TypeSymbol returnType = hasErrors ? CreateErrorType() : lhsTuple.Type!; var boundConversion = new BoundConversion( boundRHS.Syntax, boundRHS, conversion, @checked: false, explicitCastInCode: false, conversionGroupOpt: null, InConversionGroupFlags.Unspecified, constantValueOpt: null, type: returnType, hasErrors: hasErrors) { WasCompilerGenerated = true }; return new BoundDeconstructionAssignmentOperator(node, lhsTuple, boundConversion, resultIsUsed, returnType); } private static bool IsDeconstructionResultUsed(ExpressionSyntax left) { var parent = left.Parent; if (parent is null || parent.Kind() == SyntaxKind.ForEachVariableStatement) { return false; } Debug.Assert(parent.Kind() == SyntaxKind.SimpleAssignmentExpression); var grandParent = parent.Parent; if (grandParent is null) { return false; } switch (grandParent.Kind()) { case SyntaxKind.ExpressionStatement: return ((ExpressionStatementSyntax)grandParent).Expression != parent; case SyntaxKind.ForStatement: // Incrementors and Initializers don't have to produce a value var loop = (ForStatementSyntax)grandParent; return !loop.Incrementors.Contains(parent) && !loop.Initializers.Contains(parent); default: return true; } } /// <summary>When boundRHS is a tuple literal, fix it up by inferring its types.</summary> private BoundExpression FixTupleLiteral(ArrayBuilder<DeconstructionVariable> checkedVariables, BoundExpression boundRHS, CSharpSyntaxNode syntax, BindingDiagnosticBag diagnostics) { Debug.Assert(diagnostics.DiagnosticBag is object); if (boundRHS.Kind == BoundKind.TupleLiteral) { // Let's fix the literal up by figuring out its type // For declarations, that means merging type information from the LHS and RHS // For assignments, only the LHS side matters since it is necessarily typed // If we already have diagnostics at this point, it is not worth collecting likely duplicate diagnostics from making the merged type bool hadErrors = diagnostics.HasAnyErrors(); TypeSymbol? mergedTupleType = MakeMergedTupleType(checkedVariables, (BoundTupleLiteral)boundRHS, syntax, hadErrors ? null : diagnostics); if ((object?)mergedTupleType != null) { boundRHS = GenerateConversionForAssignment(mergedTupleType, boundRHS, diagnostics); } } else if ((object?)boundRHS.Type == null) { Error(diagnostics, ErrorCode.ERR_DeconstructRequiresExpression, boundRHS.Syntax); } return boundRHS; } /// <summary> /// Recursively builds a Conversion object with Kind=Deconstruction including information about any necessary /// Deconstruct method and any element-wise conversion. /// /// Note that the variables may either be plain or nested variables. /// The variables may be updated with inferred types if they didn't have types initially. /// Returns false if there was an error. /// </summary> private bool MakeDeconstructionConversion( TypeSymbol type, SyntaxNode syntax, SyntaxNode rightSyntax, BindingDiagnosticBag diagnostics, ArrayBuilder<DeconstructionVariable> variables, out Conversion conversion) { Debug.Assert((object)type != null); ImmutableArray<TypeSymbol> tupleOrDeconstructedTypes; conversion = Conversion.Deconstruction; // Figure out the deconstruct method (if one is required) and determine the types we get from the RHS at this level var deconstructMethod = default(DeconstructMethodInfo); if (type.IsTupleType) { // tuple literal such as `(1, 2)`, `(null, null)`, `(x.P, y.M())` tupleOrDeconstructedTypes = type.TupleElementTypesWithAnnotations.SelectAsArray(TypeMap.AsTypeSymbol); SetInferredTypes(variables, tupleOrDeconstructedTypes, diagnostics); if (variables.Count != tupleOrDeconstructedTypes.Length) { Error(diagnostics, ErrorCode.ERR_DeconstructWrongCardinality, syntax, tupleOrDeconstructedTypes.Length, variables.Count); return false; } } else { if (variables.Count < 2) { Error(diagnostics, ErrorCode.ERR_DeconstructTooFewElements, syntax); return false; } var inputPlaceholder = new BoundDeconstructValuePlaceholder(syntax, variableSymbol: null, isDiscardExpression: false, type); BoundExpression deconstructInvocation = MakeDeconstructInvocationExpression(variables.Count, inputPlaceholder, rightSyntax, diagnostics, outPlaceholders: out ImmutableArray<BoundDeconstructValuePlaceholder> outPlaceholders, out _, variables); if (deconstructInvocation.HasAnyErrors) { return false; } deconstructMethod = new DeconstructMethodInfo(deconstructInvocation, inputPlaceholder, outPlaceholders); tupleOrDeconstructedTypes = outPlaceholders.SelectAsArray(p => p.Type); SetInferredTypes(variables, tupleOrDeconstructedTypes, diagnostics); } // Figure out whether those types will need conversions, including further deconstructions bool hasErrors = false; int count = variables.Count; var nestedConversions = ArrayBuilder<(BoundValuePlaceholder?, BoundExpression?)>.GetInstance(count); for (int i = 0; i < count; i++) { var variable = variables[i]; Conversion nestedConversion; if (variable.NestedVariables is object) { var elementSyntax = syntax.Kind() == SyntaxKind.TupleExpression ? ((TupleExpressionSyntax)syntax).Arguments[i] : syntax; hasErrors |= !MakeDeconstructionConversion(tupleOrDeconstructedTypes[i], elementSyntax, rightSyntax, diagnostics, variable.NestedVariables, out nestedConversion); Debug.Assert(nestedConversion.Kind == ConversionKind.Deconstruction); var operandPlaceholder = new BoundValuePlaceholder(syntax, ErrorTypeSymbol.UnknownResultType).MakeCompilerGenerated(); nestedConversions.Add((operandPlaceholder, new BoundConversion(syntax, operandPlaceholder, nestedConversion, @checked: false, explicitCastInCode: false, conversionGroupOpt: null, InConversionGroupFlags.Unspecified, constantValueOpt: null, #pragma warning disable format type: ErrorTypeSymbol.UnknownResultType) { WasCompilerGenerated = true })); #pragma warning restore format } else { var single = variable.Single; Debug.Assert(single is object); Debug.Assert(single.Type is not null); CompoundUseSiteInfo<AssemblySymbol> useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics); nestedConversion = this.Conversions.ClassifyConversionFromType(tupleOrDeconstructedTypes[i], single.Type, isChecked: CheckOverflowAtRuntime, ref useSiteInfo); diagnostics.Add(single.Syntax, useSiteInfo); if (!nestedConversion.IsImplicit) { hasErrors = true; GenerateImplicitConversionError(diagnostics, Compilation, single.Syntax, nestedConversion, tupleOrDeconstructedTypes[i], single.Type); nestedConversions.Add((null, null)); } else { var operandPlaceholder = new BoundValuePlaceholder(syntax, tupleOrDeconstructedTypes[i]).MakeCompilerGenerated(); nestedConversions.Add((operandPlaceholder, CreateConversion(syntax, operandPlaceholder, nestedConversion, isCast: false, conversionGroupOpt: null, InConversionGroupFlags.Unspecified, single.Type, diagnostics))); } } } conversion = new Conversion(ConversionKind.Deconstruction, deconstructMethod, nestedConversions.ToImmutableAndFree()); return !hasErrors; } /// <summary> /// Inform the variables about found types. /// </summary> private void SetInferredTypes(ArrayBuilder<DeconstructionVariable> variables, ImmutableArray<TypeSymbol> foundTypes, BindingDiagnosticBag diagnostics) { var matchCount = Math.Min(variables.Count, foundTypes.Length); for (int i = 0; i < matchCount; i++) { var variable = variables[i]; if (variable.Single is { } pending) { if ((object?)pending.Type != null) { continue; } variables[i] = new DeconstructionVariable(SetInferredType(pending, foundTypes[i], diagnostics), variable.Syntax); } } } private BoundExpression SetInferredType(BoundExpression expression, TypeSymbol type, BindingDiagnosticBag diagnostics) { switch (expression.Kind) { case BoundKind.DeconstructionVariablePendingInference: { var pending = (DeconstructionVariablePendingInference)expression; return pending.SetInferredTypeWithAnnotations(TypeWithAnnotations.Create(type), this, diagnostics); } case BoundKind.DiscardExpression: { var pending = (BoundDiscardExpression)expression; Debug.Assert((object?)pending.Type == null); return pending.SetInferredTypeWithAnnotations(TypeWithAnnotations.Create(type)); } default: throw ExceptionUtilities.UnexpectedValue(expression.Kind); } } /// <summary> /// Find any deconstruction locals that are still pending inference and fail their inference. /// Set the safe-to-escape scope for all deconstruction locals. /// </summary> private void FailRemainingInferences(ArrayBuilder<DeconstructionVariable> variables, BindingDiagnosticBag diagnostics) { int count = variables.Count; for (int i = 0; i < count; i++) { var variable = variables[i]; if (variable.NestedVariables is object) { FailRemainingInferences(variable.NestedVariables, diagnostics); } else { Debug.Assert(variable.Single is object); switch (variable.Single.Kind) { case BoundKind.DeconstructionVariablePendingInference: BoundExpression errorLocal = ((DeconstructionVariablePendingInference)variable.Single).FailInference(this, diagnostics); variables[i] = new DeconstructionVariable(errorLocal, errorLocal.Syntax); break; case BoundKind.DiscardExpression: var pending = (BoundDiscardExpression)variable.Single; if ((object?)pending.Type == null) { Error(diagnostics, ErrorCode.ERR_TypeInferenceFailedForImplicitlyTypedDeconstructionVariable, pending.Syntax, "_"); variables[i] = new DeconstructionVariable(pending.FailInference(this, diagnostics), pending.Syntax); } break; } // at this point we expect to have a type for every lvalue Debug.Assert((object?)variables[i].Single!.Type != null); } } } /// <summary> /// Holds the variables on the LHS of a deconstruction as a tree of bound expressions. /// </summary> [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] internal sealed class DeconstructionVariable { internal readonly BoundExpression? Single; internal readonly ArrayBuilder<DeconstructionVariable>? NestedVariables; internal readonly CSharpSyntaxNode Syntax; internal DeconstructionVariable(BoundExpression variable, SyntaxNode syntax) { Single = variable; NestedVariables = null; Syntax = (CSharpSyntaxNode)syntax; } internal DeconstructionVariable(ArrayBuilder<DeconstructionVariable> variables, SyntaxNode syntax) { Single = null; NestedVariables = variables; Syntax = (CSharpSyntaxNode)syntax; } internal static void FreeDeconstructionVariables(ArrayBuilder<DeconstructionVariable> variables) { variables.FreeAll(v => v.NestedVariables); } private string GetDebuggerDisplay() { if (Single != null) { return Single.GetDebuggerDisplay(); } Debug.Assert(NestedVariables is object); return $"Nested variables ({NestedVariables.Count})"; } } /// <summary> /// For cases where the RHS of a deconstruction-declaration is a tuple literal, we merge type information from both the LHS and RHS. /// For cases where the RHS of a deconstruction-assignment is a tuple literal, the type information from the LHS determines the merged type, since all variables have a type. /// Returns null if a merged tuple type could not be fabricated. /// </summary> private TypeSymbol? MakeMergedTupleType(ArrayBuilder<DeconstructionVariable> lhsVariables, BoundTupleLiteral rhsLiteral, CSharpSyntaxNode syntax, BindingDiagnosticBag? diagnostics) { int leftLength = lhsVariables.Count; int rightLength = rhsLiteral.Arguments.Length; var typesWithAnnotationsBuilder = ArrayBuilder<TypeWithAnnotations>.GetInstance(leftLength); var locationsBuilder = ArrayBuilder<Location?>.GetInstance(leftLength); for (int i = 0; i < rightLength; i++) { BoundExpression element = rhsLiteral.Arguments[i]; TypeSymbol? mergedType = element.Type; if (i < leftLength) { var variable = lhsVariables[i]; if (variable.NestedVariables is object) { if (element.Kind == BoundKind.TupleLiteral) { // (variables) on the left and (elements) on the right mergedType = MakeMergedTupleType(variable.NestedVariables, (BoundTupleLiteral)element, syntax, diagnostics); } else if ((object?)mergedType == null && diagnostics is object) { // (variables) on the left and null on the right Error(diagnostics, ErrorCode.ERR_DeconstructRequiresExpression, element.Syntax); } } else { Debug.Assert(variable.Single is object); if ((object?)variable.Single.Type != null) { // typed-variable on the left mergedType = variable.Single.Type; } } } else { if ((object?)mergedType == null && diagnostics is object) { // a typeless element on the right, matching no variable on the left Error(diagnostics, ErrorCode.ERR_DeconstructRequiresExpression, element.Syntax); } } typesWithAnnotationsBuilder.Add(TypeWithAnnotations.Create(mergedType)); locationsBuilder.Add(element.Syntax.Location); } if (typesWithAnnotationsBuilder.Any(t => !t.HasType)) { typesWithAnnotationsBuilder.Free(); locationsBuilder.Free(); return null; } // The tuple created here is not identical to the one created by // DeconstructionVariablesAsTuple. It represents a smaller // tree of types used for figuring out natural types in tuple literal. return NamedTypeSymbol.CreateTuple( locationOpt: null, elementTypesWithAnnotations: typesWithAnnotationsBuilder.ToImmutableAndFree(), elementLocations: locationsBuilder.ToImmutableAndFree(), elementNames: default(ImmutableArray<string?>), compilation: Compilation, diagnostics: diagnostics, shouldCheckConstraints: true, includeNullability: false, errorPositions: default(ImmutableArray<bool>), syntax: syntax); } private BoundTupleExpression DeconstructionVariablesAsTuple(CSharpSyntaxNode syntax, ArrayBuilder<DeconstructionVariable> variables, BindingDiagnosticBag diagnostics, bool ignoreDiagnosticsFromTuple) { int count = variables.Count; var valuesBuilder = ArrayBuilder<BoundExpression>.GetInstance(count); var typesWithAnnotationsBuilder = ArrayBuilder<TypeWithAnnotations>.GetInstance(count); var locationsBuilder = ArrayBuilder<Location?>.GetInstance(count); var namesBuilder = ArrayBuilder<string?>.GetInstance(count); foreach (var variable in variables) { BoundExpression value; if (variable.NestedVariables is object) { value = DeconstructionVariablesAsTuple(variable.Syntax, variable.NestedVariables, diagnostics, ignoreDiagnosticsFromTuple); namesBuilder.Add(null); } else { Debug.Assert(variable.Single is object); value = variable.Single; namesBuilder.Add(ExtractDeconstructResultElementName(value)); } valuesBuilder.Add(value); typesWithAnnotationsBuilder.Add(TypeWithAnnotations.Create(value.Type)); locationsBuilder.Add(variable.Syntax.Location); } ImmutableArray<BoundExpression> arguments = valuesBuilder.ToImmutableAndFree(); var uniqueFieldNames = PooledHashSet<string>.GetInstance(); RemoveDuplicateInferredTupleNamesAndFreeIfEmptied(ref namesBuilder, uniqueFieldNames); uniqueFieldNames.Free(); ImmutableArray<string?> tupleNames = namesBuilder is null ? default : namesBuilder.ToImmutableAndFree(); ImmutableArray<bool> inferredPositions = tupleNames.IsDefault ? default : tupleNames.SelectAsArray(n => n != null); bool disallowInferredNames = this.Compilation.LanguageVersion.DisallowInferredTupleElementNames(); var type = NamedTypeSymbol.CreateTuple( syntax.Location, typesWithAnnotationsBuilder.ToImmutableAndFree(), locationsBuilder.ToImmutableAndFree(), tupleNames, this.Compilation, shouldCheckConstraints: !ignoreDiagnosticsFromTuple, includeNullability: false, errorPositions: disallowInferredNames ? inferredPositions : default, syntax: syntax, diagnostics: ignoreDiagnosticsFromTuple ? null : diagnostics); return (BoundTupleExpression)BindToNaturalType(new BoundTupleLiteral(syntax, arguments, tupleNames, inferredPositions, type), diagnostics); } /// <summary>Extract inferred name from a single deconstruction variable.</summary> private static string? ExtractDeconstructResultElementName(BoundExpression expression) { if (expression.Kind == BoundKind.DiscardExpression) { return null; } return InferTupleElementName(expression.Syntax); } /// <summary> /// Find the Deconstruct method for the expression on the right, that will fit the number of assignable variables on the left. /// Returns an invocation expression if the Deconstruct method is found. /// If so, it outputs placeholders that were coerced to the output types of the resolved Deconstruct method. /// The overload resolution is similar to writing <c>receiver.Deconstruct(out var x1, out var x2, ...)</c>. /// </summary> private BoundExpression MakeDeconstructInvocationExpression( int numCheckedVariables, BoundExpression receiver, SyntaxNode rightSyntax, BindingDiagnosticBag diagnostics, out ImmutableArray<BoundDeconstructValuePlaceholder> outPlaceholders, out bool anyApplicableCandidates, ArrayBuilder<DeconstructionVariable>? variablesOpt = null) { anyApplicableCandidates = false; var receiverSyntax = (CSharpSyntaxNode)receiver.Syntax; if (receiver.Type?.IsDynamic() ?? false) { Error(diagnostics, ErrorCode.ERR_CannotDeconstructDynamic, rightSyntax); outPlaceholders = default(ImmutableArray<BoundDeconstructValuePlaceholder>); return BadExpression(receiverSyntax, receiver); } receiver = BindToNaturalType(receiver, diagnostics); var analyzedArguments = AnalyzedArguments.GetInstance(); var outVars = ArrayBuilder<OutDeconstructVarPendingInference>.GetInstance(numCheckedVariables); try { for (int i = 0; i < numCheckedVariables; i++) { var variableOpt = variablesOpt?[i].Single; var variableSymbol = variableOpt switch { DeconstructionVariablePendingInference { VariableSymbol: var symbol } => symbol, BoundLocal { DeclarationKind: BoundLocalDeclarationKind.WithExplicitType or BoundLocalDeclarationKind.WithInferredType, LocalSymbol: var symbol } => symbol, _ => null, }; var variable = new OutDeconstructVarPendingInference(receiverSyntax, variableSymbol: variableSymbol, isDiscardExpression: variableOpt is BoundDiscardExpression); analyzedArguments.Arguments.Add(variable); analyzedArguments.RefKinds.Add(RefKind.Out); outVars.Add(variable); } const string methodName = WellKnownMemberNames.DeconstructMethodName; var memberAccess = BindInstanceMemberAccess( rightSyntax, receiverSyntax, receiver, methodName, rightArity: 0, typeArgumentsSyntax: default(SeparatedSyntaxList<TypeSyntax>), typeArgumentsWithAnnotations: default(ImmutableArray<TypeWithAnnotations>), invoked: true, indexed: false, diagnostics: diagnostics); memberAccess = CheckValue(memberAccess, BindValueKind.RValueOrMethodGroup, diagnostics); memberAccess.WasCompilerGenerated = true; if (memberAccess.Kind != BoundKind.MethodGroup) { return MissingDeconstruct(receiver, rightSyntax, numCheckedVariables, diagnostics, out outPlaceholders, receiver); } // After the overload resolution completes, the last step is to coerce the arguments with inferred types. // That step returns placeholder (of correct type) instead of the outVar nodes that were passed in as arguments. // So the generated invocation expression will contain placeholders instead of those outVar nodes. // Those placeholders are also recorded in the outVar for easy access below, by the `SetInferredType` call on the outVar nodes. BoundExpression result = BindMethodGroupInvocation( rightSyntax, rightSyntax, methodName, (BoundMethodGroup)memberAccess, analyzedArguments, diagnostics, queryClause: null, ignoreNormalFormIfHasValidParamsParameter: false, anyApplicableCandidates: out anyApplicableCandidates, disallowExpandedNonArrayParams: false, acceptOnlyMethods: true); result.WasCompilerGenerated = true; if (!anyApplicableCandidates) { return MissingDeconstruct(receiver, rightSyntax, numCheckedVariables, diagnostics, out outPlaceholders, result); } // Verify all the parameters (except "this" for extension methods) are out parameters. // This prevents, for example, an unused params parameter after the out parameters. var deconstructMethod = ((BoundCall)result).Method; var parameters = deconstructMethod.Parameters; for (int i = (deconstructMethod.IsExtensionMethod ? 1 : 0); i < parameters.Length; i++) { if (parameters[i].RefKind != RefKind.Out) { return MissingDeconstruct(receiver, rightSyntax, numCheckedVariables, diagnostics, out outPlaceholders, result); } } if (deconstructMethod.ReturnType.GetSpecialTypeSafe() != SpecialType.System_Void) { return MissingDeconstruct(receiver, rightSyntax, numCheckedVariables, diagnostics, out outPlaceholders, result); } if (outVars.Any(v => v.Placeholder is null)) { return MissingDeconstruct(receiver, rightSyntax, numCheckedVariables, diagnostics, out outPlaceholders, result); } outPlaceholders = outVars.SelectAsArray(v => v.Placeholder!); return result; } finally { analyzedArguments.Free(); outVars.Free(); } } private BoundBadExpression MissingDeconstruct(BoundExpression receiver, SyntaxNode rightSyntax, int numParameters, BindingDiagnosticBag diagnostics, out ImmutableArray<BoundDeconstructValuePlaceholder> outPlaceholders, BoundExpression childNode) { if (receiver.Type?.IsErrorType() == false) { Error(diagnostics, ErrorCode.ERR_MissingDeconstruct, rightSyntax, receiver.Type, numParameters); } outPlaceholders = default; return BadExpression(rightSyntax, childNode); } /// <summary> /// Prepares locals (or fields in global statement) and lvalue expressions corresponding to the variables of the declaration. /// The locals/fields/lvalues are kept in a tree which captures the nesting of variables. /// Each local or field is either a simple local or field access (when its type is known) or a deconstruction variable pending inference. /// The caller is responsible for releasing the nested ArrayBuilders. /// </summary> private DeconstructionVariable BindDeconstructionVariables( ExpressionSyntax node, BindingDiagnosticBag diagnostics, ref DeclarationExpressionSyntax? declaration, ref ExpressionSyntax? expression) { switch (node.Kind()) { case SyntaxKind.DeclarationExpression: { var component = (DeclarationExpressionSyntax)node; if (declaration == null) { declaration = component; } bool isVar; bool isConst = false; AliasSymbol alias; var declType = BindVariableTypeWithAnnotations(component.Designation, diagnostics, component.Type.SkipScoped(out _).SkipRef(), ref isConst, out isVar, out alias); Debug.Assert(isVar == !declType.HasType); if (component.Designation.Kind() == SyntaxKind.ParenthesizedVariableDesignation) { if (!isVar) { // An explicit type is not allowed with a parenthesized designation Error(diagnostics, ErrorCode.ERR_DeconstructionVarFormDisallowsSpecificType, component.Designation); } else if (node.Parent is not ArgumentSyntax) { // check for use of `var (x, y, z)`. Only need to report this in a non-argument // position. If it's an argument, then we have `(int x, var (y, z))` and we will have // already reported the parent tuple, so no need to report on the inner designation. MessageID.IDS_FeatureTuples.CheckFeatureAvailability(diagnostics, component.Designation); } } return BindDeconstructionVariables(declType, component.Designation, component, diagnostics); } case SyntaxKind.TupleExpression: { MessageID.IDS_FeatureTuples.CheckFeatureAvailability(diagnostics, node); var component = (TupleExpressionSyntax)node; var builder = ArrayBuilder<DeconstructionVariable>.GetInstance(component.Arguments.Count); foreach (var arg in component.Arguments) { if (arg.NameColon != null) { Error(diagnostics, ErrorCode.ERR_TupleElementNamesInDeconstruction, arg.NameColon); } builder.Add(BindDeconstructionVariables(arg.Expression, diagnostics, ref declaration, ref expression)); } return new DeconstructionVariable(builder, node); } default: var boundVariable = BindExpression(node, diagnostics, invoked: false, indexed: false); var checkedVariable = CheckValue(boundVariable, BindValueKind.Assignable, diagnostics); if (expression == null && checkedVariable.Kind != BoundKind.DiscardExpression) { expression = node; } return new DeconstructionVariable(checkedVariable, node); } } private DeconstructionVariable BindDeconstructionVariables( TypeWithAnnotations declTypeWithAnnotations, VariableDesignationSyntax node, CSharpSyntaxNode syntax, BindingDiagnosticBag diagnostics) { switch (node.Kind()) { case SyntaxKind.SingleVariableDesignation: { var single = (SingleVariableDesignationSyntax)node; return new DeconstructionVariable(BindDeconstructionVariable(declTypeWithAnnotations, single, syntax, diagnostics), syntax); } case SyntaxKind.DiscardDesignation: { var discarded = (DiscardDesignationSyntax)node; if (discarded.Parent is DeclarationExpressionSyntax declExpr && declExpr.Designation == discarded) { TypeSyntax typeSyntax = declExpr.Type; if (typeSyntax is ScopedTypeSyntax scopedType) { diagnostics.Add(ErrorCode.ERR_ScopedDiscard, scopedType.ScopedKeyword.GetLocation()); typeSyntax = scopedType.Type; } if (typeSyntax is RefTypeSyntax refType) { diagnostics.Add(ErrorCode.ERR_DeconstructVariableCannotBeByRef, refType.RefKeyword.GetLocation()); } } return new DeconstructionVariable(BindDiscardExpression(syntax, declTypeWithAnnotations), syntax); } case SyntaxKind.ParenthesizedVariableDesignation: { var tuple = (ParenthesizedVariableDesignationSyntax)node; var builder = ArrayBuilder<DeconstructionVariable>.GetInstance(); foreach (var n in tuple.Variables) { builder.Add(BindDeconstructionVariables(declTypeWithAnnotations, n, n, diagnostics)); } return new DeconstructionVariable(builder, syntax); } default: throw ExceptionUtilities.UnexpectedValue(node.Kind()); } } private BoundDiscardExpression BindDiscardExpression( SyntaxNode syntax, TypeWithAnnotations declTypeWithAnnotations) { var type = declTypeWithAnnotations.Type; return new BoundDiscardExpression(syntax, declTypeWithAnnotations.NullableAnnotation, isInferred: type is null, type); } /// <summary> /// In embedded statements, returns a BoundLocal when the type was explicit. /// In global statements, returns a BoundFieldAccess when the type was explicit. /// Otherwise returns a DeconstructionVariablePendingInference when the type is implicit. /// </summary> private BoundExpression BindDeconstructionVariable( TypeWithAnnotations declTypeWithAnnotations, SingleVariableDesignationSyntax designation, CSharpSyntaxNode syntax, BindingDiagnosticBag diagnostics) { SourceLocalSymbol localSymbol = LookupLocal(designation.Identifier); // is this a local? if ((object)localSymbol != null) { ReportFieldContextualKeywordConflictIfAny(localSymbol, designation, designation.Identifier, diagnostics); if (designation.Parent is DeclarationExpressionSyntax declExpr && declExpr.Designation == designation) { TypeSyntax typeSyntax = declExpr.Type; if (typeSyntax is ScopedTypeSyntax scopedType) { // Check for support for 'scoped'. ModifierUtils.CheckScopedModifierAvailability(typeSyntax, scopedType.ScopedKeyword, diagnostics); typeSyntax = scopedType.Type; } if (typeSyntax is RefTypeSyntax refType) { diagnostics.Add(ErrorCode.ERR_DeconstructVariableCannotBeByRef, refType.RefKeyword.GetLocation()); } if (declTypeWithAnnotations.HasType) { CheckRestrictedTypeInAsyncMethod(this.ContainingMemberOrLambda, declTypeWithAnnotations.Type, diagnostics, typeSyntax); } if (declTypeWithAnnotations.HasType && localSymbol.Scope == ScopedKind.ScopedValue && !declTypeWithAnnotations.Type.IsErrorOrRefLikeOrAllowsRefLikeType()) { diagnostics.Add(ErrorCode.ERR_ScopedRefAndRefStructOnly, typeSyntax.Location); } } // Check for variable declaration errors. // Use the binder that owns the scope for the local because this (the current) binder // might own nested scope. var hasErrors = localSymbol.ScopeBinder.ValidateDeclarationNameConflictsInScope(localSymbol, diagnostics); if (declTypeWithAnnotations.HasType) { return new BoundLocal(syntax, localSymbol, BoundLocalDeclarationKind.WithExplicitType, constantValueOpt: null, isNullableUnknown: false, type: declTypeWithAnnotations.Type, hasErrors: hasErrors); } return new DeconstructionVariablePendingInference(syntax, localSymbol, receiverOpt: null); } else { // Is this a field? GlobalExpressionVariable field = LookupDeclaredField(designation); if ((object)field == null) { // We should have the right binder in the chain, cannot continue otherwise. throw ExceptionUtilities.Unreachable(); } if (designation.Parent is DeclarationExpressionSyntax declExpr && declExpr.Designation == designation) { TypeSyntax typeSyntax = declExpr.Type; if (typeSyntax is ScopedTypeSyntax scopedType) { diagnostics.Add(ErrorCode.ERR_UnexpectedToken, scopedType.ScopedKeyword.GetLocation(), scopedType.ScopedKeyword.ValueText); typeSyntax = scopedType.Type; } if (typeSyntax is RefTypeSyntax refType) { diagnostics.Add(ErrorCode.ERR_UnexpectedToken, refType.RefKeyword.GetLocation(), refType.RefKeyword.ValueText); } } BoundThisReference receiver = ThisReference(designation, this.ContainingType, hasErrors: false, wasCompilerGenerated: true); if (declTypeWithAnnotations.HasType) { var fieldType = field.GetFieldType(this.FieldsBeingBound); Debug.Assert(TypeSymbol.Equals(declTypeWithAnnotations.Type, fieldType.Type, TypeCompareKind.ConsiderEverything2)); return new BoundFieldAccess(syntax, receiver, field, constantValueOpt: null, resultKind: LookupResultKind.Viable, isDeclaration: true, type: fieldType.Type); } return new DeconstructionVariablePendingInference(syntax, field, receiver); } } } }