File: Actions\GrammarActions.BuildImage.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.Buffers.Binary;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using Antlr4.Runtime;
using Antlr4.Runtime.Misc;

namespace ILAssembler
{
    internal sealed partial class GrammarActions
    {
        public (ImmutableArray<Diagnostic> Diagnostics, CompilationResult? Image) BuildImage()
        {
            // Default module name to output filename if no .module directive was provided
            if (_entityRegistry.Module.Name is null && _options.OutputFileName is not null)
            {
                _entityRegistry.Module.Name = _options.OutputFileName;
            }

            // Apply DebuggableAttribute AFTER all source declarations have been processed,
            // so that GetCoreLibAssemblyReference() can find the correct corelib assembly ref
            // declared in the source (e.g., System.Runtime) instead of creating a fallback mscorlib.
            if (_options.Debug || _options.DebugMode is not null)
            {
                ApplyDebuggableAttribute();
            }

            Machine requestedMachine = _options.Machine ?? Machine.I386;
            Machine machine = requestedMachine == Machine.Unknown ? Machine.I386 : requestedMachine;
            if (!(machine is Machine.I386 or Machine.Amd64 or Machine.Arm64))
            {
                throw new NotSupportedException($"Target machine '{machine}' is not supported.");
            }

            ImmutableArray<ValidatedVTableFixup> validatedVTableFixups =
                ValidateVTableFixups(machine);
            ImmutableArray<ValidatedVTableAssociation> validatedVTableAssociations =
                ValidateVTableAssociations(validatedVTableFixups);
            ImmutableArray<ILAssemblerPEBuilder.DataLabelFixup> validatedDataLabelFixups =
                ValidateDataLabelFixups();
            ImmutableArray<ValidatedExport> validatedExports =
                ValidateExports(validatedVTableAssociations, machine);
            if (_options.PseudoAttributes)
            {
                PseudoCustomAttributes.Lower(_entityRegistry, _diagnostics);
            }

            // Return early if there are structural errors that prevent building valid metadata.
            // However, allow errors in method bodies (ILA0016-0019) to pass through so we can
            // emit the assembly with the errors reported.
            // In error-tolerant mode, continue despite errors.
            var structuralErrors = _diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error && !IsRecoverableError(d.Id));
            if (structuralErrors.Any() && !_options.ErrorTolerant)
            {
                return (_diagnostics.ToImmutable(), null);
            }

            BlobBuilder ilStream = new();
            Blob mvidFixup = _entityRegistry.WriteContentTo(_metadataBuilder, ilStream, _mappedFieldDataNames, out ImmutableArray<DataLocation> mappedFields, _options.Deterministic, _options.Fold);
            // MetadataRootBuilder only supports module-wide validation suppression, which is
            // required because wrapped GenericParam numbers intentionally violate table ordering.
            bool suppressMetadataValidation =
                _options.ErrorTolerant &&
                _diagnostics.Any(diagnostic => diagnostic.Id == DiagnosticIds.TooManyGenericParameters);
            MetadataRootBuilder rootBuilder = new(
                _metadataBuilder,
                _options.MetadataVersion,
                suppressValidation: suppressMetadataValidation);

            // Apply command-line overrides
            Subsystem subsystem = _options.Subsystem ?? _subsystem;
            int fileAlignment = _options.FileAlignment ?? _alignment;
            long imageBase = _options.ImageBase ?? _imageBase;
            ushort majorSubsystemVersion = _options.SubsystemVersion?.Major ?? 4;
            ushort minorSubsystemVersion = _options.SubsystemVersion?.Minor ?? 0;

            // Build DllCharacteristics from options
            DllCharacteristics dllCharacteristics = DllCharacteristics.DynamicBase | DllCharacteristics.NxCompatible | DllCharacteristics.NoSeh | DllCharacteristics.TerminalServerAware;
            if (_options.AppContainer)
            {
                dllCharacteristics |= DllCharacteristics.AppContainer;
            }
            if (_options.HighEntropyVA)
            {
                dllCharacteristics |= DllCharacteristics.HighEntropyVirtualAddressSpace;
            }
            if (_options.StripReloc)
            {
                dllCharacteristics &= ~DllCharacteristics.DynamicBase;
            }

            Characteristics imageCharacteristics = Characteristics.ExecutableImage;
            if (_options.Dll)
            {
                imageCharacteristics |= Characteristics.Dll;
            }
            if (machine == Machine.I386)
            {
                imageCharacteristics |= Characteristics.Bit32Machine;
            }
            else if (machine is Machine.Amd64 or Machine.Arm64)
            {
                imageCharacteristics |= Characteristics.LargeAddressAware;
            }

            // Compute stack reserve: command-line option overrides directive, which overrides default
            ulong sizeOfStackReserve = (ulong)(_options.StackReserve ?? (_stackReserve != 0 ? _stackReserve : 0x00100000));

            PEHeaderBuilder header = new(
                machine: machine,
                fileAlignment: fileAlignment,
                imageBase: (ulong)imageBase,
                subsystem: subsystem,
                majorSubsystemVersion: majorSubsystemVersion,
                minorSubsystemVersion: minorSubsystemVersion,
                dllCharacteristics: dllCharacteristics,
                imageCharacteristics: imageCharacteristics,
                sizeOfStackReserve: sizeOfStackReserve);

