File: Actions\GrammarActions.Debug.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.Diagnostics;
using System.Reflection.Metadata;
using Antlr4.Runtime;

namespace ILAssembler;

internal sealed partial class GrammarActions
{
#pragma warning disable CA1822 // Parser actions are invoked through the per-parser GrammarActions instance.
    internal bool IsAutoIncrementSourceDirective(IToken token) => token.Text == "#line";
#pragma warning restore CA1822

    internal SourceDirectiveValue CreateSourceLine(
        bool autoIncrement,
        IToken line,
        IToken? path)
    {
        int lineNumber = ParseInt32(line);
        return CreateSourceDirective(
            autoIncrement, lineNumber, 0, lineNumber, null, path,
            appliesToNextLine: autoIncrement && path?.Type == CILLexer.QSTRING);
    }

    internal SourceDirectiveValue CreateSourceColumn(
        bool autoIncrement,
        IToken line,
        IToken column,
        IToken? path)
    {
        int lineNumber = ParseInt32(line);
        int columnNumber = ParseInt32(column);
        return CreateSourceDirective(
            autoIncrement,
            lineNumber,
            columnNumber,
            lineNumber,
            null,
            path);
    }

    internal SourceDirectiveValue CreateSourceColumnRange(
        bool autoIncrement,
        IToken line,
        IToken startColumn,
        IToken endColumn,
        IToken? path)
    {
        int lineNumber = ParseInt32(line);
        return CreateSourceDirective(
            autoIncrement,
            lineNumber,
            ParseInt32(startColumn),
            lineNumber,
            ParseInt32(endColumn),
            path);
    }

    internal SourceDirectiveValue CreateSourceLineRange(
        bool autoIncrement,
        IToken startLine,
        IToken endLine,
        IToken column,
        IToken? path)
    {
        int columnNumber = ParseInt32(column);
        return CreateSourceDirective(
            autoIncrement,
            ParseInt32(startLine),
            columnNumber,
            ParseInt32(endLine),
            null,
            path);
    }

    internal SourceDirectiveValue CreateSourceRange(
        bool autoIncrement,
        IToken startLine,
        IToken endLine,
        IToken startColumn,
        IToken endColumn,
        IToken? path)
        => CreateSourceDirective(
            autoIncrement,
            ParseInt32(startLine),
            ParseInt32(startColumn),
            ParseInt32(endLine),
            ParseInt32(endColumn),
            path);

    private static SourceDirectiveValue CreateSourceDirective(
        bool autoIncrement,
        int startLine,
        int startColumn,
        int endLine,
        int? endColumn,
        IToken? path,
        bool appliesToNextLine = false)
    {
        // Match Roslyn's column limit for compatibility with existing SRM readers' off-by-one check:
        // https://github.com/dotnet/roslyn/blob/e3c31f15b79e226656e3b082a921d4d60127b1fa/src/Compilers/Core/Portable/CodeGen/SequencePointList.cs#L182-L186
        const int DefaultEndColumn = ushort.MaxValue - 1;

        return new(
            autoIncrement,
            startLine,
            startColumn,
            endLine,
            endColumn ?? (startLine == SequencePoint.HiddenLine && endLine == SequencePoint.HiddenLine &&
                startColumn == 0 ? 0 : DefaultEndColumn),
            path is null ? null : StringHelpers.ParseQuotedString(path.Text))
        {
            AppliesToNextLine = appliesToNextLine,
        };
    }

    internal void EndSourceDirective(
        CILParser.ExtSourceSpecContext context,
        int initialSyntaxErrorCount)
    {
        context.HasSyntaxError =
            HasSyntaxErrorsSince(initialSyntaxErrorCount) ||
            context.exception is not null;
        if (context.HasSyntaxError ||
            context.Value is not { } value ||
            !CanApplySharedDirective(context))
        {
            context.Value = null;
            return;
        }

        ApplySourceDirective(value, context.Start);
    }

