From b071cda7f8b3fc8da2ead93c851c64a1a20d35a6 Mon Sep 17 00:00:00 2001 From: Fred Silberberg Date: Wed, 22 Sep 2021 13:48:32 -0700 Subject: [PATCH] Add support for all parenthesized binary additions of interpolated strings (#56333) Implements the decision from https://github.com/dotnet/csharplang/issues/5106. Test plan at https://github.com/dotnet/roslyn/issues/51499. --- .../Binder/Binder_InterpolatedString.cs | 153 ++--- .../Portable/Binder/Binder_Operators.cs | 47 +- .../Portable/BoundTree/BoundNodeExtensions.cs | 151 +++++ .../Portable/FlowAnalysis/AbstractFlowPass.cs | 37 +- .../Portable/FlowAnalysis/NullableWalker.cs | 4 +- .../LocalRewriter_BinaryOperator.cs | 32 +- .../Operations/CSharpOperationFactory.cs | 99 ++-- ...ationTests_IInterpolatedStringOperation.cs | 93 ++- .../Semantic/Semantics/InterpolationTests.cs | 547 +++++++++++++++++- 9 files changed, 941 insertions(+), 222 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_InterpolatedString.cs b/src/Compilers/CSharp/Portable/Binder/Binder_InterpolatedString.cs index 9379078f7928f..634cd6a1ddc66 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_InterpolatedString.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_InterpolatedString.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -187,7 +188,7 @@ bool tryBindAsHandlerType([NotNullWhen(true)] out BoundInterpolatedString? resul { result = null; - if (InExpressionTree || !ValidateInterpolatedStringParts(unconvertedInterpolatedString)) + if (InExpressionTree || !InterpolatedStringPartsAreValidInDefaultHandler(unconvertedInterpolatedString)) { return false; } @@ -204,7 +205,7 @@ bool tryBindAsHandlerType([NotNullWhen(true)] out BoundInterpolatedString? resul } } - private static bool ValidateInterpolatedStringParts(BoundUnconvertedInterpolatedString unconvertedInterpolatedString) + private static bool InterpolatedStringPartsAreValidInDefaultHandler(BoundUnconvertedInterpolatedString unconvertedInterpolatedString) => !unconvertedInterpolatedString.Parts.ContainsAwaitExpression() && unconvertedInterpolatedString.Parts.All(p => p is not BoundStringInsert { Value.Type.TypeKind: TypeKind.Dynamic }); @@ -230,71 +231,41 @@ private bool TryBindUnconvertedBinaryOperatorToDefaultInterpolatedStringHandler( return false; } - bool isConstant = true; - var stack = ArrayBuilder.GetInstance(); - var partsArrayBuilder = ArrayBuilder>.GetInstance(); - int partsCount = 0; - - BoundBinaryOperator? current = binaryOperator; - - while (current != null) + // The constant value is folded as part of creating the unconverted operator. If there is a constant value, then the top-level binary operator + // will have one. + if (binaryOperator.ConstantValue is not null) { - Debug.Assert(current.IsUnconvertedInterpolatedStringAddition); - stack.Push(current); - isConstant = isConstant && current.Right.ConstantValue is not null; - var rightInterpolatedString = (BoundUnconvertedInterpolatedString)current.Right; - - if (!ValidateInterpolatedStringParts(rightInterpolatedString)) - { - // Exception to case 3. Delegate to standard binding. - stack.Free(); - partsArrayBuilder.Free(); - return false; - } - - partsCount += rightInterpolatedString.Parts.Length; - partsArrayBuilder.Add(rightInterpolatedString.Parts); - - switch (current.Left) - { - case BoundBinaryOperator leftOperator: - current = leftOperator; - continue; - case BoundUnconvertedInterpolatedString interpolatedString: - isConstant = isConstant && interpolatedString.ConstantValue is not null; + // This is case 1. Let the standard machinery handle it + return false; + } + var partsArrayBuilder = ArrayBuilder>.GetInstance(); - if (!ValidateInterpolatedStringParts(interpolatedString)) + if (!binaryOperator.VisitBinaryOperatorInterpolatedString( + partsArrayBuilder, + static (BoundUnconvertedInterpolatedString unconvertedInterpolatedString, ArrayBuilder> partsArrayBuilder) => + { + if (!InterpolatedStringPartsAreValidInDefaultHandler(unconvertedInterpolatedString)) { - // Exception to case 3. Delegate to standard binding. - stack.Free(); - partsArrayBuilder.Free(); return false; } - partsCount += interpolatedString.Parts.Length; - partsArrayBuilder.Add(interpolatedString.Parts); - current = null; - break; - default: - throw ExceptionUtilities.UnexpectedValue(current.Left.Kind); - } + partsArrayBuilder.Add(unconvertedInterpolatedString.Parts); + return true; + })) + { + partsArrayBuilder.Free(); + return false; } - Debug.Assert(partsArrayBuilder.Count == stack.Count + 1); Debug.Assert(partsArrayBuilder.Count >= 2); - if (isConstant || - (partsCount <= 4 && partsArrayBuilder.All(static parts => AllInterpolatedStringPartsAreStrings(parts)))) + if (partsArrayBuilder.Count <= 4 && partsArrayBuilder.All(static parts => AllInterpolatedStringPartsAreStrings(parts))) { - // This is case 1 and 2. Let the standard machinery handle it - stack.Free(); + // This is case 2. Let the standard machinery handle it partsArrayBuilder.Free(); return false; } - // Parts were added to the array from right to left, but lexical order is left to right. - partsArrayBuilder.ReverseContents(); - // Case 3. Bind as handler. var (appendCalls, data) = BindUnconvertedInterpolatedPartsToHandlerType( binaryOperator.Syntax, @@ -306,51 +277,48 @@ private bool TryBindUnconvertedBinaryOperatorToDefaultInterpolatedStringHandler( additionalConstructorRefKinds: default); // Now that the parts have been bound, reconstruct the binary operators. - convertedBinaryOperator = UpdateBinaryOperatorWithInterpolatedContents(stack, appendCalls, data, binaryOperator.Syntax, diagnostics); - stack.Free(); + convertedBinaryOperator = UpdateBinaryOperatorWithInterpolatedContents(binaryOperator, appendCalls, data, binaryOperator.Syntax, diagnostics); return true; } - private BoundBinaryOperator UpdateBinaryOperatorWithInterpolatedContents(ArrayBuilder stack, ImmutableArray> appendCalls, InterpolatedStringHandlerData data, SyntaxNode rootSyntax, BindingDiagnosticBag diagnostics) + private BoundBinaryOperator UpdateBinaryOperatorWithInterpolatedContents(BoundBinaryOperator originalOperator, ImmutableArray> appendCalls, InterpolatedStringHandlerData data, SyntaxNode rootSyntax, BindingDiagnosticBag diagnostics) { - Debug.Assert(appendCalls.Length == stack.Count + 1); var @string = GetSpecialType(SpecialType.System_String, diagnostics, rootSyntax); - var bottomOperator = stack.Pop(); - var result = createBinaryOperator(bottomOperator, createInterpolation(bottomOperator.Left, appendCalls[0]), rightIndex: 1); + Func>, TypeSymbol), BoundExpression> interpolationFactory = + createInterpolation; + Func>, TypeSymbol), BoundExpression> binaryOperatorFactory = + createBinaryOperator; + + var rewritten = (BoundBinaryOperator)originalOperator.RewriteInterpolatedStringAddition((appendCalls, @string), interpolationFactory, binaryOperatorFactory); - for (int i = 2; i < appendCalls.Length; i++) + return rewritten.Update(BoundBinaryOperator.UncommonData.InterpolatedStringHandlerAddition(data)); + + static BoundInterpolatedString createInterpolation(BoundUnconvertedInterpolatedString expression, int i, (ImmutableArray> AppendCalls, TypeSymbol _) arg) { - result = createBinaryOperator(stack.Pop(), result, rightIndex: i); + Debug.Assert(arg.AppendCalls.Length > i); + return new BoundInterpolatedString( + expression.Syntax, + interpolationData: null, + arg.AppendCalls[i], + expression.ConstantValue, + expression.Type, + expression.HasErrors); } - return result.Update(BoundBinaryOperator.UncommonData.InterpolatedStringHandlerAddition(data)); - - BoundBinaryOperator createBinaryOperator(BoundBinaryOperator original, BoundExpression left, int rightIndex) + static BoundBinaryOperator createBinaryOperator(BoundBinaryOperator original, BoundExpression left, BoundExpression right, (ImmutableArray> _, TypeSymbol @string) arg) => new BoundBinaryOperator( original.Syntax, BinaryOperatorKind.StringConcatenation, left, - createInterpolation(original.Right, appendCalls[rightIndex]), + right, original.ConstantValue, methodOpt: null, constrainedToTypeOpt: null, LookupResultKind.Viable, originalUserDefinedOperatorsOpt: default, - @string, + arg.@string, original.HasErrors); - - static BoundInterpolatedString createInterpolation(BoundExpression expression, ImmutableArray parts) - { - Debug.Assert(expression is BoundUnconvertedInterpolatedString); - return new BoundInterpolatedString( - expression.Syntax, - interpolationData: null, - parts, - expression.ConstantValue, - expression.Type, - expression.HasErrors); - } } private BoundExpression BindUnconvertedInterpolatedExpressionToHandlerType( @@ -408,32 +376,14 @@ private BoundBinaryOperator BindUnconvertedBinaryOperatorToInterpolatedStringHan { Debug.Assert(binaryOperator.IsUnconvertedInterpolatedStringAddition); - var stack = ArrayBuilder.GetInstance(); var partsArrayBuilder = ArrayBuilder>.GetInstance(); - BoundBinaryOperator? current = binaryOperator; - - while (current != null) - { - stack.Push(current); - partsArrayBuilder.Add(((BoundUnconvertedInterpolatedString)current.Right).Parts); - - if (current.Left is BoundBinaryOperator next) + binaryOperator.VisitBinaryOperatorInterpolatedString(partsArrayBuilder, + static (BoundUnconvertedInterpolatedString unconvertedInterpolatedString, ArrayBuilder> partsArrayBuilder) => { - current = next; - } - else - { - partsArrayBuilder.Add(((BoundUnconvertedInterpolatedString)current.Left).Parts); - current = null; - } - } - - // Parts are added in right to left order, but lexical is left to right. - partsArrayBuilder.ReverseContents(); - - Debug.Assert(partsArrayBuilder.Count == stack.Count + 1); - Debug.Assert(partsArrayBuilder.Count >= 2); + partsArrayBuilder.Add(unconvertedInterpolatedString.Parts); + return true; + }); var (appendCalls, data) = BindUnconvertedInterpolatedPartsToHandlerType( binaryOperator.Syntax, @@ -444,8 +394,7 @@ private BoundBinaryOperator BindUnconvertedBinaryOperatorToInterpolatedStringHan additionalConstructorArguments, additionalConstructorRefKinds); - var result = UpdateBinaryOperatorWithInterpolatedContents(stack, appendCalls, data, binaryOperator.Syntax, diagnostics); - stack.Free(); + var result = UpdateBinaryOperatorWithInterpolatedContents(binaryOperator, appendCalls, data, binaryOperator.Syntax, diagnostics); return result; } diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs index fd250070ce6a1..a59054c5118be 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs @@ -539,7 +539,7 @@ private BoundExpression BindSimpleBinaryOperator(BinaryExpressionSyntax node, Bi if (leaveUnconvertedIfInterpolatedString && kind == BinaryOperatorKind.Addition && left is BoundUnconvertedInterpolatedString or BoundBinaryOperator { IsUnconvertedInterpolatedStringAddition: true } - && right is BoundUnconvertedInterpolatedString) + && right is BoundUnconvertedInterpolatedString or BoundBinaryOperator { IsUnconvertedInterpolatedStringAddition: true }) { Debug.Assert(right.Type.SpecialType == SpecialType.System_String); var stringConstant = FoldBinaryOperator(node, BinaryOperatorKind.StringConcatenation, left, right, right.Type, diagnostics); @@ -712,29 +712,38 @@ private BoundExpression RebindSimpleBinaryOperatorAsConverted(BoundBinaryOperato return convertedBinaryOperator; } - var stack = ArrayBuilder.GetInstance(); - BoundBinaryOperator? current = unconvertedBinaryOperator; + var result = doRebind(diagnostics, unconvertedBinaryOperator); + return result; - while (current != null) + BoundExpression doRebind(BindingDiagnosticBag diagnostics, BoundBinaryOperator? current) { - Debug.Assert(current.IsUnconvertedInterpolatedStringAddition); - stack.Push(current); - current = current.Left as BoundBinaryOperator; - } + var stack = ArrayBuilder.GetInstance(); + while (current != null) + { + Debug.Assert(current.IsUnconvertedInterpolatedStringAddition); + stack.Push(current); + current = current.Left as BoundBinaryOperator; + } - Debug.Assert(stack.Count > 0 && stack.Peek().Left is BoundUnconvertedInterpolatedString); + Debug.Assert(stack.Count > 0 && stack.Peek().Left is BoundUnconvertedInterpolatedString); - BoundExpression? left = null; - while (stack.TryPop(out current)) - { - Debug.Assert(current.Right is BoundUnconvertedInterpolatedString); - left = BindSimpleBinaryOperator((BinaryExpressionSyntax)current.Syntax, diagnostics, left ?? current.Left, current.Right, leaveUnconvertedIfInterpolatedString: false); - } + BoundExpression? left = null; + while (stack.TryPop(out current)) + { + var right = current.Right switch + { + BoundUnconvertedInterpolatedString s => s, + BoundBinaryOperator b => doRebind(diagnostics, b), + _ => throw ExceptionUtilities.UnexpectedValue(current.Right.Kind) + }; + left = BindSimpleBinaryOperator((BinaryExpressionSyntax)current.Syntax, diagnostics, left ?? current.Left, right, leaveUnconvertedIfInterpolatedString: false); + } - Debug.Assert(left != null); - Debug.Assert(stack.Count == 0); - stack.Free(); - return left; + Debug.Assert(left != null); + Debug.Assert(stack.Count == 0); + stack.Free(); + return left; + } } #nullable disable diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundNodeExtensions.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundNodeExtensions.cs index e328875b0d463..cd58738867cd2 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundNodeExtensions.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundNodeExtensions.cs @@ -2,10 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp { @@ -94,5 +97,153 @@ private class ContainsAwaitVisitor : BoundTreeWalkerWithStackGuardWithoutRecursi return null; } } + + /// + /// Visits the binary operator tree of interpolated string additions in a depth-first pre-order visit, + /// meaning parent, left, then right. + /// controls whether to continue the visit by returning true or false: + /// if true, the visit will continue. If false, the walk will be cut off. + /// + public static bool VisitBinaryOperatorInterpolatedString( + this BoundBinaryOperator binary, + TArg arg, + Func stringCallback, + Action? binaryOperatorCallback = null) + where TInterpolatedStringType : BoundInterpolatedStringBase + { + var stack = ArrayBuilder.GetInstance(); + + pushLeftNodes(binary, stack, arg, binaryOperatorCallback); + + while (stack.TryPop(out BoundBinaryOperator? current)) + { + switch (current.Left) + { + case BoundBinaryOperator: + break; + case TInterpolatedStringType interpolatedString: + if (!stringCallback(interpolatedString, arg)) + { + return false; + } + break; + default: + throw ExceptionUtilities.UnexpectedValue(current.Left.Kind); + } + + switch (current.Right) + { + case BoundBinaryOperator rightOperator: + pushLeftNodes(rightOperator, stack, arg, binaryOperatorCallback); + break; + case TInterpolatedStringType interpolatedString: + if (!stringCallback(interpolatedString, arg)) + { + return false; + } + break; + default: + throw ExceptionUtilities.UnexpectedValue(current.Right.Kind); + } + } + + Debug.Assert(stack.Count == 0); + stack.Free(); + return true; + + static void pushLeftNodes(BoundBinaryOperator binary, ArrayBuilder stack, TArg arg, Action? binaryOperatorCallback) + { + Debug.Assert(typeof(TInterpolatedStringType) == typeof(BoundInterpolatedString) || binary.IsUnconvertedInterpolatedStringAddition); + BoundBinaryOperator? current = binary; + while (current != null) + { + binaryOperatorCallback?.Invoke(current, arg); + stack.Push(current); + current = current.Left as BoundBinaryOperator; + } + } + } + + /// + /// Rewrites a BoundBinaryOperator composed of interpolated strings (either converted or unconverted) iteratively, without + /// recursion on the left side of the tree. Nodes of the tree are rewritten in a depth-first post-order fashion, meaning + /// left, then right, then parent. + /// + /// The original top of the binary operations. + /// The callback args. + /// + /// Rewriter for the BoundInterpolatedString or BoundUnconvertedInterpolatedString parts of the binary operator. Passed the callback + /// parameter, the original interpolated string, and the index of the interpolated string in the tree. + /// + /// + /// Rewriter for the BoundBinaryOperator parts fo the binary operator. Passed the callback parameter, the original binary operator, and + /// the rewritten left and right components. + /// + public static TResult RewriteInterpolatedStringAddition( + this BoundBinaryOperator binary, + TArg arg, + Func interpolatedStringFactory, + Func binaryOperatorFactory) + where TInterpolatedStringType : BoundInterpolatedStringBase + { + int i = 0; + + var result = doRewrite(binary, arg, interpolatedStringFactory, binaryOperatorFactory, ref i); + + return result; + + static TResult doRewrite( + BoundBinaryOperator binary, + TArg arg, + Func interpolatedStringFactory, + Func binaryOperatorFactory, + ref int i) + { + TResult? result = default; + var originalStack = ArrayBuilder.GetInstance(); + + pushLeftNodes(binary, originalStack); + + while (originalStack.TryPop(out var currentBinary)) + { + Debug.Assert(currentBinary.Left is TInterpolatedStringType || result != null); + TResult rewrittenLeft = currentBinary.Left switch + { + TInterpolatedStringType interpolatedString => interpolatedStringFactory(interpolatedString, i++, arg), + BoundBinaryOperator => result!, + _ => throw ExceptionUtilities.UnexpectedValue(currentBinary.Left.Kind) + }; + + // For simplicity, we use recursion for binary operators on the right side of the tree. We're not traditionally concerned + // with long chains of operators on the right side, as without parentheses we'll naturally make a tree that is deep on the + // left side. If this ever changes, we can make this algorithm a more complex post-order iterative rewrite instead. + + var rewrittenRight = currentBinary.Right switch + { + TInterpolatedStringType interpolatedString => interpolatedStringFactory(interpolatedString, i++, arg), + BoundBinaryOperator binaryOperator => doRewrite(binaryOperator, arg, interpolatedStringFactory, binaryOperatorFactory, ref i), + _ => throw ExceptionUtilities.UnexpectedValue(currentBinary.Right.Kind) + }; + + result = binaryOperatorFactory(currentBinary, rewrittenLeft, rewrittenRight, arg); + } + + Debug.Assert(result != null); + originalStack.Free(); + + return result; + } + + static void pushLeftNodes(BoundBinaryOperator binary, ArrayBuilder stack) + { + BoundBinaryOperator? current = binary; + while (current != null) + { + Debug.Assert(typeof(TInterpolatedStringType) == typeof(BoundInterpolatedString) || binary.IsUnconvertedInterpolatedStringAddition); + stack.Push(current); + current = current.Left as BoundBinaryOperator; + } + } + } } } diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs index 8b8dc5a21c7da..00e5eff6758e8 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs @@ -2502,15 +2502,19 @@ bool learnFromOperator(BoundBinaryOperator binary) protected void VisitBinaryInterpolatedStringAddition(BoundBinaryOperator node) { Debug.Assert(node.InterpolatedStringHandlerData.HasValue); - var stack = ArrayBuilder.GetInstance(); + var parts = ArrayBuilder.GetInstance(); var data = node.InterpolatedStringHandlerData.GetValueOrDefault(); - while (PushBinaryOperatorInterpolatedStringChildren(node, stack) is { } next) - { - node = next; - } + node.VisitBinaryOperatorInterpolatedString( + (parts, @this: this), + stringCallback: static (BoundInterpolatedString interpolatedString, (ArrayBuilder parts, AbstractFlowPass @this) arg) => + { + arg.parts.Add(interpolatedString); + return true; + }, + binaryOperatorCallback: (op, arg) => arg.@this.VisitInterpolatedStringBinaryOperatorNode(op)); - Debug.Assert(stack.Count >= 2); + Debug.Assert(parts.Count >= 2); VisitRvalue(data.Construction); @@ -2519,9 +2523,9 @@ protected void VisitBinaryInterpolatedStringAddition(BoundBinaryOperator node) bool hasConditionalEvaluation = data.UsesBoolReturns || hasTrailingHandlerValidityParameter; TLocalState? shortCircuitState = hasConditionalEvaluation ? State.Clone() : default; - while (stack.TryPop(out var currentString)) + foreach (var part in parts) { - visitedFirst |= VisitInterpolatedStringHandlerParts(currentString, data.UsesBoolReturns, firstPartIsConditional: visitedFirst || hasTrailingHandlerValidityParameter, ref shortCircuitState); + visitedFirst |= VisitInterpolatedStringHandlerParts(part, data.UsesBoolReturns, firstPartIsConditional: visitedFirst || hasTrailingHandlerValidityParameter, ref shortCircuitState); } if (hasConditionalEvaluation) @@ -2529,23 +2533,10 @@ protected void VisitBinaryInterpolatedStringAddition(BoundBinaryOperator node) Join(ref State, ref shortCircuitState); } - stack.Free(); + parts.Free(); } - protected virtual BoundBinaryOperator? PushBinaryOperatorInterpolatedStringChildren(BoundBinaryOperator node, ArrayBuilder stack) - { - stack.Push((BoundInterpolatedString)node.Right); - switch (node.Left) - { - case BoundBinaryOperator next: - return next; - case BoundInterpolatedString @string: - stack.Push(@string); - return null; - default: - throw ExceptionUtilities.UnexpectedValue(node.Left.Kind); - } - } + protected virtual void VisitInterpolatedStringBinaryOperatorNode(BoundBinaryOperator node) { } protected virtual bool VisitInterpolatedStringHandlerParts(BoundInterpolatedStringBase node, bool usesBoolReturns, bool firstPartIsConditional, ref TLocalState? shortCircuitState) { diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs index 5c6333648ff28..3b0dc4be8bdbd 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs @@ -4031,11 +4031,9 @@ protected override bool VisitInterpolatedStringHandlerParts(BoundInterpolatedStr return result; } - protected override BoundBinaryOperator? PushBinaryOperatorInterpolatedStringChildren(BoundBinaryOperator node, ArrayBuilder stack) + protected override void VisitInterpolatedStringBinaryOperatorNode(BoundBinaryOperator node) { - var result = base.PushBinaryOperatorInterpolatedStringChildren(node, stack); SetNotNullResult(node); - return result; } /// diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_BinaryOperator.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_BinaryOperator.cs index 7f07afe504fbc..4e674ed51ceb7 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_BinaryOperator.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_BinaryOperator.cs @@ -154,33 +154,13 @@ private static ImmutableArray CollectBinaryOperatorInterpolated Debug.Assert(node.OperatorKind == BinaryOperatorKind.StringConcatenation); Debug.Assert(node.InterpolatedStringHandlerData is not null); var partsBuilder = ArrayBuilder.GetInstance(); - while (true) - { - addReversedParts((BoundInterpolatedString)node.Right); - - if (node.Left is BoundBinaryOperator next) - { - node = next; - } - else + node.VisitBinaryOperatorInterpolatedString(partsBuilder, + static (BoundInterpolatedString interpolatedString, ArrayBuilder partsBuilder) => { - addReversedParts((BoundInterpolatedString)node.Left); - break; - } - } - - partsBuilder.ReverseContents(); - - ImmutableArray parts = partsBuilder.ToImmutableAndFree(); - return parts; - - void addReversedParts(BoundInterpolatedString boundInterpolated) - { - for (int i = boundInterpolated.Parts.Length - 1; i >= 0; i--) - { - partsBuilder.Add(boundInterpolated.Parts[i]); - } - } + partsBuilder.AddRange(interpolatedString.Parts); + return true; + }); + return partsBuilder.ToImmutableAndFree(); } private BoundExpression MakeBinaryOperator( diff --git a/src/Compilers/CSharp/Portable/Operations/CSharpOperationFactory.cs b/src/Compilers/CSharp/Portable/Operations/CSharpOperationFactory.cs index f605be682486e..3deb2c80e6749 100644 --- a/src/Compilers/CSharp/Portable/Operations/CSharpOperationFactory.cs +++ b/src/Compilers/CSharp/Portable/Operations/CSharpOperationFactory.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -1294,14 +1295,17 @@ private IUnaryOperation CreateBoundUnaryOperatorOperation(BoundUnaryOperator bou bool isImplicit = boundUnaryOperator.WasCompilerGenerated; return new UnaryOperation(unaryOperatorKind, operand, isLifted, isChecked, operatorMethod, _semanticModel, syntax, type, constantValue, isImplicit); } - private IOperation CreateBoundBinaryOperatorBase(BoundBinaryOperatorBase boundBinaryOperatorBase) { + if (boundBinaryOperatorBase is BoundBinaryOperator { InterpolatedStringHandlerData: not null } binary) + { + return CreateBoundInterpolatedStringBinaryOperator(binary); + } + // Binary operators can be nested _many_ levels deep, and cause a stack overflow if we manually recurse. // To solve this, we use a manual stack for the left side. var stack = ArrayBuilder.GetInstance(); BoundBinaryOperatorBase? currentBinary = boundBinaryOperatorBase; - InterpolatedStringHandlerData? interpolatedData = (boundBinaryOperatorBase as BoundBinaryOperator)?.InterpolatedStringHandlerData; do { @@ -1309,19 +1313,16 @@ private IOperation CreateBoundBinaryOperatorBase(BoundBinaryOperatorBase boundBi currentBinary = currentBinary.Left as BoundBinaryOperatorBase; } while (currentBinary is not null and not BoundBinaryOperator { InterpolatedStringHandlerData: not null }); - Debug.Assert(interpolatedData == null || interpolatedData.GetValueOrDefault().PositionInfo.Length == stack.Count + 1); - Debug.Assert(stack.Count > 0); IOperation? left = null; - int positionInfoIndex = 0; while (stack.TryPop(out currentBinary)) { - left ??= visitOperand(currentBinary.Left); - IOperation right = visitOperand(currentBinary.Right); + left ??= Create(currentBinary.Left); + IOperation right = Create(currentBinary.Right); left = currentBinary switch { - BoundBinaryOperator binaryOp => createBoundBinaryOperatorOperation(binaryOp, left, right), + BoundBinaryOperator binaryOp => CreateBoundBinaryOperatorOperation(binaryOp, left, right), BoundUserDefinedConditionalLogicalOperator logicalOp => createBoundUserDefinedConditionalLogicalOperator(logicalOp, left, right), { Kind: var kind } => throw ExceptionUtilities.UnexpectedValue(kind) }; @@ -1331,37 +1332,6 @@ private IOperation CreateBoundBinaryOperatorBase(BoundBinaryOperatorBase boundBi stack.Free(); return left; - IOperation visitOperand(BoundExpression operand) - => interpolatedData is { } data - ? CreateBoundInterpolatedStringExpressionOperation((BoundInterpolatedString)operand, data.PositionInfo[positionInfoIndex++]) - : Create(operand); - - IBinaryOperation createBoundBinaryOperatorOperation(BoundBinaryOperator boundBinaryOperator, IOperation left, IOperation right) - { - BinaryOperatorKind operatorKind = Helper.DeriveBinaryOperatorKind(boundBinaryOperator.OperatorKind); - IMethodSymbol? operatorMethod = boundBinaryOperator.Method.GetPublicSymbol(); - IMethodSymbol? unaryOperatorMethod = null; - - // For dynamic logical operator MethodOpt is actually the unary true/false operator - if (boundBinaryOperator.Type.IsDynamic() && - (operatorKind == BinaryOperatorKind.ConditionalAnd || operatorKind == BinaryOperatorKind.ConditionalOr) && - operatorMethod?.Parameters.Length == 1) - { - unaryOperatorMethod = operatorMethod; - operatorMethod = null; - } - - SyntaxNode syntax = boundBinaryOperator.Syntax; - ITypeSymbol? type = boundBinaryOperator.GetPublicTypeSymbol(); - ConstantValue? constantValue = boundBinaryOperator.ConstantValue; - bool isLifted = boundBinaryOperator.OperatorKind.IsLifted(); - bool isChecked = boundBinaryOperator.OperatorKind.IsChecked(); - bool isCompareText = false; - bool isImplicit = boundBinaryOperator.WasCompilerGenerated; - return new BinaryOperation(operatorKind, left, right, isLifted, isChecked, isCompareText, operatorMethod, unaryOperatorMethod, - _semanticModel, syntax, type, constantValue, isImplicit); - } - IBinaryOperation createBoundUserDefinedConditionalLogicalOperator(BoundUserDefinedConditionalLogicalOperator boundBinaryOperator, IOperation left, IOperation right) { BinaryOperatorKind operatorKind = Helper.DeriveBinaryOperatorKind(boundBinaryOperator.OperatorKind); @@ -1381,6 +1351,57 @@ IBinaryOperation createBoundUserDefinedConditionalLogicalOperator(BoundUserDefin } } + private IBinaryOperation CreateBoundBinaryOperatorOperation(BoundBinaryOperator boundBinaryOperator, IOperation left, IOperation right) + { + BinaryOperatorKind operatorKind = Helper.DeriveBinaryOperatorKind(boundBinaryOperator.OperatorKind); + IMethodSymbol? operatorMethod = boundBinaryOperator.Method.GetPublicSymbol(); + IMethodSymbol? unaryOperatorMethod = null; + + // For dynamic logical operator MethodOpt is actually the unary true/false operator + if (boundBinaryOperator.Type.IsDynamic() && + (operatorKind == BinaryOperatorKind.ConditionalAnd || operatorKind == BinaryOperatorKind.ConditionalOr) && + operatorMethod?.Parameters.Length == 1) + { + unaryOperatorMethod = operatorMethod; + operatorMethod = null; + } + + SyntaxNode syntax = boundBinaryOperator.Syntax; + ITypeSymbol? type = boundBinaryOperator.GetPublicTypeSymbol(); + ConstantValue? constantValue = boundBinaryOperator.ConstantValue; + bool isLifted = boundBinaryOperator.OperatorKind.IsLifted(); + bool isChecked = boundBinaryOperator.OperatorKind.IsChecked(); + bool isCompareText = false; + bool isImplicit = boundBinaryOperator.WasCompilerGenerated; + return new BinaryOperation(operatorKind, left, right, isLifted, isChecked, isCompareText, operatorMethod, unaryOperatorMethod, + _semanticModel, syntax, type, constantValue, isImplicit); + } + + private IOperation CreateBoundInterpolatedStringBinaryOperator(BoundBinaryOperator boundBinaryOperator) + { + Debug.Assert(boundBinaryOperator.InterpolatedStringHandlerData is not null); + Func createInterpolatedString + = createInterpolatedStringOperand; + + Func createBinaryOperator + = createBoundBinaryOperatorOperation; + + return boundBinaryOperator.RewriteInterpolatedStringAddition((this, boundBinaryOperator.InterpolatedStringHandlerData.GetValueOrDefault()), createInterpolatedString, createBinaryOperator); + + static IInterpolatedStringOperation createInterpolatedStringOperand( + BoundInterpolatedString boundInterpolatedString, + int i, + (CSharpOperationFactory @this, InterpolatedStringHandlerData Data) arg) + => arg.@this.CreateBoundInterpolatedStringExpressionOperation(boundInterpolatedString, arg.Data.PositionInfo[i]); + + static IBinaryOperation createBoundBinaryOperatorOperation( + BoundBinaryOperator boundBinaryOperator, + IOperation left, + IOperation right, + (CSharpOperationFactory @this, InterpolatedStringHandlerData _) arg) + => arg.@this.CreateBoundBinaryOperatorOperation(boundBinaryOperator, left, right); + } + private ITupleBinaryOperation CreateBoundTupleBinaryOperatorOperation(BoundTupleBinaryOperator boundTupleBinaryOperator) { IOperation left = Create(boundTupleBinaryOperator.Left); diff --git a/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_IInterpolatedStringOperation.cs b/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_IInterpolatedStringOperation.cs index cb57c11eb7c86..1ec5046d1be1f 100644 --- a/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_IInterpolatedStringOperation.cs +++ b/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_IInterpolatedStringOperation.cs @@ -1994,7 +1994,98 @@ public void InterpolatedStringsAddedUnderObjectAddition_DefiniteAssignment() "; VerifyOperationTreeAndDiagnosticsForTest(new[] { code, GetInterpolatedStringHandlerDefinition(includeSpanOverloads: false, useDefaultParameters: false, useBoolReturns: true) }, - expectedOperationTree, expectedDiagnostics, parseOptions: TestOptions.RegularPreview); + expectedOperationTree, expectedDiagnostics, parseOptions: TestOptions.RegularPreview); + } + + [Fact] + public void ParenthesizedInterpolatedStringsAdded() + { + var code = @" +int i1 = 1; +int i2 = 2; +int i3 = 3; +int i4 = 4; +int i5 = 5; +int i6 = 6; + +string s = /**/(($""{i1,1:d}"" + $""{i2,2:f}"") + $""{i3,3:g}"") + ($""{i4,4:h}"" + ($""{i5,5:i}"" + $""{i6,6:j}""))/**/; +"; + + var expectedDiagnostics = DiagnosticDescription.None; + var expectedOperationTree = @" +IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '(($""{i1,1:d ... {i6,6:j}""))') + Left: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '($""{i1,1:d} ... $""{i3,3:g}""') + Left: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i1,1:d}"" ... $""{i2,2:f}""') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i1,1:d}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i1,1:d}') + Expression: + ILocalReferenceOperation: i1 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i1') + Alignment: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 1) (Syntax: '1') + FormatString: + ILiteralOperation (OperationKind.Literal, Type: System.String, Constant: ""d"") (Syntax: ':d') + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i2,2:f}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i2,2:f}') + Expression: + ILocalReferenceOperation: i2 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i2') + Alignment: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 2) (Syntax: '2') + FormatString: + ILiteralOperation (OperationKind.Literal, Type: System.String, Constant: ""f"") (Syntax: ':f') + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i3,3:g}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i3,3:g}') + Expression: + ILocalReferenceOperation: i3 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i3') + Alignment: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 3) (Syntax: '3') + FormatString: + ILiteralOperation (OperationKind.Literal, Type: System.String, Constant: ""g"") (Syntax: ':g') + Right: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i4,4:h}"" ... ""{i6,6:j}"")') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i4,4:h}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i4,4:h}') + Expression: + ILocalReferenceOperation: i4 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i4') + Alignment: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 4) (Syntax: '4') + FormatString: + ILiteralOperation (OperationKind.Literal, Type: System.String, Constant: ""h"") (Syntax: ':h') + Right: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i5,5:i}"" ... $""{i6,6:j}""') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i5,5:i}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i5,5:i}') + Expression: + ILocalReferenceOperation: i5 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i5') + Alignment: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 5) (Syntax: '5') + FormatString: + ILiteralOperation (OperationKind.Literal, Type: System.String, Constant: ""i"") (Syntax: ':i') + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i6,6:j}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i6,6:j}') + Expression: + ILocalReferenceOperation: i6 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i6') + Alignment: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 6) (Syntax: '6') + FormatString: + ILiteralOperation (OperationKind.Literal, Type: System.String, Constant: ""j"") (Syntax: ':j') +"; + + VerifyOperationTreeAndDiagnosticsForTest(new[] { code, GetInterpolatedStringHandlerDefinition(includeSpanOverloads: false, useDefaultParameters: false, useBoolReturns: true) }, + expectedOperationTree, expectedDiagnostics, parseOptions: TestOptions.RegularPreview); } } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterpolationTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterpolationTests.cs index ae58ae149e0d0..8a80599b5e39a 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterpolationTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterpolationTests.cs @@ -12681,15 +12681,17 @@ public void InterpolatedStringsAddedUnderObjectAddition_DefiniteAssignment() ); } - [Fact] - public void ParenthesizedAdditiveExpression_01() + [Theory] + [InlineData(@"($""{i1}"" + $""{i2}"") + $""{i3}""")] + [InlineData(@"$""{i1}"" + ($""{i2}"" + $""{i3}"")")] + public void ParenthesizedAdditiveExpression_01(string expression) { var code = @" int i1 = 1; int i2 = 2; int i3 = 3; -CustomHandler c = ($""{i1}"" + $""{i2}"") + $""{i3}""; +CustomHandler c = " + expression + @"; System.Console.WriteLine(c.ToString());"; var comp = CreateCompilation(new[] { code, GetInterpolatedStringCustomHandlerType("CustomHandler", "struct", useBoolReturns: false) }); @@ -12760,16 +12762,543 @@ public void ParenthesizedAdditiveExpression_02() int i1 = 1; int i2 = 2; int i3 = 3; +int i4 = 4; +int i5 = 5; +int i6 = 6; -CustomHandler c = $""{i1}"" + ($""{i2}"" + $""{i3}""); +CustomHandler c = /**/((($""{i1}"" + $""{i2}"") + $""{i3}"") + ($""{i4}"" + ($""{i5}"" + $""{i6}""))) + (($""{i1}"" + ($""{i2}"" + $""{i3}"")) + (($""{i4}"" + $""{i5}"") + $""{i6}""))/**/; System.Console.WriteLine(c.ToString());"; var comp = CreateCompilation(new[] { code, GetInterpolatedStringCustomHandlerType("CustomHandler", "struct", useBoolReturns: false) }); - comp.VerifyDiagnostics( - // (6,19): error CS0029: Cannot implicitly convert type 'string' to 'CustomHandler' - // CustomHandler c = $"{i1}" + ($"{i2}" + $"{i3}"); - Diagnostic(ErrorCode.ERR_NoImplicitConv, @"$""{i1}"" + ($""{i2}"" + $""{i3}"")").WithArguments("string", "CustomHandler").WithLocation(6, 19) - ); + var verifier = CompileAndVerify(comp, expectedOutput: @" +value:1 +alignment:0 +format: +value:2 +alignment:0 +format: +value:3 +alignment:0 +format: +value:4 +alignment:0 +format: +value:5 +alignment:0 +format: +value:6 +alignment:0 +format: +value:1 +alignment:0 +format: +value:2 +alignment:0 +format: +value:3 +alignment:0 +format: +value:4 +alignment:0 +format: +value:5 +alignment:0 +format: +value:6 +alignment:0 +format: +"); + verifier.VerifyDiagnostics(); + + VerifyOperationTreeForTest(comp, @" +IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '((($""{i1}"" ... + $""{i6}""))') + Left: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '(($""{i1}"" + ... + $""{i6}""))') + Left: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '($""{i1}"" + ... ) + $""{i3}""') + Left: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i1}"" + $""{i2}""') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i1}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i1}') + Expression: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'i1') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + ILocalReferenceOperation: i1 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i1') + Alignment: + null + FormatString: + null + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i2}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i2}') + Expression: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'i2') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + ILocalReferenceOperation: i2 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i2') + Alignment: + null + FormatString: + null + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i3}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i3}') + Expression: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'i3') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + ILocalReferenceOperation: i3 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i3') + Alignment: + null + FormatString: + null + Right: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i4}"" + ( ... + $""{i6}"")') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i4}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i4}') + Expression: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'i4') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + ILocalReferenceOperation: i4 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i4') + Alignment: + null + FormatString: + null + Right: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i5}"" + $""{i6}""') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i5}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i5}') + Expression: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'i5') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + ILocalReferenceOperation: i5 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i5') + Alignment: + null + FormatString: + null + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i6}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i6}') + Expression: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'i6') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + ILocalReferenceOperation: i6 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i6') + Alignment: + null + FormatString: + null + Right: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '($""{i1}"" + ... + $""{i6}"")') + Left: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i1}"" + ( ... + $""{i3}"")') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i1}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i1}') + Expression: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'i1') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + ILocalReferenceOperation: i1 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i1') + Alignment: + null + FormatString: + null + Right: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i2}"" + $""{i3}""') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i2}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i2}') + Expression: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'i2') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + ILocalReferenceOperation: i2 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i2') + Alignment: + null + FormatString: + null + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i3}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i3}') + Expression: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'i3') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + ILocalReferenceOperation: i3 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i3') + Alignment: + null + FormatString: + null + Right: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '($""{i4}"" + ... ) + $""{i6}""') + Left: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i4}"" + $""{i5}""') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i4}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i4}') + Expression: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'i4') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + ILocalReferenceOperation: i4 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i4') + Alignment: + null + FormatString: + null + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i5}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i5}') + Expression: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'i5') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + ILocalReferenceOperation: i5 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i5') + Alignment: + null + FormatString: + null + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i6}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i6}') + Expression: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Object, IsImplicit) (Syntax: 'i6') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + ILocalReferenceOperation: i6 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i6') + Alignment: + null + FormatString: + null +"); + } + + [Fact] + public void ParenthesizedAdditiveExpression_03() + { + var code = @" +int i1 = 1; +int i2 = 2; +int i3 = 3; +int i4 = 4; +int i5 = 5; +int i6 = 6; + +string s = (($""{i1}"" + $""{i2}"") + $""{i3}"") + ($""{i4}"" + ($""{i5}"" + $""{i6}"")); +System.Console.WriteLine(s);"; + + var verifier = CompileAndVerify(code, expectedOutput: @"123456"); + verifier.VerifyDiagnostics(); + } + + [Fact] + public void ParenthesizedAdditiveExpression_04() + { + var code = @" +using System.Threading.Tasks; +int i1 = 2; +int i2 = 3; + +string s = $""{await GetInt()}"" + ($""{i1}"" + $""{i2}""); +System.Console.WriteLine(s); + +Task GetInt() => Task.FromResult(1); +"; + + var verifier = CompileAndVerify(new[] { code, GetInterpolatedStringHandlerDefinition(includeSpanOverloads: false, useDefaultParameters: false, useBoolReturns: false) }, expectedOutput: @" +1value:2 +value:3"); + verifier.VerifyDiagnostics(); + + // Note the two DefaultInterpolatedStringHandlers in the IL here. In a future rewrite step in the LocalRewriter, we can potentially + // transform the tree to change its shape and pull out all individual Append calls in a sequence (regardless of the level of the tree) + // and combine these and other unequal tree shapes. For now, we're going with a simple solution where, if the entire binary expression + // cannot be combined, none of it is. + + verifier.VerifyIL("Program.<
$>d__0.System.Runtime.CompilerServices.IAsyncStateMachine.MoveNext()", @" +{ + // Code size 244 (0xf4) + .maxstack 4 + .locals init (int V_0, + int V_1, + System.Runtime.CompilerServices.TaskAwaiter V_2, + System.Runtime.CompilerServices.DefaultInterpolatedStringHandler V_3, + System.Exception V_4) + IL_0000: ldarg.0 + IL_0001: ldfld ""int Program.<
$>d__0.<>1__state"" + IL_0006: stloc.0 + .try + { + IL_0007: ldloc.0 + IL_0008: brfalse.s IL_004f + IL_000a: ldarg.0 + IL_000b: ldc.i4.2 + IL_000c: stfld ""int Program.<
$>d__0.5__2"" + IL_0011: ldarg.0 + IL_0012: ldc.i4.3 + IL_0013: stfld ""int Program.<
$>d__0.5__3"" + IL_0018: call ""System.Threading.Tasks.Task Program.<
$>g__GetInt|0_0()"" + IL_001d: callvirt ""System.Runtime.CompilerServices.TaskAwaiter System.Threading.Tasks.Task.GetAwaiter()"" + IL_0022: stloc.2 + IL_0023: ldloca.s V_2 + IL_0025: call ""bool System.Runtime.CompilerServices.TaskAwaiter.IsCompleted.get"" + IL_002a: brtrue.s IL_006b + IL_002c: ldarg.0 + IL_002d: ldc.i4.0 + IL_002e: dup + IL_002f: stloc.0 + IL_0030: stfld ""int Program.<
$>d__0.<>1__state"" + IL_0035: ldarg.0 + IL_0036: ldloc.2 + IL_0037: stfld ""System.Runtime.CompilerServices.TaskAwaiter Program.<
$>d__0.<>u__1"" + IL_003c: ldarg.0 + IL_003d: ldflda ""System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<
$>d__0.<>t__builder"" + IL_0042: ldloca.s V_2 + IL_0044: ldarg.0 + IL_0045: call ""void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted, Program.<
$>d__0>(ref System.Runtime.CompilerServices.TaskAwaiter, ref Program.<
$>d__0)"" + IL_004a: leave IL_00f3 + IL_004f: ldarg.0 + IL_0050: ldfld ""System.Runtime.CompilerServices.TaskAwaiter Program.<
$>d__0.<>u__1"" + IL_0055: stloc.2 + IL_0056: ldarg.0 + IL_0057: ldflda ""System.Runtime.CompilerServices.TaskAwaiter Program.<
$>d__0.<>u__1"" + IL_005c: initobj ""System.Runtime.CompilerServices.TaskAwaiter"" + IL_0062: ldarg.0 + IL_0063: ldc.i4.m1 + IL_0064: dup + IL_0065: stloc.0 + IL_0066: stfld ""int Program.<
$>d__0.<>1__state"" + IL_006b: ldloca.s V_2 + IL_006d: call ""int System.Runtime.CompilerServices.TaskAwaiter.GetResult()"" + IL_0072: stloc.1 + IL_0073: ldstr ""{0}"" + IL_0078: ldloc.1 + IL_0079: box ""int"" + IL_007e: call ""string string.Format(string, object)"" + IL_0083: ldc.i4.0 + IL_0084: ldc.i4.1 + IL_0085: newobj ""System.Runtime.CompilerServices.DefaultInterpolatedStringHandler..ctor(int, int)"" + IL_008a: stloc.3 + IL_008b: ldloca.s V_3 + IL_008d: ldarg.0 + IL_008e: ldfld ""int Program.<
$>d__0.5__2"" + IL_0093: call ""void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted(int)"" + IL_0098: ldloca.s V_3 + IL_009a: call ""string System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToStringAndClear()"" + IL_009f: ldc.i4.0 + IL_00a0: ldc.i4.1 + IL_00a1: newobj ""System.Runtime.CompilerServices.DefaultInterpolatedStringHandler..ctor(int, int)"" + IL_00a6: stloc.3 + IL_00a7: ldloca.s V_3 + IL_00a9: ldarg.0 + IL_00aa: ldfld ""int Program.<
$>d__0.5__3"" + IL_00af: call ""void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted(int)"" + IL_00b4: ldloca.s V_3 + IL_00b6: call ""string System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToStringAndClear()"" + IL_00bb: call ""string string.Concat(string, string, string)"" + IL_00c0: call ""void System.Console.WriteLine(string)"" + IL_00c5: leave.s IL_00e0 + } + catch System.Exception + { + IL_00c7: stloc.s V_4 + IL_00c9: ldarg.0 + IL_00ca: ldc.i4.s -2 + IL_00cc: stfld ""int Program.<
$>d__0.<>1__state"" + IL_00d1: ldarg.0 + IL_00d2: ldflda ""System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<
$>d__0.<>t__builder"" + IL_00d7: ldloc.s V_4 + IL_00d9: call ""void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetException(System.Exception)"" + IL_00de: leave.s IL_00f3 + } + IL_00e0: ldarg.0 + IL_00e1: ldc.i4.s -2 + IL_00e3: stfld ""int Program.<
$>d__0.<>1__state"" + IL_00e8: ldarg.0 + IL_00e9: ldflda ""System.Runtime.CompilerServices.AsyncTaskMethodBuilder Program.<
$>d__0.<>t__builder"" + IL_00ee: call ""void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetResult()"" + IL_00f3: ret +}"); + } + [Fact] + public void ParenthesizedAdditiveExpression_05() + { + var code = @" +int i1 = 1; +int i2 = 2; +int i3 = 3; +int i4 = 4; +int i5 = 5; +int i6 = 6; + +string s = /**/((($""{i1}"" + $""{i2}"") + $""{i3}"") + ($""{i4}"" + ($""{i5}"" + $""{i6}""))) + (($""{i1}"" + ($""{i2}"" + $""{i3}"")) + (($""{i4}"" + $""{i5}"") + $""{i6}""))/**/; +System.Console.WriteLine(s);"; + + var comp = CreateCompilation(code); + var verifier = CompileAndVerify(comp, expectedOutput: @"123456123456"); + verifier.VerifyDiagnostics(); + + VerifyOperationTreeForTest(comp, @" +IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '((($""{i1}"" ... + $""{i6}""))') + Left: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '(($""{i1}"" + ... + $""{i6}""))') + Left: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '($""{i1}"" + ... ) + $""{i3}""') + Left: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i1}"" + $""{i2}""') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i1}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i1}') + Expression: + ILocalReferenceOperation: i1 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i1') + Alignment: + null + FormatString: + null + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i2}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i2}') + Expression: + ILocalReferenceOperation: i2 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i2') + Alignment: + null + FormatString: + null + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i3}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i3}') + Expression: + ILocalReferenceOperation: i3 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i3') + Alignment: + null + FormatString: + null + Right: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i4}"" + ( ... + $""{i6}"")') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i4}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i4}') + Expression: + ILocalReferenceOperation: i4 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i4') + Alignment: + null + FormatString: + null + Right: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i5}"" + $""{i6}""') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i5}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i5}') + Expression: + ILocalReferenceOperation: i5 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i5') + Alignment: + null + FormatString: + null + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i6}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i6}') + Expression: + ILocalReferenceOperation: i6 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i6') + Alignment: + null + FormatString: + null + Right: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '($""{i1}"" + ... + $""{i6}"")') + Left: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i1}"" + ( ... + $""{i3}"")') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i1}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i1}') + Expression: + ILocalReferenceOperation: i1 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i1') + Alignment: + null + FormatString: + null + Right: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i2}"" + $""{i3}""') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i2}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i2}') + Expression: + ILocalReferenceOperation: i2 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i2') + Alignment: + null + FormatString: + null + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i3}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i3}') + Expression: + ILocalReferenceOperation: i3 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i3') + Alignment: + null + FormatString: + null + Right: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '($""{i4}"" + ... ) + $""{i6}""') + Left: + IBinaryOperation (BinaryOperatorKind.Add) (OperationKind.Binary, Type: System.String) (Syntax: '$""{i4}"" + $""{i5}""') + Left: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i4}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i4}') + Expression: + ILocalReferenceOperation: i4 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i4') + Alignment: + null + FormatString: + null + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i5}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i5}') + Expression: + ILocalReferenceOperation: i5 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i5') + Alignment: + null + FormatString: + null + Right: + IInterpolatedStringOperation (OperationKind.InterpolatedString, Type: System.String) (Syntax: '$""{i6}""') + Parts(1): + IInterpolationOperation (OperationKind.Interpolation, Type: null) (Syntax: '{i6}') + Expression: + ILocalReferenceOperation: i6 (OperationKind.LocalReference, Type: System.Int32) (Syntax: 'i6') + Alignment: + null + FormatString: + null +"); } [Theory]