            MethodDefinitionHandle entryPoint = default;
            if (_entityRegistry.EntryPoint is not null)
            {
                entryPoint = (MethodDefinitionHandle)_entityRegistry.EntryPoint.Handle;
            }

            // Build the PDB and the debug directory that references it, if a PDB is requested
            DebugDirectoryBuilder? debugDirectoryBuilder = BuildDebugDirectory(entryPoint, out ImmutableArray<byte>? portablePdb);

            Func<IEnumerable<Blob>, BlobContentId>? deterministicIdProvider = _options.Deterministic
                ? GetDeterministicContentId
                : null;

            ImmutableArray<ILAssemblerPEBuilder.VTableFixupInfo> vtableFixupInfos =
                BuildVTableFixupInfos(
                    validatedVTableFixups,
                    validatedVTableAssociations);
            ImmutableArray<ILAssemblerPEBuilder.ExportInfo> exports =
                BuildExportInfos(validatedExports);

            // Apply CorFlags from options or directive
            CorFlags corFlags = _options.CorFlags ?? _corflags;
            if (_options.Prefer32Bit)
            {
                corFlags |= CorFlags.Requires32Bit | CorFlags.Prefers32Bit;
            }

            ILAssemblerPEBuilder peBuilder = new(
                header,
                rootBuilder,
                ilStream,
                _mappedFieldData,
                _manifestResources,
                debugDirectoryBuilder: debugDirectoryBuilder,
                entryPoint: entryPoint,
                flags: corFlags,
                deterministicIdProvider: deterministicIdProvider,
                vtableFixups: vtableFixupInfos,
                exports: exports,
                dataLabelFixups: validatedDataLabelFixups,
                tlsData: _tlsData,
                cilData: _cilData,
                mappedFields: mappedFields);

            return (_diagnostics.ToImmutable(), new CompilationResult(peBuilder, mvidFixup, portablePdb));
        }

        private static BlobContentId GetDeterministicContentId(IEnumerable<Blob> content)
            => BlobContentId.FromHash(ComputeSha256(content));

        private static byte[] ComputeSha256(IEnumerable<Blob> content)
        {
            using IncrementalHash hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
            foreach (Blob blob in content)
            {
                hash.AppendData(blob.GetBytes());
            }

            return hash.GetHashAndReset();
        }

        private ImmutableArray<ValidatedVTableFixup> ValidateVTableFixups(Machine machine)
        {
            if (_vtableFixups.Count == 0)
            {
                return ImmutableArray<ValidatedVTableFixup>.Empty;
            }

            var builder = ImmutableArray.CreateBuilder<ValidatedVTableFixup>(_vtableFixups.Count);

            for (int i = 0; i < _vtableFixups.Count; i++)
            {
                VTableFixupDeclaration declaration = _vtableFixups[i];
                if (!declaration.HasValidSlotCount)
                {
                    continue;
                }

                VTableFixupSupport.VTableFixupEntry entry = declaration.Entry;
                const ushort WidthMask =
                    VTableFixupSupport.COR_VTABLE_32BIT |
                    VTableFixupSupport.COR_VTABLE_64BIT;
                ushort width = (ushort)(entry.Flags & WidthMask);
                ushort expectedWidth = VTableFixupSupport.GetPointerSize(machine) == sizeof(long)
                    ? VTableFixupSupport.COR_VTABLE_64BIT
                    : VTableFixupSupport.COR_VTABLE_32BIT;
                if (width != expectedWidth)
                {
                    ReportError(
                        DiagnosticIds.InvalidVTableWidth,
                        string.Format(
                            DiagnosticMessageTemplates.InvalidVTableWidth,
                            width,
                            machine,
                            expectedWidth == VTableFixupSupport.COR_VTABLE_64BIT
                                ? "int64"
                                : "int32"),
                        declaration.Context);
                    continue;
                }

                if (!_mappedFieldDataNames.TryGetValue(entry.DataLabel, out DataLocation dataLocation))
                {
                    ReportError(
                        DiagnosticIds.LabelNotFound,
                        string.Format(DiagnosticMessageTemplates.LabelNotFound, entry.DataLabel),
                        declaration.Context);
                    continue;
                }

                int availableBytes = GetAvailableMappedFieldDataBytes(dataLocation);
                int requiredBytes = checked(
                    entry.SlotCount * VTableFixupSupport.GetSlotSize(entry.Flags));
                if (requiredBytes > availableBytes)
                {
                    ReportError(
                        DiagnosticIds.InsufficientVTableData,
                        string.Format(
                            DiagnosticMessageTemplates.InsufficientVTableData,
                            entry.DataLabel,
                            availableBytes,
                            requiredBytes),
                        declaration.Context);
                    continue;
                }

                builder.Add(new ValidatedVTableFixup(i + 1, entry, dataLocation));
            }

            return builder.ToImmutable();
        }

