diff --git a/docs/features/deconstruction.md b/docs/features/deconstruction.md index 8c04e0d315743..945498dff3667 100644 --- a/docs/features/deconstruction.md +++ b/docs/features/deconstruction.md @@ -30,48 +30,61 @@ Treat deconstruction of a tuple into existing variables as a kind of assignment, ###Deconstruction-assignment (deconstruction into existing variables): + This doesn't introduce any changes to the language grammar. We have an `assignment-expression` (also simply called `assignment` in the C# grammar) where the `unary-expression` (the left-hand-side) is a `tuple-literal`. -In short, what this does is find a `Deconstruct` method on the expression on the right-hand-side of the assignment, invoke it, collect its `out` parameters and assign them to the variables on the left-hand-side. +In short, what this does in the general case is find a `Deconstruct` method on the expression on the right-hand-side of the assignment, invoke it, collect its `out` parameters and assign them to the variables on the left-hand-side. And in the special case where the expression on the right-hand-side is a tuple (tuple literal or tuple type), then the elements of the tuple can be assigned to the variables on the left-hand-side without needing to call `Deconstruct`. The existing assignment binding currently checks if the variable on its left-hand-side can be assigned to and if the two sides are compatible. It will be updated to support deconstruction-assignment, ie. when the left-hand-side is a tuple-literal/tuple-expression: -- Each item on the left needs to be assignable and needs to be compatible with corresponding position on the right +- Needs to break the right-hand-side into items. That step is un-necessary if the right-hand-side is already a tuple though. +- Each item on the left needs to be assignable and needs to be compatible with corresponding position on the right (resulting from previous step). - Needs to handle nesting case such as `(x, (y, z)) = M();`, but note that the second item in the top-level group has no discernable type. -The lowering for deconstruction-assignment would translate: `(expressionX, expressionY, expressionZ) = (expressionA, expressionB, expressionC)` into: +The evaluation order can be summarized as: (1) all the side-effects on the left-hand-side, (2) all the Deconstruct invocations (if not tuple), (3) conversions (if needed), and (4) assignments. + +In the general case, the lowering for deconstruction-assignment would translate: `(expressionX, expressionY, expressionZ) = expressionRight` into: ``` +// do LHS side-effects tempX = &evaluate expressionX tempY = &evaluate expressionY tempZ = &evaluate expressionZ -tempRight = evaluate right and evaluate Deconstruct +// do Deconstruct +evaluate right and evaluate Deconstruct in three parts (tempA, tempB and tempC) -tempX = tempRight.A (including conversions) -tempY = tempRight.B (including conversions) -tempZ = tempRight.C (including conversions) +// do conversions +tempConvA = convert tempA +tempConvB = convert tempB +tempConvC = convert tempC -“return/continue” with newTupleIncludingNames tempRight (so you can do get Item1 from the assignment)? +// do assignments +tempX = tempConvA +tempY = tempConvB +tempZ = tempConvC ``` The evaluation order for nesting `(x, (y, z))` is: ``` +// do LHS side-effects tempX = &evaluate expressionX - -tempRight = evaluate right and evaluate Deconstruct - -tempX = tempRight.A (including conversions) -tempLNested = tempRight.B (no conversions) - tempY = &evaluate expressionY tempZ = &evaluate expressionZ -tempRNest = evaluate Deconstruct on tempRight +// do Deconstruct +evaluate right and evaluate Deconstruct into two parts (tempA and tempNested) +evaluate Deconstruct on tempNested intwo two parts (tempB and tempC) -tempY = tempRNest.B (including conversions) -tempZ = tempRNest.C (including conversions) +// do conversions +tempConvA = convert tempA +tempConvB = convert tempB +tempConvC = convert tempC +// do assignments +tempX = tempConvA +tempY = tempConvB +tempZ = tempConvC ``` The evaluation order for the simplest cases (locals, fields, array indexers, or anything returning ref) without needing conversion: @@ -80,23 +93,15 @@ evaluate side-effect on the left-hand-side variables evaluate Deconstruct passing the references directly in ``` -Note that the feature is built around the `Deconstruct` mechanism for deconstructing types. -`ValueTuple` and `System.Tuple` will rely on that same mechanism, except that the compiler may need to synthesize the proper `Deconstruct` methods. - +In the case where the expression on the right is a tuple, the evaluation order becomes: +``` +evaluate side-effect on the left-hand-side variables +evaluate the right-hand-side and do a tuple conversion (using a fake tuple representing the types in the left-hand-side) +assign element-wise from the right to the left +``` -**Work items, open issues and assumptions to confirm with LDM:** +Note that tuples (`System.ValueTuple` and `System.Tuple`) don't need to invoke Deconstruct. -- I assume this should work even if `System.ValueTuple` is not present. -- How is the Deconstruct method resolved? - - I assumed there can be no ambiguity. Only one `Deconstruct` is allowed (in nesting cases we have no type to guide the resolution process). - - But we may allow a little bit of ambiguity and preferring an instance over extension method. -- Do the names matter? `int x, y; (a: x, b: y) = M();` -- Can we deconstruct into a single out variable? I assume no. -- I assume no compound assignment `(x, y) += M();` -- [ ] Provide more details on the semantic of deconstruction-assignment, both static (The LHS of the an assignment-expression used be a L-value, but now it can be L-value -- which uses existing rules -- or tuple_literal. The new rules for tuple_literal on the LHS...) and dynamic. -- [ ] Discuss with Aleksey about "Deconstruct and flow analysis for nullable ref type" -- [ ] Validate any target typing or type inference scenarios. -- The deconstruction-assignment is treated separately from deconstruction-declaration, which means it doesn't allow combinations such as `int x; (x, int y) = M();`. ###Deconstruction-declaration (deconstruction into new variables): @@ -158,20 +163,11 @@ constant_declarator // not sure ; ``` -Treat deconstruction of a tuple into new variables as a new kind of node (AssignmentExpression). +Treat deconstruction of a tuple into new variables as a new kind of node (not AssignmentExpression). It would pick up the behavior of each contexts where new variables can be declared (TODO: need to list). For instance, in LINQ, new variables go into a transparent identifiers. It is seen as deconstructing into separate variables (we don't introduce transparent identifiers in contexts where they didn't exist previously). -Should we allow this? -`var t = (x: 1, y: 2); (x: var a, y: var b) = t;` -or `var (x: a, y: b) = t;` -(if not, tuple names aren't very useful?) - -- [ ] Add example: var (x, y) = -- [ ] Semantic (cardinality should match, ordering including conversion, -- [ ] What are the type rules? `(string s, int x) = (null, 3);` - -- Deconstruction for `System.ValueTuple`, `System.Tuple` and any other type involves a call to `Deconstruct`. +Just like deconstruction-assignment, deconstruction-declaration does not need to invoke `Deconstruct` for tuple types. **References** diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs index 78cf18b58d216..8caeda3dd6dae 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs @@ -1715,6 +1715,13 @@ private BoundExpression BindAssignment(AssignmentExpressionSyntax node, Diagnost return BindAssignment(node, op1, op2, diagnostics); } + /// + /// There are two kinds of deconstruction-assignments which this binding handles: tuple and non-tuple. + /// + /// Returns a BoundDeconstructionAssignmentOperator + /// - with all the fields populated except deconstructMember, for the tuple case + /// - with all the fields populated, for a non-tuple case + /// private BoundExpression BindDeconstructionAssignment(AssignmentExpressionSyntax node, DiagnosticBag diagnostics) { SeparatedSyntaxList arguments = ((TupleExpressionSyntax)node.Left).Arguments; @@ -1727,9 +1734,9 @@ private BoundExpression BindDeconstructionAssignment(AssignmentExpressionSyntax // bind the variables and check they can be assigned to var checkedVariablesBuilder = ArrayBuilder.GetInstance(numElements); - for (int i = 0; i < numElements; i++) + foreach (var argument in arguments) { - var boundVariable = BindExpression(arguments[i].Expression, diagnostics, invoked: false, indexed: false); + var boundVariable = BindExpression(argument.Expression, diagnostics, invoked: false, indexed: false); var checkedVariable = CheckValue(boundVariable, BindValueKind.Assignment, diagnostics); checkedVariablesBuilder.Add(checkedVariable); @@ -1737,6 +1744,40 @@ private BoundExpression BindDeconstructionAssignment(AssignmentExpressionSyntax var checkedVariables = checkedVariablesBuilder.ToImmutableAndFree(); + // tuple literal such as `(1, 2)`, `(null, null)`, `(x.P, y.M())` + if (boundRHS.Kind == BoundKind.TupleLiteral || ((object)boundRHS.Type != null && boundRHS.Type.IsTupleType)) + { + return BindDeconstructWithTuple(node, diagnostics, boundRHS, checkedVariables); + } + + // expression without type such as `null` + if (boundRHS.Type == null) + { + Error(diagnostics, ErrorCode.ERR_DeconstructRequiresExpression, node); + return BadExpression(node, checkedVariables.Concat(boundRHS).ToArray()); + } + + return BindDeconstructWithDeconstruct(node, diagnostics, boundRHS, checkedVariables); + } + + private BoundExpression BindDeconstructWithTuple(AssignmentExpressionSyntax node, DiagnosticBag diagnostics, BoundExpression boundRHS, ImmutableArray checkedVariables) + { + // make a conversion for the tuple based on the LHS information (this also takes care of tuple literals without a natural type) + var lhsTypes = checkedVariables.SelectAsArray(v => v.Type); + TypeSymbol lhsAsTuple = TupleTypeSymbol.Create(locationOpt: null, elementTypes: lhsTypes, elementLocations: default(ImmutableArray), elementNames: default(ImmutableArray), compilation: Compilation, diagnostics: diagnostics); + var typedRHS = GenerateConversionForAssignment(lhsAsTuple, boundRHS, diagnostics); + + ImmutableArray tupleTypes = typedRHS.Type.TupleElementTypes; + + // figure out the pairwise conversions + var assignments = checkedVariables.SelectAsArray((variable, index, types) => MakeAssignmentInfo(variable, types[index], node, diagnostics), tupleTypes); + + TypeSymbol lastType = tupleTypes.Last(); + return new BoundDeconstructionAssignmentOperator(node, checkedVariables, typedRHS, deconstructMemberOpt: null, assignments: assignments, type: lastType); + } + + private BoundExpression BindDeconstructWithDeconstruct(AssignmentExpressionSyntax node, DiagnosticBag diagnostics, BoundExpression boundRHS, ImmutableArray checkedVariables) + { // symbol and parameters for Deconstruct DiagnosticBag bag = new DiagnosticBag(); MethodSymbol deconstructMethod = FindDeconstruct(checkedVariables, boundRHS, node, bag); @@ -1754,24 +1795,27 @@ private BoundExpression BindDeconstructionAssignment(AssignmentExpressionSyntax } // figure out the pairwise conversions - var assignmentsBuilder = ArrayBuilder.GetInstance(numElements); var deconstructParameters = deconstructMethod.Parameters; - for (int i = 0; i < checkedVariables.Length; i++) - { - var leftPlaceholder = new BoundLValuePlaceholder(checkedVariables[i].Syntax, checkedVariables[i].Type) { WasCompilerGenerated = true }; - var rightPlaceholder = new BoundRValuePlaceholder(node.Right, deconstructParameters[i].Type) { WasCompilerGenerated = true }; - - // each assignment has a placeholder for a receiver and another for the source - BoundAssignmentOperator op = BindAssignment(node, leftPlaceholder, rightPlaceholder, diagnostics); - assignmentsBuilder.Add(new BoundDeconstructionAssignmentOperator.AssignmentInfo() { Assignment = op, LValuePlaceholder = leftPlaceholder, RValuePlaceholder = rightPlaceholder }); - } - - var assignments = assignmentsBuilder.ToImmutableAndFree(); + var assignments = checkedVariables.SelectAsArray((variable, index, parameters) => MakeAssignmentInfo(variable, parameters[index].Type, node, diagnostics), deconstructParameters); TypeSymbol lastType = deconstructParameters.Last().Type; return new BoundDeconstructionAssignmentOperator(node, checkedVariables, boundRHS, deconstructMethod, assignments, lastType); } + /// + /// Figures out how to assign from sourceType into receivingVariable and bundles the information (leaving holes for the actual source and receiver) into an AssignmentInfo. + /// + private BoundDeconstructionAssignmentOperator.AssignmentInfo MakeAssignmentInfo(BoundExpression receivingVariable, TypeSymbol sourceType, AssignmentExpressionSyntax node, DiagnosticBag diagnostics) + { + var leftPlaceholder = new BoundLValuePlaceholder(receivingVariable.Syntax, receivingVariable.Type) { WasCompilerGenerated = true }; + var rightPlaceholder = new BoundRValuePlaceholder(node.Right, sourceType) { WasCompilerGenerated = true }; + + // each assignment has a placeholder for a receiver and another for the source + BoundAssignmentOperator op = BindAssignment(node, leftPlaceholder, rightPlaceholder, diagnostics); + + return new BoundDeconstructionAssignmentOperator.AssignmentInfo() { Assignment = op, LValuePlaceholder = leftPlaceholder, RValuePlaceholder = rightPlaceholder }; + } + /// /// Find the Deconstruct method for the expression on the right, that will fit the assignable bound expressions on the left. /// Returns true if the Deconstruct method is found. diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml index 144df264efbd0..818c9a92dd5d5 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml @@ -397,7 +397,7 @@ - + diff --git a/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs b/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs index 6f712678266d9..3818d7c9464a0 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs +++ b/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs @@ -3094,6 +3094,15 @@ internal class CSharpResources { } } + /// + /// Looks up a localized string similar to Deconstruct assignment requires an expression with a type on the right-hand-side.. + /// + internal static string ERR_DeconstructRequiresExpression { + get { + return ResourceManager.GetString("ERR_DeconstructRequiresExpression", resourceCulture); + } + } + /// /// Looks up a localized string similar to The Deconstruct method for type '{0}' must have only out parameters.. /// diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index 6210d481314bb..1bfd74d4da545 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -4884,4 +4884,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ No Deconstruct instance or extension method was found for type '{0}'. + + Deconstruct assignment requires an expression with a type on the right-hand-side. + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index 3e5fa88eeb6d7..228829f80695d 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -1395,5 +1395,6 @@ internal enum ErrorCode ERR_AmbiguousDeconstruct = 8207, ERR_DeconstructRequiresOutParams = 8208, ERR_DeconstructWrongParams = 8209, + ERR_DeconstructRequiresExpression = 8210, } } diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_DeconstructionAssignmentOperator.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_DeconstructionAssignmentOperator.cs index 9b025171a90d9..933ff1ebc1777 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_DeconstructionAssignmentOperator.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_DeconstructionAssignmentOperator.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using Microsoft.CodeAnalysis.CSharp.Symbols; namespace Microsoft.CodeAnalysis.CSharp @@ -9,9 +12,6 @@ internal sealed partial class LocalRewriter { public override BoundNode VisitDeconstructionAssignmentOperator(BoundDeconstructionAssignmentOperator node) { - CSharpSyntaxNode syntax = node.Syntax; - int numVariables = node.LeftVariables.Length; - var temps = ArrayBuilder.GetInstance(); var stores = ArrayBuilder.GetInstance(); @@ -24,40 +24,27 @@ public override BoundNode VisitDeconstructionAssignmentOperator(BoundDeconstruct } BoundExpression loweredRight = VisitExpression(node.Right); + ImmutableArray rhsValues; - // prepare out parameters for Deconstruct - var deconstructParameters = node.DeconstructMember.Parameters; - var outParametersBuilder = ArrayBuilder.GetInstance(deconstructParameters.Length); - Debug.Assert(deconstructParameters.Length == node.LeftVariables.Length); - - for (int i = 0; i < numVariables; i++) + // get or make right-hand-side values + if (node.Right.Type.IsTupleType) { - var localSymbol = new SynthesizedLocal(_factory.CurrentMethod, deconstructParameters[i].Type, SynthesizedLocalKind.LoweringTemp); - - var localBound = new BoundLocal(syntax, - localSymbol, - null, - deconstructParameters[i].Type - ) { WasCompilerGenerated = true }; - - temps.Add(localSymbol); - outParametersBuilder.Add(localBound); + rhsValues = AccessTupleFields(node, loweredRight, temps, stores); + } + else + { + rhsValues = CallDeconstruct(node, loweredRight, temps, stores); } - var outParameters = outParametersBuilder.ToImmutableAndFree(); - - // invoke Deconstruct - var invokeDeconstruct = MakeCall(syntax, loweredRight, node.DeconstructMember, outParameters, node.DeconstructMember.ReturnType); - stores.Add(invokeDeconstruct); - - // assign from out temps to lhs receivers - for (int i = 0; i < numVariables; i++) + // assign from rhs values to lhs receivers + int numAssignments = node.Assignments.Length; + for (int i = 0; i < numAssignments; i++) { // lower the assignment and replace the placeholders for source and target in the process var assignmentInfo = node.Assignments[i]; AddPlaceholderReplacement(assignmentInfo.LValuePlaceholder, lhsReceivers[i]); - AddPlaceholderReplacement(assignmentInfo.RValuePlaceholder, outParameters[i]); + AddPlaceholderReplacement(assignmentInfo.RValuePlaceholder, rhsValues[i]); var assignment = VisitExpression(assignmentInfo.Assignment); @@ -75,5 +62,81 @@ public override BoundNode VisitDeconstructionAssignmentOperator(BoundDeconstruct return result; } + + private ImmutableArray AccessTupleFields(BoundDeconstructionAssignmentOperator node, BoundExpression loweredRight, ArrayBuilder temps, ArrayBuilder stores) + { + var tupleType = loweredRight.Type.IsTupleType ? loweredRight.Type : TupleTypeSymbol.Create((NamedTypeSymbol)loweredRight.Type); + var tupleElementTypes = tupleType.TupleElementTypes; + + var numElements = tupleElementTypes.Length; + Debug.Assert(numElements == node.LeftVariables.Length); + + CSharpSyntaxNode syntax = node.Syntax; + + // save the loweredRight as we need to access it multiple times + BoundAssignmentOperator assignmentToTemp; + var savedTuple = _factory.StoreToTemp(loweredRight, out assignmentToTemp); + stores.Add(assignmentToTemp); + temps.Add(savedTuple.LocalSymbol); + + // list the tuple fields accessors + var fieldAccessorsBuilder = ArrayBuilder.GetInstance(numElements); + var fields = tupleType.TupleElementFields; + + for (int i = 0; i < numElements; i++) + { + var field = fields[i]; + + DiagnosticInfo useSiteInfo = field.GetUseSiteDiagnostic(); + if ((object)useSiteInfo != null && useSiteInfo.Severity == DiagnosticSeverity.Error) + { + Symbol.ReportUseSiteDiagnostic(useSiteInfo, _diagnostics, syntax.Location); + } + var fieldAccess = MakeTupleFieldAccess(syntax, field, savedTuple, null, LookupResultKind.Empty, tupleElementTypes[i]); + fieldAccessorsBuilder.Add(fieldAccess); + } + + return fieldAccessorsBuilder.ToImmutableAndFree(); + } + + /// + /// Prepares local variables to be used in Deconstruct call + /// Adds a invocation of Deconstruct with those as out parameters onto the 'stores' sequence + /// Returns the expressions for those out parameters + /// + private ImmutableArray CallDeconstruct(BoundDeconstructionAssignmentOperator node, BoundExpression loweredRight, ArrayBuilder temps, ArrayBuilder stores) + { + Debug.Assert((object)node.DeconstructMemberOpt != null); + + CSharpSyntaxNode syntax = node.Syntax; + + // prepare out parameters for Deconstruct + var deconstructParameters = node.DeconstructMemberOpt.Parameters; + var outParametersBuilder = ArrayBuilder.GetInstance(deconstructParameters.Length); + Debug.Assert(deconstructParameters.Length == node.LeftVariables.Length); + + foreach (var deconstructParameter in deconstructParameters) + { + var localSymbol = new SynthesizedLocal(_factory.CurrentMethod, deconstructParameter.Type, SynthesizedLocalKind.LoweringTemp); + + var localBound = new BoundLocal(syntax, + localSymbol, + null, + deconstructParameter.Type + ) + { WasCompilerGenerated = true }; + + temps.Add(localSymbol); + outParametersBuilder.Add(localBound); + } + + var outParameters = outParametersBuilder.ToImmutableAndFree(); + + // invoke Deconstruct + var invokeDeconstruct = MakeCall(syntax, loweredRight, node.DeconstructMemberOpt, outParameters, node.DeconstructMemberOpt.ReturnType); + stores.Add(invokeDeconstruct); + + return outParameters; + } } } diff --git a/src/Compilers/CSharp/Portable/Symbols/FieldSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/FieldSymbol.cs index bd3d82b812a08..497d002609a82 100644 --- a/src/Compilers/CSharp/Portable/Symbols/FieldSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/FieldSymbol.cs @@ -397,6 +397,19 @@ public virtual FieldSymbol TupleUnderlyingField } } + /// + /// If this is a field representing a tuple element, + /// returns the index of the element (zero-based). + /// Otherwise, a negative number. + /// + public virtual int TupleElementIndex + { + get + { + return -1; + } + } + #region IFieldSymbol Members ISymbol IFieldSymbol.AssociatedSymbol diff --git a/src/Compilers/CSharp/Portable/Symbols/Tuples/TupleErrorFieldSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Tuples/TupleErrorFieldSymbol.cs index a1133cd6d024b..5c45297281321 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Tuples/TupleErrorFieldSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Tuples/TupleErrorFieldSymbol.cs @@ -48,7 +48,7 @@ public override bool IsTupleField /// id is an index of the element (zero-based). /// Otherwise, (-1 - [index in members array]); /// - public int TupleFieldId + public override int TupleElementIndex { get { diff --git a/src/Compilers/CSharp/Portable/Symbols/Tuples/TupleFieldSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Tuples/TupleFieldSymbol.cs index 60a6394779154..7826371890ad9 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Tuples/TupleFieldSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Tuples/TupleFieldSymbol.cs @@ -40,8 +40,8 @@ public override bool IsTupleField /// If this field represents a tuple element (including the name match), /// id is an index of the element (zero-based). /// Otherwise, (-1 - [index in members array]); - /// - public int TupleFieldId + /// i + public override int TupleElementIndex { get { diff --git a/src/Compilers/CSharp/Portable/Symbols/Tuples/TupleTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Tuples/TupleTypeSymbol.cs index 27f5277733b82..4c60fc8babd80 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Tuples/TupleTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Tuples/TupleTypeSymbol.cs @@ -39,6 +39,7 @@ internal sealed class TupleTypeSymbol : WrappedNamedTypeSymbol private readonly ImmutableArray _elementTypes; private ImmutableArray _lazyMembers; + private ImmutableArray _lazyFields; private SmallDictionary _lazyUnderlyingDefinitionToMemberMap; internal const int RestPosition = 8; // The Rest field is in 8th position @@ -666,6 +667,47 @@ public override ImmutableArray TupleElementNames } } + /// + /// Get the fields for the tuple's elements (in order and cached). + /// + public override ImmutableArray TupleElementFields + { + get + { + if (_lazyFields.IsDefault) + { + ImmutableInterlocked.InterlockedInitialize(ref _lazyFields, CollectTupleElementFields()); + } + + return _lazyFields; + } + } + + private ImmutableArray CollectTupleElementFields() + { + var builder = ArrayBuilder.GetInstance(_elementTypes.Length, null); + + foreach (var member in GetMembers()) + { + if (member.Kind != SymbolKind.Field) + { + continue; + } + + var field = (FieldSymbol)member; + int index = field.TupleElementIndex; + if (index >= 0) + { + Debug.Assert((object)builder[index] == null); + builder[index] = field; + } + } + + Debug.Assert(builder.All(symbol => (object)symbol != null)); + + return builder.ToImmutableAndFree(); + } + /// /// Returns all members of the tuple type - a combination of members from the underlying type /// and synthesized fields for tuple elements. diff --git a/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs index 5b2c0461be42f..0a10860fa0db9 100644 --- a/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs @@ -619,6 +619,12 @@ public virtual NamedTypeSymbol TupleUnderlyingType /// public virtual ImmutableArray TupleElementNames => default(ImmutableArray); + /// + /// If this symbol represents a tuple type, get the fields for the tuple's elements. + /// Otherwise, returns default. + /// + public virtual ImmutableArray TupleElementFields => default(ImmutableArray); + /// /// Is this type a managed type (false for everything but enum, pointer, and /// some struct types). diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs index 9d518cabfe376..e1195b0a27a52 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Linq; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Test.Utilities; using Xunit; @@ -34,6 +35,28 @@ public void Deconstruct(out int a, out string b) var comp = CompileAndVerify(source, expectedOutput: "1 hello", parseOptions: TestOptions.Regular.WithTuplesFeature()); comp.VerifyDiagnostics(); + comp.VerifyIL("C.Main", @" +{ + // Code size 40 (0x28) + .maxstack 3 + .locals init (string V_0, //y + int V_1, + string V_2) + IL_0000: newobj ""C..ctor()"" + IL_0005: ldloca.s V_1 + IL_0007: ldloca.s V_2 + IL_0009: call ""void C.Deconstruct(out int, out string)"" + IL_000e: ldloc.1 + IL_000f: conv.i8 + IL_0010: ldloc.2 + IL_0011: stloc.0 + IL_0012: box ""long"" + IL_0017: ldstr "" "" + IL_001c: ldloc.0 + IL_001d: call ""string string.Concat(object, object, object)"" + IL_0022: call ""void System.Console.WriteLine(string)"" + IL_0027: ret +}"); } [Fact] @@ -559,5 +582,731 @@ static void Main() Diagnostic(ErrorCode.ERR_InvalidExprTerm, ".").WithArguments(".").WithLocation(6, 24) ); } + + [Fact] + [CompilerTrait(CompilerFeature.RefLocalsReturns)] + public void RefReturningMethod() + { + string source = @" +class C +{ + static int i = 0; + + static void Main() + { + (M(), M()) = new C(); + System.Console.WriteLine($""Final i is {i}""); + } + + static ref int M() + { + System.Console.WriteLine($""M (previous i is {i})""); + return ref i; + } + + void Deconstruct(out int x, out int y) + { + System.Console.WriteLine(""Deconstruct""); + x = 42; + y = 43; + } +} +"; + var expected = +@"M (previous i is 0) +M (previous i is 0) +Deconstruct +Final i is 43 +"; + + var comp = CompileAndVerify(source, expectedOutput: expected, parseOptions: TestOptions.Regular.WithTuplesFeature().WithRefsFeature()); + comp.VerifyDiagnostics( + ); + } + + [Fact] + [CompilerTrait(CompilerFeature.RefLocalsReturns)] + public void RefReturningProperty() + { + string source = @" +class C +{ + static int i = 0; + + static void Main() + { + (P, P) = new C(); + System.Console.WriteLine($""Final i is {i}""); + } + + static ref int P + { + get + { + System.Console.WriteLine($""P (previous i is {i})""); + return ref i; + } + } + + void Deconstruct(out int x, out int y) + { + System.Console.WriteLine(""Deconstruct""); + x = 42; + y = 43; + } +} +"; + var expected = +@"P (previous i is 0) +P (previous i is 0) +Deconstruct +Final i is 43 +"; + + var comp = CompileAndVerify(source, expectedOutput: expected, parseOptions: TestOptions.Regular.WithTuplesFeature().WithRefsFeature()); + comp.VerifyDiagnostics( + ); + } + + [Fact(Skip = "PROTOTYPE(tuples)")] + [CompilerTrait(CompilerFeature.RefLocalsReturns)] + public void RefReturningMethod2() + { + string source = @" +class C +{ + static int i; + + static void Main() + { + (M(), M()) = new C(); + } + + static ref int M() + { + System.Console.WriteLine(""M""); + return ref i; + } + + void Deconstruct(out int i, out int j) + { + i = 42; + j = 43; + } +} +"; + + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature().WithRefsFeature()); + comp.VerifyDiagnostics(); + + // This error is wrong + // (4,16): warning CS0649: Field 'C.i' is never assigned to, and will always have its default value 0 + // static int i; + //Diagnostic(ErrorCode.WRN_UnassignedInternalField, "i").WithArguments("C.i", "0").WithLocation(4, 16) + } + + [Fact(Skip = "PROTOTYPE(tuples)")] + [CompilerTrait(CompilerFeature.RefLocalsReturns)] + public void RefReturningMethodFlow() + { + string source = @" +struct C +{ + static C i; + static C P { get { System.Console.WriteLine(""getP""); return i; } set { System.Console.WriteLine(""setP""); i = value; } } + + static void Main() + { + (M(), M()) = P; + } + + static ref C M() + { + System.Console.WriteLine($""M (previous i is {i})""); + return ref i; + } + + void Deconstruct(out int x, out int y) + { + System.Console.WriteLine(""Deconstruct""); + x = 42; + y = 43; + } + + public static implicit operator C(int x) + { + System.Console.WriteLine(""conversion""); + return new C(); + } +} +"; + + var comp = CreateCompilationWithMscorlib(source, references: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature().WithRefsFeature()); + comp.VerifyDiagnostics(); + + + var expected = + @""; + + // Should not crash! + var comp2 = CompileAndVerify(source, expectedOutput: expected, parseOptions: TestOptions.Regular.WithTuplesFeature().WithRefsFeature()); + comp2.VerifyDiagnostics(); + } + + [Fact] + public void UninitializedRight() + { + string source = @" +class C +{ + static void Main() + { + int x; + (x, x) = x; + } +} +"; + + var comp = CreateCompilationWithMscorlib(source, references: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics( + // (7,18): error CS8206: No Deconstruct instance or extension method was found for type 'int'. + // (x, x) = x; + Diagnostic(ErrorCode.ERR_MissingDeconstruct, "x").WithArguments("int").WithLocation(7, 18), + // (7,18): error CS0165: Use of unassigned local variable 'x' + // (x, x) = x; + Diagnostic(ErrorCode.ERR_UseDefViolation, "x").WithArguments("x").WithLocation(7, 18) + ); + } + + [Fact] + public void Indexers() + { + string source = @" +class C +{ + static SomeArray array; + + static void Main() + { + int y; + (Foo()[Bar()], y) = new C(); + System.Console.WriteLine($""Final array values[2] {array.values[2]}""); + } + + static SomeArray Foo() + { + System.Console.WriteLine($""Foo""); + array = new SomeArray(); + return array; + } + + static int Bar() + { + System.Console.WriteLine($""Bar""); + return 2; + } + + void Deconstruct(out int x, out int y) + { + System.Console.WriteLine(""Deconstruct""); + x = 101; + y = 102; + } +} +class SomeArray +{ + public int[] values; + public SomeArray() { values = new [] { 42, 43, 44 }; } + public int this[int index] { + get { System.Console.WriteLine($""indexGet (with value {values[index]})""); return values[index]; } + set { System.Console.WriteLine($""indexSet (with value {value})""); values[index] = value; } + } +} +"; + var expected = +@"Foo +Bar +Deconstruct +indexSet (with value 101) +Final array values[2] 101 +"; + var comp = CompileAndVerify(source, expectedOutput: expected, parseOptions: TestOptions.Regular.WithTuplesFeature().WithRefsFeature()); + comp.VerifyDiagnostics( + ); + } + + [Fact] + public void NullRight() + { + string source = @" +class C +{ + static void Main() + { + int x; + (x, x) = null; + } +} +"; + + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature().WithRefsFeature()); + comp.VerifyDiagnostics( + // (7,9): error CS8210: Deconstruct assignment requires an expression with a type on the right-hand-side. + // (x, x) = null; + Diagnostic(ErrorCode.ERR_DeconstructRequiresExpression, "(x, x) = null").WithLocation(7, 9), + // (7,10): error CS0165: Use of unassigned local variable 'x' + // (x, x) = null; + Diagnostic(ErrorCode.ERR_UseDefViolation, "x").WithArguments("x").WithLocation(7, 10) + ); + } + + [Fact] + public void VoidRight() + { + string source = @" +class C +{ + static void Main() + { + int x; + (x, x) = M(); + } + static void M() { } +} +"; + + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature().WithRefsFeature()); + comp.VerifyDiagnostics( + // (7,18): error CS8206: No Deconstruct instance or extension method was found for type 'void'. + // (x, x) = M(); + Diagnostic(ErrorCode.ERR_MissingDeconstruct, "M()").WithArguments("void").WithLocation(7, 18) + ); + } + + [Fact] + public void DeconstructTuple() + { + string source = @" +class C +{ + static void Main() + { + long x; + string y; + + (x, y) = (1, ""hello""); + System.Console.WriteLine(x + "" "" + y); + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "1 hello", additionalRefs: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact(Skip = "PROTOTYPE(tuples)")] + public void DeconstructTuple2() + { + string source = @" +class C +{ + static void Main() + { + long x; + string y; + + (x, y) = M(); + System.Console.WriteLine(x + "" "" + y); + } + + static System.ValueTuple M() + { + return (1, ""hello""); + } +} +"; + // Should not give this error: (9,18): error CS0029: Cannot implicitly convert type '(int, string)' to '(long, string)' + var comp = CompileAndVerify(source, expectedOutput: "1 hello", additionalRefs: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact] + public void DeconstructLongTuple() + { + string source = @" +class C +{ + static void Main() + { + long x; + int y; + + (x, x, x, x, x, x, x, x, x, y) = (1, 1, 1, 1, 1, 1, 1, 1, 4, 2); + System.Console.WriteLine(x + "" "" + y); + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "4 2", additionalRefs: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + comp.VerifyIL("C.Main", @" +{ + // Code size 141 (0x8d) + .maxstack 10 + .locals init (long V_0, //x + int V_1) //y + IL_0000: ldc.i4.1 + IL_0001: conv.i8 + IL_0002: ldc.i4.1 + IL_0003: conv.i8 + IL_0004: ldc.i4.1 + IL_0005: conv.i8 + IL_0006: ldc.i4.1 + IL_0007: conv.i8 + IL_0008: ldc.i4.1 + IL_0009: conv.i8 + IL_000a: ldc.i4.1 + IL_000b: conv.i8 + IL_000c: ldc.i4.1 + IL_000d: conv.i8 + IL_000e: ldc.i4.1 + IL_000f: conv.i8 + IL_0010: ldc.i4.4 + IL_0011: conv.i8 + IL_0012: ldc.i4.2 + IL_0013: newobj ""System.ValueTuple..ctor(long, long, int)"" + IL_0018: newobj ""System.ValueTuple..ctor(long, long, long, long, long, long, long, (long, long, int))"" + IL_001d: dup + IL_001e: ldfld ""long System.ValueTuple.Item1"" + IL_0023: stloc.0 + IL_0024: dup + IL_0025: ldfld ""long System.ValueTuple.Item2"" + IL_002a: stloc.0 + IL_002b: dup + IL_002c: ldfld ""long System.ValueTuple.Item3"" + IL_0031: stloc.0 + IL_0032: dup + IL_0033: ldfld ""long System.ValueTuple.Item4"" + IL_0038: stloc.0 + IL_0039: dup + IL_003a: ldfld ""long System.ValueTuple.Item5"" + IL_003f: stloc.0 + IL_0040: dup + IL_0041: ldfld ""long System.ValueTuple.Item6"" + IL_0046: stloc.0 + IL_0047: dup + IL_0048: ldfld ""long System.ValueTuple.Item7"" + IL_004d: stloc.0 + IL_004e: dup + IL_004f: ldfld ""(long, long, int) System.ValueTuple.Rest"" + IL_0054: ldfld ""long System.ValueTuple.Item1"" + IL_0059: stloc.0 + IL_005a: dup + IL_005b: ldfld ""(long, long, int) System.ValueTuple.Rest"" + IL_0060: ldfld ""long System.ValueTuple.Item2"" + IL_0065: stloc.0 + IL_0066: ldfld ""(long, long, int) System.ValueTuple.Rest"" + IL_006b: ldfld ""int System.ValueTuple.Item3"" + IL_0070: stloc.1 + IL_0071: ldloc.0 + IL_0072: box ""long"" + IL_0077: ldstr "" "" + IL_007c: ldloc.1 + IL_007d: box ""int"" + IL_0082: call ""string string.Concat(object, object, object)"" + IL_0087: call ""void System.Console.WriteLine(string)"" + IL_008c: ret +} +"); + } + + [Fact] + public void DeconstructLongTupleWithNames() + { + string source = @" +class C +{ + static void Main() + { + long x; + int y; + + (x, x, x, x, x, x, x, x, x, y) = (a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10); + System.Console.WriteLine(x + "" "" + y); + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "9 10", additionalRefs: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact(Skip = "PROTOTYPE(tuples)")] + public void DeconstructLongTuple2() + { + string source = @" +class C +{ + static void Main() + { + long x; + int y; + + (x, x, x, x, x, x, x, x, x, y) = (1, 1, 1, 1, 1, 1, 1, 1, 4, (byte)2); + System.Console.WriteLine(x + "" "" + y); + } +} +"; + + // issue with return type + var comp = CompileAndVerify(source, expectedOutput: "4 2", additionalRefs: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact] + public void DeconstructTypelessTuple() + { + string source = @" +class C +{ + static void Main() + { + string x = ""goodbye""; + string y; + + (x, y) = (null, ""hello""); + System.Console.WriteLine($""{x}{y}""); + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "hello", additionalRefs: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + comp.VerifyIL("C.Main", @" +{ + // Code size 48 (0x30) + .maxstack 3 + .locals init (string V_0, //x + string V_1) //y + IL_0000: ldstr ""goodbye"" + IL_0005: stloc.0 + IL_0006: ldnull + IL_0007: ldstr ""hello"" + IL_000c: newobj ""System.ValueTuple..ctor(string, string)"" + IL_0011: dup + IL_0012: ldfld ""string System.ValueTuple.Item1"" + IL_0017: stloc.0 + IL_0018: ldfld ""string System.ValueTuple.Item2"" + IL_001d: stloc.1 + IL_001e: ldstr ""{0}{1}"" + IL_0023: ldloc.0 + IL_0024: ldloc.1 + IL_0025: call ""string string.Format(string, object, object)"" + IL_002a: call ""void System.Console.WriteLine(string)"" + IL_002f: ret +} +"); + } + + [Fact] + public void TupleWithNoConversion() + { + string source = @" +class C +{ + static void Main() + { + byte x; + string y; + + (x, y) = (1, 2); + } +} +"; + // PROTOTYPE(tuples) This error message is misleading + var comp = CreateCompilationWithMscorlib(source, references: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics( + // (9,18): error CS0029: Cannot implicitly convert type '(int, int)' to '(byte, string)' + // (x, y) = (1, 2); + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(1, 2)").WithArguments("(int, int)", "(byte, string)").WithLocation(9, 18) + ); + } + + [Fact] + public void DeconstructIntoProperties() + { + string source = @" +class C +{ + static long x { set { System.Console.WriteLine($""setX {value}""); } } + static string y { get; set; } + + static void Main() + { + (x, y) = new C(); + System.Console.WriteLine(y); + } + + public void Deconstruct(out int a, out string b) + { + a = 1; + b = ""hello""; + } +} +"; + string expected = +@"setX 1 +hello"; + var comp = CompileAndVerify(source, expectedOutput: expected, additionalRefs: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact] + public void DeconstructTupleIntoProperties() + { + string source = @" +class C +{ + static long x { set { System.Console.WriteLine($""setX {value}""); } } + static string y { get; set; } + + static void Main() + { + (x, y) = (1, ""hello""); + System.Console.WriteLine(y); + } +} +"; + string expected = +@"setX 1 +hello"; + var comp = CompileAndVerify(source, expectedOutput: expected, additionalRefs: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact] + public void Swap() + { + string source = @" +class C +{ + static int x = 2; + static int y = 4; + + static void Main() + { + Swap(); + System.Console.WriteLine(x + "" "" + y); + } + + static void Swap() + { + (x, y) = (y, x); + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "4 2", additionalRefs: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + comp.VerifyIL("C.Swap", @" +{ + // Code size 37 (0x25) + .maxstack 2 + IL_0000: ldsfld ""int C.y"" + IL_0005: ldsfld ""int C.x"" + IL_000a: newobj ""System.ValueTuple..ctor(int, int)"" + IL_000f: dup + IL_0010: ldfld ""int System.ValueTuple.Item1"" + IL_0015: stsfld ""int C.x"" + IL_001a: ldfld ""int System.ValueTuple.Item2"" + IL_001f: stsfld ""int C.y"" + IL_0024: ret +} +"); + } + + + [Fact] + public void TupleWithUseSiteError() + { + string source = @" + +namespace System +{ + public struct ValueTuple + { + public T1 Item1; + + public ValueTuple(T1 item1, T2 item2) + { + this.Item1 = item1; + } + } +} +class C +{ + static void Main() + { + int x; + int y; + + (x, y) = (1, 2); + } +} +"; + + var comp = CreateCompilationWithMscorlib(source, assemblyName: "comp", parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + comp.VerifyEmitDiagnostics( + // (22,9): error CS8205: Member 'Item2' was not found on type 'ValueTuple' from assembly 'comp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'. + // (x, y) = (1, 2); + Diagnostic(ErrorCode.ERR_PredefinedTypeMemberNotFoundInAssembly, "(x, y) = (1, 2)").WithArguments("Item2", "System.ValueTuple", "comp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null").WithLocation(22, 9) + ); + } + + [Fact] + public void CircularFlow() + { + string source = @" +class C +{ + static void Main() + { + (object i, object ii) x = (1,2); + object y; + + (x.ii, y) = x; + System.Console.WriteLine(x + "" "" + y); + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "(1, 1) 2", additionalRefs: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact] + [CompilerTrait(CompilerFeature.RefLocalsReturns)] + public void CircularFlow2() + { + string source = @" +class C +{ + static void Main() + { + (object i, object ii) x = (1,2); + object y; + + ref var a = ref x; + + (a.ii, y) = x; + System.Console.WriteLine(x + "" "" + y); + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "(1, 1) 2", additionalRefs: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature().WithRefsFeature()); + comp.VerifyDiagnostics(); + } } } \ No newline at end of file