File: Lowering\LocalRewriter\LocalRewriter_TupleBinaryOperator.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 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.
                case BoundConversion { Conversion.IsTupleConversion: true }: // If we were not able to push this conversion down the tree before getting here, it must be performed early, otherwise it won't be properly lowered by this machinery.
                    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;
        }
    }
}