        private int GetAvailableMappedFieldDataBytes(DataLocation location)
        {
            int endOffset = GetDataBuilder(location.Section).Count;
            foreach (DataLocation other in _mappedFieldDataNames.Values)
            {
                if (other.Section == location.Section &&
                    other.Offset > location.Offset && other.Offset < endOffset)
                {
                    endOffset = other.Offset;
                }
            }

            return endOffset - location.Offset;
        }

        private ImmutableArray<ILAssemblerPEBuilder.DataLabelFixup> ValidateDataLabelFixups()
        {
            var builder =
                ImmutableArray.CreateBuilder<ILAssemblerPEBuilder.DataLabelFixup>(
                    _mappedFieldDataReferenceFixups.Count);

            foreach (CILParser.DataLabelReferenceValue reference in _mappedFieldDataReferenceFixups)
            {
                if (!_mappedFieldDataNames.TryGetValue(reference.TargetLabel, out DataLocation target))
                {
                    ReportError(
                        DiagnosticIds.LabelNotFound,
                        string.Format(
                            DiagnosticMessageTemplates.LabelNotFound,
                            reference.TargetLabel),
                        reference.Location);
                    continue;
                }

                builder.Add(new ILAssemblerPEBuilder.DataLabelFixup(
                    new DataLocation(reference.Section, reference.DataOffset),
                    target,
                    reference.PointerSize));
            }

            return builder.ToImmutable();
        }

        private ImmutableArray<ValidatedVTableAssociation> ValidateVTableAssociations(
            ImmutableArray<ValidatedVTableFixup> validatedVTableFixups)
        {
            if (_vtableFixups.Count == 0)
            {
                return ImmutableArray<ValidatedVTableAssociation>.Empty;
            }

            var fixupsByOriginalIndex = new Dictionary<int, (int SerializedIndex, ValidatedVTableFixup Fixup)>(
                validatedVTableFixups.Length);
            for (int i = 0; i < validatedVTableFixups.Length; i++)
            {
                fixupsByOriginalIndex.Add(
                    validatedVTableFixups[i].OriginalIndex,
                    (i + 1, validatedVTableFixups[i]));
            }

            var builder = ImmutableArray.CreateBuilder<ValidatedVTableAssociation>();
            foreach (EntityRegistry.MethodDefinitionEntity method in GetParsedMethods())
            {
                if (!_vtableEntryDirectiveContexts.TryGetValue(method, out ParserRuleContext? context))
                {
                    continue;
                }

                if (!fixupsByOriginalIndex.TryGetValue(
                    method.VTableEntry,
                    out (int SerializedIndex, ValidatedVTableFixup Fixup) fixup))
                {
                    ReportError(
                        DiagnosticIds.InvalidVTableEntry,
                        string.Format(
                            DiagnosticMessageTemplates.InvalidVTableEntry,
                            method.Name,
                            method.VTableEntry),
                        context);
                    continue;
                }

                if (method.VTableSlot <= 0 || method.VTableSlot > fixup.Fixup.Entry.SlotCount)
                {
                    ReportError(
                        DiagnosticIds.InvalidVTableEntry,
                        string.Format(
                            DiagnosticMessageTemplates.InvalidVTableSlot,
                            method.Name,
                            method.VTableSlot,
                            method.VTableEntry,
                            fixup.Fixup.Entry.SlotCount),
                        context);
                    continue;
                }

                builder.Add(new ValidatedVTableAssociation(method, fixup.SerializedIndex));
            }

            return builder.ToImmutable();
        }

