File: Lowering\LocalRewriter\LocalRewriter_CompoundAssignmentOperator.cs
Web Access
Project: src\src\Compilers\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.csproj (Microsoft.CodeAnalysis.CSharp)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.CodeGen;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp
{
    internal sealed partial class LocalRewriter
    {
        public override BoundNode VisitCompoundAssignmentOperator(BoundCompoundAssignmentOperator node)
        {
            return VisitCompoundAssignmentOperator(node, true);
        }
 
        private BoundExpression VisitCompoundAssignmentOperator(BoundCompoundAssignmentOperator node, bool used)
        {
            Debug.Assert(TypeSymbol.Equals(node.Right.Type, node.Operator.RightType, TypeCompareKind.ConsiderEverything2));
            BoundExpression loweredRight = VisitExpression(node.Right);
 
            var temps = ArrayBuilder<LocalSymbol>.GetInstance();
            var stores = ArrayBuilder<BoundExpression>.GetInstance();
 
            var kind = node.Operator.Kind;
            bool isChecked = kind.IsChecked();
            bool isDynamic = kind.IsDynamic();
            var binaryOperator = kind.Operator();
 
            // This will be filled in with the LHS that uses temporaries to prevent
            // double-evaluation of side effects.
            BoundExpression transformedLHS = TransformCompoundAssignmentLHS(node.Left, isRegularCompoundAssignment: true, stores, temps, isDynamic);
            var lhsRead = MakeRValue(transformedLHS);
            BoundExpression rewrittenAssignment;
 
            if (node.Left.Kind == BoundKind.DynamicMemberAccess &&
                (binaryOperator == BinaryOperatorKind.Addition || binaryOperator == BinaryOperatorKind.Subtraction))
            {
                // If this could be an event assignment at runtime, we need to rewrite to the following form:
                // Original:
                //   receiver.EV += handler
                // Rewritten:
                //   dynamic memberAccessReceiver = receiver;
                //   bool isEvent = Runtime.IsEvent(memberAccessReceiver, "EV");
                //   dynamic storeNonEvent = !isEvent ? memberAccessReceiver.EV : null;
                //   var loweredRight = handler; // Only necessary if handler can change values, or is something like a lambda
                //   isEvent ? add_Event(memberAccessReceiver, "EV", loweredRight) : transformedLHS = storeNonEvent + loweredRight;
                //
                // This is to ensure that if handler is something like a lambda, we evaluate fully evaluate the left
                // side before storing the lambda to a temp for use in both possible branches.
                // The first store to memberAccessReceiver has already been taken care of above by TransformCompoundAssignmentLHS
 
                var eventTemps = ArrayBuilder<LocalSymbol>.GetInstance();
                var sequence = ArrayBuilder<BoundExpression>.GetInstance();
 
                //   dynamic memberAccessReceiver = receiver;
                var memberAccess = (BoundDynamicMemberAccess)transformedLHS;
 
                //   bool isEvent = Runtime.IsEvent(memberAccessReceiver, "EV");
                var isEvent = _factory.StoreToTemp(_dynamicFactory.MakeDynamicIsEventTest(memberAccess.Name, memberAccess.Receiver).ToExpression(), out BoundAssignmentOperator isEventAssignment);
                eventTemps.Add(isEvent.LocalSymbol);
                sequence.Add(isEventAssignment);
 
                // dynamic storeNonEvent = !isEvent ? memberAccessReceiver.EV : null;
                lhsRead = _factory.StoreToTemp(lhsRead, out BoundAssignmentOperator receiverAssignment);
                eventTemps.Add(((BoundLocal)lhsRead).LocalSymbol);
                var storeNonEvent = _factory.StoreToTemp(_factory.Conditional(_factory.Not(isEvent), receiverAssignment, _factory.Null(receiverAssignment.Type), receiverAssignment.Type), out BoundAssignmentOperator nonEventStore);
                eventTemps.Add(storeNonEvent.LocalSymbol);
                sequence.Add(nonEventStore);
 
                // var loweredRight = handler;
                if (CanChangeValueBetweenReads(loweredRight))
                {
                    loweredRight = _factory.StoreToTemp(loweredRight, out BoundAssignmentOperator possibleHandlerAssignment);
                    eventTemps.Add(((BoundLocal)loweredRight).LocalSymbol);
                    sequence.Add(possibleHandlerAssignment);
                }
 
                // add_Event(t1, "add_EV");
                var invokeEventAccessor = _dynamicFactory.MakeDynamicEventAccessorInvocation(
                    (binaryOperator == BinaryOperatorKind.Addition ? "add_" : "remove_") + memberAccess.Name,
                    memberAccess.Receiver,
                    loweredRight);
 
                // transformedLHS = storeNonEvent + loweredRight
                rewrittenAssignment = rewriteAssignment(lhsRead);
                Debug.Assert(rewrittenAssignment.Type is { });
 
                // Final conditional
                var condition = _factory.Conditional(isEvent, invokeEventAccessor.ToExpression(), rewrittenAssignment, rewrittenAssignment.Type);
 
                rewrittenAssignment = new BoundSequence(node.Syntax, eventTemps.ToImmutableAndFree(), sequence.ToImmutableAndFree(), condition, condition.Type!);
            }
            else
            {
                rewrittenAssignment = rewriteAssignment(lhsRead);
            }
 
            Debug.Assert(rewrittenAssignment.Type is { });
            BoundExpression result = (temps.Count == 0 && stores.Count == 0) ?
                rewrittenAssignment :
                new BoundSequence(
                    node.Syntax,
                    temps.ToImmutable(),
                    stores.ToImmutable(),
                    rewrittenAssignment,
                    rewrittenAssignment.Type);
 
            temps.Free();
            stores.Free();
            return result;
 
            BoundExpression rewriteAssignment(BoundExpression leftRead)
            {
                SyntaxNode syntax = node.Syntax;
 
                // OK, we now have the temporary declarations, the temporary stores, and the transformed left hand side.
                // We need to generate
                //
                // xlhs = (FINAL)((LEFT)xlhs op rhs)
                //
                // And then wrap it up with the generated temporaries.
                //
                // (The right hand side has already been converted to the type expected by the operator.)
 
                BoundExpression opLHS = leftRead;
 
                if (!isDynamic && node.LeftConversion is not null)
                {
                    Debug.Assert(node.LeftPlaceholder is not null);
 
                    AddPlaceholderReplacement(node.LeftPlaceholder, leftRead);
                    opLHS = VisitExpression(node.LeftConversion);
                    RemovePlaceholderReplacement(node.LeftPlaceholder);
                }
 
                BoundExpression operand = MakeBinaryOperator(syntax, node.Operator.Kind, opLHS, loweredRight, node.Operator.ReturnType, node.Operator.Method, node.Operator.ConstrainedToTypeOpt, isCompoundAssignment: true);
 
                Debug.Assert(node.Left.Type is { });
                BoundExpression opFinal = operand;
 
                if (node.FinalConversion is not null)
                {
                    Debug.Assert(node.FinalPlaceholder is not null);
 
                    AddPlaceholderReplacement(node.FinalPlaceholder, operand);
                    opFinal = VisitExpression(node.FinalConversion);
                    RemovePlaceholderReplacement(node.FinalPlaceholder);
                }
 
                Debug.Assert(TypeSymbol.Equals(transformedLHS.Type, node.Left.Type, TypeCompareKind.AllIgnoreOptions));
                return MakeAssignmentOperator(syntax, transformedLHS, opFinal, used: used, isChecked: isChecked, isCompoundAssignment: true);
            }
        }
 
        private BoundExpression? TransformPropertyOrEventReceiver(Symbol propertyOrEvent, BoundExpression? receiverOpt, bool isRegularCompoundAssignment, ArrayBuilder<BoundExpression> stores, ArrayBuilder<LocalSymbol> temps)
        {
            Debug.Assert(propertyOrEvent.Kind == SymbolKind.Property || propertyOrEvent.Kind == SymbolKind.Event);
 
            // We need to stash away the receiver so that it does not get evaluated twice.
            // If the receiver is classified as a value of reference type then we can simply say
            //
            // R temp = receiver
            // temp.prop = temp.prop + rhs
            //
            // But if the receiver is classified as a variable of struct type then we
            // cannot make a copy of the value; we need to make sure that we mutate
            // the original receiver, not the copy.  We have to generate
            //
            // ref R temp = ref receiver
            // temp.prop = temp.prop + rhs
            //
            // The rules of C# (in section 7.17.1) require that if you have receiver.prop 
            // as the target of an assignment such that receiver is a value type, it must
            // be classified as a variable. If we've gotten this far in the rewriting,
            // assume that was the case.
 
            // If the property is static or if the receiver is of kind "Base" or "this", then we can just generate prop = prop + value
            if (receiverOpt == null || propertyOrEvent.IsStatic || !CanChangeValueBetweenReads(receiverOpt))
            {
                return receiverOpt;
            }
 
            Debug.Assert(receiverOpt.Kind != BoundKind.TypeExpression);
 
            BoundExpression rewrittenReceiver = VisitExpression(receiverOpt);
 
            BoundAssignmentOperator assignmentToTemp;
 
            // SPEC VIOLATION: It is not very clear when receiver of constrained callvirt is dereferenced - when pushed (in lexical order),
            // SPEC VIOLATION: or when actual call is executed. The actual behavior seems to be implementation specific in different JITs.
            // SPEC VIOLATION: To not depend on that, the right thing to do here is to store the value of the variable 
            // SPEC VIOLATION: when variable has reference type (regular temp), and store variable's location when it has a value type. (ref temp)
            // SPEC VIOLATION: in a case of unconstrained generic type parameter a runtime test (default(T) == null) would be needed
            // SPEC VIOLATION: However, for compatibility with Dev12 we will continue treating all generic type parameters, constrained or not,
            // SPEC VIOLATION: as value types.
            Debug.Assert(rewrittenReceiver.Type is { });
            var variableRepresentsLocation = rewrittenReceiver.Type.IsValueType || rewrittenReceiver.Type.Kind == SymbolKind.TypeParameter;
 
            var receiverTemp = _factory.StoreToTemp(rewrittenReceiver, out assignmentToTemp, refKind: variableRepresentsLocation ? RefKind.Ref : RefKind.None);
            temps.Add(receiverTemp.LocalSymbol);
 
            if (!isRegularCompoundAssignment &&
                receiverTemp.LocalSymbol.IsRef &&
                CodeGenerator.IsPossibleReferenceTypeReceiverOfConstrainedCall(receiverTemp) &&
                !CodeGenerator.ReceiverIsKnownToReferToTempIfReferenceType(receiverTemp))
            {
                BoundAssignmentOperator? extraRefInitialization;
                ReferToTempIfReferenceTypeReceiver(receiverTemp, ref assignmentToTemp, out extraRefInitialization, temps);
 
                if (extraRefInitialization is object)
                {
                    stores.Add(extraRefInitialization);
                }
            }
 
            stores.Add(assignmentToTemp);
 
            return receiverTemp;
        }
 
        private BoundDynamicMemberAccess TransformDynamicMemberAccess(BoundDynamicMemberAccess memberAccess, ArrayBuilder<BoundExpression> stores, ArrayBuilder<LocalSymbol> temps)
        {
            if (!CanChangeValueBetweenReads(memberAccess.Receiver))
            {
                return memberAccess;
            }
 
            // store receiver to temp:
            var rewrittenReceiver = VisitExpression(memberAccess.Receiver);
            BoundAssignmentOperator assignmentToTemp;
            var receiverTemp = _factory.StoreToTemp(rewrittenReceiver, out assignmentToTemp);
            stores.Add(assignmentToTemp);
            temps.Add(receiverTemp.LocalSymbol);
 
            return new BoundDynamicMemberAccess(memberAccess.Syntax, receiverTemp, memberAccess.TypeArgumentsOpt, memberAccess.Name, memberAccess.Invoked, memberAccess.Indexed, memberAccess.Type);
        }
 
        private BoundIndexerAccess TransformIndexerAccess(BoundIndexerAccess indexerAccess, bool isRegularCompoundAssignment, ArrayBuilder<BoundExpression> stores, ArrayBuilder<LocalSymbol> temps)
        {
            var receiverOpt = indexerAccess.ReceiverOpt;
            Debug.Assert(receiverOpt != null);
 
            BoundExpression transformedReceiver = VisitExpression(receiverOpt);
 
            // Dealing with the arguments is a bit tricky because they can be named out-of-order arguments;
            // we have to preserve both the source-code order of the side effects and the side effects
            // only being executed once.
            // 
            // This is a subtly different problem than the problem faced by the conventional call
            // rewriter; with the conventional call rewriter we already know that the side effects
            // will only be executed once because the arguments are only being pushed on the stack once. 
            // In a compound equality operator on an indexer the indices are placed on the stack twice. 
            // That is to say, if you have:
            // 
            // C().M(z : Z(), x : X(), y : Y())
            // 
            // then we can rewrite that into
            // 
            // tempc = C()
            // tempz = Z()
            // tempc.M(X(), Y(), tempz)
            // 
            // See, we can optimize away two of the temporaries, for x and y. But we cannot optimize away any of the
            // temporaries in
            // 
            // C().Collection[z : Z(), x : X(), y : Y()] += 1;
            // 
            // because we have to ensure not just that Z() happens first, but in addition that X() and Y() are only 
            // called once.  We have to generate this as
            // 
            // tempc = C().Collection
            // tempz = Z()
            // tempx = X()
            // tempy = Y()
            // tempc[tempx, tempy, tempz] = tempc[tempx, tempy, tempz] + 1;
            // 
            // Fortunately arguments to indexers are never ref or out, so we don't need to worry about that.
            // However, we can still do the optimization where constants are not stored in
            // temporaries; if we have
            // 
            // C().Collection[z : 123, y : Y(), x : X()] += 1;
            // 
            // Then we can generate that as
            // 
            // tempc = C().Collection
            // tempx = X()
            // tempy = Y()
            // tempc[tempx, tempy, 123] = tempc[tempx, tempy, 123] + 1;
 
            ImmutableArray<BoundExpression> rewrittenArguments = VisitArgumentsAndCaptureReceiverIfNeeded(
                ref transformedReceiver,
                captureReceiverMode: CanChangeValueBetweenReads(receiverOpt) ?
                                         (isRegularCompoundAssignment ?
                                              ReceiverCaptureMode.CompoundAssignment :
                                              ReceiverCaptureMode.UseTwiceComplex) :
                                         ReceiverCaptureMode.Default,
                indexerAccess.Arguments,
                indexerAccess.Indexer,
                indexerAccess.ArgsToParamsOpt,
                indexerAccess.ArgumentRefKindsOpt,
                stores,
                ref temps!);
 
            Debug.Assert(temps is object);
 
            return TransformIndexerAccessContinued(indexerAccess, transformedReceiver, rewrittenArguments, stores, temps);
        }
 
        private BoundIndexerAccess TransformIndexerAccessContinued(
            BoundIndexerAccess indexerAccess,
            BoundExpression transformedReceiver,
            ImmutableArray<BoundExpression> rewrittenArguments,
            ArrayBuilder<BoundExpression> stores,
            ArrayBuilder<LocalSymbol> temps)
        {
            SyntaxNode syntax = indexerAccess.Syntax;
            ImmutableArray<int> argsToParamsOpt = indexerAccess.ArgsToParamsOpt;
            ImmutableArray<RefKind> argumentRefKinds = indexerAccess.ArgumentRefKindsOpt;
            PropertySymbol indexer = indexerAccess.Indexer;
 
            bool expanded = indexerAccess.Expanded;
 
            ImmutableArray<ParameterSymbol> parameters = indexer.Parameters;
            BoundExpression[] actualArguments = new BoundExpression[parameters.Length]; // The actual arguments that will be passed; one actual argument per formal parameter.
            ArrayBuilder<BoundAssignmentOperator> storesToTemps = ArrayBuilder<BoundAssignmentOperator>.GetInstance(rewrittenArguments.Length);
            ArrayBuilder<RefKind> refKinds = ArrayBuilder<RefKind>.GetInstance(parameters.Length, RefKind.None);
 
            // Step one: Store everything that is non-trivial into a temporary; record the
            // stores in storesToTemps and make the actual argument a reference to the temp.
            BuildStoresToTemps(
                expanded,
                argsToParamsOpt,
                parameters,
                argumentRefKinds,
                rewrittenArguments,
                forceLambdaSpilling: true, // lambdas must produce exactly one delegate so they must be spilled into a temp
                actualArguments,
                refKinds,
                storesToTemps);
 
            if (expanded && actualArguments[actualArguments.Length - 1] is { IsParamsArrayOrCollection: true } array)
            {
                Debug.Assert(array is BoundArrayCreation);
 
                if (TryOptimizeParamsArray(array, out BoundExpression? optimized))
                {
                    array = optimized;
                }
 
                BoundAssignmentOperator storeToTemp;
                var boundTemp = _factory.StoreToTemp(array, out storeToTemp);
                stores.Add(storeToTemp);
                temps.Add(boundTemp.LocalSymbol);
                actualArguments[actualArguments.Length - 1] = boundTemp;
            }
 
            // Step three: Now fill in the optional arguments. (Dev11 uses the getter for optional arguments in
            // compound assignments, but for deconstructions we use the setter if the getter is missing.)
            var accessor = indexer.GetOwnOrInheritedGetMethod() ?? indexer.GetOwnOrInheritedSetMethod();
            Debug.Assert(accessor is not null);
 
            // For a call, step four would be to optimize away some of the temps.  However, we need them all to prevent
            // duplicate side-effects, so we'll skip that step.
 
            if (indexer.ContainingType.IsComImport)
            {
                RewriteArgumentsForComCall(parameters, actualArguments, refKinds, temps);
            }
 
            Debug.Assert(actualArguments.All(static arg => arg is not null));
            rewrittenArguments = actualArguments.AsImmutableOrNull();
 
            foreach (BoundAssignmentOperator tempAssignment in storesToTemps)
            {
                temps.Add(((BoundLocal)tempAssignment.Left).LocalSymbol);
                stores.Add(tempAssignment);
            }
 
            storesToTemps.Free();
            argumentRefKinds = GetRefKindsOrNull(refKinds);
            refKinds.Free();
 
            // This is a temporary object that will be rewritten away before the lowering completes.
            return new BoundIndexerAccess(
                syntax,
                transformedReceiver,
                initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown,
                indexer,
                rewrittenArguments,
                argumentNamesOpt: default(ImmutableArray<string?>),
                argumentRefKinds,
                expanded: false,
                accessorKind: indexerAccess.AccessorKind,
                argsToParamsOpt: default(ImmutableArray<int>),
                defaultArguments: default(BitVector),
                indexerAccess.Type);
        }
 
        private BoundExpression TransformImplicitIndexerAccess(
            BoundImplicitIndexerAccess indexerAccess,
            bool isRegularCompoundAssignment,
            ArrayBuilder<BoundExpression> stores,
            ArrayBuilder<LocalSymbol> temps,
            bool isDynamicAssignment)
        {
            if (TypeSymbol.Equals(
                indexerAccess.Argument.Type,
                _compilation.GetWellKnownType(WellKnownType.System_Index),
                TypeCompareKind.ConsiderEverything))
            {
                return TransformIndexPatternIndexerAccess(indexerAccess, isRegularCompoundAssignment, stores, temps, isDynamicAssignment);
            }
 
            throw ExceptionUtilities.UnexpectedValue(indexerAccess.Argument.Type);
        }
 
        private BoundExpression TransformIndexPatternIndexerAccess(BoundImplicitIndexerAccess implicitIndexerAccess, bool isRegularCompoundAssignment, ArrayBuilder<BoundExpression> stores, ArrayBuilder<LocalSymbol> temps, bool isDynamicAssignment)
        {
            Debug.Assert(implicitIndexerAccess.IndexerOrSliceAccess.GetRefKind() == RefKind.None);
            var access = GetUnderlyingIndexerOrSliceAccess(
                implicitIndexerAccess,
                isLeftOfAssignment: true,
                isRegularAssignmentOrRegularCompoundAssignment: isRegularCompoundAssignment,
                cacheAllArgumentsOnly: false,
                stores, temps);
 
            if (access is BoundIndexerAccess indexerAccess)
            {
                return TransformIndexerAccessContinued(indexerAccess, indexerAccess.ReceiverOpt!, indexerAccess.Arguments, stores, temps);
            }
            else
            {
                var arrayAccess = (BoundArrayAccess)access;
 
                if (isDynamicAssignment || !IsInvariantArray(arrayAccess.Expression.Type))
                {
                    // See TransformCompoundAssignmentLHS for an explanation for this transform
                    return SpillArrayElementAccess(arrayAccess.Expression, arrayAccess.Indices, stores, temps);
                }
 
                BoundAssignmentOperator assignmentToTemp;
                var variableTemp = _factory.StoreToTemp(arrayAccess, out assignmentToTemp, refKind: RefKind.Ref);
                stores.Add(assignmentToTemp);
                temps.Add(variableTemp.LocalSymbol);
                return variableTemp;
            }
        }
 
        /// <summary>
        /// Returns true if the <paramref name="receiver"/> was lowered and transformed.
        /// The <paramref name="receiver"/> is not changed if this function returns false. 
        /// </summary>
        private bool TransformCompoundAssignmentFieldOrEventAccessReceiver(Symbol fieldOrEvent, ref BoundExpression? receiver, ArrayBuilder<BoundExpression> stores, ArrayBuilder<LocalSymbol> temps)
        {
            Debug.Assert(fieldOrEvent.Kind == SymbolKind.Field || fieldOrEvent.Kind == SymbolKind.Event);
 
            //If the receiver is static or is the receiver is of kind "Base" or "this", then we can just generate field = field + value
            if (fieldOrEvent.IsStatic)
            {
                return true;
            }
 
            Debug.Assert(receiver is { Type: { } });
            if (!CanChangeValueBetweenReads(receiver))
            {
                return true;
            }
            else if (!receiver.Type.IsReferenceType)
            {
                return false;
            }
 
            Debug.Assert(receiver.Type.IsReferenceType);
            Debug.Assert(receiver.Kind != BoundKind.TypeExpression);
            BoundExpression rewrittenReceiver = VisitExpression(receiver);
 
            Debug.Assert(rewrittenReceiver.Type is { });
            if (rewrittenReceiver.Type.IsTypeParameter())
            {
                var memberContainingType = fieldOrEvent.ContainingType;
 
                // From the verifier perspective type parameters do not contain fields or methods.
                // the instance must be "boxed" to access the field
                // It makes sense to box receiver before storing into a temp - no need to box twice.
                rewrittenReceiver = BoxReceiver(rewrittenReceiver, memberContainingType);
            }
 
            BoundAssignmentOperator assignmentToTemp;
            var receiverTemp = _factory.StoreToTemp(rewrittenReceiver, out assignmentToTemp);
            stores.Add(assignmentToTemp);
            temps.Add(receiverTemp.LocalSymbol);
            receiver = receiverTemp;
            return true;
        }
 
        private BoundDynamicIndexerAccess TransformDynamicIndexerAccess(BoundDynamicIndexerAccess indexerAccess, ArrayBuilder<BoundExpression> stores, ArrayBuilder<LocalSymbol> temps)
        {
            BoundExpression loweredReceiver;
            if (CanChangeValueBetweenReads(indexerAccess.Receiver))
            {
                BoundAssignmentOperator assignmentToTemp;
                var temp = _factory.StoreToTemp(VisitExpression(indexerAccess.Receiver), out assignmentToTemp);
                stores.Add(assignmentToTemp);
                temps.Add(temp.LocalSymbol);
                loweredReceiver = temp;
            }
            else
            {
                loweredReceiver = indexerAccess.Receiver;
            }
 
            var arguments = indexerAccess.Arguments;
            var loweredArguments = new BoundExpression[arguments.Length];
 
            for (int i = 0; i < arguments.Length; i++)
            {
                if (CanChangeValueBetweenReads(arguments[i]))
                {
                    BoundAssignmentOperator assignmentToTemp;
                    var temp = _factory.StoreToTemp(VisitExpression(arguments[i]), out assignmentToTemp, indexerAccess.ArgumentRefKindsOpt.RefKinds(i) != RefKind.None ? RefKind.Ref : RefKind.None);
                    stores.Add(assignmentToTemp);
                    temps.Add(temp.LocalSymbol);
                    loweredArguments[i] = temp;
                }
                else
                {
                    loweredArguments[i] = arguments[i];
                }
            }
 
            return new BoundDynamicIndexerAccess(
                indexerAccess.Syntax,
                loweredReceiver,
                loweredArguments.AsImmutableOrNull(),
                indexerAccess.ArgumentNamesOpt,
                indexerAccess.ArgumentRefKindsOpt,
                indexerAccess.ApplicableIndexers,
                indexerAccess.Type);
        }
 
        /// <summary>
        /// In the expanded form of a compound assignment (or increment/decrement), the LHS appears multiple times.
        /// If we aren't careful, this can result in repeated side-effects.  This creates (ordered) temps for all of the
        /// subexpressions that could result in side-effects and returns a side-effect-free expression that can be used
        /// in place of the LHS in the expanded form.
        /// </summary>
        /// <param name="originalLHS">The LHS sub-expression of the compound assignment (or increment/decrement).</param>
        /// <param name="stores">Populated with a list of assignment expressions that initialize the temporary locals.</param>
        /// <param name="temps">Populated with a list of temporary local symbols.</param>
        /// <param name="isDynamicAssignment">True if the compound assignment is a dynamic operation.</param>
        /// <returns>
        /// A side-effect-free expression representing the LHS.
        /// The returned node needs to be lowered but its children are already lowered.
        /// </returns>
        private BoundExpression TransformCompoundAssignmentLHS(BoundExpression originalLHS, bool isRegularCompoundAssignment, ArrayBuilder<BoundExpression> stores, ArrayBuilder<LocalSymbol> temps, bool isDynamicAssignment)
        {
            // There are five possible cases.
            //
            // Case 1: receiver.Prop += value is transformed into
            // temp = receiver
            // temp.Prop = temp.Prop + value
            // and a later rewriting will turn that into calls to getters and setters.
            //
            // Case 2: collection[i1, i2, i3] += value is transformed into
            // tc = collection
            // t1 = i1
            // t2 = i2
            // t3 = i3
            // tc[t1, t2, t3] = tc[t1, t2, t3] + value
            // and again, a later rewriting will turn that into getters and setters of the indexer.
            //
            // Case 3: local += value (and param += value) needs no temporaries; it simply
            // becomes local = local + value.
            //
            // Case 4: staticField += value needs no temporaries either. However, classInst.field += value becomes
            // temp = classInst
            // temp.field = temp.field + value 
            //
            // Case 5: otherwise, it must be structVariable.field += value or array[index] += value. Either way
            // we have a variable on the left. Transform it into:
            // ref temp = ref variable
            // temp = temp + value
 
            switch (originalLHS.Kind)
            {
                case BoundKind.PropertyAccess:
                    {
                        // Ref returning properties count as variables and do not undergo the transformation
                        // that value returning properties require.
                        var propertyAccess = (BoundPropertyAccess)originalLHS;
                        if (propertyAccess.PropertySymbol.RefKind == RefKind.None)
                        {
                            // This is a temporary object that will be rewritten away before the lowering completes.
                            return propertyAccess.Update(TransformPropertyOrEventReceiver(propertyAccess.PropertySymbol, propertyAccess.ReceiverOpt,
                                                                                          isRegularCompoundAssignment, stores, temps),
                                                         propertyAccess.InitialBindingReceiverIsSubjectToCloning, propertyAccess.PropertySymbol, propertyAccess.ResultKind, propertyAccess.Type);
                        }
                    }
                    break;
 
                case BoundKind.IndexerAccess:
                    {
                        // Ref returning indexers count as variables and do not undergo the transformation
                        // that value returning properties require.
                        var indexerAccess = (BoundIndexerAccess)originalLHS;
                        if (indexerAccess.GetRefKind() == RefKind.None)
                        {
                            return TransformIndexerAccess((BoundIndexerAccess)originalLHS, isRegularCompoundAssignment, stores, temps);
                        }
                    }
                    break;
 
                case BoundKind.ImplicitIndexerAccess:
                    {
                        var implicitIndexerAccess = (BoundImplicitIndexerAccess)originalLHS;
                        Debug.Assert(implicitIndexerAccess.Argument.Type!.Equals(_compilation.GetWellKnownType(WellKnownType.System_Index))
                            || implicitIndexerAccess.Argument.Type!.Equals(_compilation.GetWellKnownType(WellKnownType.System_Range)));
 
                        if (implicitIndexerAccess.GetRefKind() == RefKind.None)
                        {
                            return TransformImplicitIndexerAccess(implicitIndexerAccess, isRegularCompoundAssignment, stores, temps, isDynamicAssignment);
                        }
                    }
                    break;
 
                case BoundKind.FieldAccess:
                    {
                        // * If the field is static then no temporaries are needed. 
                        // * If the field is not static and the receiver is of reference type then generate t = r; t.f = t.f + value
                        // * If the field is not static and the receiver is a variable of value type then we'll fall into the
                        //   general variable case below.
 
                        var fieldAccess = (BoundFieldAccess)originalLHS;
                        BoundExpression? receiverOpt = fieldAccess.ReceiverOpt;
 
                        if (TransformCompoundAssignmentFieldOrEventAccessReceiver(fieldAccess.FieldSymbol, ref receiverOpt, stores, temps))
                        {
                            return MakeFieldAccess(fieldAccess.Syntax, receiverOpt, fieldAccess.FieldSymbol, fieldAccess.ConstantValueOpt, fieldAccess.ResultKind, fieldAccess.Type, fieldAccess);
                        }
                    }
                    break;
 
                case BoundKind.ArrayAccess:
                    {
                        var arrayAccess = (BoundArrayAccess)originalLHS;
                        if (isDynamicAssignment || !IsInvariantArray(arrayAccess.Expression.Type))
                        {
                            // In non-dynamic, invariant array[index] op= R we emit:
                            //   T& tmp = &array[index];
                            //   *tmp = *L op R;
                            // where T is the type of L.
                            // 
                            // If L is an array access, the assignment is dynamic, the compile-time of the array is dynamic[] 
                            // and the runtime type of the array is not object[] (but e.g. string[]) the pointer approach is broken.
                            // T is Object in such case and we can't take a read-write pointer of type Object& to an array element of non-object type.
                            //
                            // In the dynamic case, or when the array may be co-variant, we rewrite the assignment as follows:
                            //
                            //   E t_array = array;
                            //   I t_index = index; (possibly more indices)
                            //   T value = t_array[t_index];
                            //   t_array[t_index] = value op R;
                            var loweredArray = VisitExpression(arrayAccess.Expression);
                            var loweredIndices = VisitList(arrayAccess.Indices);
 
                            return SpillArrayElementAccess(loweredArray, loweredIndices, stores, temps);
                        }
                    }
                    break;
 
                case BoundKind.InlineArrayAccess:
                    Debug.Assert(originalLHS.GetRefKind() == RefKind.Ref);
                    break;
 
                case BoundKind.DynamicMemberAccess:
                    return TransformDynamicMemberAccess((BoundDynamicMemberAccess)originalLHS, stores, temps);
 
                case BoundKind.DynamicIndexerAccess:
                    return TransformDynamicIndexerAccess((BoundDynamicIndexerAccess)originalLHS, stores, temps);
 
                case BoundKind.Local:
                case BoundKind.Parameter:
                case BoundKind.ThisReference: // a special kind of parameter
                case BoundKind.PseudoVariable:
                    {
                        // No temporaries are needed. Just generate local = local + value
                        var result = VisitExpression(originalLHS);
                        Debug.Assert((object)result == originalLHS || IsCapturedPrimaryConstructorParameter(originalLHS)); // If this fails, we might need to add tests for new scenarios and relax the assert.
                        return result;
                    }
 
                case BoundKind.Call:
                    Debug.Assert(((BoundCall)originalLHS).Method.RefKind != RefKind.None);
                    break;
 
                case BoundKind.FunctionPointerInvocation:
                    Debug.Assert(((BoundFunctionPointerInvocation)originalLHS).FunctionPointer.Signature.RefKind != RefKind.None);
                    break;
 
                case BoundKind.ConditionalOperator:
                    Debug.Assert(((BoundConditionalOperator)originalLHS).IsRef);
                    break;
 
                case BoundKind.AssignmentOperator:
                    Debug.Assert(((BoundAssignmentOperator)originalLHS).IsRef);
                    break;
 
                case BoundKind.PointerElementAccess:
                case BoundKind.PointerIndirectionOperator:
                case BoundKind.RefValueOperator:
                    break;
 
                case BoundKind.EventAccess:
                    {
                        var eventAccess = (BoundEventAccess)originalLHS;
                        Debug.Assert(eventAccess.IsUsableAsField);
                        BoundExpression? receiverOpt = eventAccess.ReceiverOpt;
 
                        if (eventAccess.EventSymbol.IsWindowsRuntimeEvent)
                        {
                            // This is a temporary object that will be rewritten away before the lowering completes.
                            return eventAccess.Update(TransformPropertyOrEventReceiver(eventAccess.EventSymbol, eventAccess.ReceiverOpt,
                                                                                       isRegularCompoundAssignment, stores, temps),
                                                      eventAccess.EventSymbol, eventAccess.IsUsableAsField, eventAccess.ResultKind, eventAccess.Type);
                        }
 
                        if (TransformCompoundAssignmentFieldOrEventAccessReceiver(eventAccess.EventSymbol, ref receiverOpt, stores, temps))
                        {
                            return MakeEventAccess(eventAccess.Syntax, receiverOpt, eventAccess.EventSymbol, eventAccess.ConstantValueOpt, eventAccess.ResultKind, eventAccess.Type);
                        }
                    }
                    break;
 
                default:
                    throw ExceptionUtilities.UnexpectedValue(originalLHS.Kind);
            }
 
            // We made no transformation above. Either we have array[index] += value, 
            // structVariable.field += value, or ref-returning call += value; in all cases
            // way we have a potentially complicated variable-producing expression on the
            // left. Generate ref temp = ref variable; temp = temp + value
 
            // Rewrite the variable.  Here we depend on the fact that the only forms
            // rewritten here are rewritten the same for lvalues and rvalues.
            BoundExpression rewrittenVariable = VisitExpression(originalLHS);
 
            BoundAssignmentOperator assignmentToTemp2;
            var variableTemp = _factory.StoreToTemp(rewrittenVariable, out assignmentToTemp2, refKind: RefKind.Ref);
            stores.Add(assignmentToTemp2);
            temps.Add(variableTemp.LocalSymbol);
            return variableTemp;
        }
 
        private static bool IsInvariantArray(TypeSymbol? type)
        {
            return (type as ArrayTypeSymbol)?.ElementType.IsSealed == true;
        }
 
        private BoundExpression BoxReceiver(BoundExpression rewrittenReceiver, NamedTypeSymbol memberContainingType)
        {
            return MakeConversionNode(
                rewrittenReceiver.Syntax,
                rewrittenReceiver,
                Conversion.Boxing,
                memberContainingType,
                @checked: false,
                constantValueOpt: rewrittenReceiver.ConstantValueOpt);
        }
 
        private BoundExpression SpillArrayElementAccess(
            BoundExpression loweredExpression,
            ImmutableArray<BoundExpression> loweredIndices,
            ArrayBuilder<BoundExpression> stores,
            ArrayBuilder<LocalSymbol> temps)
        {
            BoundAssignmentOperator assignmentToArrayTemp;
            var arrayTemp = _factory.StoreToTemp(loweredExpression, out assignmentToArrayTemp);
            stores.Add(assignmentToArrayTemp);
            temps.Add(arrayTemp.LocalSymbol);
            var boundTempArray = arrayTemp;
 
            var boundTempIndices = new BoundExpression[loweredIndices.Length];
            for (int i = 0; i < boundTempIndices.Length; i++)
            {
                if (CanChangeValueBetweenReads(loweredIndices[i]))
                {
                    BoundAssignmentOperator assignmentToTemp;
                    var temp = _factory.StoreToTemp(loweredIndices[i], out assignmentToTemp);
                    stores.Add(assignmentToTemp);
                    temps.Add(temp.LocalSymbol);
                    boundTempIndices[i] = temp;
                }
                else
                {
                    boundTempIndices[i] = loweredIndices[i];
                }
            }
 
            return _factory.ArrayAccess(boundTempArray, boundTempIndices);
        }
 
        /// <summary>
        /// Variables local to current frame do not need temps when re-read multiple times
        /// as long as there is no code that may write to locals in between accesses and they
        /// are not captured.
        /// 
        /// Example:
        ///        l += goo(ref l);
        /// 
        /// even though l is a local, we must access it via a temp since "goo(ref l)" may change it
        /// on between accesses.
        ///
        /// Note: In <c>this.x++</c>, <c>this</c> cannot change between reads. But in <c>(this, ...) == (..., this.Mutate())</c> it can.
        /// </summary>
        internal static bool CanChangeValueBetweenReads(
            BoundExpression expression,
            bool localsMayBeAssignedOrCaptured = true,
            bool structThisCanChangeValueBetweenReads = false)
        {
            if (expression.IsDefaultValue())
            {
                return false;
            }
 
            if (expression.ConstantValueOpt != null)
            {
                var type = expression.Type;
                return !ConstantValueIsTrivial(type);
            }
 
            // Note, we can get here with a node that hasn't been lowered yet.
            // For example, from TransformCompoundAssignmentFieldOrEventAccessReceiver.
 
            switch (expression.Kind)
            {
                case BoundKind.ThisReference:
                    return structThisCanChangeValueBetweenReads && ((BoundThisReference)expression).Type.IsStructType();
 
                case BoundKind.BaseReference:
                    return false;
 
                case BoundKind.Literal:
                    var type = expression.Type;
                    return !ConstantValueIsTrivial(type);
 
                case BoundKind.Parameter:
                    return localsMayBeAssignedOrCaptured ||
                           ((BoundParameter)expression).ParameterSymbol.RefKind != RefKind.None ||
                           IsCapturedPrimaryConstructorParameter(expression); // captured primary constructor parameters should be treated as a field
 
                case BoundKind.Local:
                    return localsMayBeAssignedOrCaptured || ((BoundLocal)expression).LocalSymbol.RefKind != RefKind.None;
 
                case BoundKind.TypeExpression:
                    return false;
 
                default:
                    return true;
            }
        }
 
        // a simple check for common non-side-effecting expressions
        internal static bool ReadIsSideeffecting(
            BoundExpression expression)
        {
            if (expression.ConstantValueOpt != null)
            {
                return false;
            }
 
            if (expression.IsDefaultValue())
            {
                return false;
            }
 
            switch (expression.Kind)
            {
                case BoundKind.Parameter:
                    Debug.Assert(!IsCapturedPrimaryConstructorParameter(expression));
                    goto case BoundKind.Local;
                case BoundKind.Local:
                case BoundKind.Lambda:
                case BoundKind.ThisReference:
                case BoundKind.BaseReference:
                case BoundKind.Literal:
                    return false;
 
                case BoundKind.Conversion:
                    var conv = (BoundConversion)expression;
                    return conv.ConversionHasSideEffects() ||
                        ReadIsSideeffecting(conv.Operand);
 
                case BoundKind.PassByCopy:
                    return ReadIsSideeffecting(((BoundPassByCopy)expression).Expression);
 
                case BoundKind.ObjectCreationExpression:
                    // common production of lowered conversions to nullable
                    // new S?(arg)
                    Debug.Assert(expression.Type is { });
                    if (expression.Type.IsNullableType())
                    {
                        var objCreation = (BoundObjectCreationExpression)expression;
                        return objCreation.Arguments.Length == 1 && ReadIsSideeffecting(objCreation.Arguments[0]);
                    }
 
                    return true;
 
                case BoundKind.Call:
                    var call = (BoundCall)expression;
                    var method = call.Method;
 
                    // common production of lowered lifted operators
                    // GetValueOrDefault is known to be not sideeffecting.
                    if (method.ContainingType?.IsNullableType() == true)
                    {
                        if (IsSpecialMember(method, SpecialMember.System_Nullable_T_GetValueOrDefault) ||
                            IsSpecialMember(method, SpecialMember.System_Nullable_T_get_HasValue))
                        {
                            Debug.Assert(call.ReceiverOpt is { });
                            return ReadIsSideeffecting(call.ReceiverOpt);
                        }
                    }
 
                    return true;
 
                default:
                    return true;
            }
        }
 
        private static bool IsSpecialMember(MethodSymbol method, SpecialMember specialMember)
        {
            method = method.OriginalDefinition;
            return method.ContainingAssembly?.GetSpecialTypeMember(specialMember) == method;
        }
 
        // nontrivial literals do not change between reads
        // but may require re-constructing, so it is better 
        // to treat them as potentially changing.
        private static bool ConstantValueIsTrivial(TypeSymbol? type)
        {
            return type is null ||
                type.SpecialType.IsClrInteger() ||
                type.IsReferenceType ||
                type.IsEnumType();
        }
 
    }
}