File: CodeGen\CodeGenerator_HasHome.cs
Web Access
Project: src\src\Compilers\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.csproj (Microsoft.CodeAnalysis.CSharp)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.CodeAnalysis.CSharp.Symbols;
 
namespace Microsoft.CodeAnalysis.CSharp.CodeGen;
 
internal partial class CodeGenerator
{
    internal enum AddressKind
    {
        // reference may be written to
        Writeable,
 
        // reference itself will not be written to, but may be used for call, callvirt.
        // for all purposes it is the same as Writeable, except when fetching an address of an array element
        // where it results in a ".readonly" prefix to deal with array covariance.
        Constrained,
 
        // reference itself will not be written to, nor it will be used to modify fields.
        ReadOnly,
 
        // same as ReadOnly, but we are not supposed to get a reference to a clone
        // regardless of compat settings.
        ReadOnlyStrict,
    }
 
    internal static bool IsAnyReadOnly(AddressKind addressKind) => addressKind >= AddressKind.ReadOnly;
 
    /// <summary>
    /// Checks if expression directly or indirectly represents a value with its own home. In
    /// such cases it is possible to get a reference without loading into a temporary.
    /// </summary>
    /// <param name="expression">
    /// This should be a lowered node. This method does NOT expect nodes from initial binding.
    /// </param>
    internal static bool HasHome(
        BoundExpression expression,
        AddressKind addressKind,
        Symbol containingSymbol,
        bool peVerifyCompatEnabled,
        HashSet<LocalSymbol> stackLocalsOpt)
    {
        Debug.Assert(containingSymbol is object);
 
        switch (expression.Kind)
        {
            case BoundKind.ArrayAccess:
                if (addressKind == AddressKind.ReadOnly && !expression.Type.IsValueType && peVerifyCompatEnabled)
                {
                    // due to array covariance getting a reference may throw ArrayTypeMismatch when element is not a struct, 
                    // passing "readonly." prefix would prevent that, but it is unverifiable, so will make a copy in compat case
                    return false;
                }
 
                return true;
 
            case BoundKind.PointerIndirectionOperator:
            case BoundKind.RefValueOperator:
                return true;
 
            case BoundKind.ThisReference:
                var type = expression.Type;
                if (type.IsReferenceType)
                {
                    Debug.Assert(IsAnyReadOnly(addressKind), "`this` is readonly in classes");
                    return true;
                }
 
                if (!IsAnyReadOnly(addressKind) && containingSymbol is MethodSymbol { ContainingSymbol: NamedTypeSymbol, IsEffectivelyReadOnly: true })
                {
                    return false;
                }
 
                return true;
 
            case BoundKind.ThrowExpression:
                // vacuously this is true, we can take address of throw without temps
                return true;
 
            case BoundKind.Parameter:
                return IsAnyReadOnly(addressKind) ||
                    ((BoundParameter)expression).ParameterSymbol.RefKind is not (RefKind.In or RefKind.RefReadOnlyParameter);
 
            case BoundKind.Local:
                // locals have home unless they are byval stack locals or ref-readonly
                // locals in a mutating call
                var local = ((BoundLocal)expression).LocalSymbol;
                return !((CodeGenerator.IsStackLocal(local, stackLocalsOpt) && local.RefKind == RefKind.None) ||
                    (!IsAnyReadOnly(addressKind) && local.RefKind == RefKind.RefReadOnly));
 
            case BoundKind.Call:
                var methodRefKind = ((BoundCall)expression).Method.RefKind;
                return methodRefKind == RefKind.Ref ||
                       (IsAnyReadOnly(addressKind) && methodRefKind == RefKind.RefReadOnly);
 
            case BoundKind.Dup:
                //NB: Dup represents locals that do not need IL slot
                var dupRefKind = ((BoundDup)expression).RefKind;
                return dupRefKind == RefKind.Ref ||
                    (IsAnyReadOnly(addressKind) && dupRefKind == RefKind.RefReadOnly);
 
            case BoundKind.FieldAccess:
                return FieldAccessHasHome((BoundFieldAccess)expression, addressKind, containingSymbol, peVerifyCompatEnabled, stackLocalsOpt);
 
            case BoundKind.Sequence:
                return HasHome(((BoundSequence)expression).Value, addressKind, containingSymbol, peVerifyCompatEnabled, stackLocalsOpt);
 
            case BoundKind.AssignmentOperator:
                var assignment = (BoundAssignmentOperator)expression;
                if (!assignment.IsRef)
                {
                    return false;
                }
                var lhsRefKind = assignment.Left.GetRefKind();
                return lhsRefKind == RefKind.Ref ||
                    (IsAnyReadOnly(addressKind) && lhsRefKind is RefKind.RefReadOnly or RefKind.RefReadOnlyParameter);
 
            case BoundKind.ComplexConditionalReceiver:
                Debug.Assert(HasHome(
                    ((BoundComplexConditionalReceiver)expression).ValueTypeReceiver,
                    addressKind,
                    containingSymbol,
                    peVerifyCompatEnabled,
                    stackLocalsOpt));
                Debug.Assert(HasHome(
                    ((BoundComplexConditionalReceiver)expression).ReferenceTypeReceiver,
                    addressKind,
                    containingSymbol,
                    peVerifyCompatEnabled,
                    stackLocalsOpt));
                goto case BoundKind.ConditionalReceiver;
 
            case BoundKind.ConditionalReceiver:
                //ConditionalReceiver is a noop from Emit point of view. - it represents something that has already been pushed. 
                //We should never need a temp for it. 
                return true;
 
            case BoundKind.ConditionalOperator:
                var conditional = (BoundConditionalOperator)expression;
 
                // only ref conditional may be referenced as a variable
                if (!conditional.IsRef)
                {
                    return false;
                }
 
                // branch that has no home will need a temporary
                // if both have no home, just say whole expression has no home 
                // so we could just use one temp for the whole thing
                return HasHome(conditional.Consequence, addressKind, containingSymbol, peVerifyCompatEnabled, stackLocalsOpt)
                    && HasHome(conditional.Alternative, addressKind, containingSymbol, peVerifyCompatEnabled, stackLocalsOpt);
 
            default:
                return false;
        }
    }
 