        private ImmutableArray<ValidatedExport> ValidateExports(
            ImmutableArray<ValidatedVTableAssociation> validatedVTableAssociations,
            Machine machine)
        {
            if (_vtableFixups.Count == 0)
            {
                return ImmutableArray<ValidatedExport>.Empty;
            }

            var associationsByMethod =
                new Dictionary<EntityRegistry.MethodDefinitionEntity, ValidatedVTableAssociation>(
                    validatedVTableAssociations.Length);
            foreach (ValidatedVTableAssociation association in validatedVTableAssociations)
            {
                associationsByMethod.Add(association.Method, association);
            }

            var candidates = ImmutableArray.CreateBuilder<ValidatedExport>();
            foreach (EntityRegistry.MethodDefinitionEntity method in GetParsedMethods())
            {
                if (method.ExportOrdinal < 0)
                {
                    continue;
                }

                ParserRuleContext context = _exportDirectiveContexts[method];
                string exportName = method.ExportAlias ?? method.Name;
                if (!ILAssemblerPEBuilder.IsExportMachineSupported(machine))
                {
                    ReportError(
                        DiagnosticIds.UnsupportedNativeExportMachine,
                        string.Format(
                            DiagnosticMessageTemplates.UnsupportedNativeExportMachine,
                            machine),
                        context);
                    continue;
                }

                if (!associationsByMethod.TryGetValue(
                    method,
                    out ValidatedVTableAssociation association))
                {
                    if (!_vtableEntryDirectiveContexts.ContainsKey(method))
                    {
                        ReportError(
                            DiagnosticIds.InvalidVTableExport,
                            string.Format(
                                DiagnosticMessageTemplates.InvalidVTableExport,
                                exportName),
                            context);
                    }

                    continue;
                }

                candidates.Add(new ValidatedExport(method, association.VTableEntryIndex));
            }

            if (candidates.Count == 0)
            {
                return ImmutableArray<ValidatedExport>.Empty;
            }

            var exportsByOrdinal = new Dictionary<int, ValidatedExport>();
            var nonConflictingExports = ImmutableArray.CreateBuilder<ValidatedExport>(candidates.Count);
            foreach (ValidatedExport export in candidates)
            {
                if (exportsByOrdinal.TryGetValue(
                    export.Method.ExportOrdinal,
                    out ValidatedExport existingExport) &&
                    (existingExport.VTableEntryIndex != export.VTableEntryIndex ||
                     existingExport.Method.VTableSlot != export.Method.VTableSlot))
                {
                    ReportError(
                        DiagnosticIds.DuplicateExportOrdinal,
                        string.Format(
                            DiagnosticMessageTemplates.DuplicateExportOrdinal,
                            export.Method.ExportAlias ?? export.Method.Name,
                            export.Method.ExportOrdinal),
                        _exportDirectiveContexts[export.Method]);
                    continue;
                }

                exportsByOrdinal.TryAdd(export.Method.ExportOrdinal, export);
                nonConflictingExports.Add(export);
            }

            int baseOrdinal = nonConflictingExports.Min(export => export.Method.ExportOrdinal);
            var validatedExports =
                ImmutableArray.CreateBuilder<ValidatedExport>(nonConflictingExports.Count);
            foreach (ValidatedExport export in nonConflictingExports)
            {
                long ordinalIndex = (long)export.Method.ExportOrdinal - baseOrdinal;
                if (ordinalIndex > ushort.MaxValue)
                {
                    ReportError(
                        DiagnosticIds.ExportOrdinalRangeTooLarge,
                        string.Format(
                            DiagnosticMessageTemplates.ExportOrdinalRangeTooLarge,
                            export.Method.ExportOrdinal,
                            baseOrdinal,
                            ushort.MaxValue),
                        _exportDirectiveContexts[export.Method]);
                    continue;
                }

                validatedExports.Add(export);
            }

            return validatedExports.ToImmutable();
        }

        private static ImmutableArray<ILAssemblerPEBuilder.VTableFixupInfo> BuildVTableFixupInfos(
            ImmutableArray<ValidatedVTableFixup> validatedVTableFixups,
            ImmutableArray<ValidatedVTableAssociation> validatedVTableAssociations)
        {
            var builder =
                ImmutableArray.CreateBuilder<ILAssemblerPEBuilder.VTableFixupInfo>(
                    validatedVTableFixups.Length);

            for (int i = 0; i < validatedVTableFixups.Length; i++)
            {
                ValidatedVTableFixup fixup = validatedVTableFixups[i];
                var methodTokens = ImmutableArray.CreateBuilder<int>(fixup.Entry.SlotCount);
                methodTokens.Count = fixup.Entry.SlotCount;

                foreach (ValidatedVTableAssociation association in validatedVTableAssociations)
                {
                    if (association.VTableEntryIndex == i + 1)
                    {
                        methodTokens[association.Method.VTableSlot - 1] =
                            MetadataTokens.GetToken(association.Method.Handle);
                    }
                }

                builder.Add(new ILAssemblerPEBuilder.VTableFixupInfo(
                    fixup.DataLocation,
                    fixup.Entry.SlotCount,
                    fixup.Entry.Flags,
                    methodTokens.MoveToImmutable()));
            }

            return builder.MoveToImmutable();
        }

        private static ImmutableArray<ILAssemblerPEBuilder.ExportInfo> BuildExportInfos(
            ImmutableArray<ValidatedExport> validatedExports)
        {
            var builder =
                ImmutableArray.CreateBuilder<ILAssemblerPEBuilder.ExportInfo>(
                    validatedExports.Length);

            foreach (ValidatedExport export in validatedExports)
            {
                builder.Add(new ILAssemblerPEBuilder.ExportInfo(
                    export.Method.ExportOrdinal,
                    export.Method.ExportAlias ?? export.Method.Name,
                    export.VTableEntryIndex,
                    export.Method.VTableSlot));
            }

            return builder.MoveToImmutable();
        }

        private IEnumerable<EntityRegistry.MethodDefinitionEntity> GetParsedMethods()
        {
            foreach (EntityRegistry.TypeDefinitionEntity type in _entityRegistry.GetSeenEntities(TableIndex.TypeDef))
            {
                foreach (EntityRegistry.MethodDefinitionEntity method in type.Methods)
                {
                    yield return method;
                }
            }
        }

        /// <summary>
        /// Gets whether a PDB is requested: <see cref="Options.Debug"/>, <see cref="Options.DebugMode"/> or
        /// <see cref="Options.Pdb"/> is set. Information that only the PDB uses, such as the lexical scopes of
        /// method bodies, is kept only when this is <see langword="true"/>.
        /// </summary>
        private bool GeneratesPdb => _options.Debug || _options.DebugMode is not null || _options.Pdb;

