diff --git a/docs/features/deconstruction.md b/docs/features/deconstruction.md new file mode 100644 index 0000000000000..8c04e0d315743 --- /dev/null +++ b/docs/features/deconstruction.md @@ -0,0 +1,180 @@ + +Deconstruction +-------------- + +This design doc will cover two kinds of deconstruction: deconstruction into existing variables (deconstruction-assignment) and deconstruction into new variables (deconstruction-declaration). +It is still very much work-in-progress. + +Here is an example of deconstruction-assignment: +```C# +class C +{ + static void Main() + { + long x; + string y; + + (x, y) = new C(); + System.Console.WriteLine(x + "" "" + y); + } + + public void Deconstruct(out int a, out string b) + { + a = 1; + b = ""hello""; + } +} +``` + +Treat deconstruction of a tuple into existing variables as a kind of assignment, using the existing AssignmentExpression. + + +###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. + +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 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: + +``` +tempX = &evaluate expressionX +tempY = &evaluate expressionY +tempZ = &evaluate expressionZ + +tempRight = evaluate right and evaluate Deconstruct + +tempX = tempRight.A (including conversions) +tempY = tempRight.B (including conversions) +tempZ = tempRight.C (including conversions) + +“return/continue” with newTupleIncludingNames tempRight (so you can do get Item1 from the assignment)? +``` + +The evaluation order for nesting `(x, (y, z))` is: +``` +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 + +tempY = tempRNest.B (including conversions) +tempZ = tempRNest.C (including conversions) + +``` + +The evaluation order for the simplest cases (locals, fields, array indexers, or anything returning ref) without needing conversion: +``` +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. + + +**Work items, open issues and assumptions to confirm with LDM:** + +- 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): + +```ANTLR +declaration_statement + : local_variable_declaration ';' + | local_constant_declaration ';' + | local_variable_combo_declaration ';' // new + ; + +local_variable_combo_declaration + : local_variable_combo_declaration_lhs '=' expression + +local_variable_combo_declaration_lhs + : 'var' '(' identifier_list ')' + | '(' local_variable_list ')' + ; + +identifier_list + : identifier ',' identifier + | identifier_list ',' identifier + ; + +local_variable_list + : local_variable_type identifier ',' local_variable_type identifier + | local_variable_list ',' local_variable_type identifier + ; + +foreach_statement + : 'foreach' '(' local_variable_type identifier 'in' expression ')' embedded_statement + | 'foreach' '(' local_variable_combo_declaration_lhs 'in' expression ')' embedded_statement // new + ; + +for_initializer + : local_variable_declaration + | local_variable_combo_declaration // new + | statement_expression_list + ; + +let_clause + : 'let' identifier '=' expression + | 'let' '(' identifier_list ')' '=' expression // new + ; + +from_clause // not sure + : 'from' type? identifier 'in' expression + ; + +join_clause // not sure + : 'join' type? identifier 'in' expression 'on' expression 'equals' expression + ; + +join_into_clause // not sure + : 'join' type? identifier 'in' expression 'on' expression 'equals' expression 'into' identifier + ; + +constant_declarator // not sure + : identifier '=' constant_expression + ; +``` + +Treat deconstruction of a tuple into new variables as a new kind of node (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`. + +**References** + +[C# Design Notes for Apr 12-22, 2016](https://github.com/dotnet/roslyn/issues/11031) + + diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs index 81833fd729b45..78cf18b58d216 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs @@ -1698,18 +1698,132 @@ private static bool CheckNotNamespaceOrType(BoundExpression expr, DiagnosticBag } } - private BoundAssignmentOperator BindAssignment(AssignmentExpressionSyntax node, DiagnosticBag diagnostics) + private BoundExpression BindAssignment(AssignmentExpressionSyntax node, DiagnosticBag diagnostics) { Debug.Assert(node != null); Debug.Assert(node.Left != null); Debug.Assert(node.Right != null); + if (node.Left.Kind() == SyntaxKind.TupleExpression) + { + return BindDeconstructionAssignment(node, diagnostics); + } + var op1 = BindValue(node.Left, diagnostics, BindValueKind.Assignment); // , BIND_MEMBERSET); var op2 = BindValue(node.Right, diagnostics, BindValueKind.RValue); // , BIND_RVALUEREQUIRED); return BindAssignment(node, op1, op2, diagnostics); } + private BoundExpression BindDeconstructionAssignment(AssignmentExpressionSyntax node, DiagnosticBag diagnostics) + { + SeparatedSyntaxList arguments = ((TupleExpressionSyntax)node.Left).Arguments; + + // receiver for Deconstruct + var boundRHS = BindValue(node.Right, diagnostics, BindValueKind.RValue); + + int numElements = arguments.Count; + Debug.Assert(numElements >= 2); // this should not have parsed as a tuple. + + // bind the variables and check they can be assigned to + var checkedVariablesBuilder = ArrayBuilder.GetInstance(numElements); + for (int i = 0; i < numElements; i++) + { + var boundVariable = BindExpression(arguments[i].Expression, diagnostics, invoked: false, indexed: false); + var checkedVariable = CheckValue(boundVariable, BindValueKind.Assignment, diagnostics); + + checkedVariablesBuilder.Add(checkedVariable); + } + + var checkedVariables = checkedVariablesBuilder.ToImmutableAndFree(); + + // symbol and parameters for Deconstruct + DiagnosticBag bag = new DiagnosticBag(); + MethodSymbol deconstructMethod = FindDeconstruct(checkedVariables, boundRHS, node, bag); + if (!diagnostics.HasAnyErrors()) + { + diagnostics.AddRange(bag); + } + + if ((object)deconstructMethod == null) + { + return new BoundDeconstructionAssignmentOperator(node, checkedVariables, boundRHS, ErrorMethodSymbol.UnknownMethod, + ImmutableArray.Empty, + ErrorTypeSymbol.UnknownResultType, + hasErrors: true); + } + + // 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(); + + TypeSymbol lastType = deconstructParameters.Last().Type; + return new BoundDeconstructionAssignmentOperator(node, checkedVariables, boundRHS, deconstructMethod, assignments, lastType); + } + + /// + /// 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. + /// If so, it outputs the method. + /// + private static MethodSymbol FindDeconstruct(ImmutableArray checkedVariables, BoundExpression boundRHS, SyntaxNode node, DiagnosticBag diagnostics) + { + // find symbol for Deconstruct member + ImmutableArray candidates = boundRHS.Type.GetMembers("Deconstruct"); + switch (candidates.Length) + { + case 0: + Error(diagnostics, ErrorCode.ERR_MissingDeconstruct, boundRHS.Syntax, boundRHS.Type); + return null; + case 1: + break; + default: + Error(diagnostics, ErrorCode.ERR_AmbiguousDeconstruct, boundRHS.Syntax, boundRHS.Type); + return null; + } + + Symbol deconstructMember = candidates[0]; + + // check that the deconstruct fits + if (deconstructMember.Kind != SymbolKind.Method) + { + Error(diagnostics, ErrorCode.ERR_MissingDeconstruct, boundRHS.Syntax, boundRHS.Type); + return null; + } + + MethodSymbol deconstructMethod = (MethodSymbol)deconstructMember; + if (deconstructMethod.MethodKind != MethodKind.Ordinary) + { + Error(diagnostics, ErrorCode.ERR_MissingDeconstruct, boundRHS.Syntax, boundRHS.Type); + return null; + } + + if (deconstructMethod.ParameterCount != checkedVariables.Length) + { + Error(diagnostics, ErrorCode.ERR_DeconstructWrongParams, boundRHS.Syntax, deconstructMethod, checkedVariables.Length); + return null; + } + + if (deconstructMethod.Parameters.Any(p => p.RefKind != RefKind.Out)) + { + Error(diagnostics, ErrorCode.ERR_DeconstructRequiresOutParams, boundRHS.Syntax, deconstructMethod); + return null; + } + + return deconstructMethod; + } + private BoundAssignmentOperator BindAssignment(AssignmentExpressionSyntax node, BoundExpression op1, BoundExpression op2, DiagnosticBag diagnostics) { Debug.Assert(op1 != null); @@ -3422,7 +3536,7 @@ private BoundCatchBlock BindCatchBlock(CatchClauseSyntax node, ArrayBuilder locals = binder.GetDeclaredLocalsForScope(node); + ImmutableArray locals = binder.GetDeclaredLocalsForScope(node); BoundExpression exceptionSource = null; LocalSymbol local = locals.FirstOrDefault(); diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs index b0243a2ea198a..9ebbdc8f0d059 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs @@ -278,6 +278,16 @@ public override Symbol ExpressionSymbol public ImmutableArray OriginalUserDefinedOperatorsOpt { get; } } + internal sealed partial class BoundDeconstructionAssignmentOperator : BoundExpression + { + internal class AssignmentInfo + { + public BoundAssignmentOperator Assignment; + public BoundLValuePlaceholder LValuePlaceholder; + public BoundRValuePlaceholder RValuePlaceholder; + } + } + internal partial class BoundCompoundAssignmentOperator { public override Symbol ExpressionSymbol diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml index 53d531f48b775..144df264efbd0 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml @@ -76,6 +76,19 @@ + + + + + + + + + + @@ -377,6 +390,19 @@ + + + + + + + + + + + + + diff --git a/src/Compilers/CSharp/Portable/BoundTree/Expression.cs b/src/Compilers/CSharp/Portable/BoundTree/Expression.cs index b41e6fa9d4fce..33c1354985cc7 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/Expression.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/Expression.cs @@ -33,6 +33,36 @@ Optional IOperation.ConstantValue public abstract TResult Accept(OperationVisitor visitor, TArgument argument); } + internal sealed partial class BoundLValuePlaceholder : BoundValuePlaceholderBase, IPlaceholderExpression + { + protected override OperationKind ExpressionKind => OperationKind.PlaceholderExpression; + + public override void Accept(OperationVisitor visitor) + { + visitor.VisitPlaceholderExpression(this); + } + + public override TResult Accept(OperationVisitor visitor, TArgument argument) + { + return visitor.VisitPlaceholderExpression(this, argument); + } + } + + internal sealed partial class BoundRValuePlaceholder : BoundValuePlaceholderBase, IPlaceholderExpression + { + protected override OperationKind ExpressionKind => OperationKind.PlaceholderExpression; + + public override void Accept(OperationVisitor visitor) + { + visitor.VisitPlaceholderExpression(this); + } + + public override TResult Accept(OperationVisitor visitor, TArgument argument) + { + return visitor.VisitPlaceholderExpression(this, argument); + } + } + internal partial class BoundCall : IInvocationExpression { IMethodSymbol IInvocationExpression.TargetMethod => this.Method; @@ -1004,6 +1034,21 @@ public override void Accept(OperationVisitor visitor) } } + internal sealed partial class BoundDeconstructionAssignmentOperator : BoundExpression + { + protected override OperationKind ExpressionKind => OperationKind.None; + + public override void Accept(OperationVisitor visitor) + { + visitor.VisitNoneOperation(this); + } + + public override TResult Accept(OperationVisitor visitor, TArgument argument) + { + return visitor.VisitNoneOperation(this, argument); + } + } + internal partial class BoundCompoundAssignmentOperator : ICompoundAssignmentExpression { BinaryOperationKind ICompoundAssignmentExpression.BinaryOperationKind => Expression.DeriveBinaryOperationKind(this.Operator.Kind); diff --git a/src/Compilers/CSharp/Portable/CSharpCodeAnalysis.csproj b/src/Compilers/CSharp/Portable/CSharpCodeAnalysis.csproj index f0f124c137c78..d40eb4009a63b 100644 --- a/src/Compilers/CSharp/Portable/CSharpCodeAnalysis.csproj +++ b/src/Compilers/CSharp/Portable/CSharpCodeAnalysis.csproj @@ -406,6 +406,7 @@ + diff --git a/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs b/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs index be6f596a5e50f..6f712678266d9 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs +++ b/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs @@ -394,6 +394,15 @@ internal class CSharpResources { } } + /// + /// Looks up a localized string similar to More than one Deconstruct instance or extension method was found for type '{0}'.. + /// + internal static string ERR_AmbiguousDeconstruct { + get { + return ResourceManager.GetString("ERR_AmbiguousDeconstruct", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot use ref or out parameter '{0}' inside an anonymous method, lambda expression, or query expression. /// @@ -3085,6 +3094,24 @@ internal class CSharpResources { } } + /// + /// Looks up a localized string similar to The Deconstruct method for type '{0}' must have only out parameters.. + /// + internal static string ERR_DeconstructRequiresOutParams { + get { + return ResourceManager.GetString("ERR_DeconstructRequiresOutParams", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Deconstruct method for type '{0}' doesn't have the number of parameters ({1}) needed for this deconstruction.. + /// + internal static string ERR_DeconstructWrongParams { + get { + return ResourceManager.GetString("ERR_DeconstructWrongParams", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot specify the DefaultMember attribute on a type containing an indexer. /// @@ -5866,6 +5893,15 @@ internal class CSharpResources { } } + /// + /// Looks up a localized string similar to No Deconstruct instance or extension method was found for type '{0}'.. + /// + internal static string ERR_MissingDeconstruct { + get { + return ResourceManager.GetString("ERR_MissingDeconstruct", resourceCulture); + } + } + /// /// Looks up a localized string similar to Command-line syntax error: Missing Guid for option '{1}'. /// diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index b79a6b5f7320c..6210d481314bb 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -242,7 +242,7 @@ replaced members - + method @@ -4872,4 +4872,16 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ tuples + + More than one Deconstruct instance or extension method was found for type '{0}'. + + + The Deconstruct method for type '{0}' must have only out parameters. + + + The Deconstruct method for type '{0}' doesn't have the number of parameters ({1}) needed for this deconstruction. + + + No Deconstruct instance or extension method was found for type '{0}'. + \ 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 150fa46996801..3e5fa88eeb6d7 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -1391,5 +1391,9 @@ internal enum ErrorCode ERR_TupleExplicitNamesOnAllMembersOrNone = 8204, ERR_PredefinedTypeMemberNotFoundInAssembly = 8205, + ERR_MissingDeconstruct = 8206, + ERR_AmbiguousDeconstruct = 8207, + ERR_DeconstructRequiresOutParams = 8208, + ERR_DeconstructWrongParams = 8209, } } diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/DataFlowPass.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/DataFlowPass.cs index 2a0d62ff521e5..4b7eb0c8c4bf4 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/DataFlowPass.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/DataFlowPass.cs @@ -1721,6 +1721,18 @@ public override BoundNode VisitAssignmentOperator(BoundAssignmentOperator node) Assign(node.Left, node.Right, refKind: node.RefKind); return null; } + + public override BoundNode VisitDeconstructionAssignmentOperator(BoundDeconstructionAssignmentOperator node) + { + base.VisitDeconstructionAssignmentOperator(node); + + foreach(BoundExpression variable in node.LeftVariables) + { + Assign(variable, null, refKind: RefKind.None); + } + + return null; + } public override BoundNode VisitIncrementOperator(BoundIncrementOperator node) { diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/PreciseAbstractFlowPass.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/PreciseAbstractFlowPass.cs index 64e22faed0b30..aab14c7e3ffdc 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/PreciseAbstractFlowPass.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/PreciseAbstractFlowPass.cs @@ -1514,6 +1514,18 @@ public override BoundNode VisitAssignmentOperator(BoundAssignmentOperator node) return null; } + public override BoundNode VisitDeconstructionAssignmentOperator(BoundDeconstructionAssignmentOperator node) + { + foreach (BoundExpression variable in node.LeftVariables) + { + VisitLvalue(variable); + } + + VisitRvalue(node.Right); + + return null; + } + public override BoundNode VisitCompoundAssignmentOperator(BoundCompoundAssignmentOperator node) { // TODO: should events be handled specially too? diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.cs index 714e47c39e9a7..a3011854e813a 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.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.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; @@ -25,6 +26,8 @@ internal sealed partial class LocalRewriter : BoundTreeRewriterWithStackGuard private bool _sawAwaitInExceptionHandler; private readonly DiagnosticBag _diagnostics; + private Dictionary _placeholderReplacementMapDoNotUseDirectly; + private LocalRewriter( CSharpCompilation compilation, MethodSymbol containingMethod, @@ -227,6 +230,63 @@ public override BoundNode VisitLocalFunctionStatement(BoundLocalFunctionStatemen } } + public override BoundNode VisitLValuePlaceholder(BoundLValuePlaceholder node) + { + return PlaceholderReplacement(node); + } + + public override BoundNode VisitRValuePlaceholder(BoundRValuePlaceholder node) + { + return PlaceholderReplacement(node); + } + + /// + /// Returns substitution currently used by the rewriter for a placeholder node. + /// Each occurrence of the placeholder node is replaced with the node returned. + /// Throws if there is no substitution. + /// + private BoundExpression PlaceholderReplacement(BoundValuePlaceholderBase placeholder) + { + var value = _placeholderReplacementMapDoNotUseDirectly[placeholder]; + AssertPlaceholderReplacement(placeholder, value); + return value; + } + + [Conditional("DEBUG")] + private static void AssertPlaceholderReplacement(BoundValuePlaceholderBase placeholder, BoundExpression value) + { + Debug.Assert(value.Type.Equals(placeholder.Type, ignoreCustomModifiersAndArraySizesAndLowerBounds: true, ignoreDynamic: true)); + } + + /// + /// Sets substitution used by the rewriter for a placeholder node. + /// Each occurrence of the placeholder node is replaced with the node returned. + /// Throws if there is already a substitution. + /// + private void AddPlaceholderReplacement(BoundValuePlaceholderBase placeholder, BoundExpression value) + { + AssertPlaceholderReplacement(placeholder, value); + + if ((object)_placeholderReplacementMapDoNotUseDirectly == null) + { + _placeholderReplacementMapDoNotUseDirectly = new Dictionary(); + } + + _placeholderReplacementMapDoNotUseDirectly.Add(placeholder, value); + } + + /// + /// Removes substitution currently used by the rewriter for a placeholder node. + /// Asserts if there isn't already a substitution. + /// + private void RemovePlaceholderReplacement(BoundValuePlaceholderBase placeholder) + { + Debug.Assert((object)placeholder != null); + bool removed = _placeholderReplacementMapDoNotUseDirectly.Remove(placeholder); + + Debug.Assert(removed); + } + public override BoundNode VisitBadExpression(BoundBadExpression node) { // Cannot recurse into BadExpression children since the BadExpression diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_DeconstructionAssignmentOperator.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_DeconstructionAssignmentOperator.cs new file mode 100644 index 0000000000000..9b025171a90d9 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_DeconstructionAssignmentOperator.cs @@ -0,0 +1,79 @@ +// 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.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Symbols; + +namespace Microsoft.CodeAnalysis.CSharp +{ + 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(); + + var lhsReceivers = ArrayBuilder.GetInstance(); + foreach (var variable in node.LeftVariables) + { + // This will be filled in with the LHS that uses temporaries to prevent + // double-evaluation of side effects. + lhsReceivers.Add(TransformCompoundAssignmentLHS(variable, stores, temps, isDynamicAssignment: false)); + } + + BoundExpression loweredRight = VisitExpression(node.Right); + + // 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++) + { + 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); + } + + 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++) + { + // 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]); + + var assignment = VisitExpression(assignmentInfo.Assignment); + + RemovePlaceholderReplacement(assignmentInfo.LValuePlaceholder); + RemovePlaceholderReplacement(assignmentInfo.RValuePlaceholder); + + stores.Add(assignment); + } + + var result = _factory.Sequence(temps.ToImmutable(), stores.ToArray()); + + temps.Free(); + stores.Free(); + lhsReceivers.Free(); + + return result; + } + } +} diff --git a/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs b/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs index 70be22b9dacb7..e2d226e476abf 100644 --- a/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs +++ b/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs @@ -6091,6 +6091,11 @@ private enum ScanTypeFlags /// Might be a pointer type or a multiplication. /// PointerOrMultiplication, + + /// + /// Might be a tuple type. + /// + TupleType, } private bool IsPossibleType() @@ -6256,8 +6261,15 @@ private ScanTypeFlags ScanNonArrayType(out SyntaxToken lastTokenOfType) return result; } + /// + /// Returns TupleType when a possible tuple type is found. + /// Note that this is not MustBeType, so that the caller can consider deconstruction syntaxes. + /// The caller is expected to have consumed the opening paren. + /// private ScanTypeFlags ScanTupleType(out SyntaxToken lastTokenOfType) { + bool mustBeType = false; + var tupleElementType = ScanType(out lastTokenOfType); if (tupleElementType != ScanTypeFlags.NotType) { @@ -6278,17 +6290,30 @@ private ScanTypeFlags ScanTupleType(out SyntaxToken lastTokenOfType) lastTokenOfType = this.EatToken(); return ScanTypeFlags.NotType; } + else if (tupleElementType == ScanTypeFlags.MustBeType) + { + mustBeType = true; + } if (this.CurrentToken.Kind == SyntaxKind.IdentifierToken) { lastTokenOfType = this.EatToken(); } - } while (this.CurrentToken.Kind == SyntaxKind.CommaToken); + } + while (this.CurrentToken.Kind == SyntaxKind.CommaToken); if (this.CurrentToken.Kind == SyntaxKind.CloseParenToken) { lastTokenOfType = this.EatToken(); - return ScanTypeFlags.MustBeType; + + if (mustBeType) + { + return ScanTypeFlags.MustBeType; + } + else + { + return ScanTypeFlags.TupleType; + } } } } @@ -9858,7 +9883,7 @@ private bool ScanCast() // check for ambiguous type or expression followed by disambiguating token. i.e. // // "(A)b" is a cast. But "(A)+b" is not a cast. - return (type == ScanTypeFlags.GenericTypeOrMethod || type == ScanTypeFlags.GenericTypeOrExpression || type == ScanTypeFlags.NonGenericTypeOrExpression) && CanFollowCast(this.CurrentToken.Kind); + return (type == ScanTypeFlags.GenericTypeOrMethod || type == ScanTypeFlags.GenericTypeOrExpression || type == ScanTypeFlags.NonGenericTypeOrExpression || type == ScanTypeFlags.TupleType) && CanFollowCast(this.CurrentToken.Kind); } private bool ScanAsyncLambda(Precedence precedence) diff --git a/src/Compilers/CSharp/Test/Emit/CSharpCompilerEmitTest.csproj b/src/Compilers/CSharp/Test/Emit/CSharpCompilerEmitTest.csproj index 758a08a66813f..4ffe9db81cd33 100644 --- a/src/Compilers/CSharp/Test/Emit/CSharpCompilerEmitTest.csproj +++ b/src/Compilers/CSharp/Test/Emit/CSharpCompilerEmitTest.csproj @@ -99,6 +99,7 @@ + diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs new file mode 100644 index 0000000000000..9d518cabfe376 --- /dev/null +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDeconstructTests.cs @@ -0,0 +1,563 @@ +// 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 Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests.CodeGen +{ + [CompilerTrait(CompilerFeature.Tuples)] + public class CodeGenDeconstructTests : CSharpTestBase + { + [Fact] + public void Deconstruct() + { + string source = @" +class C +{ + static void Main() + { + long x; + string y; + + (x, y) = new C(); + System.Console.WriteLine(x + "" "" + y); + } + + public void Deconstruct(out int a, out string b) + { + a = 1; + b = ""hello""; + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "1 hello", parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact] + public void DeconstructMethodMissing() + { + string source = @" +class C +{ + static void Main() + { + long x; + string y; + (x, y) = new C(); + } +} +"; + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics( + // (8,18): error CS8206: No Deconstruct instance or extension method was found for type 'C'. + // (x, y) = new C(); + Diagnostic(ErrorCode.ERR_MissingDeconstruct, "new C()").WithArguments("C").WithLocation(8, 18) + ); + } + + [Fact] + public void DeconstructMethodAmbiguous() + { + string source = @" +class C +{ + static void Main() + { + long x; + string y; + (x, y) = new C(); + } + + public void Deconstruct(out int a, out string b) + { + a = 1; + b = ""hello""; + } + + public void Deconstruct(out int a) + { + a = 1; + } +}"; + + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics( + // (8,18): error CS8207: More than one Deconstruct instance or extension method was found for type 'C'. + // (x, y) = new C(); + Diagnostic(ErrorCode.ERR_AmbiguousDeconstruct, "new C()").WithArguments("C").WithLocation(8, 18) + ); + } + + [Fact] + public void DeconstructWrongParams() + { + string source = @" +class C +{ + static void Main() + { + long x; + string y; + (x, y) = new C(); + } + public void Deconstruct(out int a) + { + a = 1; + } +}"; + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics( + // (8,18): error CS8209: The Deconstruct method for type 'C.Deconstruct(out int)' doesn't have the number of parameters (2) needed for this deconstruction. + // (x, y) = new C(); + Diagnostic(ErrorCode.ERR_DeconstructWrongParams, "new C()").WithArguments("C.Deconstruct(out int)", "2").WithLocation(8, 18) + ); + } + + [Fact] + public void DeconstructWithLeftHandSideErrors() + { + string source = @" +class C +{ + static void Main() + { + long x = 1; + string y = ""hello""; + (x.f, y.g) = new C(); + } + public void Deconstruct() { } +} +"; + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics( + // (8,12): error CS1061: 'long' does not contain a definition for 'f' and no extension method 'f' accepting a first argument of type 'long' could be found (are you missing a using directive or an assembly reference?) + // (x.f, y.g) = new C(); + Diagnostic(ErrorCode.ERR_NoSuchMemberOrExtension, "f").WithArguments("long", "f").WithLocation(8, 12), + // (8,17): error CS1061: 'string' does not contain a definition for 'g' and no extension method 'g' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?) + // (x.f, y.g) = new C(); + Diagnostic(ErrorCode.ERR_NoSuchMemberOrExtension, "g").WithArguments("string", "g").WithLocation(8, 17) + ); + } + + [Fact] + public void DeconstructWithInParam() + { + string source = @" +class C +{ + static void Main() + { + int x; + int y; + (x, y) = new C(); + } + public void Deconstruct(out int x, int y) { x = 1; } +} +"; + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics( + // (8,18): error CS8208: The Deconstruct method for type 'C.Deconstruct(out int, int)' must have only out parameters. + // (x, y) = new C(); + Diagnostic(ErrorCode.ERR_DeconstructRequiresOutParams, "new C()").WithArguments("C.Deconstruct(out int, int)").WithLocation(8, 18) + ); + } + + [Fact] + public void DeconstructWithRefParam() + { + string source = @" +class C +{ + static void Main() + { + int x; + int y; + (x, y) = new C(); + } + public void Deconstruct(ref int x, out int y) { x = 1; y = 2; } +} +"; + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics( + // (8,18): error CS8208: The Deconstruct method for type 'C.Deconstruct(ref int, out int)' must have only out parameters. + // (x, y) = new C(); + Diagnostic(ErrorCode.ERR_DeconstructRequiresOutParams, "new C()").WithArguments("C.Deconstruct(ref int, out int)").WithLocation(8, 18) + ); + } + + [Fact] + public void DeconstructCanHaveReturnType() + { + string source = @" +class C +{ + static void Main() + { + long x; + string y; + + (x, y) = new C(); + System.Console.WriteLine(x + "" "" + y); + } + + public int Deconstruct(out int a, out string b) + { + a = 1; + b = ""hello""; + return 42; + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "1 hello", parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact] + public void DeconstructDataFlow() + { + string source = @" +class C +{ + static void Main() + { + int x, y; + (x, y) = new C(); // x and y are assigned here, so no complaints on usage of un-initialized locals on the line below + System.Console.WriteLine(x + "" "" + y); + } + + public void Deconstruct(out int a, out int b) + { + a = 1; + b = 2; + } +} +"; + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact] + public void VerifyExecutionOrder() + { + string source = @" +using System; +class C +{ + int x { set { Console.WriteLine($""setX""); } } + int y { set { Console.WriteLine($""setY""); } } + + C getHolderForX() { Console.WriteLine(""getHolderforX""); return this; } + C getHolderForY() { Console.WriteLine(""getHolderforY""); return this; } + C getDeconstructReceiver() { Console.WriteLine(""getDeconstructReceiver""); return this; } + + static void Main() + { + C c = new C(); + int z; // PROTOTYPE(tuples) this should be removed once the return-type issue is fixed + (c.getHolderForX().x, c.getHolderForY().y, z) = c.getDeconstructReceiver(); + } + public void Deconstruct(out D1 x, out D2 y, out int z) { x = new D1(); y = new D2(); z = 3; Console.WriteLine(""Deconstruct""); } +} +class D1 +{ + public static implicit operator int(D1 d) { Console.WriteLine(""Conversion1""); return 1; } +} +class D2 +{ + public static implicit operator int(D2 d) { Console.WriteLine(""Conversion2""); return 2; } +} +"; + + string expected = +@"getHolderforX +getHolderforY +getDeconstructReceiver +Deconstruct +Conversion1 +setX +Conversion2 +setY +"; + var comp = CompileAndVerify(source, expectedOutput: expected, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact] + public void DifferentVariableKinds() + { + string source = @" +class C +{ + int[] ArrayIndexer = new int[1]; + + string property; + string Property { set { property = value; } } + + string AutoProperty { get; set; } + + static void Main() + { + C c = new C(); + (c.ArrayIndexer[0], c.Property, c.AutoProperty) = new C(); + System.Console.WriteLine(c.ArrayIndexer[0] + "" "" + c.property + "" "" + c.AutoProperty); + } + + public void Deconstruct(out int a, out string b, out string c) + { + a = 1; + b = ""hello""; + c = ""world""; + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "1 hello world", additionalRefs: new[] { SystemCoreRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact(Skip = "PROTOTYPE(tuples)")] + public void Dynamic() + { + string source = @" +class C +{ + dynamic Dynamic1; + dynamic Dynamic2; + + static void Main() + { + C c = new C(); + (c.Dynamic1, c.Dynamic2) = new C(); + System.Console.WriteLine(c.Dynamic1 + "" "" + c.Dyanmic2); + } + + public void Deconstruct(out int a, out dynamic b) + { + a = 1; + b = ""hello""; + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "1 hello", additionalRefs: new[] { SystemCoreRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact] + public void DifferentStaticVariableKinds() + { + string source = @" +class C +{ + static int[] ArrayIndexer = new int[1]; + + static string property; + static string Property { set { property = value; } } + + static string AutoProperty { get; set; } + + static void Main() + { + (C.ArrayIndexer[0], C.Property, C.AutoProperty) = new C(); + System.Console.WriteLine(C.ArrayIndexer[0] + "" "" + C.property + "" "" + C.AutoProperty); + } + + public void Deconstruct(out int a, out string b, out string c) + { + a = 1; + b = ""hello""; + c = ""world""; + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "1 hello world", additionalRefs: new[] { SystemCoreRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact(Skip = "PROTOTYPE(tuples)")] + public void DifferentVariableRefKinds() + { + string source = @" +class C +{ + static void Main() + { + long a = 1; + int b; + C.M(ref a, out b); + System.Console.WriteLine(a + "" "" + b); + } + + static void M(ref long a, out int b) + { + (a, b) = new C(); + } + + public void Deconstruct(out int x, out byte y) + { + x = 2; + y = (byte)3; + } +} +"; + + var comp = CompileAndVerify(source, expectedOutput: "2 3", additionalRefs: new[] { SystemCoreRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + } + + [Fact] + public void ConversionErrors() + { + string source = @" +class C +{ + static void Main() + { + byte x; + string y; + (x, y) = new C(); + } + + public void Deconstruct(out int a, out int b) + { + a = b = 1; + } +} +"; + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics( + // (8,18): error CS0266: Cannot implicitly convert type 'int' to 'byte'. An explicit conversion exists (are you missing a cast?) + // (x, y) = new C(); + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "new C()").WithArguments("int", "byte").WithLocation(8, 18), + // (8,18): error CS0029: Cannot implicitly convert type 'int' to 'string' + // (x, y) = new C(); + Diagnostic(ErrorCode.ERR_NoImplicitConv, "new C()").WithArguments("int", "string").WithLocation(8, 18) + ); + } + + [Fact(Skip = "PROTOTYPE(tuples)")] + public void Nesting() + { + string source = @" +class C +{ + static void Main() + { + int x, y, z; + (x, (y, z)) = new C(); + } + + public void Deconstruct(out int a, out D d) + { + a = 1; + d = new D(); + } +} +class D +{ + public void Deconstruct(out string b, out string c) + { + b = ""hello""; + c = ""world""; + } +} +"; + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); + // expect a console output + } + + [Fact(Skip = "PROTOTYPE(tuples)")] + public void ExpressionType() + { + string source = @" +class C +{ + static void Main() + { + int x, y; + var type = ((x, y) = new C()).GetType(); + System.Console.WriteLine(type); + } + + public void Deconstruct(out int a, out int b) + { + a = b = 1; + } +} +"; + + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics(); // expect an error + } + + [Fact] + public void LambdaStillNotValidStatement() + { + string source = @" +class C +{ + static void Main() + { + (a) => a; + } +} +"; + + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics( + // (6,9): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement + // (a) => a; + Diagnostic(ErrorCode.ERR_IllegalStatement, "(a) => a").WithLocation(6, 9) + ); + } + + [Fact] + public void LambdaWithBodyStillNotValidStatement() + { + string source = @" +class C +{ + static void Main() + { + (a, b) => { }; + } +} +"; + + var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics( + // (6,9): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement + // (a, b) => { }; + Diagnostic(ErrorCode.ERR_IllegalStatement, "(a, b) => { }").WithLocation(6, 9) + ); + } + + [Fact] + public void CastButNotCast() + { + // int and string must be types, so (int, string) must be type and ((int, string)) a cast, but then .String() cannot follow a cast... + string source = @" +class C +{ + static void Main() + { + ((int, string)).ToString(); + } +} +"; + + var comp = CreateCompilationWithMscorlib(source, references: new[] { ValueTupleRef, SystemRuntimeFacadeRef }, parseOptions: TestOptions.Regular.WithTuplesFeature()); + comp.VerifyDiagnostics( + // (6,24): error CS1525: Invalid expression term '.' + // ((int, string)).ToString(); + Diagnostic(ErrorCode.ERR_InvalidExprTerm, ".").WithArguments(".").WithLocation(6, 24) + ); + } + } +} \ No newline at end of file diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleTest.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleTest.cs index 46ff1a349ea80..4b3c6e822fe1a 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleTest.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleTest.cs @@ -9083,15 +9083,9 @@ void M(int x) var comp = CreateCompilationWithMscorlib(source, parseOptions: TestOptions.Regular.WithTuplesFeature()); comp.VerifyDiagnostics( - // (6,15): error CS1001: Identifier expected - // (x, x); - Diagnostic(ErrorCode.ERR_IdentifierExpected, ";").WithLocation(6, 15), - // (6,10): error CS0118: 'x' is a variable but is used like a type - // (x, x); - Diagnostic(ErrorCode.ERR_BadSKknown, "x").WithArguments("x", "variable", "type").WithLocation(6, 10), - // (6,13): error CS0118: 'x' is a variable but is used like a type + // (6,9): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement // (x, x); - Diagnostic(ErrorCode.ERR_BadSKknown, "x").WithArguments("x", "variable", "type").WithLocation(6, 13) + Diagnostic(ErrorCode.ERR_IllegalStatement, "(x, x)").WithLocation(6, 9) ); } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/SemanticErrorTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/SemanticErrorTests.cs index 29becf6eec55f..b17755460bea2 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/SemanticErrorTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/SemanticErrorTests.cs @@ -7166,41 +7166,24 @@ class A public static int Main() { (a) => a; - (a, b) => - { - }; + (a, b) => { }; int x = 0; int y = 0; x + y; x == 1; } }"; CreateCompilationWithMscorlib(text, parseOptions: TestOptions.Regular.WithTuplesFeature()).VerifyDiagnostics( - // (7,16): error CS1001: Identifier expected - // (a, b) => - Diagnostic(ErrorCode.ERR_IdentifierExpected, "=>").WithLocation(7, 16), - // (7,16): error CS1003: Syntax error, ',' expected - // (a, b) => - Diagnostic(ErrorCode.ERR_SyntaxError, "=>").WithArguments(",", "=>").WithLocation(7, 16), - // (7,18): error CS1002: ; expected - // (a, b) => - Diagnostic(ErrorCode.ERR_SemicolonExpected, "").WithLocation(7, 18), // (6,9): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement // (a) => a; Diagnostic(ErrorCode.ERR_IllegalStatement, "(a) => a").WithLocation(6, 9), - // (7,10): error CS0246: The type or namespace name 'a' could not be found (are you missing a using directive or an assembly reference?) - // (a, b) => - Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "a").WithArguments("a").WithLocation(7, 10), - // (7,13): error CS0246: The type or namespace name 'b' could not be found (are you missing a using directive or an assembly reference?) - // (a, b) => - Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "b").WithArguments("b").WithLocation(7, 13), - // (7,9): error CS0518: Predefined type 'System.ValueTuple`2' is not defined or imported - // (a, b) => - Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "(a, b)").WithArguments("System.ValueTuple`2").WithLocation(7, 9), - // (11,9): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement + // (7,9): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement + // (a, b) => { }; + Diagnostic(ErrorCode.ERR_IllegalStatement, "(a, b) => { }").WithLocation(7, 9), + // (9,9): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement // x + y; x == 1; - Diagnostic(ErrorCode.ERR_IllegalStatement, "x + y").WithLocation(11, 9), - // (11,16): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement + Diagnostic(ErrorCode.ERR_IllegalStatement, "x + y").WithLocation(9, 9), + // (9,16): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement // x + y; x == 1; - Diagnostic(ErrorCode.ERR_IllegalStatement, "x == 1").WithLocation(11, 16), + Diagnostic(ErrorCode.ERR_IllegalStatement, "x == 1").WithLocation(9, 16), // (4,23): error CS0161: 'A.Main()': not all code paths return a value // public static int Main() Diagnostic(ErrorCode.ERR_ReturnExpected, "Main").WithArguments("A.Main()").WithLocation(4, 23) @@ -7216,45 +7199,25 @@ class A public static int Main() { (a) => a; - (a, b) => - { - }; + (a, b) => { }; int x = 0; int y = 0; x + y; x == 1; } }"; var comp = CreateCompilationWithMscorlib(new[] { Parse(test, options: TestOptions.Regular.WithLanguageVersion(LanguageVersion.CSharp6)) }, new MetadataReference[] { }); comp.VerifyDiagnostics( - // (7,9): error CS8058: Feature 'tuples' is experimental and unsupported; use '/features:tuples' to enable. - // (a, b) => - Diagnostic(ErrorCode.ERR_FeatureIsExperimental, "(a, b)").WithArguments("tuples", "tuples").WithLocation(7, 9), - // (7,16): error CS1001: Identifier expected - // (a, b) => - Diagnostic(ErrorCode.ERR_IdentifierExpected, "=>").WithLocation(7, 16), - // (7,16): error CS1003: Syntax error, ',' expected - // (a, b) => - Diagnostic(ErrorCode.ERR_SyntaxError, "=>").WithArguments(",", "=>").WithLocation(7, 16), - // (7,18): error CS1002: ; expected - // (a, b) => - Diagnostic(ErrorCode.ERR_SemicolonExpected, "").WithLocation(7, 18), // (6,9): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement // (a) => a; Diagnostic(ErrorCode.ERR_IllegalStatement, "(a) => a").WithLocation(6, 9), - // (7,10): error CS0246: The type or namespace name 'a' could not be found (are you missing a using directive or an assembly reference?) - // (a, b) => - Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "a").WithArguments("a").WithLocation(7, 10), - // (7,13): error CS0246: The type or namespace name 'b' could not be found (are you missing a using directive or an assembly reference?) - // (a, b) => - Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "b").WithArguments("b").WithLocation(7, 13), - // (7,9): error CS0518: Predefined type 'System.ValueTuple`2' is not defined or imported - // (a, b) => - Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "(a, b)").WithArguments("System.ValueTuple`2").WithLocation(7, 9), - // (11,9): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement + // (7,9): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement + // (a, b) => { }; + Diagnostic(ErrorCode.ERR_IllegalStatement, "(a, b) => { }").WithLocation(7, 9), + // (9,9): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement // x + y; x == 1; - Diagnostic(ErrorCode.ERR_IllegalStatement, "x + y").WithLocation(11, 9), - // (11,16): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement + Diagnostic(ErrorCode.ERR_IllegalStatement, "x + y").WithLocation(9, 9), + // (9,16): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement // x + y; x == 1; - Diagnostic(ErrorCode.ERR_IllegalStatement, "x == 1").WithLocation(11, 16), + Diagnostic(ErrorCode.ERR_IllegalStatement, "x == 1").WithLocation(9, 16), // (4,23): error CS0161: 'A.Main()': not all code paths return a value // public static int Main() Diagnostic(ErrorCode.ERR_ReturnExpected, "Main").WithArguments("A.Main()").WithLocation(4, 23) diff --git a/src/Compilers/CSharp/Test/Syntax/CSharpCompilerSyntaxTest.csproj b/src/Compilers/CSharp/Test/Syntax/CSharpCompilerSyntaxTest.csproj index c2b7d90c1206b..7b13da8fcddcb 100644 --- a/src/Compilers/CSharp/Test/Syntax/CSharpCompilerSyntaxTest.csproj +++ b/src/Compilers/CSharp/Test/Syntax/CSharpCompilerSyntaxTest.csproj @@ -89,6 +89,7 @@ + diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/DeconstructionTest.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/DeconstructionTest.cs new file mode 100644 index 0000000000000..c75aefbc9b641 --- /dev/null +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/DeconstructionTest.cs @@ -0,0 +1,786 @@ +// 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.Syntax; +using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Parsing +{ + // Some examples of parsing subtleties: + // `(T id, ...) = ...;` is a deconstruction-declaration + // `(T id, ...) id;` starts with a tuple type + // `(T, ...) id;` starts with tuple type + // `(T, ...)[] id;` starts with a tuple type array + // `(E, ...) = ...;` is a deconstruction-assignment, which starts with a tuple literal/expression + // `(E, ...).Foo();` starts with a tuple literal/expression + // `(E, ...) + ...` also starts with a tuple literal/expression + // `(T, ...)? id;` starts with a tuple type + + [CompilerTrait(CompilerFeature.Tuples)] + public class DeconstructionTests : ParsingTests + { + protected override SyntaxTree ParseTree(string text, CSharpParseOptions options) + { + return SyntaxFactory.ParseSyntaxTree(text, options: options); + } + + [Fact] + public void ParenExpression() + { + // `(id) .` starts with a parenthesized expression + var tree = UsingTree(@" +class C +{ + void Foo() + { + (x).ToString(); + } +}", options: TestOptions.Regular.WithTuplesFeature()); + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.ClassDeclaration); + { + N(SyntaxKind.ClassKeyword); + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.MethodDeclaration); + { + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.VoidKeyword); + } + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.Block); + { + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.ExpressionStatement); + { + N(SyntaxKind.InvocationExpression); + { + N(SyntaxKind.SimpleMemberAccessExpression); + { + N(SyntaxKind.ParenthesizedExpression); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.DotToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.ArgumentList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.CloseParenToken); + } + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.CloseBraceToken); + } + } + N(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact] + public void TupleTypeWithElementNames() + { + // `(T id, ...) id` starts with a tuple type + + var tree = UsingTree(@" +class C +{ + void Foo() + { + (Int32 a, Int64 b) x; + } +}", options: TestOptions.Regular.WithTuplesFeature()); + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.ClassDeclaration); + { + N(SyntaxKind.ClassKeyword); + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.MethodDeclaration); + { + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.VoidKeyword); + } + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.Block); + { + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.LocalDeclarationStatement); + { + N(SyntaxKind.VariableDeclaration); + { + N(SyntaxKind.TupleType); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.TupleElement); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CommaToken); + N(SyntaxKind.TupleElement); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.VariableDeclarator); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.CloseBraceToken); + } + } + N(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact] + public void TupleType() + { + // `(T, ...) id` starts with a type + + var tree = UsingTree(@" +class C +{ + void Foo() + { + (Int32, Int64) x; + } +}", options: TestOptions.Regular.WithTuplesFeature()); + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.ClassDeclaration); + { + N(SyntaxKind.ClassKeyword); + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.MethodDeclaration); + { + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.VoidKeyword); + } + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.Block); + { + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.LocalDeclarationStatement); + { + N(SyntaxKind.VariableDeclaration); + { + N(SyntaxKind.TupleType); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.TupleElement); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CommaToken); + N(SyntaxKind.TupleElement); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.VariableDeclarator); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.CloseBraceToken); + } + } + N(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact] + public void TupleTypeArray() + { + // (T, ...) [] is a type + + var tree = UsingTree(@" +class C +{ + void Foo() + { + (Int32, Int64)[] x; + } +}", options: TestOptions.Regular.WithTuplesFeature()); + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.ClassDeclaration); + { + N(SyntaxKind.ClassKeyword); + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.MethodDeclaration); + { + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.VoidKeyword); + } + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.Block); + { + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.LocalDeclarationStatement); + { + N(SyntaxKind.VariableDeclaration); + { + N(SyntaxKind.ArrayType); + { + N(SyntaxKind.TupleType); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.TupleElement); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CommaToken); + N(SyntaxKind.TupleElement); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.ArrayRankSpecifier); + { + N(SyntaxKind.OpenBracketToken); + N(SyntaxKind.OmittedArraySizeExpression); + { + N(SyntaxKind.OmittedArraySizeExpressionToken); + } + N(SyntaxKind.CloseBracketToken); + } + } + N(SyntaxKind.VariableDeclarator); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.CloseBraceToken); + } + } + N(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact] + public void TupleLiteral() + { + // (E, ...) followed by ., +, -, etc. starts with a tuple literal/expression + + var tree = UsingTree(@" +class C +{ + void Foo() + { + (Int32, Int64).Foo(); + } +}", options: TestOptions.Regular.WithTuplesFeature()); + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.ClassDeclaration); + { + N(SyntaxKind.ClassKeyword); + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.MethodDeclaration); + { + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.VoidKeyword); + } + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.Block); + { + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.ExpressionStatement); + { + N(SyntaxKind.InvocationExpression); + { + N(SyntaxKind.SimpleMemberAccessExpression); + { + N(SyntaxKind.TupleExpression); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Argument); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CommaToken); + N(SyntaxKind.Argument); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.DotToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.ArgumentList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.CloseParenToken); + } + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.CloseBraceToken); + } + } + N(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact] + public void DeconstructionAssignment() + { + // (E, ...) = is a deconstruction-assignment + var tree = UsingTree(@" +class C +{ + void Foo() + { + (x, y) = foo; + } +}", options: TestOptions.Regular.WithTuplesFeature()); + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.ClassDeclaration); + { + N(SyntaxKind.ClassKeyword); + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.MethodDeclaration); + { + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.VoidKeyword); + } + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.Block); + { + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.ExpressionStatement); + { + N(SyntaxKind.SimpleAssignmentExpression); + { + N(SyntaxKind.TupleExpression); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Argument); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CommaToken); + N(SyntaxKind.Argument); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.EqualsToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.CloseBraceToken); + } + } + N(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact] + public void NestedDeconstructionAssignment() + { + // (E, ...) = is a deconstruction-assignment + var tree = UsingTree(@" +class C +{ + void Foo() + { + (x, (y, z)) = foo; + } +}", options: TestOptions.Regular.WithTuplesFeature()); + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.ClassDeclaration); + { + N(SyntaxKind.ClassKeyword); + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.MethodDeclaration); + { + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.VoidKeyword); + } + N(SyntaxKind.IdentifierToken); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.Block); + { + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.ExpressionStatement); + { + N(SyntaxKind.SimpleAssignmentExpression); + { + N(SyntaxKind.TupleExpression); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Argument); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CommaToken); + N(SyntaxKind.Argument); + { + N(SyntaxKind.TupleExpression); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Argument); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CommaToken); + N(SyntaxKind.Argument); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CloseParenToken); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.EqualsToken); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.CloseBraceToken); + } + } + N(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact(Skip = "PROTOTYPE(tuples)")] + public void DeconstructionDeclaration() + { + // `(T id, ...) = ...` is a deconstruction-declaration + var tree = UsingTree(@" +class C +{ + void Foo() + { + (Int32 a, Int64 b) = foo; + } +}", options: TestOptions.Regular.WithTuplesFeature()); + EOF(); + } + + [Fact] + public void TupleArray() + { + var text = "(T, T)[] id;"; + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.False(statement.HasErrors); + + Assert.Equal(SyntaxKind.LocalDeclarationStatement, statement.Kind()); + var declaration = ((LocalDeclarationStatementSyntax)statement).Declaration; + Assert.Equal("(T, T)[]", declaration.Type.ToString()); + Assert.Equal("id", declaration.Variables.ToString()); + } + + [Fact] + public void ParenthesizedExpression() + { + var text = "(x).ToString();"; + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.False(statement.HasErrors); + + Assert.Equal(SyntaxKind.ExpressionStatement, statement.Kind()); + var expression = ((ExpressionStatementSyntax)statement).Expression; + Assert.Equal(SyntaxKind.InvocationExpression, expression.Kind()); + } + + [Fact] + public void TupleLiteralStatement() + { + var text = "(x, x).ToString();"; + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.False(statement.HasErrors); + + Assert.Equal(SyntaxKind.ExpressionStatement, statement.Kind()); + var expression = ((ExpressionStatementSyntax)statement).Expression; + Assert.Equal(SyntaxKind.InvocationExpression, expression.Kind()); + } + + [Fact] + public void Statement4() + { + var text = "((x)).ToString();"; + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.False(statement.HasErrors); + + Assert.Equal(SyntaxKind.ExpressionStatement, statement.Kind()); + var expression = ((ExpressionStatementSyntax)statement).Expression; + Assert.Equal(SyntaxKind.InvocationExpression, expression.Kind()); + } + + [Fact] + public void Statement5() + { + var text = "((x, y) = M()).ToString();"; + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.False(statement.HasErrors); + + Assert.Equal(SyntaxKind.ExpressionStatement, statement.Kind()); + var expression = ((ExpressionStatementSyntax)statement).Expression; + Assert.Equal(SyntaxKind.InvocationExpression, expression.Kind()); + + var invocation = (InvocationExpressionSyntax)expression; + var lhs = (MemberAccessExpressionSyntax)invocation.Expression; + var lhsContent = (ParenthesizedExpressionSyntax)lhs.Expression; + Assert.Equal(SyntaxKind.SimpleAssignmentExpression, lhsContent.Expression.Kind()); + } + + [Fact] + public void CastWithTupleType() + { + var text = "(((x, y))z).Foo();"; + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.False(statement.HasErrors); + + Assert.Equal(SyntaxKind.ExpressionStatement, statement.Kind()); + var expression = ((ExpressionStatementSyntax)statement).Expression; + Assert.Equal(SyntaxKind.InvocationExpression, expression.Kind()); + + var invocation = (InvocationExpressionSyntax)expression; + var lhs = (MemberAccessExpressionSyntax)invocation.Expression; + var lhsContent = (ParenthesizedExpressionSyntax)lhs.Expression; + Assert.Equal(SyntaxKind.CastExpression, lhsContent.Expression.Kind()); + } + + [Fact] + public void NotACast() + { + var text = "((Int32.MaxValue, Int32.MaxValue)).ToString();"; + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.False(statement.HasErrors); + + var expression = ((ExpressionStatementSyntax)statement).Expression; + var invocation = (InvocationExpressionSyntax)expression; + var lhs = (MemberAccessExpressionSyntax)invocation.Expression; + var paren = (ParenthesizedExpressionSyntax)lhs.Expression; + Assert.Equal(SyntaxKind.TupleExpression, paren.Expression.Kind()); + } + + [Fact] + public void AlsoNotACast() + { + var text = "((x, y)).ToString();"; + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.False(statement.HasErrors); + + var expression = ((ExpressionStatementSyntax)statement).Expression; + var invocation = (InvocationExpressionSyntax)expression; + var lhs = (MemberAccessExpressionSyntax)invocation.Expression; + var paren = (ParenthesizedExpressionSyntax)lhs.Expression; + Assert.Equal(SyntaxKind.TupleExpression, paren.Expression.Kind()); + } + + [Fact] + public void StillNotACast() + { + var text = "((((x, y)))).ToString();"; + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.False(statement.HasErrors); + + var expression = ((ExpressionStatementSyntax)statement).Expression; + var invocation = (InvocationExpressionSyntax)expression; + var lhs = (MemberAccessExpressionSyntax)invocation.Expression; + var paren1 = (ParenthesizedExpressionSyntax)lhs.Expression; + var paren2 = (ParenthesizedExpressionSyntax)paren1.Expression; + var paren3 = (ParenthesizedExpressionSyntax)paren2.Expression; + Assert.Equal(SyntaxKind.TupleExpression, paren3.Expression.Kind()); + } + + [Fact] + public void LambdaInExpressionStatement() + { + var text = "(a) => a;"; // syntax ok + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.False(statement.HasErrors); + + var expression = ((ExpressionStatementSyntax)statement).Expression; + Assert.Equal(SyntaxKind.ParenthesizedLambdaExpression, expression.Kind()); + } + + [Fact] + public void LambdaWithBodyInExpressionStatement() + { + var text = "(a, b) => { };"; // syntax ok + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.False(statement.HasErrors); + + var expression = ((ExpressionStatementSyntax)statement).Expression; + Assert.Equal(SyntaxKind.ParenthesizedLambdaExpression, expression.Kind()); + } + + [Fact] + public void InvalidStatement() + { + var text = "(x, y)? = M();"; // error + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.True(statement.HasErrors); + } + + [Fact] + public void NullableTuple() + { + var text = "(x, y)? z = M();"; + var statement = SyntaxFactory.ParseStatement(text, offset: 0, options: TestOptions.Regular.WithTuplesFeature()); + Assert.False(statement.HasErrors); + var declaration = ((LocalDeclarationStatementSyntax)statement).Declaration; + var nullable = (NullableTypeSyntax)declaration.Type; + Assert.Equal(SyntaxKind.TupleType, nullable.ElementType.Kind()); + } + } +} diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/ParserErrorMessageTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/ParserErrorMessageTests.cs index 56de2dc79e6a6..ca82780eccca6 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/ParserErrorMessageTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/ParserErrorMessageTests.cs @@ -237,72 +237,6 @@ public static int Main() Diagnostic(ErrorCode.ERR_SyntaxError, ";").WithArguments("]", ";")); } - [WorkItem(862031, "DevDiv/Personal")] - [Fact] - public void CS0201ERR_IllegalStatement() - { - var test = @" -class A -{ - public static int Main() - { - (a) => a; - (a, b) => - { - }; - int x = 0; int y = 0; - x + y; x == 1; - } -} -"; - - ParseAndValidate(test, TestOptions.Regular.WithTuplesFeature(), - // (7,16): error CS1001: Identifier expected - // (a, b) => - Diagnostic(ErrorCode.ERR_IdentifierExpected, "=>").WithLocation(7, 16), - // (7,16): error CS1003: Syntax error, ',' expected - // (a, b) => - Diagnostic(ErrorCode.ERR_SyntaxError, "=>").WithArguments(",", "=>").WithLocation(7, 16), - // (7,18): error CS1002: ; expected - // (a, b) => - Diagnostic(ErrorCode.ERR_SemicolonExpected, "").WithLocation(7, 18) -); - } - - [WorkItem(862031, "DevDiv/Personal")] - [Fact] - public void CS0201ERR_IllegalStatementWithCSharp6() - { - var test = @" -class A -{ - public static int Main() - { - (a) => a; - (a, b) => - { - }; - int x = 0; int y = 0; - x + y; x == 1; - } -} -"; - ParseAndValidate(test, TestOptions.Regular.WithLanguageVersion(LanguageVersion.CSharp6), - // (7,9): error CS8058: Feature 'tuples' is experimental and unsupported; use '/features:tuples' to enable. - // (a, b) => - Diagnostic(ErrorCode.ERR_FeatureIsExperimental, "(a, b)").WithArguments("tuples", "tuples").WithLocation(7, 9), - // (7,16): error CS1001: Identifier expected - // (a, b) => - Diagnostic(ErrorCode.ERR_IdentifierExpected, "=>").WithLocation(7, 16), - // (7,16): error CS1003: Syntax error, ',' expected - // (a, b) => - Diagnostic(ErrorCode.ERR_SyntaxError, "=>").WithArguments(",", "=>").WithLocation(7, 16), - // (7,18): error CS1002: ; expected - // (a, b) => - Diagnostic(ErrorCode.ERR_SemicolonExpected, "").WithLocation(7, 18) - ); - } - [Fact] public void CS0230ERR_BadForeachDecl() { diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/ParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/ParsingTests.cs index 18d1325a87cfd..02cd31af7eebf 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/ParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/ParsingTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using Xunit; +using System.Linq; namespace Microsoft.CodeAnalysis.CSharp.UnitTests { diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/ValueTupleTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/ValueTupleTests.cs index 50ef756a9e956..ce9b5afeb25d5 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/ValueTupleTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/ValueTupleTests.cs @@ -97,7 +97,6 @@ class C EOF(); } - [Fact] public void LongTuple() {