    /// <summary>
    /// Applies a <c>.line</c> or <c>#line</c> directive to the source it appears in: a non-empty file name defines
    /// that file as a PDB document and makes it the source's current document, and the directive's coordinates
    /// become the coordinates of the instructions that follow in that source. The directive records no sequence
    /// point itself; the next instruction does (<see cref="RecordSequencePoint"/>), wherever it is.
    /// </summary>
    /// <param name="value">The directive.</param>
    /// <param name="directiveToken">The directive's <c>.line</c> or <c>#line</c> token, which identifies its source.</param>
    /// <remarks>
    /// The directive stays in effect until the next directive or the end of its source (the input file, or one
    /// inclusion of an <c>#include</c>d file), wherever it is and across methods, and the next instruction always
    /// gets a point. An empty file name (<c>''</c> or <c>""</c>), which ildasm writes for "the same file", and
    /// a missing one keep the source's current document: the source itself until a directive in it names another
    /// file. With <c>#line</c>, both line endpoints advance by the number of physical source lines since the
    /// directive; columns do not change. The line-only form with a double-quoted file name anchors the specified
    /// line to the following source line; all other forms anchor it to the directive's own line. Hidden sequence
    /// points stay hidden until another directive, since their line is a marker rather than a source coordinate.
    /// </remarks>
    private void ApplySourceDirective(SourceDirectiveValue value, IToken directiveToken)
    {
        SourceLineState state = GetSourceLineState(directiveToken);
        if (!string.IsNullOrEmpty(value.DocumentPath))
        {
            state.Document = _pdbDocuments.GetOrAdd(value.DocumentPath, _currentLanguageGuid);
        }

        state.HasInvalidDirective = GeneratesPdb && !ValidateSourceDirective(value, directiveToken);
        state.DirectiveToken = directiveToken;
        state.DirectiveCoordinates = (value.StartLine, value.StartColumn, value.EndLine, value.EndColumn);
        state.AutoIncrementLine = value.AutoIncrement
            ? (long)directiveToken.Line + (value.AppliesToNextLine ? 1 : 0)
            : null;
        _lastSequencePointSpan = null;
        if (_currentMethod is not null)
        {
            _currentMethod.LastRejectedSequencePointSpan = null;
        }
    }

    private bool ValidateSourceDirective(SourceDirectiveValue value, IToken directiveToken)
        => ValidateSourceCoordinates(value.StartLine, value.StartColumn, value.EndLine, value.EndColumn, directiveToken);

    private bool ValidateSourceCoordinates(long startLine, int startColumn, long endLine, int endColumn, IToken directiveToken)
    {
        // Portable PDB, Sequence Points: docs/design/specs/PortablePdb-Metadata.md#sequence-points-blob.
        const int MaximumLine = 0x1fffffff;
        string? message = null;
        if (startLine == SequencePoint.HiddenLine || endLine == SequencePoint.HiddenLine)
        {
            if (startLine != SequencePoint.HiddenLine || endLine != SequencePoint.HiddenLine ||
                startColumn != 0 || endColumn != 0)
            {
                message = DiagnosticMessageTemplates.InvalidHiddenSequencePoint;
            }
        }
        else if ((ulong)startLine > MaximumLine || (ulong)endLine > MaximumLine)
        {
            message = DiagnosticMessageTemplates.SequencePointLineOutOfRange;
        }
        else if ((uint)startColumn > ushort.MaxValue || (uint)endColumn > ushort.MaxValue)
        {
            message = DiagnosticMessageTemplates.SequencePointColumnOutOfRange;
        }
        else if (endLine < startLine ||
            (endLine == startLine && endColumn <= startColumn))
        {
            message = DiagnosticMessageTemplates.SequencePointReversedRange;
        }

        if (message is not null)
        {
            ReportError(DiagnosticIds.InvalidSequencePoint, message, directiveToken);
            return false;
        }

        return true;
    }