        /// <summary>
        /// Builds the Portable PDB and the debug directory that references it, when a PDB is requested.
        /// </summary>
        /// <remarks>
        /// <para>
        /// A PDB is produced when <see cref="Options.Debug"/>, <see cref="Options.DebugMode"/> or
        /// <see cref="Options.Pdb"/> is set; <c>.line</c> directives alone do not produce one. Otherwise this
        /// returns <see langword="null"/>, the image has no debug directory (<see cref="ILAssemblerPEBuilder"/>),
        /// and <paramref name="portablePdb"/> is <see langword="null"/>.
        /// </para>
        /// <para>
        /// The PDB is returned in <paramref name="portablePdb"/> for the caller to write as a separate file;
        /// it is not embedded in the image. The debug directory has these entries, in this order:
        /// a CodeView entry that names the PDB file (<see cref="GetPdbFilePath"/>) and carries the PDB id;
        /// a PdbChecksum entry holding the SHA-256 hash of the PDB with its 20-byte id zeroed
        /// (the PDB Checksum Debug Directory Entry in docs/design/specs/PE-COFF.md); and, with
        /// <see cref="Options.Deterministic"/>, a Reproducible entry.
        /// </para>
        /// <para>
        /// With <see cref="Options.Deterministic"/>, the PDB id is derived from that same hash
        /// (<see cref="BlobContentId.FromHash(ImmutableArray{byte})"/>), so the same input gives the same PDB
        /// bytes. The PDB bytes do not depend on <see cref="Options.PdbFilePath"/>; the image does, because
        /// the CodeView entry records that path as given (after <see cref="Options.PathMap"/>), so the same input
        /// and the same recorded PDB path give the same image bytes, and a PDB path that is a file name alone
        /// keeps the image independent of its directory. Without <see cref="Options.Deterministic"/>, the id's
        /// GUID is random and its stamp is the current Unix time in seconds. The checksum is the
        /// content hash in both cases.
        /// </para>
        /// </remarks>
        private DebugDirectoryBuilder? BuildDebugDirectory(MethodDefinitionHandle entryPoint, out ImmutableArray<byte>? portablePdb)
        {
            portablePdb = null;

            // Only /DEBUG (any mode) or /PDB produces a PDB. Without them, .line
            // directives are still parsed, but their PDB coordinates are not validated or recorded.
            if (!GeneratesPdb)
            {
                return null;
            }

            // Build PDB metadata
            BuildPdbMetadata();

            // Get row counts from main metadata for the portable PDB
            var typeSystemRowCounts = _metadataBuilder.GetRowCounts();

            // PortablePdbBuilder hands the id provider the serialized PDB with its 20-byte id zeroed.
            // The SHA-256 of that content is the PdbChecksum value, and the deterministic id.
            byte[]? pdbContentHash = null;
            Func<IEnumerable<Blob>, BlobContentId> timeBasedIdProvider = BlobContentId.GetTimeBasedProvider();
            BlobContentId PdbIdProvider(IEnumerable<Blob> content)
            {
                pdbContentHash = ComputeSha256(content);
                return _options.Deterministic
                    ? BlobContentId.FromHash(pdbContentHash)
                    : timeBasedIdProvider(content);
            }

            // Create the portable PDB
            var pdbBuilder = new PortablePdbBuilder(
                _pdbBuilder,
                typeSystemRowCounts,
                entryPoint,
                idProvider: PdbIdProvider);

            var pdbBlob = new BlobBuilder();
            var pdbContentId = pdbBuilder.Serialize(pdbBlob);
            byte[] pdbChecksum = pdbContentHash
                ?? throw new InvalidOperationException("The Portable PDB was serialized without calling its id provider.");

            // Reference the PDB file from the image: CodeView, then PdbChecksum, then (deterministic only)
            // Reproducible, as the C# compiler does.
            var debugDirectoryBuilder = new DebugDirectoryBuilder();
            debugDirectoryBuilder.AddCodeViewEntry(
                GetPdbFilePath(),
                pdbContentId,
                pdbBuilder.FormatVersion);
            debugDirectoryBuilder.AddPdbChecksumEntry(
                "SHA256",
                ImmutableArray.Create(pdbChecksum));
            if (_options.Deterministic)
            {
                debugDirectoryBuilder.AddReproducibleEntry();
            }

            portablePdb = pdbBlob.ToImmutableArray();
            return debugDirectoryBuilder;
        }

        /// <summary>
        /// Gets the PDB path recorded in the CodeView entry: <see cref="Options.PdbFilePath"/> mapped by
        /// <see cref="Options.PathMap"/> when set, otherwise <see cref="Options.OutputFileName"/> with its extension
        /// replaced by <c>.pdb</c>, otherwise <c>assembly.pdb</c>.
        /// </summary>
        /// <remarks>
        /// The two fallback names have no directory for the map to replace, so they are returned as they are.
        /// </remarks>
        private string GetPdbFilePath()
        {
            if (_options.PdbFilePath is not null)
            {
                return _options.PathMap.Map(_options.PdbFilePath);
            }

            return _options.OutputFileName is not null
                ? Path.ChangeExtension(_options.OutputFileName, ".pdb")
                : "assembly.pdb";
        }

