File: Actions\GrammarActions.MethodBodies.Locals.cs
Web Access
Project: ILAssembler.csproj (ILAssembler)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using Antlr4.Runtime;

namespace ILAssembler;

internal sealed partial class GrammarActions
{
    /// <summary>
    /// The largest slot index a local can have. <c>ldloc</c>, <c>ldloca</c> and <c>stloc</c> take a 16-bit index.
    /// </summary>
    private const int MaximumLocalSlot = ushort.MaxValue;

    /// <summary>
    /// The explicit <c>[n]</c> slot indices of the <c>.locals</c> directive being parsed, keyed by the position of
    /// the argument in the directive, with the token of <c>n</c>. <see cref="SetRawParameterAttributeElement"/>
    /// records them and <see cref="EndLocalsDirective"/> consumes and clears them.
    /// </summary>
    private readonly Dictionary<int, (int Slot, IToken Token)> _explicitLocalSlots = new();

    /// <summary>
    /// The name tokens of the named locals of the <c>.locals</c> directive being parsed, keyed by the position of
    /// the argument in the directive. <see cref="CreateSignatureArgument"/> records them, <see cref="DeclareLocals"/>
    /// gives them to the PDB's <see cref="EntityRegistry.LocalVariableRecord"/>s as their locations, and
    /// <see cref="EndLocalsDirective"/> clears them.
    /// </summary>
    private readonly Dictionary<int, IToken> _localNameTokens = new();

    /// <summary>
    /// Returns the arguments of a <c>.locals</c> directive with <see cref="SignatureArgumentValue.Slot"/> set from
    /// the recorded <c>[n]</c> elements. <c>[-1]</c> means "no explicit slot". Any other value
    /// outside [0, <see cref="MaximumLocalSlot"/>] is reported, naming the value as written in the source, and
    /// ignored, so the local takes the next free slot (which <see cref="DeclareLocals"/> checks in turn). The
    /// range is checked on the literal as written, so a literal too large for 32 bits is reported too rather than
    /// narrowed into the range or to <c>-1</c>.
    /// </summary>
    private ImmutableArray<SignatureArgumentValue> ApplyExplicitLocalSlots(ImmutableArray<SignatureArgumentValue> arguments)
    {
        if (_explicitLocalSlots.Count == 0)
        {
            return arguments;
        }

        ImmutableArray<SignatureArgumentValue>.Builder builder = arguments.ToBuilder();
        foreach ((int index, (int parsedSlot, IToken token)) in _explicitLocalSlots)
        {
            // The recorded value is ParseInt32's, which narrows the literal to 32 bits without a check:
            // [4294967296] would be slot 0 and [4294967295] would be [-1]. A literal that cannot be read has
            // already been reported by ParseInt32 and keeps its value.
            long slot = ReadLocalSlotLiteral(token.Text) ?? parsedSlot;

            // An [n] is recorded for an argument only as that argument is parsed, so every recorded position has
            // an argument; the bound check guards against a grammar change that records one for an argument
            // that is then dropped.
            if (index >= builder.Count || slot == -1)
            {
                continue;
            }

            if (slot < 0 || slot > MaximumLocalSlot)
            {
                // The token's text rather than the value read, which is capped for a literal past the range:
                // [2147483648] is reported as 2147483648.
                ReportError(
                    DiagnosticIds.LocalSlotOutOfRange,
                    string.Format(DiagnosticMessageTemplates.LocalSlotOutOfRange, token.Text, MaximumLocalSlot),
                    token);
                continue;
            }

            builder[index] = builder[index] with { Slot = (int)slot };
        }

        return builder.ToImmutable();
    }