    /// <summary>
    /// Records the sequence point of an instruction that is about to be emitted at the method body's current IL
    /// offset, when a PDB is requested (<see cref="GeneratesPdb"/>).
    /// </summary>
    /// <param name="method">The method the instruction is emitted into.</param>
    /// <param name="opcodeToken">The instruction's opcode token, which identifies its source and its line.</param>
    /// <remarks>
    /// <para>
    /// With no <c>.line</c> or <c>#line</c> directive in effect in the instruction's source, the point
    /// is on the instruction's own line of that source (the input <c>.il</c> file or the <c>#include</c>d
    /// file), columns 1 to 2; otherwise it has the coordinates of the directive in effect, in the source's
    /// current document (<see cref="ApplySourceDirective"/>).
    /// </para>
    /// <para>
    /// The instruction gets a point only when its span differs from the last point's, in any method, or a directive
    /// has been applied since. So a later method without a <c>.line</c> of its own gets no point while an earlier
    /// directive's span is still current, which is the shape ildasm writes for a method that had no sequence
    /// points, when it writes <c>.line</c> directives at all. The span includes the document, so an instruction in
    /// another document gets a point even on the lines and columns of the last point.
    /// </para>
    /// </remarks>
    private void RecordSequencePoint(CurrentMethodContext method, IToken opcodeToken)
    {
        if (!GeneratesPdb)
        {
            return;
        }

        SourceLineState state = GetSourceLineState(opcodeToken);
        if (state.HasInvalidDirective)
        {
            // An invalid range may not be representable in the sequence-points blob, even in error-tolerant mode.
            return;
        }

        int document = state.Document ??= _pdbDocuments.GetOrAdd(state.SourceName, _currentLanguageGuid);
        (int startLine, int startColumn, int endLine, int endColumn) =
            state.DirectiveCoordinates ?? (opcodeToken.Line, 1, opcodeToken.Line, 2);
        if (state.AutoIncrementLine is long directiveLine && startLine != SequencePoint.HiddenLine)
        {
            long delta = (long)opcodeToken.Line - directiveLine;
            long effectiveStartLine = startLine + delta;
            long effectiveEndLine = endLine + delta;
            Debug.Assert(state.DirectiveToken is not null);
            if (!ValidateSourceCoordinates(effectiveStartLine, startColumn, effectiveEndLine, endColumn, state.DirectiveToken))
            {
                state.HasInvalidDirective = true;
                return;
            }

            startLine = (int)effectiveStartLine;
            endLine = (int)effectiveEndLine;
        }

        var span = new SequencePointSpan(document, startLine, startColumn, endLine, endColumn);
        if (_lastSequencePointSpan == span || method.LastRejectedSequencePointSpan == span)
        {
            return;
        }

        int ilOffset = method.Definition.MethodBody.Offset;
        var point = new EntityRegistry.SequencePoint(document, ilOffset, startLine, startColumn, endLine, endColumn);
        List<EntityRegistry.SequencePoint> sequencePoints = method.Definition.DebugInfo.SequencePoints;
        if (!point.IsHidden)
        {
            // Start lines after the first non-hidden point use a signed compressed integer, across documents
            // and hidden points. Exclude a point at this offset, since it is about to be replaced.
            for (int i = sequencePoints.Count - 1; i >= 0; i--)
            {
                EntityRegistry.SequencePoint previous = sequencePoints[i];
                if (previous.IsHidden || previous.ILOffset == ilOffset)
                {
                    continue;
                }

                int delta = startLine - previous.StartLine;
                if (delta is < -0x10000000 or > 0x0fffffff)
                {
                    method.LastRejectedSequencePointSpan = span;
                    ReportError(
                        DiagnosticIds.InvalidSequencePoint,
                        DiagnosticMessageTemplates.SequencePointLineDeltaOutOfRange,
                        state.DirectiveToken ?? opcodeToken);
                    return;
                }

                break;
            }
        }

        Debug.Assert(sequencePoints.Count == 0 || sequencePoints[^1].ILOffset <= ilOffset, "IL offsets never decrease.");
        if (sequencePoints.Count > 0 && sequencePoints[^1].ILOffset == ilOffset)
        {
            // The last point's instruction wrote no bytes, so this instruction is the one at that offset. The blob
            // cannot hold two points at one offset: a zero offset delta is read as a document-record.
            sequencePoints[^1] = point;
        }
        else
        {
            sequencePoints.Add(point);
        }

        _lastSequencePointSpan = span;
        method.LastRejectedSequencePointSpan = null;
    }