        /// <summary>
        /// Adds the Document rows, one MethodDebugInformation row per MethodDef row, and each method's LocalScope and
        /// LocalVariable rows (<see cref="AddLocalScopes"/>) to the PDB metadata.
        /// </summary>
        /// <remarks>
        /// The documents are added in <see cref="PdbDocumentTable"/> order. A method without sequence points, or
        /// without an IL body (<see cref="EntityRegistry.MethodDefinitionEntity.HasBody"/>), gets a row with a nil
        /// document and no sequence points blob; a <c>.line</c> directive in a method without a body still defines
        /// its file and makes it the current document. A method whose sequence points all belong to one
        /// document names that document in its row; a method whose points span several documents has a nil
        /// document in its row, and its blob names the documents (<see cref="EncodeSequencePoints"/>).
        /// </remarks>
        private void BuildPdbMetadata()
        {
            IReadOnlyList<PdbDocument> documents = _pdbDocuments.Documents;
            var documentHandles = new DocumentHandle[documents.Count];
            for (int i = 0; i < documents.Count; i++)
            {
                PdbDocument document = documents[i];
                documentHandles[i] = _pdbBuilder.AddDocument(
                    _pdbBuilder.GetOrAddDocumentName(document.Name),
                    hashAlgorithm: default,
                    hash: default,
                    language: document.Language != Guid.Empty ? _pdbBuilder.GetOrAddGuid(document.Language) : default);
            }

            foreach (var entity in _entityRegistry.GetSeenEntities(TableIndex.MethodDef))
            {
                if (entity is not EntityRegistry.MethodDefinitionEntity method)
                {
                    continue;
                }

                List<EntityRegistry.SequencePoint> sequencePoints = method.DebugInfo.SequencePoints;
                if (sequencePoints.Count == 0 || !method.HasBody)
                {
                    // Points are recorded as instructions are emitted, so a method without an IL body has none
                    // to map; a .line directive inside it applies to the next instruction, in a later method.
                    _pdbBuilder.AddMethodDebugInformation(default, default);
                }
                else
                {
                    int firstDocument = sequencePoints[0].DocumentIndex;
                    bool singleDocument = true;
                    for (int i = 1; i < sequencePoints.Count && singleDocument; i++)
                    {
                        singleDocument = sequencePoints[i].DocumentIndex == firstDocument;
                    }

                    BlobBuilder sequencePointsBlob = EncodeSequencePoints(
                        sequencePoints,
                        method.DebugInfo.LocalSignature,
                        documentHandles,
                        singleDocument);
                    // docs/design/specs/PortablePdb-Metadata.md, MethodDebugInformation table: Document is "the row id of the
                    // single document containing all sequence points of the method, or 0 if the method doesn't have sequence
                    // points or spans multiple documents", and "_InitialDocument_ is only present if the _Document_ field of
                    // the _MethodDebugInformation_ table is nil"; the blob then names the documents (EncodeSequencePoints).
                    _pdbBuilder.AddMethodDebugInformation(
                        singleDocument ? documentHandles[firstDocument] : default,
                        _pdbBuilder.GetOrAddBlob(sequencePointsBlob));
                }

                AddLocalScopes((MethodDefinitionHandle)method.Handle, method.DebugInfo.LocalScopes);
            }
        }

