File: Binder\Binder_TupleOperators.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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp
{
    internal partial class Binder
    {
        /// <summary>
        /// If the left and right are tuples of matching cardinality, we'll try to bind the operator element-wise.
        /// When that succeeds, the element-wise conversions are collected. We keep them for semantic model.
        /// The element-wise binary operators are collected and stored as a tree for lowering.
        /// </summary>
        private BoundTupleBinaryOperator BindTupleBinaryOperator(BinaryExpressionSyntax node, BinaryOperatorKind kind,
            BoundExpression left, BoundExpression right, BindingDiagnosticBag diagnostics)
        {
            TupleBinaryOperatorInfo.Multiple operators = BindTupleBinaryOperatorNestedInfo(node, kind, left, right, diagnostics);
 
            BoundExpression convertedLeft = ApplyConvertedTypes(left, operators, isRight: false, diagnostics);
            BoundExpression convertedRight = ApplyConvertedTypes(right, operators, isRight: true, diagnostics);
 
            TypeSymbol resultType = GetSpecialType(SpecialType.System_Boolean, diagnostics, node);
 
            return new BoundTupleBinaryOperator(node, convertedLeft, convertedRight, kind, operators, resultType);
        }
 
        private BoundExpression ApplyConvertedTypes(BoundExpression expr, TupleBinaryOperatorInfo @operator, bool isRight, BindingDiagnosticBag diagnostics)
        {
            TypeSymbol convertedType = isRight ? @operator.RightConvertedTypeOpt : @operator.LeftConvertedTypeOpt;
 
            if (convertedType is null)
            {
                // Note: issues with default will already have been reported by BindSimpleBinaryOperator (ie. we couldn't find a suitable element-wise operator)
                if (@operator.InfoKind == TupleBinaryOperatorInfoKind.Multiple && expr is BoundTupleLiteral tuple)
                {
                    // Although the tuple will remain typeless, we'll give elements converted types as possible
                    var multiple = (TupleBinaryOperatorInfo.Multiple)@operator;
                    if (multiple.Operators.Length == 0)
                    {
                        return BindToNaturalType(expr, diagnostics, reportNoTargetType: false);
                    }
 
                    ImmutableArray<BoundExpression> arguments = tuple.Arguments;
                    int length = arguments.Length;
                    Debug.Assert(length == multiple.Operators.Length);
 
                    var builder = ArrayBuilder<BoundExpression>.GetInstance(length);
                    for (int i = 0; i < length; i++)
                    {
                        builder.Add(ApplyConvertedTypes(arguments[i], multiple.Operators[i], isRight, diagnostics));
                    }
 
                    return new BoundConvertedTupleLiteral(
                        tuple.Syntax, tuple, wasTargetTyped: false, builder.ToImmutableAndFree(), tuple.ArgumentNamesOpt, tuple.InferredNamesOpt, tuple.Type, tuple.HasErrors);
                }
 
                // This element isn't getting a converted type
                return BindToNaturalType(expr, diagnostics, reportNoTargetType: false);
            }
 
            // We were able to determine a converted type (for this tuple literal or element), we can just convert to it
            return GenerateConversionForAssignment(convertedType, expr, diagnostics);
        }
 
        /// <summary>
        /// Binds:
        /// 1. dynamically, if either side is dynamic
        /// 2. as tuple binary operator, if both sides are tuples of matching cardinalities
        /// 3. as regular binary operator otherwise
        /// </summary>
        private TupleBinaryOperatorInfo BindTupleBinaryOperatorInfo(BinaryExpressionSyntax node, BinaryOperatorKind kind,
            BoundExpression left, BoundExpression right, BindingDiagnosticBag diagnostics)
        {
            TypeSymbol leftType = left.Type;
            TypeSymbol rightType = right.Type;
 
            if ((object)leftType != null && leftType.IsDynamic() || (object)rightType != null && rightType.IsDynamic())
            {
                return BindTupleDynamicBinaryOperatorSingleInfo(node, kind, left, right, diagnostics);
            }
 
            if (IsTupleBinaryOperation(left, right))
            {
                return BindTupleBinaryOperatorNestedInfo(node, kind, left, right, diagnostics);
            }
 
            BoundExpression comparison = BindSimpleBinaryOperator(node, diagnostics, left, right, leaveUnconvertedIfInterpolatedString: false);
            switch (comparison)
            {
                case BoundLiteral _:
                    // this case handles `null == null` and the like
                    return new TupleBinaryOperatorInfo.NullNull(kind);
 
                case BoundBinaryOperator binary:
                    PrepareBoolConversionAndTruthOperator(binary.Type, node, kind, diagnostics,
                        out BoundExpression conversionIntoBoolOperator, out BoundValuePlaceholder conversionIntoBoolOperatorPlaceholder,
                        out UnaryOperatorSignature boolOperator);
                    CheckConstraintLanguageVersionAndRuntimeSupportForOperator(node, boolOperator.Method, isUnsignedRightShift: false, boolOperator.ConstrainedToTypeOpt, diagnostics);
 
                    return new TupleBinaryOperatorInfo.Single(binary.Left.Type, binary.Right.Type, binary.OperatorKind, binary.Method, binary.ConstrainedToType,
                        conversionIntoBoolOperatorPlaceholder, conversionIntoBoolOperator, boolOperator);
 
                default:
                    throw ExceptionUtilities.UnexpectedValue(comparison);
            }
        }
 
        /// <summary>
        /// If an element-wise binary operator returns a non-bool type, we will either:
        /// - prepare a conversion to bool if one exists
        /// - prepare a truth operator: op_false in the case of an equality (<c>a == b</c> will be lowered to <c>!((a == b).op_false)</c>) or op_true in the case of inequality,
        ///     with the conversion being used for its input.
        /// </summary>
        private void PrepareBoolConversionAndTruthOperator(TypeSymbol type, BinaryExpressionSyntax node, BinaryOperatorKind binaryOperator, BindingDiagnosticBag diagnostics,
            out BoundExpression conversionForBool, out BoundValuePlaceholder conversionForBoolPlaceholder, out UnaryOperatorSignature boolOperator)
        {
            // Is the operand implicitly convertible to bool?
 
            CompoundUseSiteInfo<AssemblySymbol> useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics);
            TypeSymbol boolean = GetSpecialType(SpecialType.System_Boolean, diagnostics, node);
            Conversion conversion = this.Conversions.ClassifyImplicitConversionFromType(type, boolean, ref useSiteInfo);
            diagnostics.Add(node, useSiteInfo);
 
            if (conversion.IsImplicit)
            {
                conversionForBoolPlaceholder = new BoundValuePlaceholder(node, type).MakeCompilerGenerated();
                conversionForBool = CreateConversion(node, conversionForBoolPlaceholder, conversion, isCast: false, conversionGroupOpt: null, boolean, diagnostics);
                boolOperator = default;
                return;
            }
 
            // It was not. Does it implement operator true (or false)?
 
            UnaryOperatorKind boolOpKind;
            switch (binaryOperator)
            {
                case BinaryOperatorKind.Equal:
                    boolOpKind = UnaryOperatorKind.False;
                    break;
                case BinaryOperatorKind.NotEqual:
                    boolOpKind = UnaryOperatorKind.True;
                    break;
                default:
                    throw ExceptionUtilities.UnexpectedValue(binaryOperator);
            }
 
            LookupResultKind resultKind;
            ImmutableArray<MethodSymbol> originalUserDefinedOperators;
            BoundExpression comparisonResult = new BoundTupleOperandPlaceholder(node, type);
            UnaryOperatorAnalysisResult best = this.UnaryOperatorOverloadResolution(boolOpKind, comparisonResult, node, diagnostics, out resultKind, out originalUserDefinedOperators);
            if (best.HasValue)
            {
                conversionForBoolPlaceholder = new BoundValuePlaceholder(node, type).MakeCompilerGenerated();
                conversionForBool = CreateConversion(node, conversionForBoolPlaceholder, best.Conversion, isCast: false, conversionGroupOpt: null, best.Signature.OperandType, diagnostics);
                boolOperator = best.Signature;
                return;
            }
 
            // It did not. Give a "not convertible to bool" error.
 
            GenerateImplicitConversionError(diagnostics, node, conversion, comparisonResult, boolean);
            conversionForBoolPlaceholder = null;
            conversionForBool = null;
            boolOperator = default;
            return;
        }
 
        private TupleBinaryOperatorInfo BindTupleDynamicBinaryOperatorSingleInfo(BinaryExpressionSyntax node, BinaryOperatorKind kind,
            BoundExpression left, BoundExpression right, BindingDiagnosticBag diagnostics)
        {
            // This method binds binary == and != operators where one or both of the operands are dynamic.
            Debug.Assert((object)left.Type != null && left.Type.IsDynamic() || (object)right.Type != null && right.Type.IsDynamic());
 
            bool hasError = false;
            if (!IsLegalDynamicOperand(left) || !IsLegalDynamicOperand(right))
            {
                // Operator '{0}' cannot be applied to operands of type '{1}' and '{2}'
                Error(diagnostics, ErrorCode.ERR_BadBinaryOps, node, node.OperatorToken.Text, left.Display, right.Display);
                hasError = true;
            }
 
            BinaryOperatorKind elementOperatorKind = hasError ? kind : kind.WithType(BinaryOperatorKind.Dynamic);
            TypeSymbol dynamicType = hasError ? CreateErrorType() : Compilation.DynamicType;
 
            // We'll want to dynamically invoke operators op_true (/op_false) for equality (/inequality) comparison, but we don't need
            // to prepare either a conversion or a truth operator. Those can just be synthesized during lowering.
            return new TupleBinaryOperatorInfo.Single(dynamicType, dynamicType, elementOperatorKind,
                methodSymbolOpt: null, constrainedToTypeOpt: null, conversionForBoolPlaceholder: null, conversionForBool: null, boolOperator: default);
        }
 
        private TupleBinaryOperatorInfo.Multiple BindTupleBinaryOperatorNestedInfo(BinaryExpressionSyntax node, BinaryOperatorKind kind,
            BoundExpression left, BoundExpression right, BindingDiagnosticBag diagnostics)
        {
            left = GiveTupleTypeToDefaultLiteralIfNeeded(left, right.Type);
            right = GiveTupleTypeToDefaultLiteralIfNeeded(right, left.Type);
 
            if (left.IsLiteralDefaultOrImplicitObjectCreation() ||
                right.IsLiteralDefaultOrImplicitObjectCreation())
            {
                ReportBinaryOperatorError(node, diagnostics, node.OperatorToken, left, right, LookupResultKind.Ambiguous);
                return TupleBinaryOperatorInfo.Multiple.ErrorInstance;
            }
 
            // Aside from default (which we fixed or ruled out above) and tuple literals,
            // we must have typed expressions at this point
            Debug.Assert((object)left.Type != null || left.Kind == BoundKind.TupleLiteral);
            Debug.Assert((object)right.Type != null || right.Kind == BoundKind.TupleLiteral);
 
            int leftCardinality = GetTupleCardinality(left);
            int rightCardinality = GetTupleCardinality(right);
 
            if (leftCardinality != rightCardinality)
            {
                Error(diagnostics, ErrorCode.ERR_TupleSizesMismatchForBinOps, node, leftCardinality, rightCardinality);
                return TupleBinaryOperatorInfo.Multiple.ErrorInstance;
            }
 
            (ImmutableArray<BoundExpression> leftParts, ImmutableArray<string> leftNames) = GetTupleArgumentsOrPlaceholders(left);
            (ImmutableArray<BoundExpression> rightParts, ImmutableArray<string> rightNames) = GetTupleArgumentsOrPlaceholders(right);
            ReportNamesMismatchesIfAny(left, right, leftNames, rightNames, diagnostics);
 
            int length = leftParts.Length;
            Debug.Assert(length == rightParts.Length);
 
            var operatorsBuilder = ArrayBuilder<TupleBinaryOperatorInfo>.GetInstance(length);
 
            for (int i = 0; i < length; i++)
            {
                operatorsBuilder.Add(BindTupleBinaryOperatorInfo(node, kind, leftParts[i], rightParts[i], diagnostics));
            }
 
            var compilation = this.Compilation;
            var operators = operatorsBuilder.ToImmutableAndFree();
 
            // typeless tuple literals are not nullable
            bool leftNullable = left.Type?.IsNullableType() == true;
            bool rightNullable = right.Type?.IsNullableType() == true;
            bool isNullable = leftNullable || rightNullable;
 
            TypeSymbol leftTupleType = MakeConvertedType(operators.SelectAsArray(o => o.LeftConvertedTypeOpt), node.Left, leftParts, leftNames, isNullable, compilation, diagnostics);
            TypeSymbol rightTupleType = MakeConvertedType(operators.SelectAsArray(o => o.RightConvertedTypeOpt), node.Right, rightParts, rightNames, isNullable, compilation, diagnostics);
 
            return new TupleBinaryOperatorInfo.Multiple(operators, leftTupleType, rightTupleType);
        }
 
        /// <summary>
        /// If an element in a tuple literal has an explicit name which doesn't match the name on the other side, we'll warn.
        /// The user can either remove the name, or fix it.
        ///
        /// This method handles two expressions, each of which is either a tuple literal or an expression with tuple type.
        /// In a tuple literal, each element can have an explicit name, an inferred name or no name.
        /// In an expression of tuple type, each element can have a name or not.
        /// </summary>
        private static void ReportNamesMismatchesIfAny(BoundExpression left, BoundExpression right,
            ImmutableArray<string> leftNames, ImmutableArray<string> rightNames, BindingDiagnosticBag diagnostics)
        {
            bool leftIsTupleLiteral = left is BoundTupleExpression;
            bool rightIsTupleLiteral = right is BoundTupleExpression;
 
            if (!leftIsTupleLiteral && !rightIsTupleLiteral)
            {
                return;
            }
 
            bool leftNoNames = leftNames.IsDefault;
            bool rightNoNames = rightNames.IsDefault;
 
            if (leftNoNames && rightNoNames)
            {
                return;
            }
 
            Debug.Assert(leftNoNames || rightNoNames || leftNames.Length == rightNames.Length);
 
            ImmutableArray<bool> leftInferred = leftIsTupleLiteral ? ((BoundTupleExpression)left).InferredNamesOpt : default;
            bool leftNoInferredNames = leftInferred.IsDefault;
 
            ImmutableArray<bool> rightInferred = rightIsTupleLiteral ? ((BoundTupleExpression)right).InferredNamesOpt : default;
            bool rightNoInferredNames = rightInferred.IsDefault;
 
            int length = leftNoNames ? rightNames.Length : leftNames.Length;
            for (int i = 0; i < length; i++)
            {
                string leftName = leftNoNames ? null : leftNames[i];
                string rightName = rightNoNames ? null : rightNames[i];
 
                bool different = string.CompareOrdinal(rightName, leftName) != 0;
                if (!different)
                {
                    continue;
                }
 
                bool leftWasInferred = leftNoInferredNames ? false : leftInferred[i];
                bool rightWasInferred = rightNoInferredNames ? false : rightInferred[i];
 
                bool leftComplaint = leftIsTupleLiteral && leftName != null && !leftWasInferred;
                bool rightComplaint = rightIsTupleLiteral && rightName != null && !rightWasInferred;
 
                if (!leftComplaint && !rightComplaint)
                {
                    // No complaints, let's move on
                    continue;
                }
 
                // When in doubt, we'll complain on the right side if it's a literal
                bool useRight = (leftComplaint && rightComplaint) ? rightIsTupleLiteral : rightComplaint;
                Location location = ((BoundTupleExpression)(useRight ? right : left)).Arguments[i].Syntax.Parent.Location;
                string complaintName = useRight ? rightName : leftName;
 
                diagnostics.Add(ErrorCode.WRN_TupleBinopLiteralNameMismatch, location, complaintName);
            }
        }
 
        internal static BoundExpression GiveTupleTypeToDefaultLiteralIfNeeded(BoundExpression expr, TypeSymbol targetType)
        {
            if (!expr.IsLiteralDefault() || targetType is null)
            {
                return expr;
            }
 
            Debug.Assert(targetType.StrippedType().IsTupleType);
            return new BoundDefaultExpression(expr.Syntax, targetType);
        }
 
        private static bool IsTupleBinaryOperation(BoundExpression left, BoundExpression right)
        {
            bool leftDefaultOrNew = left.IsLiteralDefaultOrImplicitObjectCreation();
            bool rightDefaultOrNew = right.IsLiteralDefaultOrImplicitObjectCreation();
            if (leftDefaultOrNew && rightDefaultOrNew)
            {
                return false;
            }
 
            return (GetTupleCardinality(left) > 1 || leftDefaultOrNew) &&
                   (GetTupleCardinality(right) > 1 || rightDefaultOrNew);
        }
 
        private static int GetTupleCardinality(BoundExpression expr)
        {
            if (expr is BoundTupleExpression tuple)
            {
                return tuple.Arguments.Length;
            }
 
            TypeSymbol type = expr.Type;
            if (type is null)
            {
                return -1;
            }
 
            if (type.StrippedType() is { IsTupleType: true } tupleType)
            {
                return tupleType.TupleElementTypesWithAnnotations.Length;
            }
 
            return -1;
        }
 
        /// <summary>
        /// Given a tuple literal or expression, we'll get two arrays:
        /// - the elements from the literal, or some placeholder with proper type (for tuple expressions)
        /// - the elements' names
        /// </summary>
        private static (ImmutableArray<BoundExpression> Elements, ImmutableArray<string> Names) GetTupleArgumentsOrPlaceholders(BoundExpression expr)
        {
            if (expr is BoundTupleExpression tuple)
            {
                return (tuple.Arguments, tuple.ArgumentNamesOpt);
            }
 
            // placeholder bound nodes with the proper types are sufficient to bind the element-wise binary operators
            TypeSymbol tupleType = expr.Type.StrippedType();
            ImmutableArray<BoundExpression> placeholders = tupleType.TupleElementTypesWithAnnotations
                .SelectAsArray((t, s) => (BoundExpression)new BoundTupleOperandPlaceholder(s, t.Type), expr.Syntax);
 
            return (placeholders, tupleType.TupleElementNames);
        }
 
        /// <summary>
        /// Make a tuple type (with appropriate nesting) from the types (on the left or on the right) collected
        /// from binding element-wise binary operators.
        /// If any of the elements is typeless, then the tuple is typeless too.
        /// </summary>
        private TypeSymbol MakeConvertedType(ImmutableArray<TypeSymbol> convertedTypes, CSharpSyntaxNode syntax,
            ImmutableArray<BoundExpression> elements, ImmutableArray<string> names,
            bool isNullable, CSharpCompilation compilation, BindingDiagnosticBag diagnostics)
        {
            foreach (var convertedType in convertedTypes)
            {
                if (convertedType is null)
                {
                    return null;
                }
            }
 
            ImmutableArray<Location> elementLocations = elements.SelectAsArray(e => e.Syntax.Location);
 
            var tuple = NamedTypeSymbol.CreateTuple(locationOpt: null,
                elementTypesWithAnnotations: convertedTypes.SelectAsArray(t => TypeWithAnnotations.Create(t)),
                elementLocations, elementNames: names, compilation,
                shouldCheckConstraints: true, includeNullability: false, errorPositions: default, syntax, diagnostics);
 
            if (!isNullable)
            {
                return tuple;
            }
 
            // Any violated constraints on nullable tuples would have been reported already
            NamedTypeSymbol nullableT = GetSpecialType(SpecialType.System_Nullable_T, diagnostics, syntax);
            return nullableT.Construct(tuple);
        }
    }
}