    /// <summary>
    /// Reads the value of an <c>[n]</c> slot literal without narrowing it. The bases are those of
    /// <see cref="ParseInt32"/>: <c>0x</c> is hexadecimal, and any other leading <c>0</c> is octal. A decimal or
    /// hexadecimal magnitude is read with
    /// <see cref="ulong.TryParse(ReadOnlySpan{char}, NumberStyles, IFormatProvider, out ulong)"/>. An octal
    /// literal, for which <see cref="NumberStyles"/> has no option, is read as <see cref="ParseInt32"/> reads it
    /// (<see cref="ParseIntegerValue(ReadOnlySpan{char}, out long)"/>), and one with more significant digits than
    /// <see cref="MaximumLocalSlot"/> (0177777) is past the slot range, whatever its low 64 bits are. A magnitude
    /// past <see cref="MaximumLocalSlot"/> + 1, including one past 64 bits, reads as
    /// <see cref="MaximumLocalSlot"/> + 1 with the literal's sign, so that a literal of any base outside the slot
    /// range reads as a value outside it, and not as <c>-1</c>.
    /// </summary>
    /// <returns>
    /// The value, or <see langword="null"/> for an octal literal with an 8 or a 9 in it, which
    /// <see cref="ParseInt32"/> reports.
    /// </returns>
    /// <remarks>
    /// Only local slots are read this way. A raw <c>[n]</c> on a parameter keeps <see cref="ParseInt32"/>'s value.
    /// </remarks>
    private static long? ReadLocalSlotLiteral(string text)
    {
        ReadOnlySpan<char> digits = text.AsSpan();
        bool negative = digits.StartsWith("-".AsSpan());
        if (negative)
        {
            digits = digits.Slice(1);
        }

        long magnitude;
        if (digits.StartsWith("0".AsSpan()) && !digits.StartsWith("0x".AsSpan()))
        {
            if (!ParseIntegerValue(digits, out long octal))
            {
                return null;
            }

            // ParseIntegerValue keeps only the low 64 bits of a longer octal literal. MaximumLocalSlot is 0177777, six
            // significant digits, so a magnitude with more is past the slot range, and one with six or fewer is exact.
            magnitude = digits.TrimStart('0').Length > 6
                ? MaximumLocalSlot + 1
                : Math.Min(octal, MaximumLocalSlot + 1);
        }
        else
        {
            NumberStyles style = NumberStyles.None;
            if (digits.StartsWith("0x".AsSpan()))
            {
                style = NumberStyles.AllowHexSpecifier;
                digits = digits.Slice(2);
            }

            // The integer token has only digits of the base here, so the parse fails only for a magnitude past 64 bits.
            magnitude = ulong.TryParse(digits, style, CultureInfo.InvariantCulture, out ulong value)
                ? (long)Math.Min(value, (ulong)MaximumLocalSlot + 1)
                : MaximumLocalSlot + 1;
        }

        return negative ? -magnitude : magnitude;
    }

