File: Lowering\LocalRewriter\LocalRewriter_TupleBinaryOperator.cs
Web Access
Project: ..\..\..\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 System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp
{
    internal sealed partial class LocalRewriter
    {
        /// <summary>
        /// Rewrite <c>GetTuple() == (1, 2)</c> to <c>tuple.Item1 == 1 &amp;&amp; tuple.Item2 == 2</c>.
        /// Also supports the != operator, nullable and nested tuples.
        ///
        /// Note that all the side-effects for visible expressions are evaluated first and from left to right. The initialization phase
        /// contains side-effects for:
        /// - single elements in tuple literals, like <c>a</c> in <c>(a, ...) == (...)</c> for example
        /// - nested expressions that aren't tuple literals, like <c>GetTuple()</c> in <c>(..., GetTuple()) == (..., (..., ...))</c>
        /// On the other hand, <c>Item1</c> and <c>Item2</c> of <c>GetTuple()</c> are not saved as part of the initialization phase of <c>GetTuple() == (..., ...)</c>
        ///
        /// Element-wise conversions occur late, together with the element-wise comparisons. They might not be evaluated.
        /// </summary>
        public override BoundNode VisitTupleBinaryOperator(BoundTupleBinaryOperator node)
        {
            var boolType = node.Type; // we can re-use the bool type
            var initEffects = ArrayBuilder<BoundExpression>.GetInstance();
            var temps = ArrayBuilder<LocalSymbol>.GetInstance();
 
            BoundExpression newLeft = ReplaceTerminalElementsWithTemps(node.Left, node.Operators, initEffects, temps);
            BoundExpression newRight = ReplaceTerminalElementsWithTemps(node.Right, node.Operators, initEffects, temps);
 
            var returnValue = RewriteTupleNestedOperators(node.Operators, newLeft, newRight, boolType, temps, node.OperatorKind);
            BoundExpression result = _factory.Sequence(temps.ToImmutableAndFree(), initEffects.ToImmutableAndFree(), returnValue);
            return result;
        }
 
        private bool IsLikeTupleExpression(BoundExpression expr, [NotNullWhen(true)] out BoundTupleExpression? tuple)
        {
            switch (expr)
            {
                case BoundTupleExpression t:
                    tuple = t;
                    return true;
                case BoundConversion { Conversion: { Kind: ConversionKind.Identity }, Operand: var o }:
                    return IsLikeTupleExpression(o, out tuple);
                case BoundConversion { Conversion: { Kind: ConversionKind.ImplicitTupleLiteral }, Operand: var o }:
                    // The compiler produces the implicit tuple literal conversion as an identity conversion for
                    // the benefit of the semantic model only.
                    Debug.Assert(expr.Type == (object?)o.Type || expr.Type is { } && expr.Type.Equals(o.Type, TypeCompareKind.AllIgnoreOptions));
                    return IsLikeTupleExpression(o, out tuple);
                case BoundConversion { Conversion: { Kind: var kind } c, Operand: var o } conversion when
                        c.IsTupleConversion || c.IsTupleLiteralConversion:
                    {
                        // Push tuple conversions down to the elements.
                        if (!IsLikeTupleExpression(o, out tuple)) return false;
                        var underlyingConversions = c.UnderlyingConversions;
                        c.AssertUnderlyingConversionsChecked();
                        var resultTypes = conversion.Type.TupleElementTypesWithAnnotations;
                        var builder = ArrayBuilder<BoundExpression>.GetInstance(tuple.Arguments.Length);
                        for (int i = 0; i < tuple.Arguments.Length; i++)
                        {
                            var element = tuple.Arguments[i];
                            var elementConversion = underlyingConversions[i];
                            var elementType = resultTypes[i].Type;
                            var newArgument = new BoundConversion(
                                syntax: expr.Syntax,
                                operand: element,
                                conversion: elementConversion,
                                @checked: conversion.Checked,
                                explicitCastInCode: conversion.ExplicitCastInCode,
                                conversionGroupOpt: null,
                                constantValueOpt: null,
                                type: elementType,
                                hasErrors: conversion.HasErrors);
                            builder.Add(newArgument);
                        }
                        var newArguments = builder.ToImmutableAndFree();
                        tuple = new BoundConvertedTupleLiteral(
                            tuple.Syntax, sourceTuple: null, wasTargetTyped: true, newArguments, ImmutableArray<string?>.Empty,
                            ImmutableArray<bool>.Empty, conversion.Type, conversion.HasErrors);
                        return true;
                    }
                case BoundConversion { Conversion: { Kind: var kind }, Operand: var o } when
                        (kind == ConversionKind.ImplicitNullable || kind == ConversionKind.ExplicitNullable) &&
                        expr.Type is { } exprType && exprType.IsNullableType() && exprType.StrippedType().Equals(o.Type, TypeCompareKind.AllIgnoreOptions):
                    return IsLikeTupleExpression(o, out tuple);
                default:
                    tuple = null;
                    return false;
            }
        }
 
        private BoundExpression PushDownImplicitTupleConversion(
            BoundExpression expr,
            ArrayBuilder<BoundExpression> initEffects,
            ArrayBuilder<LocalSymbol> temps)
        {
            if (expr is BoundConversion { ConversionKind: ConversionKind.ImplicitTuple, Conversion: var conversion } boundConversion)
            {
                // We push an implicit tuple converion down to its elements
                var syntax = boundConversion.Syntax;
                Debug.Assert(expr.Type is { });
                var destElementTypes = expr.Type.TupleElementTypesWithAnnotations;
                var numElements = destElementTypes.Length;
                Debug.Assert(boundConversion.Operand.Type is { });
                var srcElementFields = boundConversion.Operand.Type.TupleElements;
                var fieldAccessorsBuilder = ArrayBuilder<BoundExpression>.GetInstance(numElements);
                var savedTuple = DeferSideEffectingArgumentToTempForTupleEquality(LowerConversions(boundConversion.Operand), initEffects, temps);
                var elementConversions = conversion.UnderlyingConversions;
                conversion.AssertUnderlyingConversionsChecked();
 
                for (int i = 0; i < numElements; i++)
                {
                    var fieldAccess = MakeTupleFieldAccessAndReportUseSiteDiagnostics(savedTuple, syntax, srcElementFields[i]);
                    var convertedFieldAccess = new BoundConversion(
                        syntax, fieldAccess, elementConversions[i], boundConversion.Checked, boundConversion.ExplicitCastInCode, null, null, destElementTypes[i].Type, boundConversion.HasErrors);
                    fieldAccessorsBuilder.Add(convertedFieldAccess);
                }
 
                return new BoundConvertedTupleLiteral(
                    syntax, sourceTuple: null, wasTargetTyped: true, fieldAccessorsBuilder.ToImmutableAndFree(), ImmutableArray<string?>.Empty,
                    ImmutableArray<bool>.Empty, expr.Type, expr.HasErrors);
            }
 
            return expr;
        }
 
        /// <summary>
        /// Walk down tuple literals and replace all the side-effecting elements that need saving with temps.
        /// Expressions that are not tuple literals need saving, as are tuple literals that are involved in
        /// a simple comparison rather than a tuple comparison.
        /// </summary>
        private BoundExpression ReplaceTerminalElementsWithTemps(
            BoundExpression expr,
            TupleBinaryOperatorInfo operators,
            ArrayBuilder<BoundExpression> initEffects,
            ArrayBuilder<LocalSymbol> temps)
        {
            if (operators.InfoKind == TupleBinaryOperatorInfoKind.Multiple)
            {
                expr = PushDownImplicitTupleConversion(expr, initEffects, temps);
                if (IsLikeTupleExpression(expr, out BoundTupleExpression? tuple))
                {
                    // Example:
                    // in `(expr1, expr2) == (..., ...)` we need to save `expr1` and `expr2`
                    var multiple = (TupleBinaryOperatorInfo.Multiple)operators;
                    var builder = ArrayBuilder<BoundExpression>.GetInstance(tuple.Arguments.Length);
                    for (int i = 0; i < tuple.Arguments.Length; i++)
                    {
                        var argument = tuple.Arguments[i];
                        var newArgument = ReplaceTerminalElementsWithTemps(argument, multiple.Operators[i], initEffects, temps);
                        builder.Add(newArgument);
                    }
 
                    var newArguments = builder.ToImmutableAndFree();
                    return new BoundConvertedTupleLiteral(
                        tuple.Syntax, sourceTuple: null, wasTargetTyped: false, newArguments, ImmutableArray<string?>.Empty,
                        ImmutableArray<bool>.Empty, tuple.Type, tuple.HasErrors);
                }
            }
 
            // Examples:
            // in `expr == (..., ...)` we need to save `expr` because it's not a tuple literal
            // in `(..., expr) == (..., (..., ...))` we need to save `expr` because it is used in a simple comparison
            return DeferSideEffectingArgumentToTempForTupleEquality(expr, initEffects, temps);
        }
 
        /// <summary>
        /// Evaluate side effects into a temp, if necessary.  If there is an implicit user-defined
        /// conversion operation near the top of the arg, preserve that in the returned expression to be evaluated later.
        /// Conversions at the head of the result are unlowered, though the nested arguments within it are lowered.
        /// That resulting expression must be passed through <see cref="LowerConversions(BoundExpression)"/> to
        /// complete the lowering.
        /// </summary>
        private BoundExpression DeferSideEffectingArgumentToTempForTupleEquality(
            BoundExpression expr,
            ArrayBuilder<BoundExpression> effects,
            ArrayBuilder<LocalSymbol> temps,
            bool enclosingConversionWasExplicit = false)
        {
            switch (expr)
            {
                case { ConstantValueOpt: { } }:
                    return VisitExpression(expr);
                case BoundConversion { Conversion: { Kind: ConversionKind.DefaultLiteral } }:
                    // This conversion can be performed lazily, but need not be saved.  It is treated as non-side-effecting.
                    return EvaluateSideEffectingArgumentToTemp(expr, effects, temps);
                case BoundConversion { Conversion: { Kind: var conversionKind } conversion } when conversionMustBePerformedOnOriginalExpression(conversionKind):
                    // Some conversions cannot be performed on a copy of the argument and must be done early.
                    return EvaluateSideEffectingArgumentToTemp(expr, effects, temps);
                case BoundConversion { Conversion: { IsUserDefined: true } } conv when conv.ExplicitCastInCode || enclosingConversionWasExplicit:
                    // A user-defined conversion triggered by a cast must be performed early.
                    return EvaluateSideEffectingArgumentToTemp(expr, effects, temps);
                case BoundConversion conv:
                    {
                        // other conversions are deferred
                        var deferredOperand = DeferSideEffectingArgumentToTempForTupleEquality(conv.Operand, effects, temps, conv.ExplicitCastInCode || enclosingConversionWasExplicit);
                        return conv.UpdateOperand(deferredOperand);
                    }
                case BoundObjectCreationExpression { Arguments: { Length: 0 }, Type: { } eType } _ when eType.IsNullableType():
                    return new BoundLiteral(expr.Syntax, ConstantValue.Null, expr.Type);
                case BoundObjectCreationExpression { Arguments: { Length: 1 }, Type: { } eType } creation when eType.IsNullableType():
                    {
                        var deferredOperand = DeferSideEffectingArgumentToTempForTupleEquality(
                            creation.Arguments[0], effects, temps, enclosingConversionWasExplicit: true);
                        var conversion = Conversion.MakeNullableConversion(ConversionKind.ImplicitNullable, Conversion.Identity);
                        conversion.MarkUnderlyingConversionsChecked();
                        return new BoundConversion(
                            syntax: expr.Syntax, operand: deferredOperand,
                            conversion: conversion,
                            @checked: false, explicitCastInCode: true, conversionGroupOpt: null, constantValueOpt: null,
                            type: eType, hasErrors: expr.HasErrors);
                    }
                default:
                    // When in doubt, evaluate early to a temp.
                    return EvaluateSideEffectingArgumentToTemp(expr, effects, temps);
            }
 
            bool conversionMustBePerformedOnOriginalExpression(ConversionKind kind)
            {
                // These are conversions from-expression that
                // must be performed on the original expression, not on a copy of it.
                switch (kind)
                {
                    case ConversionKind.AnonymousFunction:       // a lambda cannot be saved without a target type
                    case ConversionKind.MethodGroup:             // similarly for a method group
                    case ConversionKind.InterpolatedString:      // an interpolated string must be saved in interpolated form
                    case ConversionKind.SwitchExpression:        // a switch expression must have its arms converted
                    case ConversionKind.StackAllocToPointerType: // a stack alloc is not well-defined without an enclosing conversion
                    case ConversionKind.ConditionalExpression:   // a conditional expression must have its alternatives converted
                    case ConversionKind.StackAllocToSpanType:
                    case ConversionKind.ObjectCreation:
                        return true;
                    default:
                        return false;
                }
            }
        }
 
        private BoundExpression RewriteTupleOperator(TupleBinaryOperatorInfo @operator,
            BoundExpression left, BoundExpression right, TypeSymbol boolType,
            ArrayBuilder<LocalSymbol> temps, BinaryOperatorKind operatorKind)
        {
            switch (@operator.InfoKind)
            {
                case TupleBinaryOperatorInfoKind.Multiple:
                    return RewriteTupleNestedOperators((TupleBinaryOperatorInfo.Multiple)@operator, left, right, boolType, temps, operatorKind);
 
                case TupleBinaryOperatorInfoKind.Single:
                    return RewriteTupleSingleOperator((TupleBinaryOperatorInfo.Single)@operator, left, right, boolType, operatorKind);
 
                case TupleBinaryOperatorInfoKind.NullNull:
                    var nullnull = (TupleBinaryOperatorInfo.NullNull)@operator;
                    return new BoundLiteral(left.Syntax, ConstantValue.Create(nullnull.Kind == BinaryOperatorKind.Equal), boolType);
 
                default:
                    throw ExceptionUtilities.UnexpectedValue(@operator.InfoKind);
            }
        }
 
        private BoundExpression RewriteTupleNestedOperators(TupleBinaryOperatorInfo.Multiple operators, BoundExpression left, BoundExpression right,
            TypeSymbol boolType, ArrayBuilder<LocalSymbol> temps, BinaryOperatorKind operatorKind)
        {
            // If either left or right is nullable, produce:
            //
            //      // outer sequence
            //      leftHasValue = left.HasValue; (or true if !leftNullable)
            //      leftHasValue == right.HasValue (or true if !rightNullable)
            //          ? leftHasValue ? ... inner sequence ... : true/false
            //          : false/true
            //
            // where inner sequence is:
            //      leftValue = left.GetValueOrDefault(); (or left if !leftNullable)
            //      rightValue = right.GetValueOrDefault(); (or right if !rightNullable)
            //      ... logical expression using leftValue and rightValue ...
            //
            // and true/false and false/true depend on operatorKind (== vs. !=)
            //
            // But if neither is nullable, then just produce the inner sequence.
            //
            // Note: all the temps are created in a single bucket (rather than different scopes of applicability) for simplicity
 
            var outerEffects = ArrayBuilder<BoundExpression>.GetInstance();
            var innerEffects = ArrayBuilder<BoundExpression>.GetInstance();
 
            BoundExpression leftHasValue, leftValue;
            bool isLeftNullable;
            MakeNullableParts(left, temps, innerEffects, outerEffects, saveHasValue: true, out leftHasValue, out leftValue, out isLeftNullable);
 
            BoundExpression rightHasValue, rightValue;
            bool isRightNullable;
            // no need for local for right.HasValue since used once
            MakeNullableParts(right, temps, innerEffects, outerEffects, saveHasValue: false, out rightHasValue, out rightValue, out isRightNullable);
 
            // Produces:
            //     ... logical expression using leftValue and rightValue ...
            BoundExpression logicalExpression = RewriteNonNullableNestedTupleOperators(operators, leftValue, rightValue, boolType, temps, operatorKind);
 
            // Produces:
            //     leftValue = left.GetValueOrDefault(); (or left if !leftNullable)
            //     rightValue = right.GetValueOrDefault(); (or right if !rightNullable)
            //     ... logical expression using leftValue and rightValue ...
            BoundExpression innerSequence = _factory.Sequence(locals: ImmutableArray<LocalSymbol>.Empty, innerEffects.ToImmutableAndFree(), logicalExpression);
 
            if (!isLeftNullable && !isRightNullable)
            {
                // The outer sequence degenerates when we know that both `leftHasValue` and `rightHasValue` are true
                return innerSequence;
            }
 
            bool boolValue = operatorKind == BinaryOperatorKind.Equal; // true/false
 
            if (rightHasValue.ConstantValueOpt == ConstantValue.False)
            {
                // The outer sequence degenerates when we known that `rightHasValue` is false
                // Produce: !leftHasValue (or leftHasValue for inequality comparison)
                return _factory.Sequence(ImmutableArray<LocalSymbol>.Empty, outerEffects.ToImmutableAndFree(),
                    result: boolValue ? _factory.Not(leftHasValue) : leftHasValue);
            }
 
            if (leftHasValue.ConstantValueOpt == ConstantValue.False)
            {
                // The outer sequence degenerates when we known that `leftHasValue` is false
                // Produce: !rightHasValue (or rightHasValue for inequality comparison)
                return _factory.Sequence(ImmutableArray<LocalSymbol>.Empty, outerEffects.ToImmutableAndFree(),
                    result: boolValue ? _factory.Not(rightHasValue) : rightHasValue);
            }
 
            // outer sequence:
            //      leftHasValue == rightHasValue
            //          ? leftHasValue ? ... inner sequence ... : true/false
            //          : false/true
            BoundExpression outerSequence =
                _factory.Sequence(ImmutableArray<LocalSymbol>.Empty, outerEffects.ToImmutableAndFree(),
                    _factory.Conditional(
                        _factory.Binary(BinaryOperatorKind.Equal, boolType, leftHasValue, rightHasValue),
                        _factory.Conditional(leftHasValue, innerSequence, MakeBooleanConstant(right.Syntax, boolValue), boolType),
                        MakeBooleanConstant(right.Syntax, !boolValue),
                        boolType));
 
            return outerSequence;
        }
 
        /// <summary>
        /// Produce a <c>.HasValue</c> and a <c>.GetValueOrDefault()</c> for nullable expressions that are neither always null or
        /// never null, and functionally equivalent parts for other cases.
        /// </summary>
        private void MakeNullableParts(BoundExpression expr, ArrayBuilder<LocalSymbol> temps, ArrayBuilder<BoundExpression> innerEffects,
            ArrayBuilder<BoundExpression> outerEffects, bool saveHasValue, out BoundExpression hasValue, out BoundExpression value, out bool isNullable)
        {
            isNullable = !(expr is BoundTupleExpression) && expr.Type is { } && expr.Type.IsNullableType();
            if (!isNullable)
            {
                hasValue = MakeBooleanConstant(expr.Syntax, true);
                expr = PushDownImplicitTupleConversion(expr, innerEffects, temps);
                value = expr;
                return;
            }
 
            // Optimization for nullable expressions that are always null
            if (NullableNeverHasValue(expr))
            {
                Debug.Assert(expr.Type is { });
                hasValue = MakeBooleanConstant(expr.Syntax, false);
                // Since there is no value in this nullable expression, we don't need to construct a `.GetValueOrDefault()`, `default(T)` will suffice
                value = new BoundDefaultExpression(expr.Syntax, expr.Type.StrippedType());
                return;
            }
 
            // Optimization for nullable expressions that are never null
            if (NullableAlwaysHasValue(expr) is BoundExpression knownValue)
            {
                hasValue = MakeBooleanConstant(expr.Syntax, true);
                // If a tuple conversion, keep its parts around with deferred conversions.
                value = PushDownImplicitTupleConversion(knownValue, innerEffects, temps);
                value = LowerConversions(value);
                isNullable = false;
                return;
            }
 
            // Regular nullable expressions
            hasValue = makeNullableHasValue(expr);
            if (saveHasValue)
            {
                hasValue = MakeTemp(hasValue, temps, outerEffects);
            }
 
            value = MakeValueOrDefaultTemp(expr, temps, innerEffects);
 
            BoundExpression makeNullableHasValue(BoundExpression expr)
            {
                // Optimize conversions where we can use the HasValue of the underlying
                Debug.Assert(expr.Type is { });
                switch (expr)
                {
                    case BoundConversion { Conversion: { IsIdentity: true }, Operand: var o }:
                        return makeNullableHasValue(o);
                    case BoundConversion { Conversion: { IsNullable: true, UnderlyingConversions: var underlying } conversion, Operand: var o }
                            when expr.Type.IsNullableType() && o.Type is { } && o.Type.IsNullableType() && !underlying[0].IsUserDefined:
                        // Note that a user-defined conversion from K to Nullable<R> which may translate
                        // a non-null K to a null value gives rise to a lifted conversion from Nullable<K> to Nullable<R> with the same property.
                        // We therefore do not attempt to optimize nullable conversions with an underlying user-defined conversion.
                        conversion.AssertUnderlyingConversionsChecked();
                        return makeNullableHasValue(o);
                    default:
                        return _factory.MakeNullableHasValue(expr.Syntax, expr);
                }
            }
        }
 
        private BoundLocal MakeTemp(BoundExpression loweredExpression, ArrayBuilder<LocalSymbol> temps, ArrayBuilder<BoundExpression> effects)
        {
            BoundLocal temp = _factory.StoreToTemp(loweredExpression, out BoundAssignmentOperator assignmentToTemp);
            effects.Add(assignmentToTemp);
            temps.Add(temp.LocalSymbol);
            return temp;
        }
 
        /// <summary>
        /// Returns a temp which is initialized with lowered-expression.HasValue
        /// </summary>
        private BoundExpression MakeValueOrDefaultTemp(
            BoundExpression expr,
            ArrayBuilder<LocalSymbol> temps,
            ArrayBuilder<BoundExpression> effects)
        {
            // Optimize conversions where we can use the underlying
            switch (expr)
            {
                case BoundConversion { Conversion: { IsIdentity: true }, Operand: var o }:
                    return MakeValueOrDefaultTemp(o, temps, effects);
                case BoundConversion { Conversion: { IsNullable: true, UnderlyingConversions: var nested }, Operand: var o } conv when
                        expr.Type is { } exprType && exprType.IsNullableType() && o.Type is { } && o.Type.IsNullableType() && nested[0] is { IsTupleConversion: true } tupleConversion:
                    {
                        Debug.Assert(expr.Type is { });
                        conv.Conversion.AssertUnderlyingConversionsChecked();
                        var operand = MakeValueOrDefaultTemp(o, temps, effects);
                        Debug.Assert(operand.Type is { });
                        var types = expr.Type.GetNullableUnderlyingType().TupleElementTypesWithAnnotations;
                        int tupleCardinality = operand.Type.TupleElementTypesWithAnnotations.Length;
                        var underlyingConversions = tupleConversion.UnderlyingConversions;
                        tupleConversion.AssertUnderlyingConversionsChecked();
                        Debug.Assert(underlyingConversions.Length == tupleCardinality);
                        var argumentBuilder = ArrayBuilder<BoundExpression>.GetInstance(tupleCardinality);
                        for (int i = 0; i < tupleCardinality; i++)
                        {
                            argumentBuilder.Add(MakeBoundConversion(GetTuplePart(operand, i), underlyingConversions[i], types[i], conv));
                        }
                        return new BoundConvertedTupleLiteral(
                            syntax: operand.Syntax,
                            sourceTuple: null,
                            wasTargetTyped: false,
                            arguments: argumentBuilder.ToImmutableAndFree(),
                            argumentNamesOpt: ImmutableArray<string?>.Empty,
                            inferredNamesOpt: ImmutableArray<bool>.Empty,
                            type: expr.Type,
                            hasErrors: expr.HasErrors).WithSuppression(expr.IsSuppressed);
                        throw null;
                    }
                default:
                    {
                        BoundExpression valueOrDefaultCall = MakeOptimizedGetValueOrDefault(expr.Syntax, expr);
                        return MakeTemp(valueOrDefaultCall, temps, effects);
                    }
            }
 
            BoundExpression MakeBoundConversion(BoundExpression expr, Conversion conversion, TypeWithAnnotations type, BoundConversion enclosing)
            {
                return new BoundConversion(
                    expr.Syntax, expr, conversion, enclosing.Checked, enclosing.ExplicitCastInCode,
                    conversionGroupOpt: null, constantValueOpt: null, type: type.Type);
            }
 
        }
 
        /// <summary>
        /// Produces a chain of equality (or inequality) checks combined logically with AND (or OR)
        /// </summary>
        private BoundExpression RewriteNonNullableNestedTupleOperators(TupleBinaryOperatorInfo.Multiple operators,
            BoundExpression left, BoundExpression right, TypeSymbol type,
            ArrayBuilder<LocalSymbol> temps, BinaryOperatorKind operatorKind)
        {
            ImmutableArray<TupleBinaryOperatorInfo> nestedOperators = operators.Operators;
 
            BoundExpression? currentResult = null;
            for (int i = 0; i < nestedOperators.Length; i++)
            {
                BoundExpression leftElement = GetTuplePart(left, i);
                BoundExpression rightElement = GetTuplePart(right, i);
                BoundExpression nextLogicalOperand = RewriteTupleOperator(nestedOperators[i], leftElement, rightElement, type, temps, operatorKind);
                if (currentResult is null)
                {
                    currentResult = nextLogicalOperand;
                }
                else
                {
                    var logicalOperator = operatorKind == BinaryOperatorKind.Equal ? BinaryOperatorKind.LogicalBoolAnd : BinaryOperatorKind.LogicalBoolOr;
                    currentResult = _factory.Binary(logicalOperator, type, currentResult, nextLogicalOperand);
                }
            }
 
            Debug.Assert(currentResult is { });
            return currentResult;
        }
 
        /// <summary>
        /// For tuple literals, we just return the element.
        /// For expressions with tuple type, we access <c>Item{i+1}</c>.
        /// </summary>
        private BoundExpression GetTuplePart(BoundExpression tuple, int i)
        {
            // Example:
            // (1, 2) == (1, 2);
            if (tuple is BoundTupleExpression tupleExpression)
            {
                return tupleExpression.Arguments[i];
            }
 
            Debug.Assert(tuple.Type is { IsTupleType: true });
 
            // Example:
            // t == GetTuple();
            // t == ((byte, byte)) (1, 2);
            // t == ((short, short))((int, int))(1L, 2L);
            return MakeTupleFieldAccessAndReportUseSiteDiagnostics(tuple, tuple.Syntax, tuple.Type.TupleElements[i]);
        }
 
        /// <summary>
        /// Produce an element-wise comparison and logic to ensure the result is a bool type.
        ///
        /// If an element-wise comparison doesn't return bool, then:
        /// - if it is dynamic, we'll do <c>!(comparisonResult.false)</c> or <c>comparisonResult.true</c>
        /// - if it implicitly converts to bool, we'll just do the conversion
        /// - otherwise, we'll do <c>!(comparisonResult.false)</c> or <c>comparisonResult.true</c> (as we'd do for <c>if</c> or <c>while</c>)
        /// </summary>
        private BoundExpression RewriteTupleSingleOperator(TupleBinaryOperatorInfo.Single single,
            BoundExpression left, BoundExpression right, TypeSymbol boolType, BinaryOperatorKind operatorKind)
        {
            // We deferred lowering some of the conversions on the operand, even though the
            // code below the conversions were lowered.  We lower the conversion part now.
            left = LowerConversions(left);
            right = LowerConversions(right);
 
            if (single.Kind.IsDynamic())
            {
                // Produce
                // !((left == right).op_false)
                // (left != right).op_true
 
                BoundExpression dynamicResult = _dynamicFactory.MakeDynamicBinaryOperator(single.Kind, left, right, isCompoundAssignment: false, _compilation.DynamicType).ToExpression();
                if (operatorKind == BinaryOperatorKind.Equal)
                {
                    return _factory.Not(MakeUnaryOperator(UnaryOperatorKind.DynamicFalse, left.Syntax, method: null, constrainedToTypeOpt: null, dynamicResult, boolType));
                }
                else
                {
                    return MakeUnaryOperator(UnaryOperatorKind.DynamicTrue, left.Syntax, method: null, constrainedToTypeOpt: null, dynamicResult, boolType);
                }
            }
 
            if (left.IsLiteralNull() && right.IsLiteralNull())
            {
                // For `null == null` this is special-cased during initial binding
                return new BoundLiteral(left.Syntax, ConstantValue.Create(operatorKind == BinaryOperatorKind.Equal), boolType);
            }
 
            BoundExpression binary = MakeBinaryOperator(_factory.Syntax, single.Kind, left, right, single.MethodSymbolOpt?.ReturnType ?? boolType, single.MethodSymbolOpt, single.ConstrainedToTypeOpt);
            UnaryOperatorSignature boolOperator = single.BoolOperator;
 
            BoundExpression result;
            BoundExpression convertedBinary = ApplyConversionIfNotIdentity(single.ConversionForBool, single.ConversionForBoolPlaceholder, binary);
 
            if (boolOperator.Kind != UnaryOperatorKind.Error)
            {
                // Produce
                // !((left == right).op_false)
                // (left != right).op_true
                Debug.Assert(boolOperator.ReturnType.SpecialType == SpecialType.System_Boolean);
                result = MakeUnaryOperator(boolOperator.Kind, binary.Syntax, boolOperator.Method, boolOperator.ConstrainedToTypeOpt, convertedBinary, boolType);
 
                if (operatorKind == BinaryOperatorKind.Equal)
                {
                    result = _factory.Not(result);
                }
            }
            else
            {
                // Produce
                // (bool)(left == right)
                // (bool)(left != right)
                result = convertedBinary;
            }
 
            return result;
        }
 
        /// <summary>
        /// Lower any conversions appearing near the top of the bound expression, assuming non-conversions
        /// appearing below them have already been lowered.
        /// </summary>
        private BoundExpression LowerConversions(BoundExpression expr)
        {
            return (expr is BoundConversion conv)
                ? MakeConversionNode(
                    oldNodeOpt: conv, syntax: conv.Syntax, rewrittenOperand: LowerConversions(conv.Operand),
                    conversion: conv.Conversion, @checked: conv.Checked, explicitCastInCode: conv.ExplicitCastInCode,
                    constantValueOpt: conv.ConstantValueOpt, rewrittenType: conv.Type)
                : expr;
        }
    }
}