        /// <summary>
        /// Adds a method's LocalScope rows, each followed by its LocalVariable rows, to the PDB metadata
        /// (docs/design/specs/PortablePdb-Metadata.md, "LocalScope Table" and "LocalVariable Table").
        /// </summary>
        /// <param name="method">The method's MethodDef row. Methods must be added in MethodDef order.</param>
        /// <param name="scopes">The method's lexical scopes (<see cref="EntityRegistry.MethodDebugInfo.LocalScopes"/>).</param>
        /// <remarks>
        /// <para>
        /// A scope gets a row only when it declares a named local, and an unnamed local gets no
        /// row; so the root scope, which spans the whole body, has a row only when the method-level <c>.locals</c>
        /// name a local. Each variable's Index is its slot in the local signature, and its attributes are 0.
        /// Import scopes and local constants are not recorded.
        /// </para>
        /// <para>
        /// The rows are written as declared: each scope that declares a named local gets a
        /// LocalScope row whatever its length, and each named local of the scope gets a LocalVariable row, in
        /// declaration order. Rows that the specification does not allow are reported as errors, so they are
        /// written only to error-tolerant output (<see cref="Options.ErrorTolerant"/>):
        /// </para>
        /// <list type="bullet">
        /// <item><description>
        /// A scope without IL (a block with no instructions, or the root scope of a method without instructions)
        /// has length 0, but a LocalScope row's Length is in "(0..0x80000000)". It is reported at the scope's first
        /// named local (<see cref="DiagnosticIds.LocalScopeWithoutInstructions"/>).
        /// </description></item>
        /// <item><description>
        /// "There shall be no duplicate rows in the LocalVariable table, based upon owner and _Name_", and none
        /// "based upon owner and _Index_". A named local whose name an earlier named local of the scope has is
        /// reported at its declaration (<see cref="DiagnosticIds.LocalNameDeclaredTwiceInScope"/>), and so is one
        /// whose slot an earlier named local of the scope has
        /// (<see cref="DiagnosticIds.LocalSlotNamedTwiceInScope"/>); a local that repeats both is reported twice.
        /// Unnamed locals have no row, so they take no part.
        /// </description></item>
        /// </list>
        /// <para>
        /// The scopes are recorded only when a PDB is requested, so neither is checked without one.
        /// </para>
        /// <para>
        /// The rows are sorted by start offset, then by length with the longest first, as the table requires;
        /// scopes with the same range keep their source order, enclosing scope first. Scopes of a method nest or
        /// are disjoint because blocks do, and the variables of a scope are added together right after it, so its
        /// VariableList run holds exactly its variables.
        /// </para>
        /// </remarks>
        private void AddLocalScopes(MethodDefinitionHandle method, List<EntityRegistry.LocalScopeRecord> scopes)
        {
            // Most methods name no local; they get no rows and need no sorting.
            if (!scopes.Exists(static scope => !scope.Variables.IsEmpty))
            {
                return;
            }

            IEnumerable<EntityRegistry.LocalScopeRecord> ordered = scopes
                .Where(scope => !scope.Variables.IsEmpty)
                .OrderBy(scope => scope.StartOffset)
                .ThenByDescending(scope => scope.Length)
                .ThenBy(scope => scope.Order);
            var names = new HashSet<string>(StringComparer.Ordinal);
            var slotNames = new Dictionary<int, string>();
            foreach (EntityRegistry.LocalScopeRecord scope in ordered)
            {
                if (scope.Length == 0)
                {
                    EntityRegistry.LocalVariableRecord first = scope.Variables[0];
                    ReportError(
                        DiagnosticIds.LocalScopeWithoutInstructions,
                        string.Format(DiagnosticMessageTemplates.LocalScopeWithoutInstructions, first.Name),
                        first.Location);
                }

                _pdbBuilder.AddLocalScope(
                    method,
                    importScope: default,
                    variableList: MetadataTokens.LocalVariableHandle(_pdbBuilder.GetRowCount(TableIndex.LocalVariable) + 1),
                    constantList: default,
                    scope.StartOffset,
                    scope.Length);
                names.Clear();
                slotNames.Clear();
                foreach (EntityRegistry.LocalVariableRecord variable in scope.Variables)
                {
                    if (!names.Add(variable.Name))
                    {
                        ReportError(
                            DiagnosticIds.LocalNameDeclaredTwiceInScope,
                            string.Format(DiagnosticMessageTemplates.LocalNameDeclaredTwiceInScope, variable.Name),
                            variable.Location);
                    }

                    if (!slotNames.TryAdd(variable.Slot, variable.Name))
                    {
                        ReportError(
                            DiagnosticIds.LocalSlotNamedTwiceInScope,
                            string.Format(
                                DiagnosticMessageTemplates.LocalSlotNamedTwiceInScope,
                                variable.Name,
                                variable.Slot,
                                slotNames[variable.Slot]),
                            variable.Location);
                    }

                    _pdbBuilder.AddLocalVariable(
                        LocalVariableAttributes.None,
                        variable.Slot,
                        _pdbBuilder.GetOrAddString(variable.Name));
                }
            }
        }

