File: Lowering\LocalRewriter\LocalRewriter_StringInterpolation.cs
Web Access
Project: src\src\Compilers\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.csproj (Microsoft.CodeAnalysis.CSharp)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.PooledObjects;
using System.Diagnostics;
using System.Linq;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp
{
    internal sealed partial class LocalRewriter
    {
        private BoundExpression RewriteInterpolatedStringConversion(BoundConversion conversion)
        {
            Debug.Assert(conversion.ConversionKind == ConversionKind.InterpolatedString);
            var interpolatedString = (BoundInterpolatedString)conversion.Operand;
            Debug.Assert(interpolatedString.InterpolationData is { Construction: not null });
            return VisitExpression(interpolatedString.InterpolationData.GetValueOrDefault().Construction);
        }
 
        /// <summary>
        /// Rewrites the given interpolated string to the set of handler creation and Append calls, returning an array builder of the append calls and the result
        /// local temp.
        /// </summary>
        /// <remarks>Caller is responsible for freeing the ArrayBuilder</remarks>
        private InterpolationHandlerResult RewriteToInterpolatedStringHandlerPattern(InterpolatedStringHandlerData data, ImmutableArray<BoundExpression> parts, SyntaxNode syntax)
        {
            Debug.Assert(data.BuilderType is not null);
            Debug.Assert(data.ReceiverPlaceholder is not null);
            Debug.Assert(parts.All(static p => p is BoundCall or BoundDynamicInvocation));
            var builderTempSymbol = _factory.InterpolatedStringHandlerLocal(data.BuilderType, syntax);
            BoundLocal builderTemp = _factory.Local(builderTempSymbol);
 
            // var handler = new HandlerType(baseStringLength, numFormatHoles, ...InterpolatedStringHandlerArgumentAttribute parameters, <optional> out bool appendShouldProceed);
            var construction = (BoundObjectCreationExpression)data.Construction;
 
            BoundLocal? appendShouldProceedLocal = null;
            if (data.HasTrailingHandlerValidityParameter)
            {
#if DEBUG
                for (int i = construction.ArgumentRefKindsOpt.Length - 1; i >= 0; i--)
                {
                    if (construction.ArgumentRefKindsOpt[i] == RefKind.Out)
                    {
                        break;
                    }
 
                    Debug.Assert(construction.ArgumentRefKindsOpt[i] == RefKind.None);
                    Debug.Assert(construction.DefaultArguments[i]);
                }
#endif
 
                BoundInterpolatedStringArgumentPlaceholder trailingParameter = data.ArgumentPlaceholders[^1];
                TypeSymbol localType = trailingParameter.Type;
                Debug.Assert(localType.SpecialType == SpecialType.System_Boolean);
                var outLocal = _factory.SynthesizedLocal(localType);
                appendShouldProceedLocal = _factory.Local(outLocal);
 
                AddPlaceholderReplacement(trailingParameter, appendShouldProceedLocal);
            }
 
            var handlerConstructionAssignment = _factory.AssignmentExpression(builderTemp, (BoundExpression)VisitObjectCreationExpression(construction));
 
            AddPlaceholderReplacement(data.ReceiverPlaceholder, builderTemp);
            bool usesBoolReturns = data.UsesBoolReturns;
            var resultExpressions = ArrayBuilder<BoundExpression>.GetInstance(parts.Length + 1);
 
            foreach (var part in parts)
            {
                if (part is BoundCall call)
                {
                    Debug.Assert(call.Type.SpecialType == SpecialType.System_Boolean == usesBoolReturns);
                    resultExpressions.Add((BoundExpression)VisitCall(call));
                }
                else if (part is BoundDynamicInvocation dynamicInvocation)
                {
                    resultExpressions.Add(VisitDynamicInvocation(dynamicInvocation, resultDiscarded: !usesBoolReturns));
                }
                else
                {
                    throw ExceptionUtilities.UnexpectedValue(part.Kind);
                }
            }
 
            RemovePlaceholderReplacement(data.ReceiverPlaceholder);
 
            if (appendShouldProceedLocal is not null)
            {
                RemovePlaceholderReplacement(data.ArgumentPlaceholders[^1]);
            }
 
            if (usesBoolReturns)
            {
                // We assume non-bool returns if there was no parts to the string, and code below is predicated on that.
                Debug.Assert(!parts.IsEmpty);
                // Start the sequence with appendProceedLocal, if appropriate
                BoundExpression? currentExpression = appendShouldProceedLocal;
 
                var boolType = _compilation.GetSpecialType(SpecialType.System_Boolean);
 
                foreach (var appendCall in resultExpressions)
                {
                    var actualCall = appendCall;
                    if (actualCall.Type!.IsDynamic())
                    {
                        actualCall = _dynamicFactory.MakeDynamicConversion(actualCall, isExplicit: false, isArrayIndex: false, isChecked: false, boolType).ToExpression();
                    }
 
                    // previousAppendCalls && appendCall
                    currentExpression = currentExpression is null
                        ? actualCall
                        : _factory.LogicalAnd(currentExpression, actualCall);
                }
 
                Debug.Assert(currentExpression != null);
 
                resultExpressions.Clear();
                resultExpressions.Add(handlerConstructionAssignment);
                resultExpressions.Add(currentExpression);
            }
            else if (appendShouldProceedLocal is not null && resultExpressions.Count > 0)
            {
                // appendCalls as expressionStatements
                var appendCallsStatements = resultExpressions.SelectAsArray(static (appendCall, @this) => (BoundStatement)@this._factory.ExpressionStatement(appendCall), this);
                resultExpressions.Free();
 
                // if (appendShouldProceedLocal) { appendCallsStatements }
                var resultIf = _factory.If(appendShouldProceedLocal, _factory.StatementList(appendCallsStatements));
 
                return new InterpolationHandlerResult(ImmutableArray.Create(_factory.ExpressionStatement(handlerConstructionAssignment), resultIf), builderTemp, appendShouldProceedLocal.LocalSymbol, this);
            }
            else
            {
                resultExpressions.Insert(0, handlerConstructionAssignment);
            }
 
            return new InterpolationHandlerResult(resultExpressions.ToImmutableAndFree(), builderTemp, appendShouldProceedLocal?.LocalSymbol, this);
        }
 
        public override BoundNode VisitInterpolatedString(BoundInterpolatedString node)
        {
            Debug.Assert(node.Type is { SpecialType: SpecialType.System_String }); // if target-converted, we should not get here.
 
            if (node.InterpolationData is InterpolatedStringHandlerData { BuilderType: not null } data)
            {
                return LowerPartsToString(data, node.Parts, node.Syntax, node.Type);
            }
            else if (node.InterpolationData is null)
            {
                // All fill-ins, if any, are strings, and none of them have alignment or format specifiers.
                // We can lower to a more efficient string concatenation
                // The normal pattern for lowering is to lower subtrees before the enclosing tree. However in this case
                // we want to lower the entire concatenation so we get the optimizations done by that lowering (e.g. constant folding).
 
                int length = node.Parts.Length;
                if (length == 0)
                {
                    // $"" -> ""
                    return _factory.StringLiteral("");
                }
 
                BoundExpression? result = null;
                for (int i = 0; i < length; i++)
                {
                    var part = node.Parts[i];
                    if (part is BoundStringInsert fillin)
                    {
                        // this is one of the filled-in expressions
                        part = fillin.Value;
                    }
                    else
                    {
                        // this is one of the literal parts
                        Debug.Assert(part is BoundLiteral && part.ConstantValueOpt?.StringValue is not null);
                        part = _factory.StringLiteral(part.ConstantValueOpt.StringValue);
                    }
 
                    result = result == null ?
                        part :
                        _factory.Binary(BinaryOperatorKind.StringConcatenation, node.Type, result, part);
                }
 
                Debug.Assert(result is not null);
 
                // We need to ensure that the result of the interpolated string is not null. If the single part has a non-null constant value
                // or is itself an interpolated string (which by proxy cannot be null), then there's nothing else that needs to be done. Otherwise,
                // we need to test for null and ensure "" if it is.
                if (length == 1 && result is not ({ Kind: BoundKind.InterpolatedString } or { ConstantValueOpt.IsString: true }))
                {
                    Debug.Assert(result is not null);
                    Debug.Assert(result.Type is not null);
                    Debug.Assert(result.Type.SpecialType == SpecialType.System_String || result.Type.IsErrorType());
                    var placeholder = new BoundValuePlaceholder(result.Syntax, result.Type);
                    result = new BoundNullCoalescingOperator(result.Syntax, result, _factory.StringLiteral(""), leftPlaceholder: placeholder, leftConversion: placeholder, BoundNullCoalescingOperatorResultKind.LeftType, @checked: false, result.Type) { WasCompilerGenerated = true };
                }
 
                return VisitExpression(result);
            }
            else
            {
                //
                // We lower an interpolated string into an invocation of String.Format.  For example, we translate the expression
                //
                //     $"Jenny don\'t change your number { 8675309 }"
                //
                // into
                //
                //     String.Format("Jenny don\'t change your number {0}", new object[] { 8675309 })
                //
 
                Debug.Assert(node.InterpolationData is { Construction: not null });
                return VisitExpression(node.InterpolationData.GetValueOrDefault().Construction);
            }
        }
 
        private BoundExpression LowerPartsToString(InterpolatedStringHandlerData data, ImmutableArray<BoundExpression> parts, SyntaxNode syntax, TypeSymbol type)
        {
            // If we can lower to the builder pattern, do so.
            InterpolationHandlerResult result = RewriteToInterpolatedStringHandlerPattern(data, parts, syntax);
 
            // resultTemp = builderTemp.ToStringAndClear();
            var toStringAndClear = (MethodSymbol)Binder.GetWellKnownTypeMember(_compilation, WellKnownMember.System_Runtime_CompilerServices_DefaultInterpolatedStringHandler__ToStringAndClear, _diagnostics, syntax: syntax);
            BoundExpression toStringAndClearCall = toStringAndClear is not null
                ? BoundCall.Synthesized(syntax, result.HandlerTemp, initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown, toStringAndClear)
                : new BoundBadExpression(syntax, LookupResultKind.Empty, symbols: ImmutableArray<Symbol?>.Empty, childBoundNodes: ImmutableArray<BoundExpression>.Empty, type);
 
            return result.WithFinalResult(toStringAndClearCall);
        }
 
        [Conditional("DEBUG")]
        private static void AssertNoImplicitInterpolatedStringHandlerConversions(ImmutableArray<BoundExpression> arguments, bool allowConversionsWithNoContext = false)
        {
            if (allowConversionsWithNoContext)
            {
                foreach (var arg in arguments)
                {
                    if (arg is BoundConversion { Conversion: { Kind: ConversionKind.InterpolatedStringHandler }, ExplicitCastInCode: false, Operand: var operand })
                    {
                        var data = operand.GetInterpolatedStringHandlerData();
                        Debug.Assert(((BoundObjectCreationExpression)data.Construction).Arguments.All(
                            a => a is BoundInterpolatedStringArgumentPlaceholder { ArgumentIndex: BoundInterpolatedStringArgumentPlaceholder.TrailingConstructorValidityParameter }
                                      or not BoundInterpolatedStringArgumentPlaceholder));
                    }
                }
            }
            else
            {
                Debug.Assert(arguments.All(arg => arg is not BoundConversion { Conversion: { IsInterpolatedStringHandler: true }, ExplicitCastInCode: false }));
            }
        }
 
        private readonly struct InterpolationHandlerResult
        {
            private readonly ImmutableArray<BoundStatement> _statements;
            private readonly ImmutableArray<BoundExpression> _expressions;
            private readonly LocalRewriter _rewriter;
            private readonly LocalSymbol? _outTemp;
 
            public readonly BoundLocal HandlerTemp;
 
            public InterpolationHandlerResult(ImmutableArray<BoundStatement> statements, BoundLocal handlerTemp, LocalSymbol outTemp, LocalRewriter rewriter)
            {
                _statements = statements;
                _expressions = default;
                _outTemp = outTemp;
                HandlerTemp = handlerTemp;
                _rewriter = rewriter;
            }
 
            public InterpolationHandlerResult(ImmutableArray<BoundExpression> expressions, BoundLocal handlerTemp, LocalSymbol? outTemp, LocalRewriter rewriter)
            {
                _statements = default;
                _expressions = expressions;
                _outTemp = outTemp;
                HandlerTemp = handlerTemp;
                _rewriter = rewriter;
            }
 
            public BoundExpression WithFinalResult(BoundExpression result)
            {
                Debug.Assert(_statements.IsDefault ^ _expressions.IsDefault);
                var locals = _outTemp != null
                    ? ImmutableArray.Create(HandlerTemp.LocalSymbol, _outTemp)
                    : ImmutableArray.Create(HandlerTemp.LocalSymbol);
 
                if (_statements.IsDefault)
                {
                    return _rewriter._factory.Sequence(locals, _expressions, result);
                }
                else
                {
                    _rewriter._needsSpilling = true;
                    return _rewriter._factory.SpillSequence(locals, _statements, result);
                }
            }
        }
    }
}