File: Emit\EditAndContinue\EncVariableSlotAllocator.cs
Web Access
Project: src\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis)
// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Reflection.Metadata;
using Microsoft.CodeAnalysis.CodeGen;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Symbols;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Emit
{
    internal sealed class EncVariableSlotAllocator : VariableSlotAllocator
    {
        // symbols:
        private readonly SymbolMatcher _symbolMap;
 
        // syntax:
        private readonly EncMappedMethod _mappedMethod;
        private readonly DebugId? _methodId;
 
        // locals:
        private readonly IReadOnlyDictionary<EncLocalInfo, int> _previousLocalSlots;
        private readonly ImmutableArray<EncLocalInfo> _previousLocals;
 
        // previous state machine:
        private readonly string? _stateMachineTypeName;
        private readonly int _hoistedLocalSlotCount;
        private readonly IReadOnlyDictionary<EncHoistedLocalInfo, int>? _hoistedLocalSlots;
        private readonly int _awaiterCount;
        private readonly IReadOnlyDictionary<Cci.ITypeReference, int>? _awaiterMap;
        private readonly IReadOnlyDictionary<(int syntaxOffset, AwaitDebugId awaitId), StateMachineState>? _stateMachineStateMap;
        private readonly StateMachineState? _firstUnusedDecreasingStateMachineState;
        private readonly StateMachineState? _firstUnusedIncreasingStateMachineState;
 
        // closures (keyed by syntax offset):
        private readonly IReadOnlyDictionary<int, EncLambdaMapValue>? _lambdaMap;
        private readonly IReadOnlyDictionary<int, EncClosureMapValue>? _closureMap;
 
        private readonly LambdaSyntaxFacts _lambdaSyntaxFacts;
 
        public EncVariableSlotAllocator(
            SymbolMatcher symbolMap,
            EncMappedMethod mappedMethod,
            DebugId? methodId,
            ImmutableArray<EncLocalInfo> previousLocals,
            IReadOnlyDictionary<int, EncLambdaMapValue>? lambdaMap,
            IReadOnlyDictionary<int, EncClosureMapValue>? closureMap,
            string? stateMachineTypeName,
            int hoistedLocalSlotCount,
            IReadOnlyDictionary<EncHoistedLocalInfo, int>? hoistedLocalSlots,
            int awaiterCount,
            IReadOnlyDictionary<Cci.ITypeReference, int>? awaiterMap,
            IReadOnlyDictionary<(int syntaxOffset, AwaitDebugId awaitId), StateMachineState>? stateMachineStateMap,
            StateMachineState? firstUnusedIncreasingStateMachineState,
            StateMachineState? firstUnusedDecreasingStateMachineState,
            LambdaSyntaxFacts lambdaSyntaxFacts)
        {
            Debug.Assert(!previousLocals.IsDefault);
 
            _symbolMap = symbolMap;
            _mappedMethod = mappedMethod;
            _previousLocals = previousLocals;
            _methodId = methodId;
            _hoistedLocalSlots = hoistedLocalSlots;
            _hoistedLocalSlotCount = hoistedLocalSlotCount;
            _stateMachineTypeName = stateMachineTypeName;
            _awaiterCount = awaiterCount;
            _awaiterMap = awaiterMap;
            _stateMachineStateMap = stateMachineStateMap;
            _lambdaMap = lambdaMap;
            _closureMap = closureMap;
            _lambdaSyntaxFacts = lambdaSyntaxFacts;
            _firstUnusedIncreasingStateMachineState = firstUnusedIncreasingStateMachineState;
            _firstUnusedDecreasingStateMachineState = firstUnusedDecreasingStateMachineState;
 
            // Create a map from local info to slot.
            var previousLocalInfoToSlot = new Dictionary<EncLocalInfo, int>();
            for (int slot = 0; slot < previousLocals.Length; slot++)
            {
                var localInfo = previousLocals[slot];
                Debug.Assert(!localInfo.IsDefault);
                if (localInfo.IsUnused)
                {
                    // Unrecognized or deleted local.
                    continue;
                }
 
                previousLocalInfoToSlot.Add(localInfo, slot);
            }
 
            _previousLocalSlots = previousLocalInfoToSlot;
        }
 
        public override DebugId? MethodId => _methodId;
 
        private int CalculateSyntaxOffsetInPreviousMethod(SyntaxNode node)
        {
            // Note that syntax offset of a syntax node contained in a lambda body is calculated by the containing top-level method,
            // not by the lambda method. The offset is thus relative to the top-level method body start. We can thus avoid mapping 
            // the current lambda symbol or body to the corresponding previous lambda symbol or body, which is non-trivial. 
            return _mappedMethod.PreviousMethod.CalculateLocalSyntaxOffset(_lambdaSyntaxFacts.GetDeclaratorPosition(node), node.SyntaxTree);
        }
 
        public override void AddPreviousLocals(ArrayBuilder<Cci.ILocalDefinition> builder)
        {
            builder.AddRange(_previousLocals.Select((info, index) =>
            {
                RoslynDebug.AssertNotNull(info.Signature);
                return new SignatureOnlyLocalDefinition(info.Signature, index);
            }));
        }
 
        private bool TryGetPreviousLocalId(SyntaxNode currentDeclarator, LocalDebugId currentId, out LocalDebugId previousId)
        {
            if (_mappedMethod.SyntaxMap == null)
            {
                // no syntax map 
                // => the source of the current method is the same as the source of the previous method 
                // => relative positions are the same 
                // => synthesized ids are the same
                previousId = currentId;
                return true;
            }
 
            SyntaxNode? previousDeclarator = _mappedMethod.SyntaxMap(currentDeclarator);
            if (previousDeclarator == null)
            {
                previousId = default;
                return false;
            }
 
            int syntaxOffset = CalculateSyntaxOffsetInPreviousMethod(previousDeclarator);
            previousId = new LocalDebugId(syntaxOffset, currentId.Ordinal);
            return true;
        }
 
        public override LocalDefinition? GetPreviousLocal(
            Cci.ITypeReference currentType,
            ILocalSymbolInternal currentLocalSymbol,
            string? name,
            SynthesizedLocalKind kind,
            LocalDebugId id,
            LocalVariableAttributes pdbAttributes,
            LocalSlotConstraints constraints,
            ImmutableArray<bool> dynamicTransformFlags,
            ImmutableArray<string> tupleElementNames)
        {
            if (id.IsNone)
            {
                return null;
            }
 
            if (!TryGetPreviousLocalId(currentLocalSymbol.GetDeclaratorSyntax(), id, out LocalDebugId previousId))
            {
                return null;
            }
 
            var previousType = _symbolMap.MapReference(currentType);
            if (previousType == null)
            {
                return null;
            }
 
            // TODO (bug #781309): Should report a warning if the type of the local has changed
            // and the previous value will be dropped.
            var localKey = new EncLocalInfo(new LocalSlotDebugInfo(kind, previousId), previousType, constraints, signature: null);
 
            if (!_previousLocalSlots.TryGetValue(localKey, out int slot))
            {
                return null;
            }
 
            return new LocalDefinition(
                currentLocalSymbol,
                name,
                currentType,
                slot,
                kind,
                id,
                pdbAttributes,
                constraints,
                dynamicTransformFlags,
                tupleElementNames);
        }
 
        public override string? PreviousStateMachineTypeName => _stateMachineTypeName;
 
        public override bool TryGetPreviousHoistedLocalSlotIndex(
            SyntaxNode currentDeclarator,
            Cci.ITypeReference currentType,
            SynthesizedLocalKind synthesizedKind,
            LocalDebugId currentId,
            DiagnosticBag diagnostics,
            out int slotIndex)
        {
            // The previous method was not a state machine (it is allowed to change non-state machine to a state machine):
            if (_hoistedLocalSlots == null)
            {
                slotIndex = -1;
                return false;
            }
 
            if (!TryGetPreviousLocalId(currentDeclarator, currentId, out LocalDebugId previousId))
            {
                slotIndex = -1;
                return false;
            }
 
            var previousType = _symbolMap.MapReference(currentType);
            if (previousType == null)
            {
                slotIndex = -1;
                return false;
            }
 
            // TODO (bug #781309): Should report a warning if the type of the local has changed
            // and the previous value will be dropped.
            var localKey = new EncHoistedLocalInfo(new LocalSlotDebugInfo(synthesizedKind, previousId), previousType);
 
            return _hoistedLocalSlots.TryGetValue(localKey, out slotIndex);
        }
 
        public override int PreviousHoistedLocalSlotCount => _hoistedLocalSlotCount;
        public override int PreviousAwaiterSlotCount => _awaiterCount;
 
        public override bool TryGetPreviousAwaiterSlotIndex(Cci.ITypeReference currentType, DiagnosticBag diagnostics, out int slotIndex)
        {
            // The previous method was not a state machine (it is allowed to change non-state machine to a state machine):
            if (_awaiterMap == null)
            {
                slotIndex = -1;
                return false;
            }
 
            var typeRef = _symbolMap.MapReference(currentType);
            RoslynDebug.AssertNotNull(typeRef);
 
            return _awaiterMap.TryGetValue(typeRef, out slotIndex);
        }
 
        private bool TryGetPreviousSyntaxOffset(SyntaxNode currentSyntax, out int previousSyntaxOffset)
        {
            // no syntax map 
            // => the source of the current method is the same as the source of the previous method 
            // => relative positions are the same 
            // => ids are the same
            SyntaxNode? previousSyntax = _mappedMethod.SyntaxMap?.Invoke(currentSyntax);
            if (previousSyntax == null)
            {
                previousSyntaxOffset = 0;
                return false;
            }
 
            previousSyntaxOffset = CalculateSyntaxOffsetInPreviousMethod(previousSyntax);
            return true;
        }
 
        private bool TryGetPreviousLambdaSyntaxOffset(SyntaxNode lambdaOrLambdaBodySyntax, bool isLambdaBody, out int previousSyntaxOffset)
        {
            // Syntax map contains mapping for lambdas, but not their bodies. 
            // Map the lambda first and then determine the corresponding body.
            var currentLambdaSyntax = isLambdaBody
                ? _lambdaSyntaxFacts.GetLambda(lambdaOrLambdaBodySyntax)
                : lambdaOrLambdaBodySyntax;
 
            // no syntax map 
            // => the source of the current method is the same as the source of the previous method 
            // => relative positions are the same 
            // => ids are the same
            SyntaxNode? previousLambdaSyntax = _mappedMethod.SyntaxMap?.Invoke(currentLambdaSyntax);
            if (previousLambdaSyntax == null)
            {
                previousSyntaxOffset = 0;
                return false;
            }
 
            SyntaxNode? previousSyntax;
            if (isLambdaBody)
            {
                previousSyntax = _lambdaSyntaxFacts.TryGetCorrespondingLambdaBody(previousLambdaSyntax, lambdaOrLambdaBodySyntax);
                if (previousSyntax == null)
                {
                    previousSyntaxOffset = 0;
                    return false;
                }
            }
            else
            {
                previousSyntax = previousLambdaSyntax;
            }
 
            previousSyntaxOffset = CalculateSyntaxOffsetInPreviousMethod(previousSyntax);
            return true;
        }
 
        public override bool TryGetPreviousClosure(
            SyntaxNode scopeSyntax,
            DebugId? parentClosureId,
            ImmutableArray<string> structCaptures,
            out DebugId closureId,
            out RuntimeRudeEdit? runtimeRudeEdit)
        {
            if (_closureMap != null && TryGetPreviousSyntaxOffset(scopeSyntax, out int syntaxOffset))
            {
                if (_closureMap.TryGetValue(syntaxOffset, out var closureMapValue) &&
                    closureMapValue.IsCompatibleWith(parentClosureId, structCaptures))
                {
                    closureId = closureMapValue.Id;
                    runtimeRudeEdit = _mappedMethod.RuntimeRudeEdit?.Invoke(scopeSyntax);
                    return true;
                }
 
                // closure shape changed:
                closureId = default;
                runtimeRudeEdit = new RuntimeRudeEdit(HotReloadExceptionCode.UnsupportedChangeToCapturedVariables);
                return false;
            }
 
            // closure added:
            closureId = default;
            runtimeRudeEdit = null;
            return false;
        }
 
        public override bool TryGetPreviousLambda(SyntaxNode lambdaOrLambdaBodySyntax, bool isLambdaBody, int closureOrdinal, ImmutableArray<DebugId> structClosureIds, out DebugId lambdaId, out RuntimeRudeEdit? runtimeRudeEdit)
        {
            Debug.Assert(closureOrdinal >= LambdaDebugInfo.MinClosureOrdinal);
 
            if (_lambdaMap != null && TryGetPreviousLambdaSyntaxOffset(lambdaOrLambdaBodySyntax, isLambdaBody, out int syntaxOffset))
            {
                if (_lambdaMap.TryGetValue(syntaxOffset, out var lambdaMapValue) && lambdaMapValue.IsCompatibleWith(closureOrdinal, structClosureIds))
                {
                    // Rude edit map contains mapping for lambdas, but not their bodies. 
                    runtimeRudeEdit = _mappedMethod.RuntimeRudeEdit?.Invoke(isLambdaBody ? _lambdaSyntaxFacts.GetLambda(lambdaOrLambdaBodySyntax) : lambdaOrLambdaBodySyntax);
 
                    lambdaId = lambdaMapValue.Id;
                    return true;
                }
 
                // lambda closure changed:
                lambdaId = default;
                runtimeRudeEdit = new RuntimeRudeEdit(HotReloadExceptionCode.UnsupportedChangeToCapturedVariables);
                return false;
            }
 
            // lambda added:
            lambdaId = default;
            runtimeRudeEdit = null;
            return false;
        }
 
        public override StateMachineState? GetFirstUnusedStateMachineState(bool increasing)
            => increasing ? _firstUnusedIncreasingStateMachineState : _firstUnusedDecreasingStateMachineState;
 
        public override bool TryGetPreviousStateMachineState(SyntaxNode syntax, AwaitDebugId awaitId, out StateMachineState state)
        {
            if (_stateMachineStateMap != null &&
                TryGetPreviousSyntaxOffset(syntax, out int syntaxOffset) &&
                _stateMachineStateMap.TryGetValue((syntaxOffset, awaitId), out state))
            {
                return true;
            }
 
            state = default;
            return false;
        }
    }
}