// 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
    {
        public override BoundNode VisitBinaryOperator(BoundBinaryOperator node)
        {
            return VisitBinaryOperator(node, null);
        }

        public override BoundNode VisitUserDefinedConditionalLogicalOperator(BoundUserDefinedConditionalLogicalOperator node)
        {
            // Yes, we could have a lifted, logical, user-defined operator:
            //
            // struct C { 
            //   public static C operator &(C x, C y) {...}
            //   public static bool operator true(C? c) { ... }
            //   public static bool operator false(C? c) { ... }
            // }
            //
            // If we have C? q, r and we say q && r then this gets bound as 
            // C? tempQ = q ;
            // C.false(tempQ) ? 
            //     tempQ : 
            //     ( 
            //         C? tempR = r ; 
            //         tempQ.HasValue & tempR.HasValue ? 
            //           new C?(C.&(tempQ.GetValueOrDefault(), tempR.GetValueOrDefault())) :
            //           default C?()
            //     )
            //
            // Note that the native compiler does not allow q && r. However, the native compiler
            // *does* allow q && r if C is defined as:
            //
            // struct C { 
            //   public static C? operator &(C? x, C? y) {...}
            //   public static bool operator true(C? c) { ... }
            //   public static bool operator false(C? c) { ... }
            // }
            //
            // It seems unusual and wrong that an & operator should be allowed to become
            // a && operator if there is a "manually lifted" operator in source, but not
            // if there is a "synthesized" lifted operator.  Roslyn fixes this bug.
            //
            // Anyway, in this case we must lower this to its non-logical form, and then
            // lower the interior of that to its non-lifted form.

            // See comments in method IsValidUserDefinedConditionalLogicalOperator for information
            // on some subtle aspects of this lowering.

            // We generate one of:
            //
            // x || y --> temp = x; T.true(temp)  ? temp : T.|(temp, y);
            // x && y --> temp = x; T.false(temp) ? temp : T.&(temp, y);
            //
            // For the ease of naming locals, we'll assume we're doing an &&.

            // TODO: We generate every one of these as "temp = x; T.false(temp) ? temp : T.&(temp, y)" even
            // TODO: when x has no side effects. We can optimize away the temporary if there are no side effects.

            var syntax = node.Syntax;
            var operatorKind = node.OperatorKind;
            var type = node.Type;

            BoundExpression loweredLeft = VisitExpression(node.Left);
            BoundExpression loweredRight = VisitExpression(node.Right);

            if (_inExpressionLambda)
            {
                return node.Update(operatorKind, node.LogicalOperator, node.TrueOperator, node.FalseOperator, trueFalseOperandPlaceholder: null, trueFalseOperandConversion: null, node.ConstrainedToTypeOpt, node.ResultKind, originalUserDefinedOperatorsOpt: default, loweredLeft, loweredRight, type);
            }

            BoundAssignmentOperator tempAssignment;
            var boundTemp = _factory.StoreToTemp(loweredLeft, out tempAssignment);

            // T.false(temp)
            var falseOperatorCall = BoundCall.Synthesized(syntax, receiverOpt: node.ConstrainedToTypeOpt is null ? null : new BoundTypeExpression(syntax, aliasOpt: null, node.ConstrainedToTypeOpt),
                                                          initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown,
                                                          operatorKind.Operator() == BinaryOperatorKind.And ? node.FalseOperator : node.TrueOperator,
                                                          ApplyConversionIfNotIdentity(node.TrueFalseOperandConversion, node.TrueFalseOperandPlaceholder, boundTemp));

            // T.&(temp, y)
            var andOperatorCall = LowerUserDefinedBinaryOperator(syntax, operatorKind & ~BinaryOperatorKind.Logical, boundTemp, loweredRight, type, node.LogicalOperator, node.ConstrainedToTypeOpt);

            // T.false(temp) ? temp : T.&(temp, y)
            BoundExpression conditionalExpression = RewriteConditionalOperator(
                syntax: syntax,
                rewrittenCondition: falseOperatorCall,
                rewrittenConsequence: boundTemp,
                rewrittenAlternative: andOperatorCall,
                constantValueOpt: null,
                rewrittenType: type,
                isRef: false);

            // temp = x; T.false(temp) ? temp : T.&(temp, y)
            return new BoundSequence(
                syntax: syntax,
                locals: ImmutableArray.Create(boundTemp.LocalSymbol),
                sideEffects: ImmutableArray.Create<BoundExpression>(tempAssignment),
                value: conditionalExpression,
                type: type);
        }

        public BoundExpression VisitBinaryOperator(BoundBinaryOperator node, BoundUnaryOperator? applyParentUnaryOperator)
        {
            if (node.InterpolatedStringHandlerData is InterpolatedStringHandlerData data)
            {
                Debug.Assert(node.Type.SpecialType == SpecialType.System_String, "Non-string binary addition should have been handled by VisitConversion or VisitArguments");
                ImmutableArray<BoundExpression> parts = CollectBinaryOperatorInterpolatedStringParts(node);
                return LowerPartsToString(data, parts, node.Syntax, node.Type);
            }

            if (node.OperatorKind is BinaryOperatorKind.Utf8Addition)
            {
                Debug.Assert(applyParentUnaryOperator is null);
                return VisitUtf8Addition(node);
            }

            if (IsBinaryStringConcatenation(node))
            {
                Debug.Assert(applyParentUnaryOperator is null);
                return VisitStringConcatenation(node);
            }

            // In machine-generated code we frequently end up with binary operator trees that are deep on the left,
            // such as a + b + c + d ...
            // To avoid blowing the call stack, we make an explicit stack of the binary operators to the left, 
            // and then lower by traversing the explicit stack.

            var stack = ArrayBuilder<BoundBinaryOperator>.GetInstance();

            for (BoundBinaryOperator? current = node; current != null && current.ConstantValueOpt == null; current = current.Left as BoundBinaryOperator)
            {
                // The regular visit mechanism will handle this.
                if (current.InterpolatedStringHandlerData is not null || current.OperatorKind is BinaryOperatorKind.Utf8Addition || IsBinaryStringConcatenation(current))
                {
                    Debug.Assert(stack.Count >= 1);
                    break;
                }

                stack.Push(current);
            }

            BoundExpression loweredLeft = VisitExpression(stack.Peek().Left);
            while (stack.Count > 0)
            {
                BoundBinaryOperator original = stack.Pop();
                BoundExpression loweredRight = VisitExpression(original.Right);
                loweredLeft = MakeBinaryOperator(original, original.Syntax, original.OperatorKind, loweredLeft, loweredRight, original.Type, original.LeftTruthOperatorMethod ?? original.BinaryOperatorMethod, original.ConstrainedToType,
                    applyParentUnaryOperator: (stack.Count == 0) ? applyParentUnaryOperator : null);
            }

            stack.Free();
            return loweredLeft;
        }

        private static ImmutableArray<BoundExpression> CollectBinaryOperatorInterpolatedStringParts(BoundBinaryOperator node)
        {
            Debug.Assert(node.OperatorKind == BinaryOperatorKind.StringConcatenation);
            Debug.Assert(node.InterpolatedStringHandlerData is not null);
            var partsBuilder = ArrayBuilder<BoundExpression>.GetInstance();
            node.VisitBinaryOperatorInterpolatedString(partsBuilder,
                static (BoundInterpolatedString interpolatedString, ArrayBuilder<BoundExpression> partsBuilder) =>
                {
                    partsBuilder.AddRange(interpolatedString.Parts);
                    return true;
                });
            return partsBuilder.ToImmutableAndFree();
        }

        private BoundExpression MakeBinaryOperator(
            SyntaxNode syntax,
            BinaryOperatorKind operatorKind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight,
            TypeSymbol type,
            MethodSymbol? method,
            TypeSymbol? constrainedToTypeOpt,
            bool isPointerElementAccess = false,
            bool isCompoundAssignment = false,
            BoundUnaryOperator? applyParentUnaryOperator = null)
        {
            return MakeBinaryOperator(null, syntax, operatorKind, loweredLeft, loweredRight, type, method, constrainedToTypeOpt, isPointerElementAccess, isCompoundAssignment, applyParentUnaryOperator);
        }

        private BoundExpression MakeBinaryOperator(
            BoundBinaryOperator? oldNode,
            SyntaxNode syntax,
            BinaryOperatorKind operatorKind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight,
            TypeSymbol type,
            MethodSymbol? method,
            TypeSymbol? constrainedToTypeOpt,
            bool isPointerElementAccess = false,
            bool isCompoundAssignment = false,
            BoundUnaryOperator? applyParentUnaryOperator = null)
        {
            Debug.Assert(oldNode == null || (oldNode.Syntax == syntax));

            if (_inExpressionLambda)
            {
                switch (operatorKind.Operator() | operatorKind.OperandTypes())
                {
                    case BinaryOperatorKind.ObjectAndStringConcatenation:
                    case BinaryOperatorKind.StringAndObjectConcatenation:
                    case BinaryOperatorKind.StringConcatenation:
                        throw ExceptionUtilities.UnexpectedValue(operatorKind);
                    case BinaryOperatorKind.DelegateCombination:
                        return RewriteDelegateOperation(syntax, operatorKind, loweredLeft, loweredRight, type, SpecialMember.System_Delegate__Combine);
                    case BinaryOperatorKind.DelegateRemoval:
                        return RewriteDelegateOperation(syntax, operatorKind, loweredLeft, loweredRight, type, SpecialMember.System_Delegate__Remove);
                    case BinaryOperatorKind.DelegateEqual:
                        return RewriteDelegateOperation(syntax, operatorKind, loweredLeft, loweredRight, type, SpecialMember.System_Delegate__op_Equality);
                    case BinaryOperatorKind.DelegateNotEqual:
                        return RewriteDelegateOperation(syntax, operatorKind, loweredLeft, loweredRight, type, SpecialMember.System_Delegate__op_Inequality);
                }
            }
            else
            // try to lower the expression.
            {
                if (operatorKind.IsDynamic())
                {
                    Debug.Assert(!isPointerElementAccess);

                    if (operatorKind.IsLogical())
                    {
                        return MakeDynamicLogicalBinaryOperator(syntax, operatorKind, loweredLeft, loweredRight, method, constrainedToTypeOpt, type, isCompoundAssignment, applyParentUnaryOperator);
                    }
                    else
                    {
                        Debug.Assert(method is null);
                        return _dynamicFactory.MakeDynamicBinaryOperator(operatorKind, loweredLeft, loweredRight, isCompoundAssignment, type).ToExpression();
                    }
                }

                if (operatorKind.IsLifted())
                {
                    return RewriteLiftedBinaryOperator(syntax, operatorKind, loweredLeft, loweredRight, type, method, constrainedToTypeOpt);
                }

                if (operatorKind.IsUserDefined())
                {
                    return LowerUserDefinedBinaryOperator(syntax, operatorKind, loweredLeft, loweredRight, type, method, constrainedToTypeOpt);
                }

                switch (operatorKind.OperatorWithLogical() | operatorKind.OperandTypes())
                {
                    case BinaryOperatorKind.NullableNullEqual:
                    case BinaryOperatorKind.NullableNullNotEqual:
                        return _factory.RewriteNullableNullEquality(syntax, operatorKind, loweredLeft, loweredRight, type);

                    case BinaryOperatorKind.ObjectAndStringConcatenation:
                    case BinaryOperatorKind.StringAndObjectConcatenation:
                    case BinaryOperatorKind.StringConcatenation:
                        throw ExceptionUtilities.UnexpectedValue(operatorKind);

                    case BinaryOperatorKind.StringEqual:
                        return RewriteStringEquality(oldNode, syntax, operatorKind, loweredLeft, loweredRight, type, SpecialMember.System_String__op_Equality);

                    case BinaryOperatorKind.StringNotEqual:
                        return RewriteStringEquality(oldNode, syntax, operatorKind, loweredLeft, loweredRight, type, SpecialMember.System_String__op_Inequality);

                    case BinaryOperatorKind.DelegateCombination:
                        return RewriteDelegateOperation(syntax, operatorKind, loweredLeft, loweredRight, type, SpecialMember.System_Delegate__Combine);

                    case BinaryOperatorKind.DelegateRemoval:
                        return RewriteDelegateOperation(syntax, operatorKind, loweredLeft, loweredRight, type, SpecialMember.System_Delegate__Remove);

                    case BinaryOperatorKind.DelegateEqual:
                        return RewriteDelegateOperation(syntax, operatorKind, loweredLeft, loweredRight, type, SpecialMember.System_Delegate__op_Equality);

                    case BinaryOperatorKind.DelegateNotEqual:
                        return RewriteDelegateOperation(syntax, operatorKind, loweredLeft, loweredRight, type, SpecialMember.System_Delegate__op_Inequality);

                    case BinaryOperatorKind.LogicalBoolAnd:
                        if (loweredRight.ConstantValueOpt == ConstantValue.True) return loweredLeft;
                        if (loweredLeft.ConstantValueOpt == ConstantValue.True) return loweredRight;
                        if (loweredLeft.ConstantValueOpt == ConstantValue.False) return loweredLeft;

                        if (loweredRight.Kind == BoundKind.Local || loweredRight.Kind == BoundKind.Parameter)
                        {
                            operatorKind &= ~BinaryOperatorKind.Logical;
                        }

                        goto default;

                    case BinaryOperatorKind.LogicalBoolOr:
                        if (loweredRight.ConstantValueOpt == ConstantValue.False) return loweredLeft;
                        if (loweredLeft.ConstantValueOpt == ConstantValue.False) return loweredRight;
                        if (loweredLeft.ConstantValueOpt == ConstantValue.True) return loweredLeft;

                        if (loweredRight.Kind == BoundKind.Local || loweredRight.Kind == BoundKind.Parameter)
                        {
                            operatorKind &= ~BinaryOperatorKind.Logical;
                        }

                        goto default;

                    case BinaryOperatorKind.BoolAnd:
                        if (loweredRight.ConstantValueOpt == ConstantValue.True) return loweredLeft;
                        if (loweredLeft.ConstantValueOpt == ConstantValue.True) return loweredRight;

                        // Note that we are using IsDefaultValue instead of False.
                        // That is just to catch cases like default(bool) or others resulting in 
                        // a default bool value, that we know to be "false"
                        // bool? generally should not reach here, since it is handled by RewriteLiftedBinaryOperator.
                        // Regardless, the following code should handle default(bool?) correctly since
                        // default(bool?) & <expr> == default(bool?)  with sideeffects of <expr>
                        if (loweredLeft.IsDefaultValue())
                        {
                            return _factory.MakeSequence(loweredRight, loweredLeft);
                        }
                        if (loweredRight.IsDefaultValue())
                        {
                            return _factory.MakeSequence(loweredLeft, loweredRight);
                        }

                        goto default;

                    case BinaryOperatorKind.BoolOr:
                        if (loweredRight.ConstantValueOpt == ConstantValue.False) return loweredLeft;
                        if (loweredLeft.ConstantValueOpt == ConstantValue.False) return loweredRight;
                        goto default;

                    case BinaryOperatorKind.BoolEqual:
                        if (loweredLeft.ConstantValueOpt == ConstantValue.True) return loweredRight;
                        if (loweredRight.ConstantValueOpt == ConstantValue.True) return loweredLeft;

                        Debug.Assert(loweredLeft.Type is { });
                        Debug.Assert(loweredRight.Type is { });
                        if (loweredLeft.ConstantValueOpt == ConstantValue.False)
                            return MakeUnaryOperator(UnaryOperatorKind.BoolLogicalNegation, syntax, method: null, constrainedToTypeOpt: null, loweredRight, loweredRight.Type);

                        if (loweredRight.ConstantValueOpt == ConstantValue.False)
                            return MakeUnaryOperator(UnaryOperatorKind.BoolLogicalNegation, syntax, method: null, constrainedToTypeOpt: null, loweredLeft, loweredLeft.Type);

                        goto default;

                    case BinaryOperatorKind.BoolNotEqual:
                        if (loweredLeft.ConstantValueOpt == ConstantValue.False) return loweredRight;
                        if (loweredRight.ConstantValueOpt == ConstantValue.False) return loweredLeft;

                        Debug.Assert(loweredLeft.Type is { });
                        Debug.Assert(loweredRight.Type is { });
                        if (loweredLeft.ConstantValueOpt == ConstantValue.True)
                            return MakeUnaryOperator(UnaryOperatorKind.BoolLogicalNegation, syntax, method: null, constrainedToTypeOpt: null, loweredRight, loweredRight.Type);

                        if (loweredRight.ConstantValueOpt == ConstantValue.True)
                            return MakeUnaryOperator(UnaryOperatorKind.BoolLogicalNegation, syntax, method: null, constrainedToTypeOpt: null, loweredLeft, loweredLeft.Type);

                        goto default;

                    case BinaryOperatorKind.BoolXor:
                        if (loweredLeft.ConstantValueOpt == ConstantValue.False) return loweredRight;
                        if (loweredRight.ConstantValueOpt == ConstantValue.False) return loweredLeft;

                        Debug.Assert(loweredLeft.Type is { });
                        Debug.Assert(loweredRight.Type is { });
                        if (loweredLeft.ConstantValueOpt == ConstantValue.True)
                            return MakeUnaryOperator(UnaryOperatorKind.BoolLogicalNegation, syntax, method: null, constrainedToTypeOpt: null, loweredRight, loweredRight.Type);

                        if (loweredRight.ConstantValueOpt == ConstantValue.True)
                            return MakeUnaryOperator(UnaryOperatorKind.BoolLogicalNegation, syntax, method: null, constrainedToTypeOpt: null, loweredLeft, loweredLeft.Type);

                        goto default;

                    case BinaryOperatorKind.IntLeftShift:
                    case BinaryOperatorKind.UIntLeftShift:
                    case BinaryOperatorKind.IntRightShift:
                    case BinaryOperatorKind.UIntRightShift:
                    case BinaryOperatorKind.IntUnsignedRightShift:
                    case BinaryOperatorKind.UIntUnsignedRightShift:
                        return RewriteBuiltInShiftOperation(oldNode, syntax, operatorKind, loweredLeft, loweredRight, type, 0x1F);

                    case BinaryOperatorKind.LongLeftShift:
                    case BinaryOperatorKind.ULongLeftShift:
                    case BinaryOperatorKind.LongRightShift:
                    case BinaryOperatorKind.ULongRightShift:
                    case BinaryOperatorKind.LongUnsignedRightShift:
                    case BinaryOperatorKind.ULongUnsignedRightShift:
                        return RewriteBuiltInShiftOperation(oldNode, syntax, operatorKind, loweredLeft, loweredRight, type, 0x3F);

                    case BinaryOperatorKind.NIntRightShift:
                    case BinaryOperatorKind.NUIntRightShift:
                    case BinaryOperatorKind.NIntUnsignedRightShift:
                    case BinaryOperatorKind.NUIntUnsignedRightShift:
                    case BinaryOperatorKind.NIntLeftShift:
                    case BinaryOperatorKind.NUIntLeftShift:
                        return RewriteBuiltInNativeShiftOperation(oldNode, syntax, operatorKind, loweredLeft, loweredRight, type);

                    case BinaryOperatorKind.DecimalAddition:
                    case BinaryOperatorKind.DecimalSubtraction:
                    case BinaryOperatorKind.DecimalMultiplication:
                    case BinaryOperatorKind.DecimalDivision:
                    case BinaryOperatorKind.DecimalRemainder:
                    case BinaryOperatorKind.DecimalEqual:
                    case BinaryOperatorKind.DecimalNotEqual:
                    case BinaryOperatorKind.DecimalLessThan:
                    case BinaryOperatorKind.DecimalLessThanOrEqual:
                    case BinaryOperatorKind.DecimalGreaterThan:
                    case BinaryOperatorKind.DecimalGreaterThanOrEqual:
                        return RewriteDecimalBinaryOperation(syntax, loweredLeft, loweredRight, operatorKind);

                    case BinaryOperatorKind.PointerAndIntAddition:
                    case BinaryOperatorKind.PointerAndUIntAddition:
                    case BinaryOperatorKind.PointerAndLongAddition:
                    case BinaryOperatorKind.PointerAndULongAddition:
                    case BinaryOperatorKind.PointerAndIntSubtraction:
                    case BinaryOperatorKind.PointerAndUIntSubtraction:
                    case BinaryOperatorKind.PointerAndLongSubtraction:
                    case BinaryOperatorKind.PointerAndULongSubtraction:
                        if (loweredRight.IsDefaultValue())
                        {
                            return loweredLeft;
                        }
                        return RewritePointerNumericOperator(syntax, operatorKind, loweredLeft, loweredRight, type, isPointerElementAccess, isLeftPointer: true);

                    case BinaryOperatorKind.IntAndPointerAddition:
                    case BinaryOperatorKind.UIntAndPointerAddition:
                    case BinaryOperatorKind.LongAndPointerAddition:
                    case BinaryOperatorKind.ULongAndPointerAddition:
                        if (loweredLeft.IsDefaultValue())
                        {
                            return loweredRight;
                        }
                        return RewritePointerNumericOperator(syntax, operatorKind, loweredLeft, loweredRight, type, isPointerElementAccess, isLeftPointer: false);

                    case BinaryOperatorKind.PointerSubtraction:
                        return RewritePointerSubtraction(operatorKind, loweredLeft, loweredRight, type);

                    case BinaryOperatorKind.IntAddition:
                    case BinaryOperatorKind.UIntAddition:
                    case BinaryOperatorKind.LongAddition:
                    case BinaryOperatorKind.ULongAddition:
                        if (loweredLeft.IsDefaultValue())
                        {
                            return loweredRight;
                        }
                        if (loweredRight.IsDefaultValue())
                        {
                            return loweredLeft;
                        }
                        goto default;

                    case BinaryOperatorKind.IntSubtraction:
                    case BinaryOperatorKind.LongSubtraction:
                    case BinaryOperatorKind.UIntSubtraction:
                    case BinaryOperatorKind.ULongSubtraction:
                        if (loweredRight.IsDefaultValue())
                        {
                            return loweredLeft;
                        }
                        goto default;

                    case BinaryOperatorKind.IntMultiplication:
                    case BinaryOperatorKind.LongMultiplication:
                    case BinaryOperatorKind.UIntMultiplication:
                    case BinaryOperatorKind.ULongMultiplication:
                        if (loweredLeft.IsDefaultValue())
                        {
                            return _factory.MakeSequence(loweredRight, loweredLeft);
                        }
                        if (loweredRight.IsDefaultValue())
                        {
                            return _factory.MakeSequence(loweredLeft, loweredRight);
                        }
                        if (loweredLeft.ConstantValueOpt?.UInt64Value == 1)
                        {
                            return loweredRight;
                        }
                        if (loweredRight.ConstantValueOpt?.UInt64Value == 1)
                        {
                            return loweredLeft;
                        }
                        goto default;

                    case BinaryOperatorKind.IntGreaterThan:
                    case BinaryOperatorKind.IntLessThanOrEqual:
                        if (loweredLeft.Kind == BoundKind.ArrayLength && loweredRight.IsDefaultValue())
                        {
                            //array length is never negative
                            var newOp = operatorKind == BinaryOperatorKind.IntGreaterThan ?
                                                        BinaryOperatorKind.NotEqual :
                                                        BinaryOperatorKind.Equal;

                            operatorKind &= ~BinaryOperatorKind.OpMask;
                            operatorKind |= newOp;
                            loweredLeft = UnconvertArrayLength((BoundArrayLength)loweredLeft);
                        }
                        goto default;

                    case BinaryOperatorKind.IntLessThan:
                    case BinaryOperatorKind.IntGreaterThanOrEqual:
                        if (loweredRight.Kind == BoundKind.ArrayLength && loweredLeft.IsDefaultValue())
                        {
                            //array length is never negative
                            var newOp = operatorKind == BinaryOperatorKind.IntLessThan ?
                                                        BinaryOperatorKind.NotEqual :
                                                        BinaryOperatorKind.Equal;

                            operatorKind &= ~BinaryOperatorKind.OpMask;
                            operatorKind |= newOp;
                            loweredRight = UnconvertArrayLength((BoundArrayLength)loweredRight);
                        }
                        goto default;

                    case BinaryOperatorKind.IntEqual:
                    case BinaryOperatorKind.IntNotEqual:
                        if (loweredLeft.Kind == BoundKind.ArrayLength && loweredRight.IsDefaultValue())
                        {
                            loweredLeft = UnconvertArrayLength((BoundArrayLength)loweredLeft);
                        }
                        else if (loweredRight.Kind == BoundKind.ArrayLength && loweredLeft.IsDefaultValue())
                        {
                            loweredRight = UnconvertArrayLength((BoundArrayLength)loweredRight);
                        }

                        goto default;

                    case BinaryOperatorKind.Utf8Addition:
                        throw ExceptionUtilities.UnexpectedValue(operatorKind);

                    default:
                        break;
                }
            }

            Debug.Assert(!operatorKind.IsDynamic());
            return (oldNode != null) ?
                oldNode.Update(operatorKind, oldNode.ConstantValueOpt, oldNode.BinaryOperatorMethod, oldNode.ConstrainedToType, oldNode.ResultKind, loweredLeft, loweredRight, type) :
                new BoundBinaryOperator(syntax, operatorKind, constantValueOpt: null, methodOpt: null, constrainedToTypeOpt: null, LookupResultKind.Viable, loweredLeft, loweredRight, type);
        }

        private BoundExpression RewriteLiftedBinaryOperator(SyntaxNode syntax, BinaryOperatorKind operatorKind, BoundExpression loweredLeft, BoundExpression loweredRight, TypeSymbol type, MethodSymbol? method, TypeSymbol? constrainedToTypeOpt)
        {
            var conditionalLeft = loweredLeft as BoundLoweredConditionalAccess;

            // NOTE: we could in theory handle side-effecting loweredRight here too
            //       by including it as a part of whenNull, but there is a concern 
            //       that it can lead to code duplication
            var optimize = conditionalLeft != null &&
                operatorKind != BinaryOperatorKind.LiftedBoolOr && operatorKind != BinaryOperatorKind.LiftedBoolAnd &&
                !ReadIsSideeffecting(loweredRight) &&
                (conditionalLeft.WhenNullOpt == null || conditionalLeft.WhenNullOpt.IsDefaultValue());

            if (optimize)
            {
                loweredLeft = conditionalLeft!.WhenNotNull;
            }

            var result = operatorKind.IsComparison() ?
                            operatorKind.IsUserDefined() ?
                                LowerLiftedUserDefinedComparisonOperator(syntax, operatorKind, loweredLeft, loweredRight, method, constrainedToTypeOpt) :
                                LowerLiftedBuiltInComparisonOperator(syntax, operatorKind, loweredLeft, loweredRight) :
                            LowerLiftedBinaryArithmeticOperator(syntax, operatorKind, loweredLeft, loweredRight, type, method, constrainedToTypeOpt);

            if (optimize)
            {
                BoundExpression? whenNullOpt = null;

                // for all operators null-in means null-out
                // except for the Equal/NotEqual since null == null ==> true
                if (operatorKind.Operator() == BinaryOperatorKind.NotEqual ||
                    operatorKind.Operator() == BinaryOperatorKind.Equal)
                {
                    Debug.Assert(loweredLeft.Type is { });
                    whenNullOpt = RewriteLiftedBinaryOperator(syntax, operatorKind, _factory.Default(loweredLeft.Type), loweredRight, type, method, constrainedToTypeOpt);
                }

                result = conditionalLeft!.Update(
                    conditionalLeft.Receiver,
                    conditionalLeft.HasValueMethodOpt,
                    whenNotNull: result,
                    whenNullOpt: whenNullOpt,
                    id: conditionalLeft.Id,
                    forceCopyOfNullableValueType: conditionalLeft.ForceCopyOfNullableValueType,
                    type: result.Type!
                );
            }

            return result;
        }

        //array length produces native uint, so the node typically implies a conversion to int32/int64.  
        //Sometimes the conversion is not necessary - i.e. when we just check for 0
        //This helper removes unnecessary implied conversion from ArrayLength node.
        private BoundExpression UnconvertArrayLength(BoundArrayLength arrLength)
        {
            return arrLength.Update(arrLength.Expression, _factory.SpecialType(SpecialType.System_UIntPtr));
        }

        private BoundExpression MakeDynamicLogicalBinaryOperator(
            SyntaxNode syntax,
            BinaryOperatorKind operatorKind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight,
            MethodSymbol? leftTruthOperator,
            TypeSymbol? constrainedToTypeOpt,
            TypeSymbol type,
            bool isCompoundAssignment,
            BoundUnaryOperator? applyParentUnaryOperator)
        {
            Debug.Assert(operatorKind.Operator() == BinaryOperatorKind.And || operatorKind.Operator() == BinaryOperatorKind.Or);

            // Dynamic logical && and || operators are lowered as follows:
            //   left && right  ->  IsFalse(left) ? left : And(left, right)
            //   left || right  ->  IsTrue(left) ? left : Or(left, right)
            // 
            // Optimization: If the binary AND/OR is directly contained in IsFalse/IsTrue operator (parentUnaryOperator != null)
            // we can avoid calling IsFalse/IsTrue twice on the same object.
            //   IsFalse(left && right)  ->  IsFalse(left) || IsFalse(And(left, right))
            //   IsTrue(left || right)   ->  IsTrue(left) || IsTrue(Or(left, right))

            bool isAnd = operatorKind.Operator() == BinaryOperatorKind.And;

            // Operator to be used to test the left operand:
            var testOperator = isAnd ? UnaryOperatorKind.DynamicFalse : UnaryOperatorKind.DynamicTrue;

            // VisitUnaryOperator ensures we are never called with parentUnaryOperator != null when we can't perform the optimization.
            Debug.Assert(applyParentUnaryOperator == null || applyParentUnaryOperator.OperatorKind == testOperator);

            ConstantValue? constantLeft = loweredLeft.ConstantValueOpt ?? UnboxConstant(loweredLeft);
            if (testOperator == UnaryOperatorKind.DynamicFalse && constantLeft == ConstantValue.False ||
                testOperator == UnaryOperatorKind.DynamicTrue && constantLeft == ConstantValue.True)
            {
                Debug.Assert(leftTruthOperator == null);

                if (applyParentUnaryOperator != null)
                {
                    // IsFalse(false && right) -> true
                    // IsTrue(true || right)   -> true
                    return _factory.Literal(true);
                }
                else
                {
                    // false && right  ->  box(false)
                    // true || right   ->  box(true)
                    return MakeConversionNode(loweredLeft, type, @checked: false);
                }
            }

            BoundExpression result;
            var boolean = _compilation.GetSpecialType(SpecialType.System_Boolean);

            // Store left to local if needed. If constant or already local we don't need a temp 
            // since the value of left can't change until right is evaluated.
            BoundAssignmentOperator? tempAssignment;
            BoundLocal? temp;
            if (constantLeft == null && loweredLeft.Kind != BoundKind.Local && loweredLeft.Kind != BoundKind.Parameter)
            {
                BoundAssignmentOperator assignment;
                var local = _factory.StoreToTemp(loweredLeft, out assignment);
                loweredLeft = local;
                tempAssignment = assignment;
                temp = local;
            }
            else
            {
                tempAssignment = null;
                temp = null;
            }

            var op = _dynamicFactory.MakeDynamicBinaryOperator(operatorKind, loweredLeft, loweredRight, isCompoundAssignment, type).ToExpression();

            // IsFalse(true) or IsTrue(false) are always false:
            bool leftTestIsConstantFalse = testOperator == UnaryOperatorKind.DynamicFalse && constantLeft == ConstantValue.True ||
                                           testOperator == UnaryOperatorKind.DynamicTrue && constantLeft == ConstantValue.False;

            if (applyParentUnaryOperator != null)
            {
                // IsFalse(left && right)  ->  IsFalse(left) || IsFalse(And(left, right))
                // IsTrue(left || right)   ->  IsTrue(left) || IsTrue(Or(left, right))

                result = _dynamicFactory.MakeDynamicUnaryOperator(testOperator, op, boolean).ToExpression();
                if (!leftTestIsConstantFalse)
                {
                    BoundExpression leftTest = MakeTruthTestForDynamicLogicalOperator(syntax, operatorKind, loweredLeft, boolean, leftTruthOperator, constrainedToTypeOpt, negative: isAnd);
                    result = _factory.Binary(BinaryOperatorKind.LogicalOr, boolean, leftTest, result);
                }
            }
            else
            {
                // left && right  ->  IsFalse(left) ? left : And(left, right)
                // left || right  ->  IsTrue(left) ? left : Or(left, right)

                if (leftTestIsConstantFalse)
                {
                    result = op;
                }
                else
                {
                    // We might need to box.
                    BoundExpression leftTest = MakeTruthTestForDynamicLogicalOperator(syntax, operatorKind, loweredLeft, boolean, leftTruthOperator, constrainedToTypeOpt, negative: isAnd);
                    var convertedLeft = MakeConversionNode(loweredLeft, type, @checked: false);
                    result = _factory.Conditional(leftTest, convertedLeft, op, type);
                }
            }

            if (tempAssignment != null)
            {
                Debug.Assert(temp is { });
                return _factory.Sequence(ImmutableArray.Create(temp.LocalSymbol), ImmutableArray.Create<BoundExpression>(tempAssignment), result);
            }

            return result;
        }

        private static ConstantValue? UnboxConstant(BoundExpression expression)
        {
            if (expression.Kind == BoundKind.Conversion)
            {
                var conversion = (BoundConversion)expression;
                if (conversion.ConversionKind == ConversionKind.Boxing)
                {
                    return conversion.Operand.ConstantValueOpt;
                }
            }

            return null;
        }

        private BoundExpression MakeTruthTestForDynamicLogicalOperator(SyntaxNode syntax, BinaryOperatorKind operatorKind, BoundExpression loweredLeft, TypeSymbol boolean, MethodSymbol? leftTruthOperator, TypeSymbol? constrainedToTypeOpt, bool negative)
        {
            if (loweredLeft.HasDynamicType())
            {
                Debug.Assert(leftTruthOperator == null);
                return _dynamicFactory.MakeDynamicUnaryOperator(negative ? UnaryOperatorKind.DynamicFalse : UnaryOperatorKind.DynamicTrue, loweredLeft, boolean).ToExpression();
            }

            // Although the spec doesn't capture it we do the same that Dev11 does:
            // Use implicit conversion to Boolean if it is defined on the static type of the left operand.
            // If not the type has to implement IsTrue/IsFalse operator - we checked it during binding.

            CompoundUseSiteInfo<AssemblySymbol> useSiteInfo = GetNewCompoundUseSiteInfo();
            var conversion = _compilation.Conversions.ClassifyConversionFromExpression(loweredLeft, boolean, isChecked: false, ref useSiteInfo);

            if (conversion.IsImplicit)
            {
                Debug.Assert(leftTruthOperator == null);

                _diagnostics.Add(loweredLeft.Syntax, useSiteInfo);

                var converted = MakeConversionNode(loweredLeft, boolean, @checked: false, markAsChecked: true); // The conversion was checked in binding
                if (negative)
                {
                    return new BoundUnaryOperator(syntax, UnaryOperatorKind.BoolLogicalNegation, converted, ConstantValue.NotAvailable, MethodSymbol.None, constrainedToTypeOpt: null, LookupResultKind.Viable, boolean)
                    {
                        WasCompilerGenerated = true
                    };
                }
                else
                {
                    return converted;
                }
            }

            Debug.Assert(leftTruthOperator != null);

            var parameterType = leftTruthOperator.Parameters[0].Type;
            conversion = _compilation.Conversions.ClassifyConversionFromType(loweredLeft.Type, parameterType, isChecked: operatorKind.IsChecked(), ref useSiteInfo);
            Debug.Assert(conversion.IsImplicit);

            loweredLeft = MakeConversionNode(loweredLeft, parameterType, @checked: operatorKind.IsChecked(), markAsChecked: true); // The conversion was checked in binding

            _diagnostics.Add(loweredLeft.Syntax, useSiteInfo);

            return BoundCall.Synthesized(
                syntax,
                receiverOpt: constrainedToTypeOpt is null ? null : new BoundTypeExpression(syntax, aliasOpt: null, constrainedToTypeOpt),
                initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown,
                leftTruthOperator,
                loweredLeft);
        }

        private BoundExpression LowerUserDefinedBinaryOperator(
            SyntaxNode syntax,
            BinaryOperatorKind operatorKind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight,
            TypeSymbol type,
            MethodSymbol? method,
            TypeSymbol? constrainedToTypeOpt)
        {
            Debug.Assert(!operatorKind.IsLogical());

            if (operatorKind.IsLifted())
            {
                return RewriteLiftedBinaryOperator(syntax, operatorKind, loweredLeft, loweredRight, type, method, constrainedToTypeOpt);
            }

            // Otherwise, nothing special here.
            Debug.Assert(method is { });
            Debug.Assert(TypeSymbol.Equals(method.ReturnType, type, TypeCompareKind.ConsiderEverything2));
            return BoundCall.Synthesized(
                syntax,
                receiverOpt: constrainedToTypeOpt is null ? null : new BoundTypeExpression(syntax, aliasOpt: null, constrainedToTypeOpt),
                initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown,
                method,
                loweredLeft,
                loweredRight);
        }

        private BoundExpression? TrivialLiftedComparisonOperatorOptimizations(
            SyntaxNode syntax,
            BinaryOperatorKind kind,
            BoundExpression left,
            BoundExpression right,
            MethodSymbol? method,
            TypeSymbol? constrainedToTypeOpt)
        {
            Debug.Assert(left != null);
            Debug.Assert(right != null);

            // Optimization #1: if both sides are null then the result 
            // is either true (for equality) or false (for everything else.)

            bool leftAlwaysNull = NullableNeverHasValue(left);
            bool rightAlwaysNull = NullableNeverHasValue(right);

            TypeSymbol boolType = _compilation.GetSpecialType(SpecialType.System_Boolean);

            if (leftAlwaysNull && rightAlwaysNull)
            {
                return MakeLiteral(syntax, ConstantValue.Create(kind.Operator() == BinaryOperatorKind.Equal), boolType);
            }

            // Optimization #2: If both sides are non-null then we can again eliminate the lifting entirely.

            BoundExpression? leftNonNull = NullableAlwaysHasValue(left);
            BoundExpression? rightNonNull = NullableAlwaysHasValue(right);

            if (leftNonNull != null && rightNonNull != null)
            {
                return MakeBinaryOperator(
                    syntax: syntax,
                    operatorKind: kind.Unlifted(),
                    loweredLeft: leftNonNull,
                    loweredRight: rightNonNull,
                    type: boolType,
                    method: method,
                    constrainedToTypeOpt: constrainedToTypeOpt);
            }

            // Optimization #3: If one side is null and the other is definitely not, then we generate the side effects
            // of the non-null side and result in true (for not-equals) or false (for everything else.)

            BinaryOperatorKind operatorKind = kind.Operator();

            if (leftAlwaysNull && rightNonNull != null || rightAlwaysNull && leftNonNull != null)
            {
                BoundExpression result = MakeLiteral(syntax, ConstantValue.Create(operatorKind == BinaryOperatorKind.NotEqual), boolType);

                BoundExpression? nonNull = leftAlwaysNull ? rightNonNull : leftNonNull;
                Debug.Assert(nonNull is { });

                if (ReadIsSideeffecting(nonNull))
                {
                    result = new BoundSequence(
                                    syntax: syntax,
                                    locals: ImmutableArray<LocalSymbol>.Empty,
                                    sideEffects: ImmutableArray.Create<BoundExpression>(nonNull),
                                    value: result,
                                    type: boolType);
                }

                return result;
            }

            // Optimization #4: If one side is null and the other is unknown, then we have three cases:
            // #4a: If we have x == null then that becomes !x.HasValue.
            // #4b: If we have x != null then that becomes x.HasValue.
            // #4c: If we have x OP null then that becomes side effects of x, result in false.

            if (leftAlwaysNull || rightAlwaysNull)
            {
                BoundExpression maybeNull = leftAlwaysNull ? right : left;

                if (operatorKind == BinaryOperatorKind.Equal || operatorKind == BinaryOperatorKind.NotEqual)
                {
                    BoundExpression callHasValue = _factory.MakeNullableHasValue(syntax, maybeNull);
                    BoundExpression result = operatorKind == BinaryOperatorKind.Equal ?
                        MakeUnaryOperator(UnaryOperatorKind.BoolLogicalNegation, syntax, method: null, constrainedToTypeOpt: null, callHasValue, boolType) :
                        callHasValue;
                    return result;
                }
                else
                {
                    BoundExpression falseExpr = MakeBooleanConstant(syntax, operatorKind == BinaryOperatorKind.NotEqual);
                    return _factory.MakeSequence(maybeNull, falseExpr);
                }
            }

            return null;
        }

        private BoundExpression MakeOptimizedGetValueOrDefault(SyntaxNode syntax, BoundExpression expression)
        {
            Debug.Assert(expression.Type is { });
            // If the expression is of nullable type then call GetValueOrDefault. If not,
            // then just use its value.

            if (expression.Type.IsNullableType())
            {
                return BoundCall.Synthesized(
                    syntax,
                    expression,
                    initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown,
                    UnsafeGetNullableMethod(syntax, expression.Type, SpecialMember.System_Nullable_T_GetValueOrDefault));
            }

            return expression;
        }

        private BoundExpression MakeBooleanConstant(SyntaxNode syntax, bool value)
        {
            return MakeLiteral(syntax, ConstantValue.Create(value), _compilation.GetSpecialType(SpecialType.System_Boolean));
        }

        private BoundExpression MakeOptimizedHasValue(SyntaxNode syntax, BoundExpression expression)
        {
            Debug.Assert(expression.Type is { });

            // If the expression is of nullable type then call HasValue. If not, then it has a value,
            // so return constant true.
            if (expression.Type.IsNullableType())
            {
                return _factory.MakeNullableHasValue(syntax, expression);
            }

            return MakeBooleanConstant(syntax, true);
        }

        private BoundExpression MakeNullableHasValue(SyntaxNode syntax, BoundExpression expression)
        {
            Debug.Assert(expression.Type is { });
            return BoundCall.Synthesized(
                syntax,
                expression,
                initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown,
                UnsafeGetNullableMethod(syntax, expression.Type, SpecialMember.System_Nullable_T_get_HasValue));
        }

        private BoundExpression LowerLiftedBuiltInComparisonOperator(
            SyntaxNode syntax,
            BinaryOperatorKind kind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight)
        {
            // SPEC: For the equality operators == != :
            // SPEC: The lifted operator considers two null values equal and a null value unequal to
            // SPEC: any non-null value. If both operands are non-null the lifted operator unwraps
            // SPEC: the operands and applies the underlying operator to produce the bool result.
            // SPEC:
            // SPEC: For the relational operators < > <= >= :
            // SPEC: The lifted operator produces the value false if one or both operands
            // SPEC: are null. Otherwise the lifted operator unwraps the operands and
            // SPEC: applies the underlying operator to produce the bool result.

            // Note that this means that x == y is true but x <= y is false if both are null.
            // x <= y is not the same as (x < y) || (x == y).

            // Start with some simple optimizations for cases like one side being null.
            BoundExpression? optimized = TrivialLiftedComparisonOperatorOptimizations(syntax, kind, loweredLeft, loweredRight, method: null, constrainedToTypeOpt: null);
            if (optimized != null)
            {
                return optimized;
            }

            // We rewrite x == y as 
            //
            // tempx = x; 
            // tempy = y;
            // result = (tempx.GetValueOrDefault() == tempy.GetValueOrDefault()) &
            //          (tempx.HasValue == tempy.HasValue);
            //
            // and x != y as
            //
            // tempx = x; 
            // tempy = y;
            // result = !((tempx.GetValueOrDefault() == tempy.GetValueOrDefault()) &
            //            (tempx.HasValue == tempy.HasValue));
            //
            // Otherwise, we rewrite x OP y as
            //
            // tempx = x;
            // tempy = y;
            // result = (tempx.GetValueOrDefault() OP tempy.GetValueOrDefault()) &
            //          (tempx.HasValue & tempy.HasValue);
            //
            //
            // Note that there is no reason to generate "&&" over "&"; the cost of
            // the added code for the conditional branch would be about the same as simply doing 
            // the bitwise & in the first place.
            //
            // We have not yet optimized the case where we have a known-not-null value on one side, 
            // and an unknown value on the other. In those cases we will still generate a temp, but
            // we will not generate the call to the unnecessary nullable ctor or to GetValueOrDefault.
            // Rather, we will generate the value's temp instead of a call to GetValueOrDefault, and generate
            // literal true for HasValue. The tree construction methods we call will use those constants
            // to eliminate unnecessary branches.

            BoundExpression? xNonNull = NullableAlwaysHasValue(loweredLeft);
            BoundExpression? yNonNull = NullableAlwaysHasValue(loweredRight);

            TypeSymbol boolType = _compilation.GetSpecialType(SpecialType.System_Boolean);

            // Optimization: If one side is non-default constant, checking HasValue is not needed.
            if (kind.Operator() is BinaryOperatorKind.Equal or BinaryOperatorKind.NotEqual)
            {
                if (canNotBeEqualToDefaultValue(xNonNull?.ConstantValueOpt))
                {
                    Debug.Assert(yNonNull is null, "Handled by trivial optimization above; otherwise we should use yNonNull here.");
                    return MakeBinaryOperator(
                        syntax: syntax,
                        operatorKind: kind.Unlifted(),
                        loweredLeft: xNonNull,
                        loweredRight: MakeOptimizedGetValueOrDefault(syntax, loweredRight),
                        type: boolType,
                        method: null,
                        constrainedToTypeOpt: null);
                }

                if (canNotBeEqualToDefaultValue(yNonNull?.ConstantValueOpt))
                {
                    Debug.Assert(xNonNull is null, "Handled by trivial optimization above; otherwise we should use xNonNull here.");
                    return MakeBinaryOperator(
                        syntax: syntax,
                        operatorKind: kind.Unlifted(),
                        loweredLeft: MakeOptimizedGetValueOrDefault(syntax, loweredLeft),
                        loweredRight: yNonNull,
                        type: boolType,
                        method: null,
                        constrainedToTypeOpt: null);
                }
            }

            BoundLocal boundTempX = _factory.StoreToTemp(xNonNull ?? loweredLeft, out BoundAssignmentOperator tempAssignmentX);
            BoundLocal boundTempY = _factory.StoreToTemp(yNonNull ?? loweredRight, out BoundAssignmentOperator tempAssignmentY);

            BoundExpression callX_GetValueOrDefault = MakeOptimizedGetValueOrDefault(syntax, boundTempX);
            BoundExpression callY_GetValueOrDefault = MakeOptimizedGetValueOrDefault(syntax, boundTempY);
            BoundExpression callX_HasValue = MakeOptimizedHasValue(syntax, boundTempX);
            BoundExpression callY_HasValue = MakeOptimizedHasValue(syntax, boundTempY);

            BinaryOperatorKind leftOperator;
            BinaryOperatorKind rightOperator;

            BinaryOperatorKind operatorKind = kind.Operator();
            switch (operatorKind)
            {
                case BinaryOperatorKind.Equal:
                case BinaryOperatorKind.NotEqual:
                    leftOperator = BinaryOperatorKind.Equal;
                    rightOperator = BinaryOperatorKind.BoolEqual;
                    break;
                default:
                    leftOperator = operatorKind;
                    rightOperator = BinaryOperatorKind.BoolAnd;
                    break;
            }

            // (tempx.GetValueOrDefault() OP tempy.GetValueOrDefault())
            BoundExpression leftExpression = MakeBinaryOperator(
                syntax: syntax,
                operatorKind: leftOperator.WithType(kind.OperandTypes()),
                loweredLeft: callX_GetValueOrDefault,
                loweredRight: callY_GetValueOrDefault,
                type: boolType,
                method: null,
                constrainedToTypeOpt: null);

            // (tempx.HasValue OP tempy.HasValue)
            BoundExpression rightExpression = MakeBinaryOperator(
                syntax: syntax,
                operatorKind: rightOperator,
                loweredLeft: callX_HasValue,
                loweredRight: callY_HasValue,
                type: boolType,
                method: null,
                constrainedToTypeOpt: null);

            // result = (tempx.GetValueOrDefault() OP tempy.GetValueOrDefault()) &
            //          (tempx.HasValue OP tempy.HasValue)
            BoundExpression binaryExpression = MakeBinaryOperator(
                syntax: syntax,
                operatorKind: BinaryOperatorKind.BoolAnd,
                loweredLeft: leftExpression,
                loweredRight: rightExpression,
                type: boolType,
                method: null,
                constrainedToTypeOpt: null);

            // result = !((tempx.GetValueOrDefault() == tempy.GetValueOrDefault()) &
            //            (tempx.HasValue == tempy.HasValue));
            if (operatorKind == BinaryOperatorKind.NotEqual)
            {
                binaryExpression = _factory.Not(binaryExpression);
            }

            // tempx = x; 
            // tempy = y;
            // result = (tempx.GetValueOrDefault() == tempy.GetValueOrDefault()) &
            //          (tempx.HasValue == tempy.HasValue);
            return new BoundSequence(
                syntax: syntax,
                locals: ImmutableArray.Create<LocalSymbol>(boundTempX.LocalSymbol, boundTempY.LocalSymbol),
                sideEffects: ImmutableArray.Create<BoundExpression>(tempAssignmentX, tempAssignmentY),
                value: binaryExpression,
                type: boolType);

            static bool canNotBeEqualToDefaultValue(
                [NotNullWhen(returnValue: true)] ConstantValue? constantValue)
            {
                // This is an explicit list so new constant values are not accidentally supported without consideration.
                // Decimal is not in the list because it is possible to have a non-default decimal constant
                // which is equal to the default decimal (0.0m == default(decimal)).
                return constantValue is
                {
                    IsDefaultValue: false,
                    Discriminator: ConstantValueTypeDiscriminator.Boolean
                        or ConstantValueTypeDiscriminator.Double
                        or ConstantValueTypeDiscriminator.Int32
                        or ConstantValueTypeDiscriminator.Int64
                        or ConstantValueTypeDiscriminator.NInt
                        or ConstantValueTypeDiscriminator.NUInt
                        or ConstantValueTypeDiscriminator.Single
                        or ConstantValueTypeDiscriminator.UInt32
                        or ConstantValueTypeDiscriminator.UInt64
                };
            }
        }

        private BoundExpression LowerLiftedUserDefinedComparisonOperator(
            SyntaxNode syntax,
            BinaryOperatorKind kind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight,
            MethodSymbol? method,
            TypeSymbol? constrainedToTypeOpt)
        {
            // If both sides are null, or neither side is null, then we can do some simple optimizations.

            BoundExpression? optimized = TrivialLiftedComparisonOperatorOptimizations(syntax, kind, loweredLeft, loweredRight, method, constrainedToTypeOpt);
            if (optimized != null)
            {
                return optimized;
            }

            // Otherwise, the expression
            //
            // x == y 
            //
            // becomes
            //
            // tempX = x;
            // tempY = y;
            // result = tempX.HasValue == tempY.HasValue ? 
            //            (tempX.HasValue ? 
            //              tempX.GetValueOrDefault() == tempY.GetValueOrDefault() : 
            //              true) : 
            //          false;
            //
            //
            // the expression
            //
            // x != y 
            //
            // becomes
            //
            // tempX = x;
            // tempY = y;
            // result = tempX.HasValue == tempY.HasValue ? 
            //            (tempX.HasValue ? 
            //              tempX.GetValueOrDefault() != tempY.GetValueOrDefault() : 
            //              false) : 
            //            true;
            //
            //
            // For the other comparison operators <, <=, >, >=,
            //
            // x OP y 
            //
            // becomes
            //
            // tempX = x;
            // tempY = y;
            // result = tempX.HasValue & tempY.HasValue ? 
            //              tempX.GetValueOrDefault() OP tempY.GetValueOrDefault() : 
            //              false;
            //
            // We have not yet optimized the case where we have a known-not-null value on one side, 
            // and an unknown value on the other. In those cases we will still generate a temp, but
            // we will not generate the call to the unnecessary nullable ctor or to GetValueOrDefault.
            // Rather, we will generate the value's temp instead of a call to GetValueOrDefault, and generate
            // literal true for HasValue. The tree construction methods we call will use those constants
            // to eliminate unnecessary branches.

            BoundExpression? xNonNull = NullableAlwaysHasValue(loweredLeft);
            BoundExpression? yNonNull = NullableAlwaysHasValue(loweredRight);

            // TODO: (This TODO applies throughout this file, not just to this method.)
            // TODO: We might be storing a constant to this temporary that we could simply inline.
            // TODO: (There are other expressions that can be safely moved around other than constants
            // TODO: as well -- for example a boxing conversion of a constant int to object.)
            // TODO: Build a better temporary-storage management system that decides whether or not
            // TODO: to store a temporary.

            BoundAssignmentOperator tempAssignmentX;
            BoundLocal boundTempX = _factory.StoreToTemp(xNonNull ?? loweredLeft, out tempAssignmentX);
            BoundAssignmentOperator tempAssignmentY;
            BoundLocal boundTempY = _factory.StoreToTemp(yNonNull ?? loweredRight, out tempAssignmentY);

            BoundExpression callX_GetValueOrDefault = MakeOptimizedGetValueOrDefault(syntax, boundTempX);
            BoundExpression callY_GetValueOrDefault = MakeOptimizedGetValueOrDefault(syntax, boundTempY);
            BoundExpression callX_HasValue = MakeOptimizedHasValue(syntax, boundTempX);
            BoundExpression callY_HasValue = MakeOptimizedHasValue(syntax, boundTempY);

            // tempx.HasValue == tempy.HasValue
            BinaryOperatorKind conditionOperator;
            BinaryOperatorKind operatorKind = kind.Operator();
            switch (operatorKind)
            {
                case BinaryOperatorKind.Equal:
                case BinaryOperatorKind.NotEqual:
                    conditionOperator = BinaryOperatorKind.BoolEqual;
                    break;
                default:
                    conditionOperator = BinaryOperatorKind.BoolAnd;
                    break;
            }

            TypeSymbol boolType = _compilation.GetSpecialType(SpecialType.System_Boolean);

            BoundExpression condition = MakeBinaryOperator(
                syntax: syntax,
                operatorKind: conditionOperator,
                loweredLeft: callX_HasValue,
                loweredRight: callY_HasValue,
                type: boolType,
                method: null,
                constrainedToTypeOpt: null);

            // tempX.GetValueOrDefault() OP tempY.GetValueOrDefault()
            BoundExpression unliftedOp = MakeBinaryOperator(
                syntax: syntax,
                operatorKind: kind.Unlifted(),
                loweredLeft: callX_GetValueOrDefault,
                loweredRight: callY_GetValueOrDefault,
                type: boolType,
                method: method,
                constrainedToTypeOpt: constrainedToTypeOpt);

            BoundExpression consequence;
            if (operatorKind == BinaryOperatorKind.Equal || operatorKind == BinaryOperatorKind.NotEqual)
            {
                // If one operand is known to be non-null, we would get `x.HasValue ? (x.HasValue ? x.GVOD() OP y.GVOD() : true) : false`.
                // Hence, we don't need the nested conditional, only its consequence.
                if (xNonNull != null || yNonNull != null)
                {
                    consequence = unliftedOp;
                }
                else
                {
                    // tempx.HasValue ? tempX.GetValueOrDefault() == tempY.GetValueOrDefault() : true
                    consequence = RewriteConditionalOperator(
                        syntax: syntax,
                        rewrittenCondition: callX_HasValue,
                        rewrittenConsequence: unliftedOp,
                        rewrittenAlternative: MakeLiteral(syntax, ConstantValue.Create(operatorKind == BinaryOperatorKind.Equal), boolType),
                        constantValueOpt: null,
                        rewrittenType: boolType,
                        isRef: false);
                }
            }
            else
            {
                // tempX.GetValueOrDefault() OP tempY.GetValueOrDefault()
                consequence = unliftedOp;
            }

            // false
            BoundExpression alternative = MakeBooleanConstant(syntax, operatorKind == BinaryOperatorKind.NotEqual);

            BoundExpression conditionalExpression = RewriteConditionalOperator(
                syntax: syntax,
                rewrittenCondition: condition,
                rewrittenConsequence: consequence,
                rewrittenAlternative: alternative,
                constantValueOpt: null,
                rewrittenType: boolType,
                isRef: false);

            return new BoundSequence(
                syntax: syntax,
                locals: ImmutableArray.Create<LocalSymbol>(boundTempX.LocalSymbol, boundTempY.LocalSymbol),
                sideEffects: ImmutableArray.Create<BoundExpression>(tempAssignmentX, tempAssignmentY),
                value: conditionalExpression,
                type: boolType);
        }

        private BoundExpression? TrivialLiftedBinaryArithmeticOptimizations(
            SyntaxNode syntax,
            BinaryOperatorKind kind,
            BoundExpression left,
            BoundExpression right,
            TypeSymbol type,
            MethodSymbol? method,
            TypeSymbol? constrainedToTypeOpt)
        {
            // We begin with a trivial optimization: if both operands are null then the 
            // result is known to be null.

            Debug.Assert(left != null);
            Debug.Assert(right != null);

            // Optimization #1: if both sides are null then the result 
            // is either true (for equality) or false (for everything else.)

            bool leftAlwaysNull = NullableNeverHasValue(left);
            bool rightAlwaysNull = NullableNeverHasValue(right);

            if (leftAlwaysNull && rightAlwaysNull)
            {
                // default(R?)
                return new BoundDefaultExpression(syntax, type);
            }

            // Optimization #2: If both sides are non-null then we can again eliminate the lifting entirely.

            BoundExpression? leftNonNull = NullableAlwaysHasValue(left);
            BoundExpression? rightNonNull = NullableAlwaysHasValue(right);

            if (leftNonNull != null && rightNonNull != null)
            {
                return MakeLiftedBinaryOperatorConsequence(syntax, kind, leftNonNull, rightNonNull, type, method, constrainedToTypeOpt);
            }

            return null;
        }

        private BoundExpression MakeLiftedBinaryOperatorConsequence(
            SyntaxNode syntax,
            BinaryOperatorKind kind,
            BoundExpression left,
            BoundExpression right,
            TypeSymbol type,
            MethodSymbol? method,
            TypeSymbol? constrainedToTypeOpt)
        {
            // tempX.GetValueOrDefault() OP tempY.GetValueOrDefault()
            BoundExpression unliftedOp = MakeBinaryOperator(
                syntax: syntax,
                operatorKind: kind.Unlifted(),
                loweredLeft: left,
                loweredRight: right,
                type: type.GetNullableUnderlyingType(),
                method: method,
                constrainedToTypeOpt: constrainedToTypeOpt);

            // new R?(tempX.GetValueOrDefault() OP tempY.GetValueOrDefault)
            return new BoundObjectCreationExpression(
                syntax,
                UnsafeGetNullableMethod(syntax, type, SpecialMember.System_Nullable_T__ctor),
                unliftedOp);
        }

        private static BoundExpression? OptimizeLiftedArithmeticOperatorOneNull(
            SyntaxNode syntax,
            BoundExpression left,
            BoundExpression right,
            TypeSymbol type)
        {
            // Here we optimize the cases where one side is known to be null. If we have
            // null + M() or null + new int?(M()) then we simply generate M() as a side 
            // effect and produce null. Note that we can optimize away the unnecessary
            // constructor.

            bool leftAlwaysNull = NullableNeverHasValue(left);
            bool rightAlwaysNull = NullableNeverHasValue(right);

            Debug.Assert(!(leftAlwaysNull && rightAlwaysNull)); // We've already optimized this case.

            if (!(leftAlwaysNull || rightAlwaysNull))
            {
                return null;
            }

            BoundExpression notAlwaysNull = leftAlwaysNull ? right : left;
            BoundExpression? neverNull = NullableAlwaysHasValue(notAlwaysNull);
            BoundExpression sideEffect = neverNull ?? notAlwaysNull;

            // If the "side effect" is a constant then we simply elide it entirely.

            // TODO: There are expressions other than constants that have no side effects
            // TODO: or elidable side effects.

            if (sideEffect.ConstantValueOpt != null)
            {
                return new BoundDefaultExpression(syntax, type);
            }

            return new BoundSequence(
                syntax: syntax,
                locals: ImmutableArray<LocalSymbol>.Empty,
                sideEffects: ImmutableArray.Create<BoundExpression>(sideEffect),
                value: new BoundDefaultExpression(syntax, type),
                type: type);
        }

        private BoundExpression LowerLiftedBinaryArithmeticOperator(
            SyntaxNode syntax,
            BinaryOperatorKind kind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight,
            TypeSymbol type,
            MethodSymbol? method,
            TypeSymbol? constrainedToTypeOpt)
        {
            // We have a lifted * / % + - << >> ^ & | binary operator. We begin with trivial
            // optimizations; if both sides are null or neither side is null then we can
            // eliminate the lifting altogether.

            BoundExpression? optimized = OptimizeLiftedBinaryArithmetic(syntax, kind, loweredLeft, loweredRight, type, method, constrainedToTypeOpt);
            if (optimized != null)
            {
                return optimized;
            }

            // We now know that neither side is null. However, we might have an operand that is known
            // to be non-null. If neither side is known to be non-null then we generate:
            //
            // S? tempX = left;
            // S? tempY = right;
            // R? r = tempX.HasValue & tempY.HasValue ? 
            //        new R?(tempX.GetValueOrDefault() OP tempY.GetValueOrDefault())  :
            //        default(R?);
            //
            // If one of the operands, say the right, is non-null, then we generate:
            //
            // S? tempX = left;
            // S tempY = right; // not null
            // R? r = tempX.HasValue  ? 
            //        new R?(tempX.GetValueOrDefault() OP tempY)  :
            //        default(R?);
            //

            var sideeffects = ArrayBuilder<BoundExpression>.GetInstance();
            var locals = ArrayBuilder<LocalSymbol>.GetInstance();

            BoundExpression? leftNeverNull = NullableAlwaysHasValue(loweredLeft);
            BoundExpression? rightNeverNull = NullableAlwaysHasValue(loweredRight);

            BoundExpression boundTempX = leftNeverNull ?? loweredLeft;
            boundTempX = CaptureExpressionInTempIfNeeded(boundTempX, sideeffects, locals);

            BoundExpression boundTempY = rightNeverNull ?? loweredRight;
            boundTempY = CaptureExpressionInTempIfNeeded(boundTempY, sideeffects, locals);

            BoundExpression callX_GetValueOrDefault = MakeOptimizedGetValueOrDefault(syntax, boundTempX);
            BoundExpression callY_GetValueOrDefault = MakeOptimizedGetValueOrDefault(syntax, boundTempY);
            BoundExpression callX_HasValue = MakeOptimizedHasValue(syntax, boundTempX);
            BoundExpression callY_HasValue = MakeOptimizedHasValue(syntax, boundTempY);

            // tempX.HasValue & tempY.HasValue
            TypeSymbol boolType = _compilation.GetSpecialType(SpecialType.System_Boolean);
            BoundExpression condition = MakeBinaryOperator(syntax, BinaryOperatorKind.BoolAnd, callX_HasValue, callY_HasValue, boolType, method: null, constrainedToTypeOpt: null);

            // new R?(tempX.GetValueOrDefault() OP tempY.GetValueOrDefault)
            BoundExpression consequence = MakeLiftedBinaryOperatorConsequence(syntax, kind, callX_GetValueOrDefault, callY_GetValueOrDefault, type, method, constrainedToTypeOpt);

            // default(R?)
            BoundExpression alternative = new BoundDefaultExpression(syntax, type);

            // tempX.HasValue & tempY.HasValue ? 
            //          new R?(tempX.GetValueOrDefault() OP tempY.GetValueOrDefault()) : 
            //          default(R?);
            BoundExpression conditionalExpression = RewriteConditionalOperator(
                syntax: syntax,
                rewrittenCondition: condition,
                rewrittenConsequence: consequence,
                rewrittenAlternative: alternative,
                constantValueOpt: null,
                rewrittenType: type,
                isRef: false);

            return new BoundSequence(
                syntax: syntax,
                locals: locals.ToImmutableAndFree(),
                sideEffects: sideeffects.ToImmutableAndFree(),
                value: conditionalExpression,
                type: type);
        }

        private BoundExpression CaptureExpressionInTempIfNeeded(
            BoundExpression operand,
            ArrayBuilder<BoundExpression> sideeffects,
            ArrayBuilder<LocalSymbol> locals,
            SynthesizedLocalKind kind = SynthesizedLocalKind.LoweringTemp)
        {
            if (CanChangeValueBetweenReads(operand))
            {
                BoundAssignmentOperator tempAssignment;
                var tempAccess = _factory.StoreToTemp(operand, out tempAssignment, kind: kind);
                sideeffects.Add(tempAssignment);
                locals.Add(tempAccess.LocalSymbol);
                operand = tempAccess;
            }

            return operand;
        }

        private BoundExpression? OptimizeLiftedBinaryArithmetic(
            SyntaxNode syntax,
            BinaryOperatorKind kind,
            BoundExpression left,
            BoundExpression right,
            TypeSymbol type,
            MethodSymbol? method,
            TypeSymbol? constrainedToTypeOpt)
        {
            BoundExpression? optimized = TrivialLiftedBinaryArithmeticOptimizations(syntax, kind, left, right, type, method, constrainedToTypeOpt);
            if (optimized != null)
            {
                return optimized;
            }

            // Boolean & and | operators have completely different codegen in non-trivial cases.
            if (kind == BinaryOperatorKind.LiftedBoolAnd || kind == BinaryOperatorKind.LiftedBoolOr)
            {
                return LowerLiftedBooleanOperator(syntax, kind, left, right);
            }

            // If one side is null then we can lower this to a side effect and a null value.
            optimized = OptimizeLiftedArithmeticOperatorOneNull(syntax, left, right, type);
            if (optimized != null)
            {
                return optimized;
            }

            // This is a bit of a complicated optimization; it is a more complex version of the 
            // "distributed" optimizations for lifted conversions and lifted unary operators.
            //
            // Suppose we have a lifted binary operation where the left side might be null or not,
            // and the right side is definitely not, and moreover, it is a constant.  We are particularly
            // concerned about this because expressions like "i++" and "i += 1" are lowered to "i = i + 1";
            // it is quite common for there to be a constant on the left hand side of a lifted binop.
            //
            // Here N() returns int?:
            //
            // return N() + 1;
            //
            // In LowerLiftedBinaryOperator, above, we would optimize this as:
            //
            // int? n = N();
            // int v = 1;
            // return n.HasValue ? new int?(n.Value + v) : new int?()
            //
            // This is unfortunate in that we generate an unnecessary temporary, but that's
            // not what we're going to optimize away here. This codegen is pretty good. 
            //
            // Now let's suppose that instead of N(), we have a lifted operation on the left side:
            //
            // return (N1() * N2()) + 1;
            //
            // We could realize the left hand term as a lifted multiplication, produce a nullable int,
            // assign it to a temporary, and do the lifted addition:
            //
            // int? n1 = N1();
            // int? n2 = N2();
            // int? r = n1.HasValue & n2.HasValue ? new int?(n1.Value * n2.Value) : new int?();
            // int v = 1;
            // return r.HasValue ? new int?(r.Value + v) : new int?();
            //
            // But what is the point of the temporary r in this expansion?  We can eliminate it, and 
            // thereby eliminate two int? constructors in the process.  We can realize this as:
            //
            // int? n1 = N1();
            // int? n2 = N2();
            // return n1.HasValue & n2.HasValue ? new int?(n1.Value * n2.Value + 1) : new int?();
            //
            // We eliminate both the temporaries r and v.
            //
            // Now, a reasonable question at this point would be "well, suppose the expression on
            // the right side is not a constant, but is known to be non-null; can we do the same
            // optimization?"  No, and here's why. Suppose we have:
            //
            // return (N1() * N2()) + V();
            //
            // We cannot realize this as:
            //
            // int? n1 = N1();
            // int? n2 = N2();
            // int v = V();
            // return n1.HasValue & n2.HasValue ? new int?(n1.Value * n2.Value + v) : new int?();
            //
            // because this changes the order in which the operations happen. Before we had N1(),
            // N2(), the multiplication -- which might be a user-defined operator with a side effect,
            // or might be in a checked context and overflow -- and then V().  Now we have V() before
            // the multiplication, changing the order in which side effects occur.
            //
            // We could solve this problem by generating:
            //
            // int? n1 = N1();
            // int? n2 = N2();
            // return n1.HasValue & n2.HasValue ? new int?(n1.Value * n2.Value + V()) : (V(), new int?());

            // so that the side effects of V() happen after the multiplication or before the value is 
            // produced, but now we have generated IL for V() twice. That could be a complex expression
            // and the whole point of this optimization is to make less IL. 
            //
            // We could, however, optimize it if the non-null V() was on the left hand side. At this
            // time we will not pursue that optimization. We're really just hoping to get the N1() * N2() + 1
            // operation smaller; the rest is gravy.

            BoundExpression? nonNullRight = NullableAlwaysHasValue(right);
            if (nonNullRight != null && nonNullRight.ConstantValueOpt != null && left.Kind == BoundKind.Sequence)
            {
                BoundSequence seq = (BoundSequence)left;
                if (seq.Value.Kind == BoundKind.ConditionalOperator)
                {
                    BoundConditionalOperator conditional = (BoundConditionalOperator)seq.Value;
                    Debug.Assert(TypeSymbol.Equals(seq.Type, conditional.Type, TypeCompareKind.ConsiderEverything2));
                    Debug.Assert(TypeSymbol.Equals(conditional.Type, conditional.Consequence.Type, TypeCompareKind.ConsiderEverything2));
                    Debug.Assert(TypeSymbol.Equals(conditional.Type, conditional.Alternative.Type, TypeCompareKind.ConsiderEverything2));

                    if (NullableAlwaysHasValue(conditional.Consequence) != null && NullableNeverHasValue(conditional.Alternative))
                    {
                        return new BoundSequence(
                            syntax,
                            seq.Locals,
                            seq.SideEffects,
                            RewriteConditionalOperator(
                                syntax,
                                conditional.Condition,
                                MakeBinaryOperator(syntax, kind, conditional.Consequence, right, type, method, constrainedToTypeOpt),
                                MakeBinaryOperator(syntax, kind, conditional.Alternative, right, type, method, constrainedToTypeOpt),
                                ConstantValue.NotAvailable,
                                type,
                                isRef: false),
                            type);
                    }
                }
            }

            return null;
        }

        private BoundExpression MakeNewNullableBoolean(SyntaxNode syntax, bool? value)
        {
            TypeSymbol boolType = _compilation.GetSpecialType(SpecialType.System_Boolean);
            NamedTypeSymbol nullableBoolType = _compilation.GetOrCreateNullableType(boolType);
            if (value == null)
            {
                return new BoundDefaultExpression(syntax, nullableBoolType);
            }

            return new BoundObjectCreationExpression(
                syntax,
                UnsafeGetNullableMethod(syntax, nullableBoolType, SpecialMember.System_Nullable_T__ctor),
                MakeBooleanConstant(syntax, value.GetValueOrDefault()));
        }

        private BoundExpression? OptimizeLiftedBooleanOperatorOneNull(
            SyntaxNode syntax,
            BinaryOperatorKind kind,
            BoundExpression left,
            BoundExpression right)
        {
            // Here we optimize the cases where one side is known to be null.

            bool leftAlwaysNull = NullableNeverHasValue(left);
            bool rightAlwaysNull = NullableNeverHasValue(right);

            Debug.Assert(!(leftAlwaysNull && rightAlwaysNull)); // We've already optimized this case.

            if (!(leftAlwaysNull || rightAlwaysNull))
            {
                return null;
            }

            // First, if one operand is null and the other is definitely non null, then we can eliminate
            // all the temporaries:
            //
            // new bool?() & new bool?(B())
            // new bool?() | new bool?(B())
            //
            // can be generated as
            //
            // B() ? new bool?() : new bool?(false)
            // B() ? new bool?(true) : new bool?()
            //
            // respectively.

            BoundExpression alwaysNull = leftAlwaysNull ? left : right;
            BoundExpression notAlwaysNull = leftAlwaysNull ? right : left;
            BoundExpression? neverNull = NullableAlwaysHasValue(notAlwaysNull);
            Debug.Assert(alwaysNull.Type is { });
            BoundExpression nullBool = new BoundDefaultExpression(syntax, alwaysNull.Type);

            if (neverNull != null)
            {
                BoundExpression newNullBool = MakeNewNullableBoolean(syntax, kind == BinaryOperatorKind.LiftedBoolOr);

                return RewriteConditionalOperator(
                    syntax: syntax,
                    rewrittenCondition: neverNull,
                    rewrittenConsequence: kind == BinaryOperatorKind.LiftedBoolAnd ? nullBool : newNullBool,
                    rewrittenAlternative: kind == BinaryOperatorKind.LiftedBoolAnd ? newNullBool : nullBool,
                    constantValueOpt: null,
                    rewrittenType: alwaysNull.Type,
                    isRef: false);
            }

            // Now we optimize the case where one operand is null and the other is not. We generate
            //
            // new bool?() & M() 
            // new bool?() | M()
            //
            // as 
            //
            // bool? t = M(), t.GetValueOrDefault() ? new bool?() : t
            // bool? t = M(), t.GetValueOrDefault() ? t : new bool?()
            //
            // respectively.

            BoundAssignmentOperator tempAssignment;
            BoundLocal boundTemp = _factory.StoreToTemp(notAlwaysNull, out tempAssignment);
            BoundExpression condition = MakeOptimizedGetValueOrDefault(syntax, boundTemp);
            BoundExpression consequence = kind == BinaryOperatorKind.LiftedBoolAnd ? nullBool : boundTemp;
            BoundExpression alternative = kind == BinaryOperatorKind.LiftedBoolAnd ? boundTemp : nullBool;
            BoundExpression conditionalExpression = RewriteConditionalOperator(
                syntax: syntax,
                rewrittenCondition: condition,
                rewrittenConsequence: consequence,
                rewrittenAlternative: alternative,
                constantValueOpt: null,
                rewrittenType: alwaysNull.Type,
                isRef: false);
            Debug.Assert(conditionalExpression.Type is { });
            return new BoundSequence(
                syntax: syntax,
                locals: ImmutableArray.Create<LocalSymbol>(boundTemp.LocalSymbol),
                sideEffects: ImmutableArray.Create<BoundExpression>(tempAssignment),
                value: conditionalExpression,
                type: conditionalExpression.Type);
        }

        private BoundExpression? OptimizeLiftedBooleanOperatorOneNonNull(
            SyntaxNode syntax,
            BinaryOperatorKind kind,
            BoundExpression left,
            BoundExpression right)
        {
            // Here we optimize the cases where one side is known to be non-null.  We generate:
            //
            // new bool?(B()) & N()
            // N() & new bool?(B())
            // new bool?(B()) | N()
            // N() | new bool?(B())
            //
            // as
            //
            // bool b = B(), bool? n = N(), b ? n : new bool?(false)
            // bool? n = N(), bool b = B(), b ? n : new bool?(false)
            // bool b = B(), bool? n = N(), b ? new bool?(true) : n
            // bool? n = N(), bool b = B(), b ? new bool?(true) : n
            //
            // respectively.

            BoundExpression? leftNonNull = NullableAlwaysHasValue(left);
            BoundExpression? rightNonNull = NullableAlwaysHasValue(right);

            Debug.Assert(leftNonNull == null || rightNonNull == null); // We've already optimized the case where they are both non-null.
            Debug.Assert(!NullableNeverHasValue(left) && !NullableNeverHasValue(right)); // We've already optimized the case where one is null.

            if (leftNonNull == null && rightNonNull == null)
            {
                return null;
            }

            // One is definitely not null and the other might be null.

            BoundAssignmentOperator tempAssignmentX;
            BoundLocal boundTempX = _factory.StoreToTemp(leftNonNull ?? left, out tempAssignmentX);
            BoundAssignmentOperator tempAssignmentY;
            BoundLocal boundTempY = _factory.StoreToTemp(rightNonNull ?? right, out tempAssignmentY);
            BoundExpression nonNullTemp = leftNonNull == null ? boundTempY : boundTempX;
            BoundExpression maybeNullTemp = leftNonNull == null ? boundTempX : boundTempY;
            BoundExpression condition = nonNullTemp;
            BoundExpression newNullBool = MakeNewNullableBoolean(syntax, kind == BinaryOperatorKind.LiftedBoolOr);
            BoundExpression consequence = kind == BinaryOperatorKind.LiftedBoolOr ? newNullBool : maybeNullTemp;
            BoundExpression alternative = kind == BinaryOperatorKind.LiftedBoolOr ? maybeNullTemp : newNullBool;
            BoundExpression conditionalExpression = RewriteConditionalOperator(
                syntax: syntax,
                rewrittenCondition: condition,
                rewrittenConsequence: consequence,
                rewrittenAlternative: alternative,
                constantValueOpt: null,
                rewrittenType: newNullBool.Type!,
                isRef: false);
            return new BoundSequence(
                syntax: syntax,
                locals: ImmutableArray.Create<LocalSymbol>(boundTempX.LocalSymbol, boundTempY.LocalSymbol),
                sideEffects: ImmutableArray.Create<BoundExpression>(tempAssignmentX, tempAssignmentY),
                value: conditionalExpression,
                type: conditionalExpression.Type!);
        }

        private BoundExpression LowerLiftedBooleanOperator(
            SyntaxNode syntax,
            BinaryOperatorKind kind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight)
        {
            // x & y and x | y have special codegen if x and y are nullable Booleans.

            // We have already optimized cases where both operands are null or both are non-null.
            // Now optimize cases where one side is known to be null or one side is known to be non-null.
            BoundExpression? optimized = OptimizeLiftedBooleanOperatorOneNull(syntax, kind, loweredLeft, loweredRight);
            if (optimized != null)
            {
                return optimized;
            }

            optimized = OptimizeLiftedBooleanOperatorOneNonNull(syntax, kind, loweredLeft, loweredRight);
            if (optimized != null)
            {
                return optimized;
            }

            // x & y is realized as (x.GetValueOrDefault() || !(y.GetValueOrDefault() || x.HasValue)) ? y : x
            // x | y is realized as (x.GetValueOrDefault() || !(y.GetValueOrDefault() || x.HasValue)) ? x : y

            // CONSIDER: Consider realizing these using | instead of ||. 
            // CONSIDER: The operations are extremely low cost and the added bulk to the code might not be worthwhile.

            BoundAssignmentOperator tempAssignmentX;
            BoundLocal boundTempX = _factory.StoreToTemp(loweredLeft, out tempAssignmentX);
            BoundAssignmentOperator tempAssignmentY;
            BoundLocal boundTempY = _factory.StoreToTemp(loweredRight, out tempAssignmentY);

            TypeSymbol boolType = _compilation.GetSpecialType(SpecialType.System_Boolean);

            MethodSymbol getValueOrDefaultX = UnsafeGetNullableMethod(syntax, boundTempX.Type, SpecialMember.System_Nullable_T_GetValueOrDefault);
            MethodSymbol getValueOrDefaultY = UnsafeGetNullableMethod(syntax, boundTempY.Type, SpecialMember.System_Nullable_T_GetValueOrDefault);

            // tempx.GetValueOrDefault()
            BoundExpression callX_GetValueOrDefault = BoundCall.Synthesized(syntax, boundTempX, initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown, getValueOrDefaultX);
            // tempy.GetValueOrDefault()
            BoundExpression callY_GetValueOrDefault = BoundCall.Synthesized(syntax, boundTempY, initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown, getValueOrDefaultY);
            // tempx.HasValue
            BoundExpression callX_HasValue = _factory.MakeNullableHasValue(syntax, boundTempX);

            // (tempy.GetValueOrDefault || tempx.HasValue)
            BoundExpression innerOr = MakeBinaryOperator(
                syntax: syntax,
                operatorKind: BinaryOperatorKind.LogicalBoolOr,
                loweredLeft: callY_GetValueOrDefault,
                loweredRight: callX_HasValue,
                type: boolType,
                method: null,
                constrainedToTypeOpt: null);

            // !(tempy.GetValueOrDefault || tempx.HasValue)
            BoundExpression invert = MakeUnaryOperator(UnaryOperatorKind.BoolLogicalNegation, syntax, method: null, constrainedToTypeOpt: null, innerOr, boolType);

            // (x.GetValueOrDefault() || !(y.GetValueOrDefault() || x.HasValue))
            BoundExpression condition = MakeBinaryOperator(
                syntax: syntax,
                operatorKind: BinaryOperatorKind.LogicalBoolOr,
                loweredLeft: callX_GetValueOrDefault,
                loweredRight: invert,
                type: boolType,
                method: null,
                constrainedToTypeOpt: null);

            BoundExpression consequence = kind == BinaryOperatorKind.LiftedBoolAnd ? boundTempY : boundTempX;
            BoundExpression alternative = kind == BinaryOperatorKind.LiftedBoolAnd ? boundTempX : boundTempY;

            BoundExpression conditionalExpression = RewriteConditionalOperator(
                syntax: syntax,
                rewrittenCondition: condition,
                rewrittenConsequence: consequence,
                rewrittenAlternative: alternative,
                constantValueOpt: null,
                rewrittenType: alternative.Type!,
                isRef: false);

            return new BoundSequence(
                syntax: syntax,
                locals: ImmutableArray.Create<LocalSymbol>(boundTempX.LocalSymbol, boundTempY.LocalSymbol),
                sideEffects: ImmutableArray.Create<BoundExpression>(tempAssignmentX, tempAssignmentY),
                value: conditionalExpression,
                type: conditionalExpression.Type!);
        }

        /// <summary>
        /// This function provides a false sense of security, it is likely going to surprise you when the requested member is missing.
        /// Recommendation: Do not use, use <see cref="TryGetNullableMethod"/> instead! 
        /// If used, a unit-test with a missing member is absolutely a must have.
        /// </summary>
        private MethodSymbol UnsafeGetNullableMethod(SyntaxNode syntax, TypeSymbol nullableType, SpecialMember member)
        {
            return UnsafeGetNullableMethod(syntax, nullableType, member, _compilation, _diagnostics);
        }

        /// <summary>
        /// This function provides a false sense of security, it is likely going to surprise you when the requested member is missing.
        /// Recommendation: Do not use, use <see cref="TryGetNullableMethod"/> instead! 
        /// If used, a unit-test with a missing member is absolutely a must have.
        /// </summary>
        internal static MethodSymbol UnsafeGetNullableMethod(SyntaxNode syntax, TypeSymbol nullableType, SpecialMember member, CSharpCompilation compilation, BindingDiagnosticBag diagnostics)
        {
            var nullableType2 = nullableType as NamedTypeSymbol;
            Debug.Assert(nullableType2 is { });
            return UnsafeGetSpecialTypeMethod(syntax, member, compilation, diagnostics).AsMember(nullableType2);
        }

        private bool TryGetNullableMethod(SyntaxNode syntax, TypeSymbol nullableType, SpecialMember member, out MethodSymbol result, bool isOptional = false)
        {
            var nullableType2 = (NamedTypeSymbol)nullableType;
            if (TryGetSpecialTypeMethod(syntax, member, out result, isOptional))
            {
                result = result.AsMember(nullableType2);
                return true;
            }

            return false;
        }

        private BoundExpression RewriteNullableNullEquality(
            SyntaxNode syntax,
            BinaryOperatorKind kind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight,
            TypeSymbol returnType)
        {
            // This handles the case where we have a nullable user-defined struct type compared against null, eg:
            //
            // struct S {} ... S? s = whatever; if (s != null)
            //
            // If S does not define an overloaded != operator then this is lowered to s.HasValue.
            //
            // If the type already has a user-defined or built-in operator then comparing to null is
            // treated as a lifted equality operator.

            Debug.Assert(loweredLeft != null);
            Debug.Assert(loweredRight != null);
            Debug.Assert((object)returnType != null);
            Debug.Assert(returnType.SpecialType == SpecialType.System_Boolean);
            Debug.Assert(loweredLeft.IsLiteralNull() != loweredRight.IsLiteralNull());

            BoundExpression nullable = loweredRight.IsLiteralNull() ? loweredLeft : loweredRight;

            // If the other side is known to always be null then we can simply generate true or false, as appropriate.

            if (NullableNeverHasValue(nullable))
            {
                return MakeLiteral(syntax, ConstantValue.Create(kind == BinaryOperatorKind.NullableNullEqual), returnType);
            }

            BoundExpression? nonNullValue = NullableAlwaysHasValue(nullable);
            if (nonNullValue != null)
            {
                // We have something like "if (new int?(M()) != null)". We can optimize this to
                // evaluate M() for its side effects and then result in true or false, as appropriate.

                // TODO: If the expression has no side effects then it can be optimized away here as well.

                return new BoundSequence(
                    syntax: syntax,
                    locals: ImmutableArray<LocalSymbol>.Empty,
                    sideEffects: ImmutableArray.Create<BoundExpression>(nonNullValue),
                    value: MakeBooleanConstant(syntax, kind == BinaryOperatorKind.NullableNullNotEqual),
                    type: returnType);
            }

            // arr?.Length == null
            var conditionalAccess = nullable as BoundLoweredConditionalAccess;
            if (conditionalAccess != null &&
                (conditionalAccess.WhenNullOpt == null || conditionalAccess.WhenNullOpt.IsDefaultValue()))
            {
                BoundExpression whenNotNull = RewriteNullableNullEquality(
                    syntax,
                    kind,
                    conditionalAccess.WhenNotNull,
                    loweredLeft.IsLiteralNull() ? loweredLeft : loweredRight,
                    returnType);

                var whenNull = kind == BinaryOperatorKind.NullableNullEqual ? MakeBooleanConstant(syntax, true) : null;

                return conditionalAccess.Update(conditionalAccess.Receiver, conditionalAccess.HasValueMethodOpt, whenNotNull, whenNull, conditionalAccess.Id, conditionalAccess.ForceCopyOfNullableValueType, whenNotNull.Type!);
            }

            BoundExpression call = MakeNullableHasValue(syntax, nullable);
            BoundExpression result = kind == BinaryOperatorKind.NullableNullNotEqual ?
                call :
                new BoundUnaryOperator(syntax, UnaryOperatorKind.BoolLogicalNegation, call, ConstantValue.NotAvailable, null, constrainedToTypeOpt: null, LookupResultKind.Viable, returnType);

            return result;
        }

        private BoundExpression RewriteStringEquality(BoundBinaryOperator? oldNode, SyntaxNode syntax, BinaryOperatorKind operatorKind, BoundExpression loweredLeft, BoundExpression loweredRight, TypeSymbol type, SpecialMember member)
        {
            if (oldNode != null && (loweredLeft.ConstantValueOpt == ConstantValue.Null || loweredRight.ConstantValueOpt == ConstantValue.Null))
            {
                return oldNode.Update(operatorKind, oldNode.ConstantValueOpt, oldNode.BinaryOperatorMethod, oldNode.ConstrainedToType, oldNode.ResultKind, loweredLeft, loweredRight, type);
            }

            var method = UnsafeGetSpecialTypeMethod(syntax, member);
            Debug.Assert((object)method != null);

            return BoundCall.Synthesized(syntax, receiverOpt: null, initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown, method, loweredLeft, loweredRight);
        }

        private BoundExpression RewriteDelegateOperation(SyntaxNode syntax, BinaryOperatorKind operatorKind, BoundExpression loweredLeft, BoundExpression loweredRight, TypeSymbol type, SpecialMember member)
        {
            MethodSymbol method;
            if (operatorKind == BinaryOperatorKind.DelegateEqual || operatorKind == BinaryOperatorKind.DelegateNotEqual)
            {
                method = (MethodSymbol)_compilation.Assembly.GetSpecialTypeMember(member);
                if (loweredRight.IsLiteralNull() ||
                    loweredLeft.IsLiteralNull() ||
                    (object)(method = (MethodSymbol)_compilation.Assembly.GetSpecialTypeMember(member)) == null)
                {
                    // use reference equality in the absence of overloaded operators for System.Delegate.
                    operatorKind = (operatorKind & (~BinaryOperatorKind.Delegate)) | BinaryOperatorKind.Object;
                    return new BoundBinaryOperator(syntax, operatorKind, constantValueOpt: null, methodOpt: null, constrainedToTypeOpt: null, LookupResultKind.Empty, loweredLeft, loweredRight, type);
                }
            }
            else
            {
                method = UnsafeGetSpecialTypeMethod(syntax, member);
            }

            Debug.Assert((object)method != null);
            BoundExpression call = _inExpressionLambda
                ? new BoundBinaryOperator(syntax, operatorKind, null, method, constrainedToTypeOpt: null, default(LookupResultKind), loweredLeft, loweredRight, method.ReturnType)
                : (BoundExpression)BoundCall.Synthesized(syntax, receiverOpt: null, initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown, method, loweredLeft, loweredRight);
            BoundExpression result = method.ReturnType.SpecialType == SpecialType.System_Delegate ?
                MakeConversionNode(syntax, call, Conversion.ExplicitReference, type, @checked: false) :
                call;
            return result;
        }

        private BoundExpression RewriteDecimalBinaryOperation(SyntaxNode syntax, BoundExpression loweredLeft, BoundExpression loweredRight, BinaryOperatorKind operatorKind)
        {
            Debug.Assert(loweredLeft.Type is { SpecialType: SpecialType.System_Decimal });
            Debug.Assert(loweredRight.Type is { SpecialType: SpecialType.System_Decimal });

            SpecialMember member;

            switch (operatorKind)
            {
                case BinaryOperatorKind.DecimalAddition: member = SpecialMember.System_Decimal__op_Addition; break;
                case BinaryOperatorKind.DecimalSubtraction: member = SpecialMember.System_Decimal__op_Subtraction; break;
                case BinaryOperatorKind.DecimalMultiplication: member = SpecialMember.System_Decimal__op_Multiply; break;
                case BinaryOperatorKind.DecimalDivision: member = SpecialMember.System_Decimal__op_Division; break;
                case BinaryOperatorKind.DecimalRemainder: member = SpecialMember.System_Decimal__op_Modulus; break;
                case BinaryOperatorKind.DecimalEqual: member = SpecialMember.System_Decimal__op_Equality; break;
                case BinaryOperatorKind.DecimalNotEqual: member = SpecialMember.System_Decimal__op_Inequality; break;
                case BinaryOperatorKind.DecimalLessThan: member = SpecialMember.System_Decimal__op_LessThan; break;
                case BinaryOperatorKind.DecimalLessThanOrEqual: member = SpecialMember.System_Decimal__op_LessThanOrEqual; break;
                case BinaryOperatorKind.DecimalGreaterThan: member = SpecialMember.System_Decimal__op_GreaterThan; break;
                case BinaryOperatorKind.DecimalGreaterThanOrEqual: member = SpecialMember.System_Decimal__op_GreaterThanOrEqual; break;
                default:
                    throw ExceptionUtilities.UnexpectedValue(operatorKind);
            }

            // call Operator (left, right)
            var method = UnsafeGetSpecialTypeMethod(syntax, member);
            Debug.Assert((object)method != null);

            return BoundCall.Synthesized(syntax, receiverOpt: null, initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown, method, loweredLeft, loweredRight);
        }

        private BoundExpression MakeNullCheck(SyntaxNode syntax, BoundExpression rewrittenExpr, BinaryOperatorKind operatorKind)
        {
            Debug.Assert((operatorKind == BinaryOperatorKind.Equal) || (operatorKind == BinaryOperatorKind.NotEqual) ||
                (operatorKind == BinaryOperatorKind.NullableNullEqual) || (operatorKind == BinaryOperatorKind.NullableNullNotEqual));

            TypeSymbol? exprType = rewrittenExpr.Type;

            // Don't even call this method if the expression cannot be nullable.
            Debug.Assert(
                exprType is null ||
                exprType.IsNullableTypeOrTypeParameter() ||
                !exprType.IsValueType ||
                exprType.IsPointerOrFunctionPointer());

            TypeSymbol boolType = _compilation.GetSpecialType(SpecialType.System_Boolean);

            // Fold compile-time comparisons.
            if (rewrittenExpr.ConstantValueOpt != null)
            {
                switch (operatorKind)
                {
                    case BinaryOperatorKind.Equal:
                        return MakeLiteral(syntax, ConstantValue.Create(rewrittenExpr.ConstantValueOpt.IsNull, ConstantValueTypeDiscriminator.Boolean), boolType);
                    case BinaryOperatorKind.NotEqual:
                        return MakeLiteral(syntax, ConstantValue.Create(!rewrittenExpr.ConstantValueOpt.IsNull, ConstantValueTypeDiscriminator.Boolean), boolType);
                }
            }

            TypeSymbol objectType = _compilation.GetSpecialType(SpecialType.System_Object);

            if (exprType is { })
            {
                if (exprType.Kind == SymbolKind.TypeParameter)
                {
                    // Box type parameters.
                    rewrittenExpr = MakeConversionNode(syntax, rewrittenExpr, Conversion.Boxing, objectType, @checked: false);
                }
                else if (exprType.IsNullableType())
                {
                    operatorKind |= BinaryOperatorKind.NullableNull;
                }
            }

            return MakeBinaryOperator(
                syntax,
                operatorKind,
                rewrittenExpr,
                MakeLiteral(syntax, ConstantValue.Null, objectType),
                boolType,
                method: null,
                constrainedToTypeOpt: null);
        }

        /// <summary>
        /// Spec section 7.9: if the left operand is int or uint, mask the right operand with 0x1F;
        /// if the left operand is long or ulong, mask the right operand with 0x3F.
        /// </summary>
        private BoundExpression RewriteBuiltInShiftOperation(
            BoundBinaryOperator? oldNode,
            SyntaxNode syntax,
            BinaryOperatorKind operatorKind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight,
            TypeSymbol type,
            int rightMask)
        {
            SyntaxNode rightSyntax = loweredRight.Syntax;
            ConstantValue? rightConstantValue = loweredRight.ConstantValueOpt;
            Debug.Assert(loweredRight.Type is { });
            TypeSymbol rightType = loweredRight.Type;
            Debug.Assert(rightType.SpecialType == SpecialType.System_Int32);

            if (rightConstantValue != null && rightConstantValue.IsIntegral)
            {
                int shiftAmount = rightConstantValue.Int32Value & rightMask;
                if (shiftAmount == 0)
                {
                    return loweredLeft;
                }

                loweredRight = MakeLiteral(rightSyntax, ConstantValue.Create(shiftAmount), rightType);
            }
            else
            {
                BinaryOperatorKind andOperatorKind = (operatorKind & ~BinaryOperatorKind.OpMask) | BinaryOperatorKind.And;
                loweredRight = new BoundBinaryOperator(
                    rightSyntax,
                    andOperatorKind,
                    null,
                    methodOpt: null,
                    constrainedToTypeOpt: null,
                    LookupResultKind.Viable,
                    loweredRight,
                    MakeLiteral(rightSyntax, ConstantValue.Create(rightMask), rightType),
                    rightType);
            }

            return oldNode == null
                ? new BoundBinaryOperator(
                    syntax,
                    operatorKind,
                    null,
                    methodOpt: null,
                    constrainedToTypeOpt: null,
                    LookupResultKind.Viable,
                    loweredLeft,
                    loweredRight,
                    type)
                : oldNode.Update(
                    operatorKind,
                    null,
                    methodOpt: null,
                    constrainedToTypeOpt: null,
                    oldNode.ResultKind,
                    loweredLeft,
                    loweredRight,
                    type);
        }

        private BoundExpression RewriteBuiltInNativeShiftOperation(
            BoundBinaryOperator? oldNode,
            SyntaxNode syntax,
            BinaryOperatorKind operatorKind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight,
            TypeSymbol type)
        {
            // For the predefined operators, the number of bits to shift in `x << count`, `x >> count` or `x >>> count` is computed as follows:
            // [...]
            // - When the type of `x` is `nint` or `nuint`, the shift count is given by
            //   the low-order five bits of `count` on a 32 bit platform, or
            //   the lower-order six bits of `count` on a 64 bit platform.
            //   The shift count is computed as `count & (sizeof(nint) * 8 - 1)` or `count & (sizeof(nuint) * 8 - 1)`,
            //   which is `count & 0x1F` on a 32 bit platform and `count & 0x3F` on a 64 bit platform.

            Debug.Assert(loweredLeft.Type is { });
            TypeSymbol leftType = loweredLeft.Type;
            Debug.Assert(leftType.SpecialType is SpecialType.System_IntPtr or SpecialType.System_UIntPtr);

            ConstantValue? rightConstantValue = loweredRight.ConstantValueOpt;
            Debug.Assert(loweredRight.Type is { });
            TypeSymbol rightType = loweredRight.Type;
            Debug.Assert(rightType.SpecialType == SpecialType.System_Int32);

            var oldSyntax = _factory.Syntax;
            _factory.Syntax = loweredRight.Syntax;

            if (rightConstantValue != null
                && rightConstantValue.Discriminator == ConstantValueTypeDiscriminator.Int32
                && rightConstantValue.Int32Value is >= 0 and <= 0x1F)
            {
                // For cases where count is small enough, we don't need any masking.
                int shiftAmount = rightConstantValue.Int32Value;
                if (shiftAmount == 0)
                {
                    return loweredLeft;
                }

                loweredRight = _factory.Literal(shiftAmount);
            }
            else
            {
                BinaryOperatorKind andOperatorKind = (operatorKind & ~BinaryOperatorKind.OpMask) | BinaryOperatorKind.And;

                // count & (sizeof(nint) * 8 - 1)
                loweredRight = _factory.Binary(andOperatorKind, rightType,
                    loweredRight,
                    _factory.IntSubtract(_factory.IntMultiply(_factory.Sizeof(leftType), _factory.Literal(8)), _factory.Literal(1)));
            }

            _factory.Syntax = syntax;
            var result = oldNode == null
                ? _factory.Binary(operatorKind, type, loweredLeft, loweredRight)
                : oldNode.Update(operatorKind, null, methodOpt: null, constrainedToTypeOpt: null,
                    oldNode.ResultKind, loweredLeft, loweredRight, type);

            _factory.Syntax = oldSyntax;
            return result;
        }

        private BoundExpression RewritePointerNumericOperator(
            SyntaxNode syntax,
            BinaryOperatorKind kind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight,
            TypeSymbol returnType,
            bool isPointerElementAccess,
            bool isLeftPointer)
        {
            if (isLeftPointer)
            {
                Debug.Assert(loweredLeft.Type is { TypeKind: TypeKind.Pointer });
                loweredRight = MakeSizeOfMultiplication(loweredRight, (PointerTypeSymbol)loweredLeft.Type, kind.IsChecked());
            }
            else
            {
                Debug.Assert(loweredRight.Type is { TypeKind: TypeKind.Pointer });
                loweredLeft = MakeSizeOfMultiplication(loweredLeft, (PointerTypeSymbol)loweredRight.Type, kind.IsChecked());
            }

            if (isPointerElementAccess)
            {
                Debug.Assert(kind.Operator() == BinaryOperatorKind.Addition);

                // NOTE: This is here to persist a bug in Dev10.  checked(p[n]) should be equivalent to checked(*(p + n)),
                // but Dev10 omits the check on the addition (though it retains the check on the multiplication of n by
                // the size).
                kind = kind & ~BinaryOperatorKind.Checked;
            }

            return new BoundBinaryOperator(
                            syntax,
                            kind,
                            ConstantValue.NotAvailable,
                            methodOpt: null,
                            constrainedToTypeOpt: null,
                            LookupResultKind.Viable,
                            loweredLeft,
                            loweredRight,
                            returnType);
        }

        /// <summary>
        /// This rather confusing method tries to reproduce the functionality of ExpressionBinder::bindPtrAddMul and
        /// ExpressionBinder::bindPtrMul.  The basic idea is that we have a numeric expression, x, and a pointer type, 
        /// T*, and we want to multiply x by sizeof(T).  Unfortunately, we need to stick in some conversions to make
        /// everything work.
        /// 
        ///   1) If x is an int, then convert it to an IntPtr (i.e. a native int).  Dev10 offers no explanation (ExpressionBinder::bindPtrMul).
        ///   2) Do overload resolution based on the (possibly converted) type of X and int (the type of sizeof(T)).
        ///   3) If the result type of the chosen multiplication operator is signed, convert the product to IntPtr;
        ///      otherwise, convert the product to UIntPtr.
        /// </summary>
        private BoundExpression MakeSizeOfMultiplication(BoundExpression numericOperand, PointerTypeSymbol pointerType, bool isChecked)
        {
            var sizeOfExpression = _factory.Sizeof(pointerType.PointedAtType);
            Debug.Assert(sizeOfExpression.Type is { SpecialType: SpecialType.System_Int32 });

            // Common case: adding or subtracting one  (e.g. for ++)
            if (numericOperand.ConstantValueOpt?.UInt64Value == 1)
            {
                // We could convert this to a native int (as the unoptimized multiplication would be),
                // but that would be a no-op (int to native int), so don't bother.
                return sizeOfExpression;
            }

            Debug.Assert(numericOperand.Type is { });
            var numericSpecialType = numericOperand.Type.SpecialType;

            // Optimization: the size is exactly one byte, then multiplication is unnecessary.
            if (sizeOfExpression.ConstantValueOpt?.Int32Value == 1)
            {
                // As in ExpressionBinder::bindPtrAddMul, we apply the following conversions:
                //   int -> int (add allows int32 operands and will extend to native int if necessary)
                //   uint -> native uint (add will sign-extend 32bit operand on 64bit, we do not want that happening)
                //   long -> native int
                //   ulong -> native uint
                // Note that these are not the types we would see if we let the multiplication happen.
                // ACASEY: These rules are inferred from the native compiler.

                SpecialType destinationType = numericSpecialType;
                switch (numericSpecialType)
                {
                    case SpecialType.System_Int32:
                        // add operator can take int32 and extend to 64bit if necessary
                        // however in a case of checked operation, the operation is treated as unsigned with overflow ( add.ovf.un , sub.ovf.un )
                        // the IL spec is a bit vague whether JIT should sign or zero extend the shorter operand in such case
                        // and there could be inconsistencies in implementation or bugs.
                        // As a result, in checked contexts, we will force sign-extending cast to be sure
                        if (isChecked)
                        {
                            var constVal = numericOperand.ConstantValueOpt;
                            if (constVal == null || constVal.Int32Value < 0)
                            {
                                destinationType = SpecialType.System_IntPtr;
                            }
                        }
                        break;
                    case SpecialType.System_UInt32:
                        {
                            // add operator treats operands as signed and will sign-extend on x64
                            // to prevent sign-extending, convert the operand to unsigned native int.
                            var constVal = numericOperand.ConstantValueOpt;
                            if (constVal == null || constVal.UInt32Value > int.MaxValue)
                            {
                                destinationType = SpecialType.System_UIntPtr;
                            }
                        }
                        break;
                    case SpecialType.System_Int64:
                        destinationType = SpecialType.System_IntPtr;
                        break;
                    case SpecialType.System_UInt64:
                        destinationType = SpecialType.System_UIntPtr;
                        break;
                    default:
                        throw ExceptionUtilities.UnexpectedValue(numericSpecialType);
                }

                return destinationType == numericSpecialType
                    ? numericOperand
                    : _factory.Convert(_factory.SpecialType(destinationType), numericOperand, Conversion.IntegerToPointer);
            }

            BinaryOperatorKind multiplicationKind = BinaryOperatorKind.Multiplication;

            TypeSymbol multiplicationResultType;
            TypeSymbol convertedMultiplicationResultType;
            switch (numericSpecialType)
            {
                case SpecialType.System_Int32:
                    {
                        TypeSymbol nativeIntType = _factory.SpecialType(SpecialType.System_IntPtr);

                        // From ExpressionBinder::bindPtrMul:
                        // this multiplication needs to be done as natural ints, but since a (int * natint) ==> natint,
                        // we only need to promote one side

                        numericOperand = _factory.Convert(nativeIntType, numericOperand, Conversion.IntegerToPointer, isChecked);
                        multiplicationKind |= BinaryOperatorKind.Int; //i.e. signed
                        multiplicationResultType = nativeIntType;
                        convertedMultiplicationResultType = nativeIntType;
                        break;
                    }
                case SpecialType.System_UInt32:
                    {
                        TypeSymbol longType = _factory.SpecialType(SpecialType.System_Int64);
                        TypeSymbol nativeIntType = _factory.SpecialType(SpecialType.System_IntPtr);

                        // We're multiplying a uint by an int, so promote both to long (same as normal operator overload resolution).
                        numericOperand = _factory.Convert(longType, numericOperand, Conversion.ExplicitNumeric, isChecked);
                        sizeOfExpression = _factory.Convert(longType, sizeOfExpression, Conversion.ExplicitNumeric, isChecked);
                        multiplicationKind |= BinaryOperatorKind.Long;
                        multiplicationResultType = longType;
                        convertedMultiplicationResultType = nativeIntType;
                        break;
                    }
                case SpecialType.System_Int64:
                    {
                        TypeSymbol longType = _factory.SpecialType(SpecialType.System_Int64);
                        TypeSymbol nativeIntType = _factory.SpecialType(SpecialType.System_IntPtr);

                        // We're multiplying a long by an int, so promote the int to long (same as normal operator overload resolution).
                        sizeOfExpression = _factory.Convert(longType, sizeOfExpression, Conversion.ExplicitNumeric, isChecked);
                        multiplicationKind |= BinaryOperatorKind.Long;
                        multiplicationResultType = longType;
                        convertedMultiplicationResultType = nativeIntType;
                        break;
                    }
                case SpecialType.System_UInt64:
                    {
                        TypeSymbol ulongType = _factory.SpecialType(SpecialType.System_UInt64);
                        TypeSymbol nativeUIntType = _factory.SpecialType(SpecialType.System_UIntPtr);

                        // We're multiplying a ulong by an int, so promote the int to ulong (same as normal operator overload resolution).
                        sizeOfExpression = _factory.Convert(ulongType, sizeOfExpression, Conversion.ExplicitNumeric, isChecked);
                        multiplicationKind |= BinaryOperatorKind.ULong;
                        multiplicationResultType = ulongType;
                        convertedMultiplicationResultType = nativeUIntType; //unsigned since multiplicationResultType is unsigned
                        break;
                    }
                default:
                    {
                        throw ExceptionUtilities.UnexpectedValue(numericSpecialType);
                    }
            }

            if (isChecked)
            {
                multiplicationKind |= BinaryOperatorKind.Checked;
            }
            var multiplication = _factory.Binary(multiplicationKind, multiplicationResultType, numericOperand, sizeOfExpression);
            return TypeSymbol.Equals(convertedMultiplicationResultType, multiplicationResultType, TypeCompareKind.ConsiderEverything2)
                ? multiplication
                : _factory.Convert(convertedMultiplicationResultType, multiplication, Conversion.IntegerToPointer); // NOTE: for some reason, dev10 doesn't check this conversion.
        }

        private BoundExpression RewritePointerSubtraction(
            BinaryOperatorKind kind,
            BoundExpression loweredLeft,
            BoundExpression loweredRight,
            TypeSymbol returnType)
        {
            Debug.Assert(loweredLeft.Type is { TypeKind: TypeKind.Pointer });
            Debug.Assert(loweredRight.Type is { TypeKind: TypeKind.Pointer });
            Debug.Assert(returnType.SpecialType == SpecialType.System_Int64);

            PointerTypeSymbol pointerType = (PointerTypeSymbol)loweredLeft.Type;
            var sizeOfExpression = _factory.Sizeof(pointerType.PointedAtType);

            // NOTE: to match dev10, the result of the subtraction is treated as an IntPtr
            // and then the result of the division is converted to long.
            // NOTE: dev10 doesn't optimize away division by 1.
            return _factory.Convert(
                returnType,
                _factory.Binary(
                    BinaryOperatorKind.Division,
                    _factory.SpecialType(SpecialType.System_IntPtr),
                    _factory.Binary(
                        kind & ~BinaryOperatorKind.Checked, // For some reason, dev10 never checks for subtraction overflow.
                        returnType,
                        loweredLeft,
                        loweredRight),
                    sizeOfExpression),
                Conversion.PointerToInteger);
        }
    }
}