    /// <summary>
    /// Declares the locals of a <c>.locals</c> directive in the innermost open lexical scope, in order.
    /// </summary>
    /// <remarks>
    /// A local without an explicit slot takes the next slot after the highest one declared so far in the method.
    /// A local with slot <c>n</c> takes slot <c>n</c>; when <c>n</c> is past the end of the slot table, the slots
    /// before it are added without a type, to be typed by a later declaration. Declaring a slot that already
    /// exists is allowed: if the slot is in use (<see cref="LocalSlot.InScope"/>), the declaration is reported
    /// as a warning, and if its type differs from the slot's, as an error. Either way the slot takes the new
    /// type. A slot freed when its scope closed may be declared again with the same type without a diagnostic;
    /// that is how a method can have fewer slots than local declarations. A local that would take the next slot
    /// when that is past <see cref="MaximumLocalSlot"/> (a slot after 65535) is reported and not declared: no
    /// local instruction could name it, and the PDB could not record it. A <c>...</c> sentinel in the list is not
    /// a local: it takes no slot. That is only a <c>...</c> standing alone as an argument; in <c>... int32 a</c>
    /// the sentinel is part of the type, and the argument is a local like any other. A named local is added to
    /// the scope's names unless the scope already has a local with that name, so the first declaration of a name
    /// in a scope is the one that name refers to.
    /// </remarks>
    private void DeclareLocals(
        CurrentMethodContext method,
        ImmutableArray<SignatureArgumentValue> arguments,
        CILParser.LocalsDeclContext context)
    {
        ImmutableArray<SignatureArg> locals = MaterializeSignatureArguments(arguments);
        LexicalScope scope = method.OpenScopes[^1];
        List<LocalSlot> slots = method.LocalSlots;
        for (int i = 0; i < locals.Length; i++)
        {
            SignatureArg local = locals[i];
            if (local.IsSentinel)
            {
                continue;
            }

            int index = arguments[i].Slot ?? slots.Count;

            // The one check of the slot a declaration gets. ApplyExplicitLocalSlots has replaced an out-of-range
            // [n] with "next slot", so what fails here is a next slot past the last one, after [65535].
            if (index < 0 || index > MaximumLocalSlot)
            {
                ReportError(
                    DiagnosticIds.LocalSlotOutOfRange,
                    string.Format(DiagnosticMessageTemplates.LocalSlotOutOfRange, index, MaximumLocalSlot),
                    context);
                continue;
            }

            if (index < slots.Count)
            {
                LocalSlot existing = slots[index];
                if (existing.InScope)
                {
                    ReportWarning(
                        DiagnosticIds.LocalSlotInUse,
                        string.Format(DiagnosticMessageTemplates.LocalSlotInUse, index),
                        context);
                }

                if (existing.Type is not null && !existing.Type.ContentEquals(local.SignatureBlob))
                {
                    ReportError(
                        DiagnosticIds.LocalSlotTypeConflict,
                        string.Format(DiagnosticMessageTemplates.LocalSlotTypeConflict, index),
                        context);
                }
            }
            else
            {
                while (slots.Count <= index)
                {
                    slots.Add(new LocalSlot(context.Start));
                }
            }

            LocalSlot slot = slots[index];
            slot.Type = local.SignatureBlob;
            slot.InScope = true;
            scope.DeclaredSlots.Add(index);
            if (local.Name is not null)
            {
                scope.Names.TryAdd(local.Name, index);
                if (GeneratesPdb)
                {
                    IToken nameToken = _localNameTokens.TryGetValue(i, out IToken? token) ? token : context.Start;
                    scope.Variables.Add(new EntityRegistry.LocalVariableRecord(
                        local.Name,
                        index,
                        Location.From(nameToken, _documents)));
                }
            }
        }
    }

    /// <summary>
    /// Opens a lexical scope for a <c>{ }</c> block of the current method body, starting at the current offset.
    /// </summary>
    private void OpenLexicalScope(CurrentMethodContext method)
        => method.OpenScopes.Add(new LexicalScope(CurrentMethodBodyOffset, method.NextScopeOrder++));

    /// <summary>
    /// Closes the innermost open lexical scopes of the method, at the current offset, until <paramref name="count"/>
    /// remain open. The slots that a closed scope declared are no longer in use, even when an enclosing scope that
    /// is still open declared the same slot (<see cref="LocalSlot.InScope"/>). When a PDB is requested
    /// (<see cref="GeneratesPdb"/>), the scope is recorded in the method's
    /// <see cref="EntityRegistry.MethodDebugInfo.LocalScopes"/> for the PDB.
    /// </summary>
    private void CloseLexicalScopes(CurrentMethodContext method, int count)
    {
        bool recordScopes = GeneratesPdb;
        while (method.OpenScopes.Count > count)
        {
            LexicalScope scope = method.OpenScopes[^1];
            method.OpenScopes.RemoveAt(method.OpenScopes.Count - 1);
            foreach (int slot in scope.DeclaredSlots)
            {
                method.LocalSlots[slot].InScope = false;
            }

            if (recordScopes)
            {
                method.Definition.DebugInfo.LocalScopes.Add(new EntityRegistry.LocalScopeRecord(
                    scope.StartOffset,
                    CurrentMethodBodyOffset,
                    scope.Order,
                    scope.Variables.ToImmutableArray()));
            }
        }
    }