    /// <summary>
    /// Special HasHome for fields.
    /// A field has a readable home unless the field is a constant.
    /// A ref readonly field doesn't have a writable home.
    /// Other fields have a writable home unless the field is a readonly value
    /// and is used outside of a constructor or init method.
    /// </summary>
    private static bool FieldAccessHasHome(
        BoundFieldAccess fieldAccess,
        AddressKind addressKind,
        Symbol containingSymbol,
        bool peVerifyCompatEnabled,
        HashSet<LocalSymbol> stackLocalsOpt)
    {
        Debug.Assert(containingSymbol is object);
 
        FieldSymbol field = fieldAccess.FieldSymbol;
 
        // const fields are literal values with no homes. (ex: decimal.Zero)
        if (field.IsConst)
        {
            return false;
        }
 
        if (field.RefKind is RefKind.Ref)
        {
            return true;
        }
 
        // in readonly situations where ref to a copy is not allowed, consider fields as addressable
        if (addressKind == AddressKind.ReadOnlyStrict)
        {
            return true;
        }
 
        // ReadOnly references can always be taken unless we are in peverify compat mode
        if (addressKind == AddressKind.ReadOnly && !peVerifyCompatEnabled)
        {
            return true;
        }
 
        // Some field accesses must be values; values do not have homes.
        if (fieldAccess.IsByValue)
        {
            return false;
        }
 
        if (field.RefKind == RefKind.RefReadOnly)
        {
            return false;
        }
 
        Debug.Assert(field.RefKind == RefKind.None);
 
        if (!field.IsReadOnly)
        {
            // in a case if we have a writeable struct field with a receiver that only has a readable home we would need to pass it via a temp.
            // it would be advantageous to make a temp for the field, not for the outer struct, since the field is smaller and we can get to is by fetching references.
            // NOTE: this would not be profitable if we have to satisfy verifier, since for verifiability 
            //       we would not be able to dig for the inner field using references and the outer struct will have to be copied to a temp anyways.
            if (!peVerifyCompatEnabled)
            {
                Debug.Assert(!IsAnyReadOnly(addressKind));
 
                var receiver = fieldAccess.ReceiverOpt;
                if (receiver?.Type.IsValueType == true)
                {
                    // Check receiver:
                    // has writeable home -> return true - the whole chain has writeable home (also a more common case)
                    // has readable home -> return false - we need to copy the field
                    // otherwise         -> return true  - the copy will be made at higher level so the leaf field can have writeable home
 
                    return HasHome(receiver, addressKind, containingSymbol, peVerifyCompatEnabled, stackLocalsOpt)
                        || !HasHome(receiver, AddressKind.ReadOnly, containingSymbol, peVerifyCompatEnabled, stackLocalsOpt);
                }
            }
 
            return true;
        }
 
        // while readonly fields have home it is not valid to refer to it when not constructing.
        if (!TypeSymbol.Equals(field.ContainingType, containingSymbol.ContainingSymbol as NamedTypeSymbol, TypeCompareKind.AllIgnoreOptions))
        {
            return false;
        }
 
        if (field.IsStatic)
        {
            return containingSymbol is MethodSymbol { MethodKind: MethodKind.StaticConstructor } or FieldSymbol { IsStatic: true };
        }
        else
        {
            return (containingSymbol is MethodSymbol { MethodKind: MethodKind.Constructor } or FieldSymbol { IsStatic: false } or MethodSymbol { IsInitOnly: true }) &&
                fieldAccess.ReceiverOpt.Kind == BoundKind.ThisReference;
        }
    }
}