    /// <summary>Gets the <c>.line</c> state of the source a token was read from, creating it on first use.</summary>
    private SourceLineState GetSourceLineState(IToken token)
    {
        // Every token the parser reads comes from a lexer: the input file's, an inclusion's, or, for the tokens of
        // a #define macro, the lexer of the token the macro replaces (PreprocessedTokenSource).
        ITokenSource source = token.TokenSource;
        Debug.Assert(source is not null, "Parsed tokens come from a lexer.");
        if (!_sourceLineStates.TryGetValue(source, out SourceLineState? state))
        {
            state = new SourceLineState(Location.GetSourceName(token));
            _sourceLineStates.Add(source, state);
        }

        return state;
    }

    /// <summary>
    /// The <c>.line</c> state of one source of the input file being parsed: the input file itself, or one inclusion
    /// of an <c>#include</c>d file. A directive applies only to the rest of its source: an included file starts
    /// without one, and does not change the state of the source that includes it.
    /// </summary>
    /// <param name="sourceName">The source's <see cref="SourceText.Path"/>.</param>
    private sealed class SourceLineState(string sourceName)
    {
        /// <summary>Gets the source's name, which is the name of its own PDB document.</summary>
        public string SourceName { get; } = sourceName;

        /// <summary>
        /// Gets or sets the index in the compilation's <see cref="PdbDocumentTable"/> of the source's current document:
        /// the file named by the last directive of the source that named one, otherwise the source itself. It is
        /// <see langword="null"/> while it is the source itself and no point has needed it, so that an included
        /// file becomes a document only when one of its instructions gets a point.
        /// </summary>
        public int? Document { get; set; }

        /// <summary>
        /// Gets or sets the start line, start column, end line and end column of the last directive applied in the
        /// source, or <see langword="null"/> when none has been.
        /// </summary>
        public (int StartLine, int StartColumn, int EndLine, int EndColumn)? DirectiveCoordinates { get; set; }

        public bool HasInvalidDirective { get; set; }

        public long? AutoIncrementLine { get; set; }

        public IToken? DirectiveToken { get; set; }
    }

    /// <summary>The document and source span of a recorded sequence point.</summary>
    private readonly record struct SequencePointSpan(int Document, int StartLine, int StartColumn, int EndLine, int EndColumn);

#pragma warning disable CA1822 // Parser actions are invoked through the per-parser GrammarActions instance.
    internal string ParseLanguageString(IToken token)
        => StringHelpers.ParseQuotedString(token.Text);

    internal LanguageDirectiveValue CreateLanguageDirective(string language)
        => new LanguageDirectiveValue(language, null, null);

    internal LanguageDirectiveValue CreateLanguageDirective(string language, string vendor)
        => new LanguageDirectiveValue(language, vendor, null);

    internal LanguageDirectiveValue CreateLanguageDirective(
        string language,
        string vendor,
        string documentType)
        => new LanguageDirectiveValue(language, vendor, documentType);
#pragma warning restore CA1822

    internal void EndLanguageDirective(
        CILParser.LanguageDeclContext context,
        int initialSyntaxErrorCount)
    {
        context.HasSyntaxError =
            HasSyntaxErrorsSince(initialSyntaxErrorCount) ||
            context.exception is not null;
        if (context.HasSyntaxError ||
            context.Value is not { } value ||
            !CanApplySharedDirective(context))
        {
            context.Value = null;
            return;
        }

        if (Guid.TryParse(value.Language, out Guid language))
        {
            _currentLanguageGuid = language;
        }
        if (value.Vendor is not null && Guid.TryParse(value.Vendor, out Guid vendor))
        {
            _currentLanguageVendorGuid = vendor;
        }
        if (value.DocumentType is not null &&
            Guid.TryParse(value.DocumentType, out Guid documentType))
        {
            _currentDocumentTypeGuid = documentType;
        }
    }

    private bool CanApplySharedDirective(ParserRuleContext context)
        => !IsDeclarationSuppressed &&
            (context.Parent is not CILParser.MethodDeclContext || _currentMethod is not null);

}