From cf5ee99de1d4705b60ee763eef9ba133dc27b937 Mon Sep 17 00:00:00 2001 From: Andrey Shchekin Date: Thu, 28 Apr 2016 22:07:31 +1200 Subject: [PATCH] Initial rough prototype of ??= (coalesce-assignment). --- .../Portable/Binder/Binder_Expressions.cs | 3 + .../Portable/Binder/Binder_Operators.cs | 60 ++++++++++++----- .../CSharp/Portable/BoundTree/BoundNodes.xml | 6 ++ .../Portable/BoundTree/BoundTreeVisitors.cs | 2 + .../CSharp/Portable/BoundTree/Expression.cs | 18 +++++ .../CSharp/Portable/CSharpCodeAnalysis.csproj | 3 +- .../Portable/FlowAnalysis/DataFlowPass.cs | 7 ++ .../FlowAnalysis/PreciseAbstractFlowPass.cs | 31 ++++++--- .../LocalRewriter_ExpressionStatement.cs | 3 + ...writer_NullCoalescingAssignmentOperator.cs | 66 +++++++++++++++++++ .../CSharp/Portable/Parser/LanguageParser.cs | 3 + src/Compilers/CSharp/Portable/Parser/Lexer.cs | 10 ++- .../CSharp/Portable/PublicAPI.Unshipped.txt | 2 + .../CSharp/Portable/Syntax/Syntax.xml | 2 + .../CSharp/Portable/Syntax/SyntaxFacts.cs | 1 + .../CSharp/Portable/Syntax/SyntaxKind.cs | 2 + .../CSharp/Portable/Syntax/SyntaxKindFacts.cs | 6 ++ .../Test/Emit/CodeGen/CodeGenDynamicTests.cs | 31 +++++++++ .../Test/Semantic/Semantics/DynamicTests.cs | 2 +- .../IncrementalParsing/CompoundAssignment.cs | 8 +++ .../Test/Syntax/LexicalAndXml/LexicalTests.cs | 1 + .../Syntax/Parsing/ExpressionParsingTests.cs | 1 + .../Rewriters/MayHaveSideEffectsVisitor.cs | 5 ++ .../Classification/ClassificationHelpers.cs | 1 + .../ContextQuery/SyntaxTreeExtensions.cs | 4 +- .../Extensions/ExpressionSyntaxExtensions.cs | 1 + .../Extensions/SyntaxNodeExtensions.cs | 1 + ...CSharpTypeInferenceService.TypeInferrer.cs | 1 + 28 files changed, 252 insertions(+), 29 deletions(-) create mode 100644 src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_NullCoalescingAssignmentOperator.cs diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs index 6f898b5132e5e..78c745e141db6 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs @@ -526,6 +526,9 @@ private BoundExpression BindExpressionInternal(ExpressionSyntax node, Diagnostic case SyntaxKind.SubtractAssignmentExpression: return BindCompoundAssignment((AssignmentExpressionSyntax)node, diagnostics); + case SyntaxKind.CoalesceAssignmentExpression: + return BindNullCoalescingAssignment((AssignmentExpressionSyntax)node, diagnostics); + case SyntaxKind.AliasQualifiedName: case SyntaxKind.PredefinedType: return this.BindNamespaceOrType(node, diagnostics); diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs index 21d0af0040280..798632714adfa 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs @@ -2438,6 +2438,7 @@ private static BindValueKind GetBinaryAssignmentKind(SyntaxKind kind) case SyntaxKind.OrAssignmentExpression: case SyntaxKind.RightShiftAssignmentExpression: case SyntaxKind.SubtractAssignmentExpression: + case SyntaxKind.CoalesceAssignmentExpression: return BindValueKind.CompoundAssignment; default: return BindValueKind.RValue; @@ -3128,22 +3129,41 @@ internal static ConstantValue GetAsOperatorConstantResult(TypeSymbol operandType return null; } - private BoundExpression GenerateNullCoalescingBadBinaryOpsError(BinaryExpressionSyntax node, BoundExpression leftOperand, BoundExpression rightOperand, Conversion leftConversion, DiagnosticBag diagnostics) + private BoundExpression GenerateNullCoalescingBadBinaryOpsError(bool isAssignment, ExpressionSyntax node, BoundExpression leftOperand, BoundExpression rightOperand, Conversion leftConversion, DiagnosticBag diagnostics) { - Error(diagnostics, ErrorCode.ERR_BadBinaryOps, node, SyntaxFacts.GetText(node.OperatorToken.Kind()), leftOperand.Display, rightOperand.Display); - return new BoundNullCoalescingOperator(node, leftOperand, rightOperand, - leftConversion, CreateErrorType(), hasErrors: true); + var operatorKind = isAssignment ? SyntaxKind.QuestionQuestionEqualsToken : SyntaxKind.QuestionQuestionToken; + Error(diagnostics, ErrorCode.ERR_BadBinaryOps, node, SyntaxFacts.GetText(operatorKind), leftOperand.Display, rightOperand.Display); + if (isAssignment) + { + return new BoundNullCoalescingAssignmentOperator(node, leftOperand, rightOperand, leftConversion, CreateErrorType(), hasErrors: true); + } + else + { + return new BoundNullCoalescingOperator(node, leftOperand, rightOperand, leftConversion, CreateErrorType(), hasErrors: true); + } } - private BoundExpression BindNullCoalescingOperator(BinaryExpressionSyntax node, DiagnosticBag diagnostics) + private BoundExpression GenerateNullCoalescingAssignmentOrOperator(bool isAssignment, ExpressionSyntax node, BoundExpression leftOperand, BoundExpression rightOperand, Conversion leftConversion, TypeSymbol type, bool hasErrors = false) { - var leftOperand = BindValue(node.Left, diagnostics, BindValueKind.RValue); - var rightOperand = BindValue(node.Right, diagnostics, BindValueKind.RValue); + if (isAssignment) + { + return new BoundNullCoalescingAssignmentOperator(node, leftOperand, rightOperand, leftConversion, type, hasErrors); + } + else + { + return new BoundNullCoalescingOperator(node, leftOperand, rightOperand, Conversion.NoConversion, type, hasErrors); + } + } + + private BoundExpression BindNullCoalescingOperatorOrAssignment(bool isAssignment, ExpressionSyntax node, ExpressionSyntax left, ExpressionSyntax right, DiagnosticBag diagnostics) + { + var leftOperand = BindValue(left, diagnostics, isAssignment ? BindValueKind.CompoundAssignment : BindValueKind.RValue); + var rightOperand = BindValue(right, diagnostics, BindValueKind.RValue); // If either operand is bad, bail out preventing more cascading errors if (leftOperand.HasAnyErrors || rightOperand.HasAnyErrors) { - return new BoundNullCoalescingOperator(node, leftOperand, rightOperand, + return GenerateNullCoalescingAssignmentOrOperator(isAssignment, node, leftOperand, rightOperand, Conversion.NoConversion, CreateErrorType(), hasErrors: true); } @@ -3172,14 +3192,14 @@ private BoundExpression BindNullCoalescingOperator(BinaryExpressionSyntax node, if (leftOperand.Kind == BoundKind.UnboundLambda || leftOperand.Kind == BoundKind.MethodGroup) { - return GenerateNullCoalescingBadBinaryOpsError(node, leftOperand, rightOperand, Conversion.NoConversion, diagnostics); + return GenerateNullCoalescingBadBinaryOpsError(isAssignment, node, leftOperand, rightOperand, Conversion.NoConversion, diagnostics); } // SPEC: Otherwise, if A exists and is not a nullable type or a reference type, a compile-time error occurs. if ((object)optLeftType != null && !optLeftType.IsReferenceType && !isLeftNullable) { - return GenerateNullCoalescingBadBinaryOpsError(node, leftOperand, rightOperand, Conversion.NoConversion, diagnostics); + return GenerateNullCoalescingBadBinaryOpsError(isAssignment, node, leftOperand, rightOperand, Conversion.NoConversion, diagnostics); } // SPEC: If b is a dynamic expression, the result is dynamic. At runtime, a is first @@ -3193,7 +3213,7 @@ private BoundExpression BindNullCoalescingOperator(BinaryExpressionSyntax node, { var leftConversion = Conversions.ClassifyConversionFromExpression(leftOperand, GetSpecialType(SpecialType.System_Object, diagnostics, node), ref useSiteDiagnostics); diagnostics.Add(node, useSiteDiagnostics); - return new BoundNullCoalescingOperator(node, leftOperand, rightOperand, + return GenerateNullCoalescingAssignmentOrOperator(isAssignment, node, leftOperand, rightOperand, leftConversion, optRightType); } @@ -3210,7 +3230,7 @@ private BoundExpression BindNullCoalescingOperator(BinaryExpressionSyntax node, var leftConversion = Conversions.ClassifyConversionFromExpression(leftOperand, optLeftType0, ref useSiteDiagnostics); diagnostics.Add(node, useSiteDiagnostics); var convertedRightOperand = CreateConversion(rightOperand, rightConversion, optLeftType0, diagnostics); - return new BoundNullCoalescingOperator(node, leftOperand, convertedRightOperand, + return GenerateNullCoalescingAssignmentOrOperator(isAssignment, node, leftOperand, convertedRightOperand, leftConversion, optLeftType0); } } @@ -3227,7 +3247,7 @@ private BoundExpression BindNullCoalescingOperator(BinaryExpressionSyntax node, var convertedRightOperand = CreateConversion(rightOperand, rightConversion, optLeftType, diagnostics); var leftConversion = Conversion.Identity; diagnostics.Add(node, useSiteDiagnostics); - return new BoundNullCoalescingOperator(node, leftOperand, convertedRightOperand, + return GenerateNullCoalescingAssignmentOrOperator(isAssignment, node, leftOperand, convertedRightOperand, leftConversion, optLeftType); } } @@ -3298,13 +3318,23 @@ private BoundExpression BindNullCoalescingOperator(BinaryExpressionSyntax node, } diagnostics.Add(node, useSiteDiagnostics); - return new BoundNullCoalescingOperator(node, leftOperand, rightOperand, leftConversion, optRightType); + return GenerateNullCoalescingAssignmentOrOperator(isAssignment, node, leftOperand, rightOperand, leftConversion, optRightType); } } // SPEC: Otherwise, a and b are incompatible, and a compile-time error occurs. diagnostics.Add(node, useSiteDiagnostics); - return GenerateNullCoalescingBadBinaryOpsError(node, leftOperand, rightOperand, Conversion.NoConversion, diagnostics); + return GenerateNullCoalescingBadBinaryOpsError(isAssignment, node, leftOperand, rightOperand, Conversion.NoConversion, diagnostics); + } + + private BoundExpression BindNullCoalescingOperator(BinaryExpressionSyntax node, DiagnosticBag diagnostics) + { + return BindNullCoalescingOperatorOrAssignment(false, node, node.Left, node.Right, diagnostics); + } + + private BoundExpression BindNullCoalescingAssignment(AssignmentExpressionSyntax node, DiagnosticBag diagnostics) + { + return BindNullCoalescingOperatorOrAssignment(true, node, node.Left, node.Right, diagnostics); } /// diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml index 220db9916ea87..d9d1862563aaf 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml @@ -382,6 +382,12 @@ + + + + + + diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundTreeVisitors.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundTreeVisitors.cs index f84414e9fb59e..7bccfd167953e 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundTreeVisitors.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundTreeVisitors.cs @@ -42,6 +42,8 @@ public virtual R Visit(BoundNode node, A arg) return VisitAssignmentOperator(node as BoundAssignmentOperator, arg); case BoundKind.NullCoalescingOperator: return VisitNullCoalescingOperator(node as BoundNullCoalescingOperator, arg); + case BoundKind.NullCoalescingAssignmentOperator: + return VisitNullCoalescingAssignmentOperator(node as BoundNullCoalescingAssignmentOperator, arg); case BoundKind.ConditionalOperator: return VisitConditionalOperator(node as BoundConditionalOperator, arg); case BoundKind.ArrayAccess: diff --git a/src/Compilers/CSharp/Portable/BoundTree/Expression.cs b/src/Compilers/CSharp/Portable/BoundTree/Expression.cs index 088efa06ff189..62108a5e23a24 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/Expression.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/Expression.cs @@ -1180,6 +1180,24 @@ public override TResult Accept(OperationVisitor(OperationVisitor visitor, TArgument argument) { + throw new NotImplementedException(); + } + } + internal partial class BoundAwaitExpression : IAwaitExpression { IOperation IAwaitExpression.AwaitedValue => this.Expression; diff --git a/src/Compilers/CSharp/Portable/CSharpCodeAnalysis.csproj b/src/Compilers/CSharp/Portable/CSharpCodeAnalysis.csproj index 70c214b560c20..f6fe97c21de9d 100644 --- a/src/Compilers/CSharp/Portable/CSharpCodeAnalysis.csproj +++ b/src/Compilers/CSharp/Portable/CSharpCodeAnalysis.csproj @@ -396,6 +396,7 @@ + @@ -889,4 +890,4 @@ - + \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/DataFlowPass.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/DataFlowPass.cs index 92896e8788564..07a66cd965efb 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/DataFlowPass.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/DataFlowPass.cs @@ -1565,6 +1565,13 @@ public override BoundNode VisitCompoundAssignmentOperator(BoundCompoundAssignmen return null; } + public override BoundNode VisitNullCoalescingAssignmentOperator(BoundNullCoalescingAssignmentOperator node) + { + base.VisitNullCoalescingAssignmentOperator(node); + Assign(node.Left, node.Right); + return null; + } + public override BoundNode VisitAddressOfOperator(BoundAddressOfOperator node) { BoundExpression operand = node.Operand; diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/PreciseAbstractFlowPass.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/PreciseAbstractFlowPass.cs index 7b20505b79458..2f355fea70936 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/PreciseAbstractFlowPass.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/PreciseAbstractFlowPass.cs @@ -1483,27 +1483,38 @@ public override BoundNode VisitAssignmentOperator(BoundAssignmentOperator node) } public override BoundNode VisitCompoundAssignmentOperator(BoundCompoundAssignmentOperator node) + { + return VisitCompositeAssignmentInternal(node, node.Left, node.Right); + } + + public override BoundNode VisitNullCoalescingAssignmentOperator(BoundNullCoalescingAssignmentOperator node) + { + // TODO (coalesce-assignment): should we mark whole thing as unreachable/noop if the right part is constant null? + return VisitCompositeAssignmentInternal(node, node.Left, node.Right); + } + + private BoundNode VisitCompositeAssignmentInternal(BoundExpression node, BoundExpression left, BoundExpression right) { // TODO: should events be handled specially too? - if (RegularPropertyAccess(node.Left)) + if (RegularPropertyAccess(left)) { - var left = (BoundPropertyAccess)node.Left; - var property = left.PropertySymbol; + var leftPropertyAccess = (BoundPropertyAccess)left; + var property = leftPropertyAccess.PropertySymbol; var readMethod = property.GetOwnOrInheritedGetMethod() ?? property.SetMethod; var writeMethod = property.GetOwnOrInheritedSetMethod() ?? property.GetMethod; Debug.Assert(node.HasAnyErrors || (object)readMethod != (object)writeMethod); - VisitReceiverBeforeCall(left.ReceiverOpt, readMethod); + VisitReceiverBeforeCall(leftPropertyAccess.ReceiverOpt, readMethod); if (_trackExceptions) NotePossibleException(node); - VisitReceiverAfterCall(left.ReceiverOpt, readMethod); - VisitRvalue(node.Right); - PropertySetter(node, left.ReceiverOpt, writeMethod); + VisitReceiverAfterCall(leftPropertyAccess.ReceiverOpt, readMethod); + VisitRvalue(right); + PropertySetter(node, leftPropertyAccess.ReceiverOpt, writeMethod); if (_trackExceptions) NotePossibleException(node); - VisitReceiverAfterCall(left.ReceiverOpt, writeMethod); + VisitReceiverAfterCall(leftPropertyAccess.ReceiverOpt, writeMethod); } else { - VisitRvalue(node.Left); - VisitRvalue(node.Right); + VisitRvalue(left); + VisitRvalue(right); if (_trackExceptions && node.HasExpressionSymbols()) NotePossibleException(node); } diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ExpressionStatement.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ExpressionStatement.cs index 8c9f83575a457..6c592a3c04645 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ExpressionStatement.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ExpressionStatement.cs @@ -42,6 +42,9 @@ private BoundExpression VisitUnusedExpression(BoundExpression expression) case BoundKind.CompoundAssignmentOperator: return VisitCompoundAssignmentOperator((BoundCompoundAssignmentOperator)expression, used: false); + case BoundKind.NullCoalescingAssignmentOperator: + return VisitNullCoalescingAssignmentOperator((BoundNullCoalescingAssignmentOperator)expression, used: false); + case BoundKind.Call: if (_allowOmissionOfConditionalCalls) { diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_NullCoalescingAssignmentOperator.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_NullCoalescingAssignmentOperator.cs new file mode 100644 index 0000000000000..646e0f2b55dca --- /dev/null +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_NullCoalescingAssignmentOperator.cs @@ -0,0 +1,66 @@ +// 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.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Symbols; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp +{ + internal sealed partial class LocalRewriter + { + public override BoundNode VisitNullCoalescingAssignmentOperator(BoundNullCoalescingAssignmentOperator node) + { + return VisitNullCoalescingAssignmentOperator(node, true); + } + + private BoundExpression VisitNullCoalescingAssignmentOperator(BoundNullCoalescingAssignmentOperator node, bool used) + { + BoundExpression rewrittenRight = VisitExpression(node.Right); + + var temps = ArrayBuilder.GetInstance(); + var stores = ArrayBuilder.GetInstance(); + + // This will be filled in with the LHS that uses temporaries to prevent + // double-evaluation of side effects. + var transformedLeft = TransformCompoundAssignmentLHS(node.Left, stores, temps, false); + + CSharpSyntaxNode syntax = node.Syntax; + + // OK now we have all the temporaries. + // What we want to generate is: + // + // xlhs = xlhs ?? xrhs; + // + // TODO (coalesce-assignment): this is of course a bit lazy, what we might want is + // (xlhs != null ? xlhs : (xlhs = MakeConversion(xhrs))); + + var readLeft = MakeRValue(transformedLeft); + var coalesce = MakeNullCoalescingOperator(syntax, readLeft, rewrittenRight, node.LeftConversion, node.Type); + var assignment = MakeAssignmentOperator(syntax, transformedLeft, coalesce, node.Left.Type, used: used, isChecked: false, isCompoundAssignment: true); + + // OK, at this point we have: + // + // * temps evaluating and storing portions of the LHS that must be evaluated only once. + // * the "transformed" left hand side, rebuilt to use temps where necessary + // * the assignment "xlhs = ((LEFT)xlhs ?? rhs)" + // + // Notice that we have recursively rewritten the bound nodes that are things stored in + // the temps, and by calling the "Make" methods we have rewritten the conversions and + // assignments too, if necessary. + + BoundExpression result = (temps.Count == 0 && stores.Count == 0) ? + assignment : + new BoundSequence( + syntax, + temps.ToImmutable(), + stores.ToImmutable(), + assignment, + assignment.Type); + + temps.Free(); + stores.Free(); + return result; + } + } +} diff --git a/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs b/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs index 633f146da3bef..76df827c1adf4 100644 --- a/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs +++ b/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs @@ -8172,6 +8172,7 @@ internal static bool IsRightAssociative(SyntaxKind op) case SyntaxKind.LeftShiftAssignmentExpression: case SyntaxKind.RightShiftAssignmentExpression: case SyntaxKind.CoalesceExpression: + case SyntaxKind.CoalesceAssignmentExpression: return true; default: return false; @@ -8193,6 +8194,7 @@ private static uint GetPrecedence(SyntaxKind op) case SyntaxKind.OrAssignmentExpression: case SyntaxKind.LeftShiftAssignmentExpression: case SyntaxKind.RightShiftAssignmentExpression: + case SyntaxKind.CoalesceAssignmentExpression: return 1; case SyntaxKind.CoalesceExpression: return 2; @@ -9272,6 +9274,7 @@ private static bool CanFollowCast(SyntaxKind kind) case SyntaxKind.BarEqualsToken: case SyntaxKind.LessThanLessThanEqualsToken: case SyntaxKind.GreaterThanGreaterThanEqualsToken: + case SyntaxKind.QuestionQuestionEqualsToken: case SyntaxKind.QuestionToken: case SyntaxKind.ColonToken: case SyntaxKind.BarBarToken: diff --git a/src/Compilers/CSharp/Portable/Parser/Lexer.cs b/src/Compilers/CSharp/Portable/Parser/Lexer.cs index 1589bcd511184..78b710ce2a868 100644 --- a/src/Compilers/CSharp/Portable/Parser/Lexer.cs +++ b/src/Compilers/CSharp/Portable/Parser/Lexer.cs @@ -556,7 +556,15 @@ private void ScanSyntaxToken(ref TokenInfo info) if (TextWindow.PeekChar() == '?') { TextWindow.AdvanceChar(); - info.Kind = SyntaxKind.QuestionQuestionToken; + if (TextWindow.PeekChar() == '=') + { + TextWindow.AdvanceChar(); + info.Kind = SyntaxKind.QuestionQuestionEqualsToken; + } + else + { + info.Kind = SyntaxKind.QuestionQuestionToken; + } } else { diff --git a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt index e69de29bb2d1d..efa155e54e48a 100644 --- a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.CodeAnalysis.CSharp.SyntaxKind.CoalesceAssignmentExpression = 8725 -> Microsoft.CodeAnalysis.CSharp.SyntaxKind +Microsoft.CodeAnalysis.CSharp.SyntaxKind.QuestionQuestionEqualsToken = 8284 -> Microsoft.CodeAnalysis.CSharp.SyntaxKind \ No newline at end of file diff --git a/src/Compilers/CSharp/Portable/Syntax/Syntax.xml b/src/Compilers/CSharp/Portable/Syntax/Syntax.xml index e9b3412b92fc4..fc8bbeb6f737a 100644 --- a/src/Compilers/CSharp/Portable/Syntax/Syntax.xml +++ b/src/Compilers/CSharp/Portable/Syntax/Syntax.xml @@ -539,6 +539,7 @@ + ExpressionSyntax node representing the expression on the left of the assignment operator. @@ -556,6 +557,7 @@ + SyntaxToken representing the operator of the assignment expression. diff --git a/src/Compilers/CSharp/Portable/Syntax/SyntaxFacts.cs b/src/Compilers/CSharp/Portable/Syntax/SyntaxFacts.cs index 4083b48d8c937..c385cb232dd0e 100644 --- a/src/Compilers/CSharp/Portable/Syntax/SyntaxFacts.cs +++ b/src/Compilers/CSharp/Portable/Syntax/SyntaxFacts.cs @@ -338,6 +338,7 @@ internal static bool IsStatementExpression(CSharpSyntaxNode syntax) case ExclusiveOrAssignmentExpression: case LeftShiftAssignmentExpression: case RightShiftAssignmentExpression: + case CoalesceAssignmentExpression: case PostIncrementExpression: case PostDecrementExpression: case PreIncrementExpression: diff --git a/src/Compilers/CSharp/Portable/Syntax/SyntaxKind.cs b/src/Compilers/CSharp/Portable/Syntax/SyntaxKind.cs index 40467894de48d..766d73a803ca0 100644 --- a/src/Compilers/CSharp/Portable/Syntax/SyntaxKind.cs +++ b/src/Compilers/CSharp/Portable/Syntax/SyntaxKind.cs @@ -74,6 +74,7 @@ public enum SyntaxKind : ushort MinusEqualsToken = 8281, CaretEqualsToken = 8282, PercentEqualsToken = 8283, + QuestionQuestionEqualsToken = 8284, // Keywords BoolKeyword = 8304, @@ -371,6 +372,7 @@ public enum SyntaxKind : ushort OrAssignmentExpression = 8722, LeftShiftAssignmentExpression = 8723, RightShiftAssignmentExpression = 8724, + CoalesceAssignmentExpression = 8725, // unary expressions UnaryPlusExpression = 8730, diff --git a/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs b/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs index e86f713efbeae..18b611891356a 100644 --- a/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs +++ b/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs @@ -620,6 +620,7 @@ public static bool IsAssignmentExpression(SyntaxKind kind) case SyntaxKind.MultiplyAssignmentExpression: case SyntaxKind.DivideAssignmentExpression: case SyntaxKind.ModuloAssignmentExpression: + case SyntaxKind.CoalesceAssignmentExpression: case SyntaxKind.SimpleAssignmentExpression: return true; default: @@ -641,6 +642,7 @@ public static bool IsAssignmentExpressionOperatorToken(SyntaxKind token) case SyntaxKind.AsteriskEqualsToken: case SyntaxKind.SlashEqualsToken: case SyntaxKind.PercentEqualsToken: + case SyntaxKind.QuestionQuestionEqualsToken: case SyntaxKind.EqualsToken: return true; default: @@ -672,6 +674,8 @@ public static SyntaxKind GetAssignmentExpression(SyntaxKind token) return SyntaxKind.DivideAssignmentExpression; case SyntaxKind.PercentEqualsToken: return SyntaxKind.ModuloAssignmentExpression; + case SyntaxKind.QuestionQuestionEqualsToken: + return SyntaxKind.CoalesceAssignmentExpression; case SyntaxKind.EqualsToken: return SyntaxKind.SimpleAssignmentExpression; default: @@ -1308,6 +1312,8 @@ public static string GetText(SyntaxKind kind) return "^="; case SyntaxKind.PercentEqualsToken: return "%="; + case SyntaxKind.QuestionQuestionEqualsToken: + return "??="; // Keywords case SyntaxKind.BoolKeyword: diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDynamicTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDynamicTests.cs index 0e214d338f63e..a7a8754cc55a8 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDynamicTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenDynamicTests.cs @@ -6021,6 +6021,37 @@ .maxstack 1 "); } + [Fact] + public void NullCoalescingAssignment() { + string source = @" +class C +{ + dynamic d = null; + object o = null; + + void M() + { + d ??= o; + } +}"; + CompileAndVerifyIL(source, "C.M", @" +{ + // Code size 23 (0x17) + .maxstack 3 + IL_0000: ldarg.0 + IL_0001: ldarg.0 + IL_0002: ldfld ""dynamic C.d"" + IL_0007: dup + IL_0008: brtrue.s IL_0011 + IL_000a: pop + IL_000b: ldarg.0 + IL_000c: ldfld ""object C.o"" + IL_0011: stfld ""dynamic C.d"" + IL_0016: ret +} +"); + } + #endregion #region Invoke, InvokeMember, InvokeConstructor diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/DynamicTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/DynamicTests.cs index bc43a0e747a65..f8229981e8579 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/DynamicTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/DynamicTests.cs @@ -647,7 +647,7 @@ static async void M() public void TestDynamicSimpleBinaryOps() { // Test binary * / % + - << >> < > <= >= != == ^ & | but not - // && || = *= /= %= += -= <<= >>= &= |= ^= ?? + // && || = *= /= %= += -= <<= >>= &= |= ^= ?? ??= string source = @" class C { diff --git a/src/Compilers/CSharp/Test/Syntax/IncrementalParsing/CompoundAssignment.cs b/src/Compilers/CSharp/Test/Syntax/IncrementalParsing/CompoundAssignment.cs index bacef33a7c93e..74777c31b9356 100644 --- a/src/Compilers/CSharp/Test/Syntax/IncrementalParsing/CompoundAssignment.cs +++ b/src/Compilers/CSharp/Test/Syntax/IncrementalParsing/CompoundAssignment.cs @@ -72,6 +72,12 @@ public void AssignToOr() MakeAssignmentChange(SyntaxKind.SimpleAssignmentExpression, SyntaxKind.OrAssignmentExpression); } + [Fact] + public void AssignToCoalesce() + { + MakeAssignmentChange(SyntaxKind.SimpleAssignmentExpression, SyntaxKind.CoalesceAssignmentExpression); + } + #region Helper Methods private static void MakeAssignmentChange(SyntaxKind oldStyle, SyntaxKind newStyle) { @@ -124,6 +130,8 @@ private static string GetExpressionString(SyntaxKind oldStyle) return "<<="; case SyntaxKind.RightShiftAssignmentExpression: return ">>="; + case SyntaxKind.CoalesceAssignmentExpression: + return "??="; default: throw new Exception("No operator found"); } diff --git a/src/Compilers/CSharp/Test/Syntax/LexicalAndXml/LexicalTests.cs b/src/Compilers/CSharp/Test/Syntax/LexicalAndXml/LexicalTests.cs index 62aaf7dada9d4..9675a832bd0f7 100644 --- a/src/Compilers/CSharp/Test/Syntax/LexicalAndXml/LexicalTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/LexicalAndXml/LexicalTests.cs @@ -592,6 +592,7 @@ public void TestAllLanguagePunctuation() TestPunctuation(SyntaxKind.MinusEqualsToken); TestPunctuation(SyntaxKind.CaretEqualsToken); TestPunctuation(SyntaxKind.PercentEqualsToken); + TestPunctuation(SyntaxKind.QuestionQuestionEqualsToken); } private void TestPunctuation(SyntaxKind kind) diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/ExpressionParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/ExpressionParsingTests.cs index a1f2b642809c9..f01e9f100d3b5 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/ExpressionParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/ExpressionParsingTests.cs @@ -313,6 +313,7 @@ public void TestAssignmentOperators() TestAssignment(SyntaxKind.AmpersandEqualsToken); TestAssignment(SyntaxKind.BarEqualsToken); TestAssignment(SyntaxKind.CaretEqualsToken); + TestAssignment(SyntaxKind.QuestionQuestionEqualsToken); } private void TestMemberAccess(SyntaxKind kind) diff --git a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Rewriters/MayHaveSideEffectsVisitor.cs b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Rewriters/MayHaveSideEffectsVisitor.cs index 9d82aa4874d42..ff16362b5b10b 100644 --- a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Rewriters/MayHaveSideEffectsVisitor.cs +++ b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Rewriters/MayHaveSideEffectsVisitor.cs @@ -45,6 +45,11 @@ public override BoundNode VisitCompoundAssignmentOperator(BoundCompoundAssignmen return this.SetMayHaveSideEffects(); } + public override BoundNode VisitNullCoalescingAssignmentOperator(BoundNullCoalescingAssignmentOperator node) + { + return this.SetMayHaveSideEffects(); + } + public override BoundNode VisitEventAssignmentOperator(BoundEventAssignmentOperator node) { return this.SetMayHaveSideEffects(); diff --git a/src/Workspaces/CSharp/Portable/Classification/ClassificationHelpers.cs b/src/Workspaces/CSharp/Portable/Classification/ClassificationHelpers.cs index 1e9fa6dd28f97..9eb3aa5092443 100644 --- a/src/Workspaces/CSharp/Portable/Classification/ClassificationHelpers.cs +++ b/src/Workspaces/CSharp/Portable/Classification/ClassificationHelpers.cs @@ -229,6 +229,7 @@ private static bool IsOperator(this SyntaxKind kind) case SyntaxKind.MinusEqualsToken: case SyntaxKind.CaretEqualsToken: case SyntaxKind.PercentEqualsToken: + case SyntaxKind.QuestionQuestionEqualsToken: return true; default: diff --git a/src/Workspaces/CSharp/Portable/Extensions/ContextQuery/SyntaxTreeExtensions.cs b/src/Workspaces/CSharp/Portable/Extensions/ContextQuery/SyntaxTreeExtensions.cs index ac44b1d529601..ad6272833e97d 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/ContextQuery/SyntaxTreeExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/ContextQuery/SyntaxTreeExtensions.cs @@ -1664,6 +1664,7 @@ public static bool IsExpressionContext( // q |= | // q <<= | // q >>= | + // q ??= | if (token.IsKind(SyntaxKind.EqualsToken) || token.IsKind(SyntaxKind.MinusEqualsToken) || token.IsKind(SyntaxKind.AsteriskEqualsToken) || @@ -1675,7 +1676,8 @@ public static bool IsExpressionContext( token.IsKind(SyntaxKind.BarEqualsToken) || token.IsKind(SyntaxKind.PercentEqualsToken) || token.IsKind(SyntaxKind.LessThanLessThanEqualsToken) || - token.IsKind(SyntaxKind.GreaterThanGreaterThanEqualsToken)) + token.IsKind(SyntaxKind.GreaterThanGreaterThanEqualsToken) || + token.IsKind(SyntaxKind.QuestionQuestionEqualsToken)) { return true; } diff --git a/src/Workspaces/CSharp/Portable/Extensions/ExpressionSyntaxExtensions.cs b/src/Workspaces/CSharp/Portable/Extensions/ExpressionSyntaxExtensions.cs index 0ff6ea0f57748..36dbdc8299c6d 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/ExpressionSyntaxExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/ExpressionSyntaxExtensions.cs @@ -2445,6 +2445,7 @@ public static OperatorPrecedence GetOperatorPrecedence(this ExpressionSyntax exp case SyntaxKind.AndAssignmentExpression: case SyntaxKind.ExclusiveOrAssignmentExpression: case SyntaxKind.OrAssignmentExpression: + case SyntaxKind.CoalesceAssignmentExpression: case SyntaxKind.SimpleLambdaExpression: case SyntaxKind.ParenthesizedLambdaExpression: // From C# spec, 7.3.1: diff --git a/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.cs b/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.cs index 49eaddbd25e3f..4d8175db1122b 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/SyntaxNodeExtensions.cs @@ -704,6 +704,7 @@ public static bool IsCompoundAssignExpression(this SyntaxNode node) case SyntaxKind.OrAssignmentExpression: case SyntaxKind.LeftShiftAssignmentExpression: case SyntaxKind.RightShiftAssignmentExpression: + case SyntaxKind.CoalesceAssignmentExpression: return true; } diff --git a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpTypeInferenceService.TypeInferrer.cs b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpTypeInferenceService.TypeInferrer.cs index b0a1bd163ce9b..f1c6ba7b80376 100644 --- a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpTypeInferenceService.TypeInferrer.cs +++ b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpTypeInferenceService.TypeInferrer.cs @@ -823,6 +823,7 @@ private IEnumerable InferTypeInBinaryOrAssignmentExpression(Express // operator's token. Contract.ThrowIfTrue(previousToken.HasValue && previousToken.Value != operatorToken); + // TODO?: (coalesce-assignment) if (binop.Kind() == SyntaxKind.CoalesceExpression) { return InferTypeInCoalesceExpression((BinaryExpressionSyntax)binop, expressionOpt, previousToken);