        /// <summary>
        /// Encodes a method's sequence points as a sequence points blob (docs/design/specs/PortablePdb-Metadata.md,
        /// "Sequence Points Blob").
        /// </summary>
        /// <param name="sequencePoints">The method's sequence points, in increasing IL offset order. Not empty.</param>
        /// <param name="localSignature">
        /// The local signature the method body references (<see cref="EntityRegistry.MethodDebugInfo.LocalSignature"/>),
        /// or a nil handle when it has none.
        /// </param>
        /// <param name="documentHandles">The Document rows, indexed by <see cref="EntityRegistry.SequencePoint.DocumentIndex"/>.</param>
        /// <param name="singleDocument">
        /// <see langword="true"/> when every point belongs to the document of the first point, which the method's
        /// MethodDebugInformation row then names.
        /// </param>
        /// <remarks>
        /// The header is the LocalSignature, the StandAloneSig row number of the body's local signature or 0
        /// when the body has none, followed, when the points span several documents, by the InitialDocument:
        /// the document of the first point. A document-record precedes each non-hidden point whose
        /// document differs from the current one. A hidden point has no document-record of its own and
        /// belongs to the current document.
        /// </remarks>
        private static BlobBuilder EncodeSequencePoints(
            List<EntityRegistry.SequencePoint> sequencePoints,
            StandaloneSignatureHandle localSignature,
            DocumentHandle[] documentHandles,
            bool singleDocument)
        {
            Debug.Assert(sequencePoints.Count > 0);
            var builder = new BlobBuilder();

            // LocalSignature (0 for a nil handle)
            builder.WriteCompressedInteger(MetadataTokens.GetRowNumber(localSignature));

            int currentDocument = sequencePoints[0].DocumentIndex;
            if (!singleDocument)
            {
                // InitialDocument
                builder.WriteCompressedInteger(MetadataTokens.GetRowNumber(documentHandles[currentDocument]));
            }

            int previousOffset = 0;
            int previousStartLine = -1;
            int previousStartColumn = -1;

            foreach (var sp in sequencePoints)
            {
                if (!sp.IsHidden && sp.DocumentIndex != currentDocument)
                {
                    // document-record: a zero IL offset delta, then the Document row number.
                    currentDocument = sp.DocumentIndex;
                    builder.WriteCompressedInteger(0);
                    builder.WriteCompressedInteger(MetadataTokens.GetRowNumber(documentHandles[currentDocument]));
                }

                // IL offset delta
                int offsetDelta = sp.ILOffset - previousOffset;
                builder.WriteCompressedInteger(offsetDelta);
                previousOffset = sp.ILOffset;

                if (sp.IsHidden)
                {
                    // Hidden sequence point: delta lines = 0, delta columns = 0
                    builder.WriteCompressedInteger(0);
                    builder.WriteCompressedInteger(0);
                }
                else
                {
                    // Delta lines
                    int deltaLines = sp.EndLine - sp.StartLine;
                    builder.WriteCompressedInteger(deltaLines);

                    // Delta columns
                    int deltaColumns = sp.EndColumn - sp.StartColumn;
                    if (deltaLines == 0)
                    {
                        builder.WriteCompressedInteger(deltaColumns);
                    }
                    else
                    {
                        builder.WriteCompressedSignedInteger(deltaColumns);
                    }

                    // Start line delta (signed)
                    if (previousStartLine < 0)
                    {
                        builder.WriteCompressedInteger(sp.StartLine);
                    }
                    else
                    {
                        builder.WriteCompressedSignedInteger(sp.StartLine - previousStartLine);
                    }

                    // Start column delta (signed)
                    if (previousStartColumn < 0)
                    {
                        builder.WriteCompressedInteger(sp.StartColumn);
                    }
                    else
                    {
                        builder.WriteCompressedSignedInteger(sp.StartColumn - previousStartColumn);
                    }

                    previousStartLine = sp.StartLine;
                    previousStartColumn = sp.StartColumn;
                }
            }

            return builder;
        }

        /// <summary>
        /// Add DebuggableAttribute to the assembly based on debug options.
        /// - /DEBUG: 0x101 = Default | DisableOptimizations
        /// - /DEBUG=OPT: 0x03 = Default | IgnoreSymbolStoreSequencePoints
        /// - /DEBUG=IMPL: 0x103 = Default | IgnoreSymbolStoreSequencePoints | DisableOptimizations
        /// </summary>
        private void ApplyDebuggableAttribute()
        {
            // DebuggingModes enum values from System.Diagnostics.DebuggableAttribute:
            // None = 0x00, Default = 0x01, IgnoreSymbolStoreSequencePoints = 0x02,
            // EnableEditAndContinue = 0x04, DisableOptimizations = 0x100
            const int DebuggingModesDefault = 0x101;  // Default | DisableOptimizations
            const int DebuggingModesOpt = 0x03;       // Default | IgnoreSymbolStoreSequencePoints
            const int DebuggingModesImpl = 0x103;     // Default | IgnoreSymbolStoreSequencePoints | DisableOptimizations

            int debuggingModes = _options.DebugMode switch
            {
                DebugMode.Opt => DebuggingModesOpt,
                DebugMode.Impl => DebuggingModesImpl,
                _ => DebuggingModesDefault
            };

            // Get reference to core library
            var coreAsmRef = _entityRegistry.GetCoreLibAssemblyReference();

            // Create reference to System.Diagnostics.DebuggableAttribute
            var debuggableAttrType = _entityRegistry.GetOrCreateTypeReference(
                coreAsmRef,
                new TypeName(null, "System.Diagnostics.DebuggableAttribute"));

            var debuggingModesType = _entityRegistry.GetOrCreateTypeReference(
                debuggableAttrType,
                new TypeName(null, "DebuggingModes"));

            BlobBuilder ctorSig = new();
            var sigEncoder = new BlobEncoder(ctorSig);
            sigEncoder.MethodSignature(SignatureCallingConvention.Default, 0, isInstanceMethod: true)
                .Parameters(1,
                    returnType => returnType.Void(),
                    parameters => parameters.AddParameter().Type().Type(debuggingModesType.Handle, isValueType: true));

            var ctor = _entityRegistry.CreateLazilyRecordedMemberReference(debuggableAttrType, ".ctor", ctorSig);

            BlobBuilder attrValue = new();
            attrValue.WriteUInt16(0x0001); // Prolog
            attrValue.WriteInt32(debuggingModes);
            attrValue.WriteUInt16(0x0000); // No named arguments

            // Create and attach the custom attribute
            var customAttr = _entityRegistry.CreateCustomAttribute(ctor, attrValue);
            // Netmodules carry assembly attributes on this TypeRef for the assembly linker.
            customAttr.Owner = (EntityRegistry.EntityBase?)_entityRegistry.Assembly
                ?? _entityRegistry.GetOrCreateTypeReference(
                    coreAsmRef,
                    new TypeName(null, "System.Runtime.CompilerServices.AssemblyAttributesGoHere"));
        }

    }
}