    /// <summary>
    /// Sets the method body's local signature from the slot table: one entry per slot, in slot order.
    /// A slot that no declaration typed is reported and written as <c>int32</c> so the signature stays
    /// well-formed for error-tolerant output.
    /// </summary>
    private void EmitLocalSignature(CurrentMethodContext method)
    {
        List<LocalSlot> slots = method.LocalSlots;
        if (slots.Count == 0)
        {
            return;
        }

        BlobBuilder localsSignature = new();
        LocalVariablesEncoder localsEncoder = new BlobEncoder(localsSignature).LocalVariableSignature(slots.Count);
        for (int i = 0; i < slots.Count; i++)
        {
            if (slots[i].Type is { } type)
            {
                type.WriteContentTo(localsEncoder.AddVariable().Builder);
            }
            else
            {
                ReportError(
                    DiagnosticIds.UndefinedLocalSlotType,
                    string.Format(DiagnosticMessageTemplates.UndefinedLocalSlotType, i, method.Definition.Name),
                    slots[i].Declaration);
                localsEncoder.AddVariable().Type().Int32();
            }
        }

        method.Definition.LocalsSignature = _entityRegistry.GetOrCreateStandaloneSignature(localsSignature);
    }

    /// <summary>
    /// A slot of a method's local signature.
    /// </summary>
    private sealed class LocalSlot
    {
        /// <summary>Creates a slot that no declaration has typed yet and that is not in use.</summary>
        /// <param name="declaration">The first token of the <c>.locals</c> directive that added the slot.</param>
        public LocalSlot(IToken declaration) => Declaration = declaration;

        /// <summary>
        /// Gets the first token of the <c>.locals</c> directive that added the slot: the one that declared it, or,
        /// for a slot added as padding before an explicit slot index, the one that declared that index.
        /// </summary>
        public IToken Declaration { get; }

        /// <summary>
        /// Gets or sets the type blob of the slot's latest declaration, or <see langword="null"/> when no
        /// declaration has typed it.
        /// </summary>
        public BlobBuilder? Type { get; set; }

        /// <summary>
        /// Gets or sets whether the slot is in use, which makes declaring it again a warning. There is one
        /// such flag per slot: every declaration of the slot sets it and the closing of any scope that declared
        /// the slot clears it. So when a block declares a slot that an enclosing scope also declared and then
        /// closes, the slot is no longer in use, although the enclosing scope is still open.
        /// </summary>
        public bool InScope { get; set; }
    }

    /// <summary>
    /// An open lexical scope of a method body: the method's root scope or a <c>{ }</c> block, including the bodies
    /// of <c>.try</c>, <c>catch</c>, <c>filter</c>, <c>finally</c> and <c>fault</c>.
    /// </summary>
    private sealed class LexicalScope
    {
        /// <summary>Creates an open scope that has declared no locals yet.</summary>
        /// <param name="startOffset">The IL offset at which the scope starts.</param>
        /// <param name="order">The position of the scope in source order (<see cref="EntityRegistry.LocalScopeRecord.Order"/>).</param>
        public LexicalScope(int startOffset, int order)
        {
            StartOffset = startOffset;
            Order = order;
        }

        /// <summary>Gets the IL offset at which the scope starts: 0 for the root scope, the offset at <c>{</c> for a block.</summary>
        public int StartOffset { get; }

        /// <summary>Gets the position of the scope in source order: 0 for the root scope, then each block as it opens.</summary>
        public int Order { get; }

        /// <summary>
        /// Gets the named locals declared in this scope, in declaration order, including a second declaration of a
        /// name or a slot; each gets a LocalVariable row. Filled only when a PDB is requested
        /// (<see cref="GeneratesPdb"/>).
        /// </summary>
        public List<EntityRegistry.LocalVariableRecord> Variables { get; } = new();

        /// <summary>
        /// Gets the slot of each name declared in this scope. Names resolve from the innermost open scope outward.
        /// </summary>
        public Dictionary<string, int> Names { get; } = new(StringComparer.Ordinal);

        /// <summary>Gets the slot of each local declared in this scope, named or not, in declaration order.</summary>
        public List<int> DeclaredSlots { get; } = new();
    }
}