diff --git a/src/tools/ilasm/src/ILAssembler/CIL.g4 b/src/tools/ilasm/src/ILAssembler/CIL.g4 index 0ed37a1baee599..41fe4f88c5e820 100644 --- a/src/tools/ilasm/src/ILAssembler/CIL.g4 +++ b/src/tools/ilasm/src/ILAssembler/CIL.g4 @@ -5,8 +5,6 @@ The .NET Foundation licenses this file to you under the MIT license. grammar CIL; -import Instructions; - tokens { IncludedFileEof, SyntheticIncludedFileEof } INT32: '-'? ('0x' [0-9A-Fa-f]+ | [0-9]+); @@ -122,6 +120,258 @@ PP_ENDIF: '#endif'; PP_INCLUDE: '#include'; MRESOURCE: '.mresource'; +// Instruction tokens MUST be defined before DOTTEDNAME and ID to ensure they take precedence +// For example, "ldc.r8" must be recognized as INSTR_R token, not as DOTTEDNAME +INSTR_NONE: + 'nop' + | 'break' + | 'ldarg.0' + | 'ldarg.1' + | 'ldarg.2' + | 'ldarg.3' + | 'ldloc.0' + | 'ldloc.1' + | 'ldloc.2' + | 'ldloc.3' + | 'stloc.0' + | 'stloc.1' + | 'stloc.2' + | 'stloc.3' + | 'ldnull' + | 'ldc.i4.m1' + | 'ldc.i4.0' + | 'ldc.i4.1' + | 'ldc.i4.2' + | 'ldc.i4.3' + | 'ldc.i4.4' + | 'ldc.i4.5' + | 'ldc.i4.6' + | 'ldc.i4.7' + | 'ldc.i4.8' + | 'dup' + | 'pop' + | 'ret' + | 'ldind.i1' + | 'ldind.u1' + | 'ldind.i2' + | 'ldind.u2' + | 'ldind.i4' + | 'ldind.u4' + | 'ldind.i8' + | 'ldind.i' + | 'ldind.r4' + | 'ldind.r8' + | 'ldind.ref' + | 'stind.ref' + | 'stind.i1' + | 'stind.i2' + | 'stind.i4' + | 'stind.i8' + | 'stind.r4' + | 'stind.r8' + | 'add' + | 'sub' + | 'mul' + | 'div' + | 'div.un' + | 'rem' + | 'rem.un' + | 'and' + | 'or' + | 'xor' + | 'shl' + | 'shr' + | 'shr.un' + | 'neg' + | 'not' + | 'conv.i1' + | 'conv.i2' + | 'conv.i4' + | 'conv.i8' + | 'conv.r4' + | 'conv.r8' + | 'conv.u4' + | 'conv.u8' + | 'conv.r.un' + | 'throw' + | 'conv.ovf.i1.un' + | 'conv.ovf.i2.un' + | 'conv.ovf.i4.un' + | 'conv.ovf.i8.un' + | 'conv.ovf.u1.un' + | 'conv.ovf.u2.un' + | 'conv.ovf.u4.un' + | 'conv.ovf.u8.un' + | 'conv.ovf.i.un' + | 'conv.ovf.u.un' + | 'ldlen' + | 'ldelem.i1' + | 'ldelem.u1' + | 'ldelem.i2' + | 'ldelem.u2' + | 'ldelem.i4' + | 'ldelem.u4' + | 'ldelem.i8' + | 'ldelem.i' + | 'ldelem.r4' + | 'ldelem.r8' + | 'ldelem.ref' + | 'stelem.i' + | 'stelem.i1' + | 'stelem.i2' + | 'stelem.i4' + | 'stelem.i8' + | 'stelem.r4' + | 'stelem.r8' + | 'stelem.ref' + | 'conv.ovf.i1' + | 'conv.ovf.u1' + | 'conv.ovf.i2' + | 'conv.ovf.u2' + | 'conv.ovf.i4' + | 'conv.ovf.u4' + | 'conv.ovf.i8' + | 'conv.ovf.u8' + | 'ckfinite' + | 'conv.u2' + | 'conv.u1' + | 'conv.i' + | 'conv.ovf.i' + | 'conv.ovf.u' + | 'add.ovf' + | 'add.ovf.un' + | 'mul.ovf' + | 'mul.ovf.un' + | 'sub.ovf' + | 'sub.ovf.un' + | 'endfinally' + | 'stind.i' + | 'conv.u' + | 'prefix7' + | 'prefix6' + | 'prefix5' + | 'prefix4' + | 'prefix3' + | 'prefix2' + | 'prefix1' + | 'prefixref' + | 'arglist' + | 'ceq' + | 'cgt' + | 'cgt.un' + | 'clt' + | 'clt.un' + | 'localloc' + | 'endfilter' + | 'volatile.' + | 'tail.' + | 'cpblk' + | 'initblk' + | 'rethrow' + | 'refanytype' + | 'readonly.' + | 'illegal' + | 'endmac'; + +INSTR_VAR: + 'ldarg.s' + | 'ldarga.s' + | 'starg.s' + | 'ldloc.s' + | 'ldloca.s' + | 'stloc.s' + | 'ldarg' + | 'ldarga' + | 'starg' + | 'ldloc' + | 'ldloca' + | 'stloc'; + +INSTR_I: + 'ldc.i4.s' + | 'ldc.i4' + | 'unaligned.' + | 'no.'; + +INSTR_I8: 'ldc.i8'; + +INSTR_R: + 'ldc.r4' + | 'ldc.r8'; + +INSTR_METHOD: + 'jmp' + | 'call' + | 'callvirt' + | 'newobj' + | 'ldftn' + | 'ldvirtftn'; + +INSTR_SIG: 'calli'; + +INSTR_BRTARGET: + 'br.s' + | 'brfalse.s' + | 'brtrue.s' + | 'beq.s' + | 'bge.s' + | 'bgt.s' + | 'ble.s' + | 'blt.s' + | 'bne.un.s' + | 'bge.un.s' + | 'bgt.un.s' + | 'ble.un.s' + | 'blt.un.s' + | 'br' + | 'brfalse' + | 'brtrue' + | 'beq' + | 'bge' + | 'bgt' + | 'ble' + | 'blt' + | 'bne.un' + | 'bge.un' + | 'bgt.un' + | 'ble.un' + | 'blt.un' + | 'leave' + | 'leave.s'; + +INSTR_SWITCH: 'switch'; + +INSTR_TYPE: + 'cpobj' + | 'ldobj' + | 'castclass' + | 'isinst' + | 'unbox' + | 'stobj' + | 'box' + | 'newarr' + | 'ldelema' + | 'ldelem' + | 'stelem' + | 'unbox.any' + | 'refanyval' + | 'mkrefany' + | 'initobj' + | 'constrained.' + | 'sizeof'; + +INSTR_STRING: 'ldstr'; + +INSTR_FIELD: + 'ldfld' + | 'ldflda' + | 'stfld' + | 'ldsfld' + | 'ldsflda' + | 'stsfld'; + +INSTR_TOK: 'ldtoken'; + // ID needs to be last to ensure it doesn't take priority over other token types fragment IDSTART: [A-Za-z_#$@]; fragment IDCONT: [A-Za-z0-9_#?$@`]; @@ -414,6 +664,7 @@ instr: | instr_r float64 | instr_r int64 | instr_r '(' bytes ')' + | instr_r 'bytearray' '(' bytes ')' // Support bytearray syntax for floating point instructions | instr_brtarget int32 | instr_brtarget id | instr_method methodRef @@ -922,7 +1173,8 @@ VTENTRY: '.vtentry'; methodDecls: methodDecl*; methodDecl: - EMITBYTE int32 + instr // MOVED TO TOP - instructions must be matched first! + | EMITBYTE int32 | sehBlock | MAXSTACK int32 | LOCALS sigArgs @@ -930,12 +1182,11 @@ methodDecl: | ENTRYPOINT | ZEROINIT | dataDecl - | instr - | id ':' + | labelDecl | secDecl - | extSourceSpec // Leave for later when I get to generating symbols. - | languageDecl // Leave for later when I get to generating symbols. - | customAttrDecl + | extSourceSpec + | languageDecl + | customDescrInMethodBody // Only customDescr and customDescrWithOwner, NOT bare typedefs | compControl | EXPORT '[' int32 ']' | EXPORT '[' int32 ']' 'as' id @@ -949,6 +1200,12 @@ methodDecl: | PARAM CONSTRAINT dottedName ',' typeSpec customAttrDecl* | PARAM '[' int32 ']' initOpt customAttrDecl*; +labelDecl: id ':'; + +customDescrInMethodBody: + customDescr + | customDescrWithOwner; + scopeBlock: '{' methodDecls '}'; /* Structured exception handling directives */ diff --git a/src/tools/ilasm/src/ILAssembler/Diagnostic.cs b/src/tools/ilasm/src/ILAssembler/Diagnostic.cs index 2c653352264e8f..39f3561e8b60f8 100644 --- a/src/tools/ilasm/src/ILAssembler/Diagnostic.cs +++ b/src/tools/ilasm/src/ILAssembler/Diagnostic.cs @@ -40,6 +40,16 @@ public static class DiagnosticIds public const string ArgumentNotFound = "ILA0018"; public const string LocalNotFound = "ILA0019"; public const string TypedefNotFound = "ILA0020"; + public const string AbstractMethodNotInAbstractType = "ILA0021"; + public const string InvalidPInvokeSignature = "ILA0022"; + public const string MissingInstanceCallConv = "ILA0023"; + public const string DeprecatedNativeType = "ILA0024"; + public const string DeprecatedCustomMarshaller = "ILA0025"; + public const string UnsupportedSecurityDeclaration = "ILA0026"; + public const string GenericParameterIndexOutOfRange = "ILA0027"; + public const string UnknownGenericParameter = "ILA0028"; + public const string ParameterIndexOutOfRange = "ILA0029"; + public const string DuplicateMethod = "ILA0030"; } internal static class DiagnosticMessageTemplates @@ -64,4 +74,14 @@ internal static class DiagnosticMessageTemplates public const string ArgumentNotFound = "Argument '{0}' not found"; public const string LocalNotFound = "Local variable '{0}' not found"; public const string TypedefNotFound = "Typedef '{0}' not found"; + public const string AbstractMethodNotInAbstractType = "Abstract method '{0}' cannot be declared in a non-abstract type"; + public const string InvalidPInvokeSignature = "Invalid P/Invoke signature: module name is required"; + public const string MissingInstanceCallConv = "Instance call convention required for method reference"; + public const string DeprecatedNativeType = "Native type '{0}' is deprecated"; + public const string DeprecatedCustomMarshaller = "The 4-string form of custom marshaller is deprecated"; + public const string UnsupportedSecurityDeclaration = "Individual SecurityAttribute permissions are not supported; use PermissionSet instead"; + public const string GenericParameterIndexOutOfRange = "Generic parameter index {0} is out of range"; + public const string UnknownGenericParameter = "Unknown generic parameter '{0}'"; + public const string ParameterIndexOutOfRange = "Parameter index {0} is out of range"; + public const string DuplicateMethod = "Duplicate method definition"; } diff --git a/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs b/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs index cb1a2a8a51eb1d..04e80a6e69444d 100644 --- a/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs +++ b/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs @@ -83,7 +83,7 @@ public EntityRegistry() }); } - private IReadOnlyList GetSeenEntities(TableIndex table) + public IReadOnlyList GetSeenEntities(TableIndex table) { if (_seenEntities.TryGetValue(table, out var entities)) { @@ -177,15 +177,15 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadO builder.GetOrAddString(type.Namespace), builder.GetOrAddString(type.Name), type.BaseType is null ? default : type.BaseType.Handle, - (FieldDefinitionHandle)GetHandleForList(type.Fields, GetSeenEntities(TableIndex.TypeDef), type => ((TypeDefinitionEntity)type).Fields, i, TableIndex.Field), - (MethodDefinitionHandle)GetHandleForList(type.Methods, GetSeenEntities(TableIndex.TypeDef), type => ((TypeDefinitionEntity)type).Methods, i, TableIndex.MethodDef)); + GetFieldHandleForList(type.Fields, GetSeenEntities(TableIndex.TypeDef), type => ((TypeDefinitionEntity)type).Fields, i), + GetMethodHandleForList(type.Methods, GetSeenEntities(TableIndex.TypeDef), type => ((TypeDefinitionEntity)type).Methods, i)); builder.AddEventMap( (TypeDefinitionHandle)type.Handle, - (EventDefinitionHandle)GetHandleForList(type.Events, GetSeenEntities(TableIndex.TypeDef), type => ((TypeDefinitionEntity)type).Events, i, TableIndex.Event)); + GetEventHandleForList(type.Events, GetSeenEntities(TableIndex.TypeDef), type => ((TypeDefinitionEntity)type).Events, i)); builder.AddPropertyMap( (TypeDefinitionHandle)type.Handle, - (PropertyDefinitionHandle)GetHandleForList(type.Properties, GetSeenEntities(TableIndex.TypeDef), type => ((TypeDefinitionEntity)type).Properties, i, TableIndex.Property)); + GetPropertyHandleForList(type.Properties, GetSeenEntities(TableIndex.TypeDef), type => ((TypeDefinitionEntity)type).Properties, i)); if (type.PackingSize is not null || type.ClassSize is not null) { @@ -222,6 +222,11 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadO { builder.AddMarshallingDescriptor(fieldDef.Handle, builder.GetOrAddBlob(fieldDef.MarshallingDescriptor)); } + + if (fieldDef.HasConstant) + { + builder.AddConstant(fieldDef.Handle, fieldDef.ConstantValue); + } } for (int i = 0; i < GetSeenEntities(TableIndex.MethodDef).Count; i++) @@ -241,7 +246,7 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadO builder.GetOrAddString(methodDef.Name), builder.GetOrAddBlob(methodDef.MethodSignature!), rva, - (ParameterHandle)GetHandleForList(methodDef.Parameters, GetSeenEntities(TableIndex.MethodDef), method => ((MethodDefinitionEntity)method).Parameters, i, TableIndex.Param)); + GetParameterHandleForList(methodDef.Parameters, GetSeenEntities(TableIndex.MethodDef), method => ((MethodDefinitionEntity)method).Parameters, i)); if (methodDef.MethodImportInformation is not null) { @@ -385,12 +390,27 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadO builder.AddMethodSpecification(methodSpec.Parent.Handle, builder.GetOrAddBlob(methodSpec.Signature)); } + static FieldDefinitionHandle GetFieldHandleForList(IReadOnlyList list, IReadOnlyList listOwner, Func> getList, int ownerIndex) + => (FieldDefinitionHandle)GetHandleForList(list, listOwner, getList, ownerIndex, TableIndex.Field); + + static MethodDefinitionHandle GetMethodHandleForList(IReadOnlyList list, IReadOnlyList listOwner, Func> getList, int ownerIndex) + => (MethodDefinitionHandle)GetHandleForList(list, listOwner, getList, ownerIndex, TableIndex.MethodDef); + + static PropertyDefinitionHandle GetPropertyHandleForList(IReadOnlyList list, IReadOnlyList listOwner, Func> getList, int ownerIndex) + => (PropertyDefinitionHandle)GetHandleForList(list, listOwner, getList, ownerIndex, TableIndex.Property); + + static EventDefinitionHandle GetEventHandleForList(IReadOnlyList list, IReadOnlyList listOwner, Func> getList, int ownerIndex) + => (EventDefinitionHandle)GetHandleForList(list, listOwner, getList, ownerIndex, TableIndex.Event); + + static ParameterHandle GetParameterHandleForList(IReadOnlyList list, IReadOnlyList listOwner, Func> getList, int ownerIndex) + => (ParameterHandle)GetHandleForList(list, listOwner, getList, ownerIndex, TableIndex.Param); + static EntityHandle GetHandleForList(IReadOnlyList list, IReadOnlyList listOwner, Func> getList, int ownerIndex, TableIndex tokenType) { // Return the first entry in the list. // If the list is empty, return the start of the next list. // If there is no next list, return one past the end of the previous list. - if (list.Count != 0) + if (list.Count != 0 && !list[0].Handle.IsNil) { return list[0].Handle; } @@ -398,7 +418,7 @@ static EntityHandle GetHandleForList(IReadOnlyList list, IReadOnlyLi for (int i = 0; i < listOwner.Count; i++) { var otherList = getList(listOwner[i]); - if (otherList.Count != 0) + if (otherList.Count != 0 && !otherList[0].Handle.IsNil) { return otherList[0].Handle; } @@ -407,11 +427,12 @@ static EntityHandle GetHandleForList(IReadOnlyList list, IReadOnlyLi for (int i = ownerIndex - 1; i >= 0; i--) { var otherList = getList(listOwner[i]); - if (otherList.Count != 0) + if (otherList.Count != 0 && !otherList[otherList.Count - 1].Handle.IsNil) { return MetadataTokens.EntityHandle(tokenType, MetadataTokens.GetRowNumber(otherList[otherList.Count - 1].Handle) + 1); } } + // If all lists are empty, return a nil handle for the right table return MetadataTokens.EntityHandle(tokenType, 0); } } @@ -1206,6 +1227,11 @@ public sealed class MethodDefinitionEntity(TypeDefinitionEntity containingType, public (ModuleReferenceEntity ModuleName, string? EntryPointName, MethodImportAttributes Attributes)? MethodImportInformation { get; set; } public MethodImplAttributes ImplementationAttributes { get; set; } + + /// + /// Debug information for this method (sequence points, document). + /// + public MethodDebugInfo DebugInfo { get; } = new(); } public sealed class ParameterEntity(ParameterAttributes attributes, string? name, BlobBuilder marshallingDescriptor, int sequence) : EntityBase @@ -1293,6 +1319,10 @@ public sealed class FieldDefinitionEntity(FieldAttributes attributes, TypeDefini // FieldLayout table field (explicit field offset) public int? Offset { get; set; } + + // Constant table entry + public bool HasConstant { get; set; } + public object? ConstantValue { get; set; } } public sealed class InterfaceImplementationEntity(TypeDefinitionEntity type, TypeEntity interfaceType) : EntityBase @@ -1375,5 +1405,42 @@ public ExportedTypeEntity(string name, string @namespace, EntityBase? implementa public int TypeDefinitionId { get; set; } } + + /// + /// Represents a sequence point mapping IL offset to source location. + /// + public readonly struct SequencePoint + { + public SequencePoint(int ilOffset, int startLine, int startColumn, int endLine, int endColumn) + { + ILOffset = ilOffset; + StartLine = startLine; + StartColumn = startColumn; + EndLine = endLine; + EndColumn = endColumn; + } + + public int ILOffset { get; } + public int StartLine { get; } + public int StartColumn { get; } + public int EndLine { get; } + public int EndColumn { get; } + + /// + /// Creates a hidden sequence point (used for compiler-generated code). + /// + public static SequencePoint Hidden(int ilOffset) => new(ilOffset, 0xFEEFEE, 0, 0xFEEFEE, 0); + + public bool IsHidden => StartLine == 0xFEEFEE; + } + + /// + /// Debug information for a method, including sequence points and local scopes. + /// + public sealed class MethodDebugInfo + { + public string? DocumentPath { get; set; } + public List SequencePoints { get; } = new(); + } } } diff --git a/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs b/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs index c75506b2af541b..069a2b494dc2c6 100644 --- a/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs +++ b/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -12,11 +12,8 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; using System.Text; -using System.Xml.XPath; using Antlr4.Runtime; using Antlr4.Runtime.Misc; using Antlr4.Runtime.Tree; @@ -92,6 +89,14 @@ internal sealed class GrammarVisitor : ICILVisitor // Typedef aliases - maps alias name to the resolved entity private readonly Dictionary _typedefs = new(); + // Debug info tracking + private Guid _currentLanguageGuid = Guid.Empty; + private Guid _currentLanguageVendorGuid = Guid.Empty; + private Guid _currentDocumentTypeGuid = Guid.Empty; + private string? _currentDocumentPath; + private readonly Dictionary _documentHandles = new(); + private readonly MetadataBuilder _pdbBuilder = new(); + public GrammarVisitor(IReadOnlyDictionary documents, Options options, Func resourceLocator) { _documents = documents; @@ -124,7 +129,11 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC public (ImmutableArray Diagnostics, PEBuilder? Image) BuildImage() { - if (_diagnostics.Any(diag => diag.Severity == DiagnosticSeverity.Error)) + // 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. + var structuralErrors = _diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error && !IsRecoverableError(d.Id)); + if (structuralErrors.Any()) { return (_diagnostics.ToImmutable(), null); } @@ -143,17 +152,203 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC entryPoint = (MethodDefinitionHandle)_entityRegistry.EntryPoint.Handle; } + // Build debug directory if we have any debug info + DebugDirectoryBuilder? debugDirectoryBuilder = BuildDebugDirectory(entryPoint); + ManagedPEBuilder peBuilder = new( header, rootBuilder, ilStream, _mappedFieldData, _manifestResources, - flags: CorFlags.ILOnly, entryPoint: entryPoint); + flags: CorFlags.ILOnly, + entryPoint: entryPoint, + debugDirectoryBuilder: debugDirectoryBuilder); return (_diagnostics.ToImmutable(), peBuilder); } + private DebugDirectoryBuilder? BuildDebugDirectory(MethodDefinitionHandle entryPoint) + { + // Check if we have any methods with debug info + bool hasDebugInfo = false; + foreach (var entity in _entityRegistry.GetSeenEntities(TableIndex.MethodDef)) + { + if (entity is EntityRegistry.MethodDefinitionEntity method && + method.DebugInfo.SequencePoints.Count > 0) + { + hasDebugInfo = true; + break; + } + } + + if (!hasDebugInfo) + { + return null; + } + + // Build PDB metadata + BuildPdbMetadata(); + + // Get row counts from main metadata for the portable PDB + var typeSystemRowCounts = _metadataBuilder.GetRowCounts(); + + // Create the portable PDB + var pdbBuilder = new PortablePdbBuilder( + _pdbBuilder, + typeSystemRowCounts, + entryPoint, + idProvider: content => new BlobContentId(Guid.NewGuid(), 0x04030201)); + + var pdbBlob = new BlobBuilder(); + var pdbContentId = pdbBuilder.Serialize(pdbBlob); + + // Create debug directory with embedded PDB + var debugDirectoryBuilder = new DebugDirectoryBuilder(); + debugDirectoryBuilder.AddCodeViewEntry( + $"assembly.pdb", + pdbContentId, + pdbBuilder.FormatVersion); + debugDirectoryBuilder.AddEmbeddedPortablePdbEntry(pdbBlob, pdbBuilder.FormatVersion); + + return debugDirectoryBuilder; + } + + private void BuildPdbMetadata() + { + // Add documents and sequence points to the PDB metadata builder + foreach (var entity in _entityRegistry.GetSeenEntities(TableIndex.MethodDef)) + { + if (entity is not EntityRegistry.MethodDefinitionEntity method) + { + continue; + } + + var debugInfo = method.DebugInfo; + if (debugInfo.SequencePoints.Count == 0) + { + // Add empty debug info entry for methods without sequence points + _pdbBuilder.AddMethodDebugInformation(default, default); + continue; + } + + // Get or create document handle + DocumentHandle documentHandle = default; + if (debugInfo.DocumentPath is not null) + { + if (!_documentHandles.TryGetValue(debugInfo.DocumentPath, out documentHandle)) + { + var nameHandle = _pdbBuilder.GetOrAddDocumentName(debugInfo.DocumentPath); + var languageGuidHandle = _currentLanguageGuid != Guid.Empty + ? _pdbBuilder.GetOrAddGuid(_currentLanguageGuid) + : default; + documentHandle = _pdbBuilder.AddDocument( + nameHandle, + default, // hash algorithm + default, // hash + languageGuidHandle); + _documentHandles[debugInfo.DocumentPath] = documentHandle; + } + } + + // Encode sequence points + var sequencePointsBlob = EncodeSequencePoints(debugInfo.SequencePoints); + var sequencePointsBlobHandle = _pdbBuilder.GetOrAddBlob(sequencePointsBlob); + + _pdbBuilder.AddMethodDebugInformation(documentHandle, sequencePointsBlobHandle); + } + } + + private static BlobBuilder EncodeSequencePoints(List sequencePoints) + { + var builder = new BlobBuilder(); + + if (sequencePoints.Count == 0) + { + return builder; + } + + // LocalSignature (not used here, write 0) + builder.WriteCompressedInteger(0); + + int previousOffset = 0; + int previousStartLine = 0; + int previousStartColumn = 0; + + foreach (var sp in sequencePoints) + { + // 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; + } + + private static bool IsRecoverableError(string diagnosticId) + { + // Method body and signature diagnostics are recoverable - we emit the assembly but report the error. + // This matches native ilasm behavior where errors during method/field emission don't prevent + // the assembly from being written when the /ERR (OnErrGo) flag is set. + return diagnosticId is DiagnosticIds.ByteArrayTooShort + or DiagnosticIds.ArgumentNotFound + or DiagnosticIds.LocalNotFound + or DiagnosticIds.LabelNotFound + or DiagnosticIds.GenericParameterIndexOutOfRange + or DiagnosticIds.ParameterIndexOutOfRange + or DiagnosticIds.GenericParameterNotFound + or DiagnosticIds.UnknownGenericParameter + or DiagnosticIds.MissingInstanceCallConv; + } + public GrammarResult Visit(IParseTree tree) => tree.Accept(this); GrammarResult ICILVisitor.VisitAlignment(CILParser.AlignmentContext context) => VisitAlignment(context); @@ -531,6 +726,10 @@ public CurrentMethodContext(EntityRegistry.MethodDefinitionEntity definition) public Dictionary Labels { get; } = new(); + public HashSet DeclaredLabels { get; } = new(); + + public Dictionary UndefinedLabelReferences { get; } = new(); + public Dictionary ArgumentNames { get; } = new(); public List> LocalsScopes { get; } = new(); @@ -552,6 +751,8 @@ public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) { _currentMethod = new(VisitMethodHead(methodHead).Value); VisitMethodDecls(context.methodDecls()); + // Validate that all referenced labels were declared + ValidateLabelReferences(); _currentMethod = null; } else if (context.secDecl() is {} secDecl) @@ -819,7 +1020,12 @@ EntityRegistry.TypeEntity ResolveTypeDef() TypeName typeName = VisitSlashedName(slashedName).Value; if (typeName.ContainingTypeName is null) { - // TODO: Check for typedef. + // Check for typedef. + var typedefResult = TryResolveTypedefAsType(typeName.DottedName); + if (typedefResult is not null) + { + return typedefResult; + } } Stack containingTypes = new(); for (TypeName? containingType = typeName; containingType is not null; containingType = containingType.ContainingTypeName) @@ -952,6 +1158,25 @@ private static GrammarResult.String VisitCompQstring(CILParser.CompQstringContex throw new UnreachableException(); } + GrammarResult ICILVisitor.VisitCustomDescrInMethodBody(CILParser.CustomDescrInMethodBodyContext context) => VisitCustomDescrInMethodBody(context); + public GrammarResult.Literal VisitCustomDescrInMethodBody(CILParser.CustomDescrInMethodBodyContext context) + { + if (context.customDescrWithOwner() is {} descrWithOwner) + { + // Visit the custom attribute descriptor to record it, + // but don't return it as it will already have its owner recorded. + _ = VisitCustomDescrWithOwner(descrWithOwner); + return new(null); + } + if (context.customDescr() is {} descr) + { +#nullable disable // Disable nullability to work around lack of variance. + return VisitCustomDescr(descr); +#nullable restore + } + throw new UnreachableException(); + } + GrammarResult ICILVisitor.VisitCustomBlobArgs(CILParser.CustomBlobArgsContext context) => VisitCustomBlobArgs(context); public GrammarResult.FormattedBlob VisitCustomBlobArgs(CILParser.CustomBlobArgsContext context) { @@ -1536,7 +1761,9 @@ public GrammarResult.FormattedBlob VisitElementType(CILParser.ElementTypeContext } public GrammarResult VisitErrorNode(IErrorNode node) => throw new UnreachableException(NodeShouldNeverBeDirectlyVisited); - public GrammarResult VisitEsHead(CILParser.EsHeadContext context) => throw new NotImplementedException("TODO: Symbols"); + + // esHead is '.line' or '#line' - this is just the keyword, actual parsing is in VisitExtSourceSpec. + public GrammarResult VisitEsHead(CILParser.EsHeadContext context) => throw new UnreachableException(NodeShouldNeverBeDirectlyVisited); GrammarResult ICILVisitor.VisitEventAttr(CILParser.EventAttrContext context) => VisitEventAttr(context); public GrammarResult.Flag VisitEventAttr(CILParser.EventAttrContext context) @@ -1586,7 +1813,6 @@ public GrammarResult.Flag VisitEventAttr(CILParser.EventAttrCon } public GrammarResult VisitExportHead(CILParser.ExportHeadContext context) => throw new NotImplementedException("Obsolete syntax"); - GrammarResult ICILVisitor.VisitExptAttr(CILParser.ExptAttrContext context) => VisitExptAttr(context); public static GrammarResult.Flag VisitExptAttr(CILParser.ExptAttrContext context) { @@ -1704,7 +1930,8 @@ public static GrammarResult.Flag VisitExptAttr(CILParser.ExptAtt TypeName typeName = VisitSlashedName(slashedName).Value; if (typeName.ContainingTypeName is null) { - // TODO: Check for typedef. + // Check for typedef - typedefs resolve to TypeEntity, not ExportedTypeEntity + // so we skip the typedef check for exported type resolution } Stack containingTypes = new(); for (TypeName? containingType = typeName; containingType is not null; containingType = containingType.ContainingTypeName) @@ -1753,7 +1980,86 @@ public static GrammarResult.Flag VisitExptAttr(CILParser.ExptAtt } } - public GrammarResult VisitExtSourceSpec(CILParser.ExtSourceSpecContext context) => throw new NotImplementedException("TODO: Symbols"); + public GrammarResult VisitExtSourceSpec(CILParser.ExtSourceSpecContext context) + { + // Parse .line directive to extract source location info + // Grammar: esHead int32 (',' int32)? (':' int32 (',' int32)?)? (SQSTRING | QSTRING)? + var int32s = context.int32(); + var sqstring = context.SQSTRING(); + var qstring = context.QSTRING(); + + // Extract line/column info based on number of int32s + int startLine = 0, endLine = 0, startColumn = 0, endColumn = 0; + + if (int32s.Length >= 1) + { + startLine = VisitInt32(int32s[0]).Value; + endLine = startLine; + } + if (int32s.Length >= 2) + { + // Could be endLine or startColumn depending on separator + string contextText = context.GetText(); + if (contextText.Contains(',') && contextText.IndexOf(',') < contextText.IndexOf(':')) + { + // Format: startLine,endLine:... + endLine = VisitInt32(int32s[1]).Value; + } + else + { + // Format: line:column... + startColumn = VisitInt32(int32s[1]).Value; + endColumn = startColumn; + } + } + if (int32s.Length >= 3) + { + startColumn = VisitInt32(int32s[2]).Value; + endColumn = startColumn; + } + if (int32s.Length >= 4) + { + endColumn = VisitInt32(int32s[3]).Value; + } + + // Extract filename if present + string? filePath = null; + if (sqstring is not null) + { + filePath = StringHelpers.ParseQuotedString(sqstring.GetText()); + } + else if (qstring is not null) + { + filePath = StringHelpers.ParseQuotedString(qstring.GetText()); + } + + // Update current document path if specified + if (filePath is not null) + { + _currentDocumentPath = filePath; + } + + // If we're in a method, record the sequence point + if (_currentMethod is not null && _currentDocumentPath is not null) + { + int ilOffset = _currentMethod.Definition.MethodBody.Offset; + _currentMethod.Definition.DebugInfo.DocumentPath ??= _currentDocumentPath; + + // 0xFEEFEE indicates a hidden sequence point + if (startLine == 0xFEEFEE) + { + _currentMethod.Definition.DebugInfo.SequencePoints.Add( + EntityRegistry.SequencePoint.Hidden(ilOffset)); + } + else + { + _currentMethod.Definition.DebugInfo.SequencePoints.Add( + new EntityRegistry.SequencePoint(ilOffset, startLine, startColumn, endLine, endColumn)); + } + } + + return GrammarResult.SentinelValue.Result; + } GrammarResult ICILVisitor.VisitF32seq(CILParser.F32seqContext context) => VisitF32seq(context); public GrammarResult.FormattedBlob VisitF32seq(CILParser.F32seqContext context) @@ -1828,7 +2134,7 @@ public GrammarResult VisitFieldDecl(CILParser.FieldDeclContext context) var name = VisitDottedName(context.dottedName()).Value; var rvaOffset = VisitAtOpt(context.atOpt()).Value; var fieldOffset = VisitRepeatOpt(context.repeatOpt()).Value; - _ = VisitInitOpt(context.initOpt()); + var constantValue = VisitInitOpt(context.initOpt()).Value; var signature = new BlobEncoder(new BlobBuilder()); _ = signature.Field(); @@ -1841,12 +2147,67 @@ public GrammarResult VisitFieldDecl(CILParser.FieldDeclContext context) field.MarshallingDescriptor = marshalBlob; field.DataDeclarationName = rvaOffset; field.Offset = fieldOffset; + if (constantValue is not NoConstantSentinel) + { + field.ConstantValue = constantValue; + field.HasConstant = true; + } } return GrammarResult.SentinelValue.Result; } - public GrammarResult VisitFieldInit(CILParser.FieldInitContext context) => throw new NotImplementedException("TODO-SRM: Need support for an arbitrary byte blob as a constant value"); + GrammarResult ICILVisitor.VisitFieldInit(CILParser.FieldInitContext context) => VisitFieldInit(context); + public GrammarResult.Literal VisitFieldInit(CILParser.FieldInitContext context) + { + // fieldInit: fieldSerInit | compQstring | NULLREF; + if (context.NULLREF() is not null) + { + return new(null); + } + if (context.compQstring() is CILParser.CompQstringContext compQstring) + { + return new(VisitCompQstring(compQstring).Value); + } + if (context.fieldSerInit() is CILParser.FieldSerInitContext fieldSerInit) + { + // fieldSerInit returns a blob with type byte prefix - extract the actual value + var blob = VisitFieldSerInit(fieldSerInit).Value; + return new(ExtractConstantFromSerInit(blob)); + } + return new(null); + } + + private static object? ExtractConstantFromSerInit(BlobBuilder blob) + { + var bytes = blob.ToImmutableArray(); + if (bytes.Length == 0) + { + return null; + } + + var typeCode = (SerializationTypeCode)bytes[0]; + var valueBytes = bytes.AsSpan().Slice(1); + + return typeCode switch + { + SerializationTypeCode.Boolean => valueBytes.Length >= 1 && valueBytes[0] != 0, + SerializationTypeCode.Char => valueBytes.Length >= 2 ? BitConverter.ToChar(valueBytes) : '\0', + SerializationTypeCode.SByte => valueBytes.Length >= 1 ? (sbyte)valueBytes[0] : (sbyte)0, + SerializationTypeCode.Byte => valueBytes.Length >= 1 ? valueBytes[0] : (byte)0, + SerializationTypeCode.Int16 => valueBytes.Length >= 2 ? BitConverter.ToInt16(valueBytes) : (short)0, + SerializationTypeCode.UInt16 => valueBytes.Length >= 2 ? BitConverter.ToUInt16(valueBytes) : (ushort)0, + SerializationTypeCode.Int32 => valueBytes.Length >= 4 ? BitConverter.ToInt32(valueBytes) : 0, + SerializationTypeCode.UInt32 => valueBytes.Length >= 4 ? BitConverter.ToUInt32(valueBytes) : 0u, + SerializationTypeCode.Int64 => valueBytes.Length >= 8 ? BitConverter.ToInt64(valueBytes) : 0L, + SerializationTypeCode.UInt64 => valueBytes.Length >= 8 ? BitConverter.ToUInt64(valueBytes) : 0uL, + SerializationTypeCode.Single => valueBytes.Length >= 4 ? BitConverter.ToSingle(valueBytes) : 0f, + SerializationTypeCode.Double => valueBytes.Length >= 8 ? BitConverter.ToDouble(valueBytes) : 0d, + SerializationTypeCode.String => Encoding.Unicode.GetString(valueBytes), + // TODO: Support arbitrary byte blobs that don't correspond to any currently-valid format. + _ => null + }; + } public GrammarResult VisitFieldOrProp(CILParser.FieldOrPropContext context) => throw new UnreachableException(NodeShouldNeverBeDirectlyVisited); @@ -2186,17 +2547,24 @@ public GrammarResult.Flag VisitImplAttr(CILParser.ImplAttr return new(builder.ToImmutable()); } - public GrammarResult VisitInitOpt(CILParser.InitOptContext context) + GrammarResult ICILVisitor.VisitInitOpt(CILParser.InitOptContext context) => VisitInitOpt(context); + public GrammarResult.Literal VisitInitOpt(CILParser.InitOptContext context) { - if (context.fieldInit() is {}) + if (context.fieldInit() is CILParser.FieldInitContext fieldInit) { - // TODO: Change fieldSerInit to return a parsed System.Object value to construct the constant row entry. - // TODO-SRM: AddConstant does not support providing an arbitrary byte array as a constant value. - // Propose MetadataBuilder.AddConstant(EntityHandle parent, PrimitiveTypeCode type, BlobBuilder value) overload? - throw new NotImplementedException(); + return VisitFieldInit(fieldInit); } - return GrammarResult.SentinelValue.Result; + // No initializer - return a sentinel indicating no constant + return new(NoConstantSentinel.Instance); } + + // Sentinel to distinguish "no constant" from "constant is null" + private sealed class NoConstantSentinel + { + public static readonly NoConstantSentinel Instance = new(); + private NoConstantSentinel() { } + } + public GrammarResult VisitInstr(CILParser.InstrContext context) { var instrContext = context.GetRuleContext(0); @@ -2211,7 +2579,13 @@ public GrammarResult VisitInstr(CILParser.InstrContext context) string label = VisitId(id).Value; if (!_currentMethod!.Labels.TryGetValue(label, out var handle)) { - _currentMethod.Labels.Add(label, handle = _currentMethod.Definition.MethodBody.DefineLabel()); + handle = _currentMethod.Definition.MethodBody.DefineLabel(); + _currentMethod.Labels[label] = handle; + // Track undefined label references for later validation + if (!_currentMethod.UndefinedLabelReferences.ContainsKey(label)) + { + _currentMethod.UndefinedLabelReferences[label] = context; + } } _currentMethod.Definition.MethodBody.Branch(opcode, handle); } @@ -2401,7 +2775,13 @@ public GrammarResult VisitInstr(CILParser.InstrContext context) string labelName = VisitId(id).Value; if (!_currentMethod!.Labels.TryGetValue(labelName, out var handle)) { - _currentMethod.Labels.Add(labelName, handle = _currentMethod.Definition.MethodBody.DefineLabel()); + handle = _currentMethod.Definition.MethodBody.DefineLabel(); + _currentMethod.Labels[labelName] = handle; + // Track undefined label references for later validation + if (!_currentMethod.UndefinedLabelReferences.ContainsKey(labelName)) + { + _currentMethod.UndefinedLabelReferences[labelName] = context; + } } labels.Add((handle, null)); } @@ -2637,8 +3017,68 @@ public GrammarResult.Literal VisitInt64(CILParser.Int64Context context) GrammarResult ICILVisitor.VisitIntOrWildcard(CILParser.IntOrWildcardContext context) => VisitIntOrWildcard(context); public GrammarResult.Literal VisitIntOrWildcard(CILParser.IntOrWildcardContext context) => context.int32() is {} int32 ? new(VisitInt32(int32).Value) : new(null); + + private void ValidateLabelReferences() + { + if (_currentMethod is null) + { + return; + } + + // Report errors for any labels that were referenced but never declared + foreach (var undefinedLabel in _currentMethod.UndefinedLabelReferences) + { + string labelName = undefinedLabel.Key; + ParserRuleContext context = undefinedLabel.Value; + + // Only report if the label was never declared + if (!_currentMethod.DeclaredLabels.Contains(labelName)) + { + ReportError(DiagnosticIds.LabelNotFound, + string.Format(DiagnosticMessageTemplates.LabelNotFound, labelName), + context); + } + } + } + public GrammarResult VisitLabels(CILParser.LabelsContext context) => throw new UnreachableException(NodeShouldNeverBeDirectlyVisited); - public GrammarResult VisitLanguageDecl(CILParser.LanguageDeclContext context) => throw new NotImplementedException("TODO: Symbols"); + + GrammarResult ICILVisitor.VisitLabelDecl(CILParser.LabelDeclContext context) => VisitLabelDecl(context); + public GrammarResult VisitLabelDecl(CILParser.LabelDeclContext context) + { + var labelId = context.id(); + string labelName = VisitId(labelId).Value; + _currentMethod!.DeclaredLabels.Add(labelName); + if (!_currentMethod!.Labels.TryGetValue(labelName, out var label)) + { + label = _currentMethod.Definition.MethodBody.DefineLabel(); + _currentMethod.Labels[labelName] = label; + } + _currentMethod.Definition.MethodBody.MarkLabel(label); + return GrammarResult.SentinelValue.Result; + } + + public GrammarResult VisitLanguageDecl(CILParser.LanguageDeclContext context) + { + // .language SQSTRING (',' SQSTRING (',' SQSTRING)?)? + // First GUID: language (e.g., C#, VB, IL) + // Second GUID: vendor (optional) + // Third GUID: document type (optional) + var strings = context.SQSTRING(); + if (strings.Length >= 1 && Guid.TryParse(StringHelpers.ParseQuotedString(strings[0].GetText()), out var languageGuid)) + { + _currentLanguageGuid = languageGuid; + } + if (strings.Length >= 2 && Guid.TryParse(StringHelpers.ParseQuotedString(strings[1].GetText()), out var vendorGuid)) + { + _currentLanguageVendorGuid = vendorGuid; + } + if (strings.Length >= 3 && Guid.TryParse(StringHelpers.ParseQuotedString(strings[2].GetText()), out var docTypeGuid)) + { + _currentDocumentTypeGuid = docTypeGuid; + } + return GrammarResult.SentinelValue.Result; + } public GrammarResult VisitManifestResDecl(CILParser.ManifestResDeclContext context) => throw new UnreachableException(NodeShouldNeverBeDirectlyVisited); GrammarResult ICILVisitor.VisitManifestResDecls(CILParser.ManifestResDeclsContext context) => VisitManifestResDecls(context); @@ -2798,11 +3238,11 @@ public GrammarResult.Flag VisitMethAttr(CILParser.MethAttrCont _ => throw new UnreachableException(), }; } - public GrammarResult VisitMethodDecl(CILParser.MethodDeclContext context) { Debug.Assert(_currentMethod is not null); var currentMethod = _currentMethod!; + if (context.EMITBYTE() is not null) { currentMethod.Definition.MethodBody.CodeBuilder.WriteByte((byte)VisitInt32(context.GetChild(0)).Value); @@ -2845,8 +3285,9 @@ public GrammarResult VisitMethodDecl(CILParser.MethodDeclContext context) currentMethod.AllLocals.Add(loc); } } - else if (context.ChildCount == 2 && context.GetChild(0) is CILParser.IdContext labelId) + else if (context.labelDecl() is CILParser.LabelDeclContext labelDecl) { + var labelId = labelDecl.id(); string labelName = VisitId(labelId).Value; if (!currentMethod.Labels.TryGetValue(labelName, out var label)) { @@ -2910,7 +3351,9 @@ public GrammarResult VisitMethodDecl(CILParser.MethodDeclContext context) int index = VisitInt32(int32[0]).Value; if (index < 0 || index >= currentMethod.Definition.GenericParameters.Count) { - // TODO: Report generic parameter index out of range + ReportError(DiagnosticIds.GenericParameterIndexOutOfRange, + string.Format(DiagnosticMessageTemplates.GenericParameterIndexOutOfRange, index), + context); return GrammarResult.SentinelValue.Result; } param = currentMethod.Definition.GenericParameters[index]; @@ -2928,7 +3371,9 @@ public GrammarResult VisitMethodDecl(CILParser.MethodDeclContext context) } if (param is null) { - // TODO: Report unknown generic parameter + ReportError(DiagnosticIds.UnknownGenericParameter, + string.Format(DiagnosticMessageTemplates.UnknownGenericParameter, name), + context); return GrammarResult.SentinelValue.Result; } } @@ -2947,7 +3392,9 @@ public GrammarResult VisitMethodDecl(CILParser.MethodDeclContext context) int index = VisitInt32(int32[0]).Value; if (index < 0 || index >= currentMethod.Definition.GenericParameters.Count) { - // TODO: Report generic parameter index out of range + ReportError(DiagnosticIds.GenericParameterIndexOutOfRange, + string.Format(DiagnosticMessageTemplates.GenericParameterIndexOutOfRange, index), + context); return GrammarResult.SentinelValue.Result; } param = currentMethod.Definition.GenericParameters[index]; @@ -2965,7 +3412,9 @@ public GrammarResult VisitMethodDecl(CILParser.MethodDeclContext context) } if (param is null) { - // TODO: Report unknown generic parameter + ReportError(DiagnosticIds.UnknownGenericParameter, + string.Format(DiagnosticMessageTemplates.UnknownGenericParameter, name), + context); return GrammarResult.SentinelValue.Result; } } @@ -2998,7 +3447,9 @@ public GrammarResult VisitMethodDecl(CILParser.MethodDeclContext context) int index = VisitInt32(context.int32()[0]).Value; if (index < 0 || index >= currentMethod.Definition.Parameters.Count) { - // TODO: Report parameter index out of range + ReportError(DiagnosticIds.ParameterIndexOutOfRange, + string.Format(DiagnosticMessageTemplates.ParameterIndexOutOfRange, index), + context); return GrammarResult.SentinelValue.Result; } @@ -3020,20 +3471,20 @@ public GrammarResult VisitMethodDecl(CILParser.MethodDeclContext context) var declarativeSecurity = VisitSecDecl(secDecl).Value; declarativeSecurity?.Parent = currentMethod.Definition; } - else if (context.customAttrDecl() is {} customAttr) + else if (context.GetChild(0) is CILParser.InstrContext instr) { - foreach (var attr in customAttr) - { - var customAttrDecl = VisitCustomAttrDecl(attr).Value; - customAttrDecl?.Owner = currentMethod.Definition; - } + _ = VisitInstr(instr); } else { - _ = context.children[0].Accept(this); + // Handle other methodDecl alternatives + var child = context.children[0]; + _ = child.Accept(this); } return GrammarResult.SentinelValue.Result; } + + public GrammarResult VisitMethodDecls(CILParser.MethodDeclsContext context) { foreach (var decl in context.methodDecl()) @@ -3065,7 +3516,9 @@ public GrammarResult VisitMethodDecls(CILParser.MethodDeclsContext context) if (methodDefinition.MethodAttributes.HasFlag(MethodAttributes.Abstract) && !methodDefinition.ContainingType.Attributes.HasFlag(TypeAttributes.Abstract)) { - // TODO:Emit error + ReportError(DiagnosticIds.AbstractMethodNotInAbstractType, + string.Format(DiagnosticMessageTemplates.AbstractMethodNotInAbstractType, methodDefinition.Name), + context); } (EntityRegistry.ModuleReferenceEntity Module, string? EntryPoint, MethodImportAttributes Attributes)? pInvokeInformation = null; @@ -3074,7 +3527,9 @@ public GrammarResult VisitMethodDecls(CILParser.MethodDeclsContext context) var (moduleName, entryPoint, attributes) = VisitPinvImpl(pInvokeInfo).Value; if (moduleName is null) { - // TODO: Emit error + ReportError(DiagnosticIds.InvalidPInvokeSignature, + DiagnosticMessageTemplates.InvalidPInvokeSignature, + pInvokeInfo); continue; } pInvokeInformation = (_entityRegistry.GetOrCreateModuleReference(moduleName, _ => { }), entryPoint, attributes); @@ -3130,7 +3585,9 @@ public GrammarResult VisitMethodDecls(CILParser.MethodDeclsContext context) methodDefinition.ImplementationAttributes = context.implAttr().Aggregate((MethodImplAttributes)0, (acc, attr) => acc | VisitImplAttr(attr)); if (!EntityRegistry.TryAddMethodDefinitionToContainingType(methodDefinition)) { - // TODO: Report duplicate method + ReportError(DiagnosticIds.DuplicateMethod, + DiagnosticMessageTemplates.DuplicateMethod, + context); } return new(methodDefinition); @@ -3205,7 +3662,9 @@ public GrammarResult.String VisitMethodName(CILParser.MethodNameContext context) } if (_expectInstance && (callConv & (byte)SignatureAttributes.Instance) == 0) { - // TODO: Warn for missing instance call-conv + ReportWarning(DiagnosticIds.MissingInstanceCallConv, + DiagnosticMessageTemplates.MissingInstanceCallConv, + context); callConv |= (byte)SignatureAttributes.Instance; } methodRefSignature.WriteByte(callConv); @@ -3243,6 +3702,10 @@ public GrammarResult VisitModuleHead(CILParser.ModuleHeadContext context) _entityRegistry.Module.Name = VisitDottedName(context.dottedName()).Value; return GrammarResult.SentinelValue.Result; } + + // .mscorlib directive indicates the assembly being compiled is mscorlib itself. + // This is currently a no-op; the flag would be used to affect type resolution + // when support for compiling mscorlib is added. public GrammarResult VisitMscorlib(CILParser.MscorlibContext context) => GrammarResult.SentinelValue.Result; GrammarResult ICILVisitor.VisitNameSpaceHead(CILParser.NameSpaceHeadContext context) => VisitNameSpaceHead(context); @@ -3273,7 +3736,9 @@ public GrammarResult.FormattedBlob VisitNativeType(CILParser.NativeTypeContext c { if (arrayPointerInfo[i] is CILParser.PointerNativeTypeContext) { - // TODO: warn on deprecated native type + ReportWarning(DiagnosticIds.DeprecatedNativeType, + string.Format(DiagnosticMessageTemplates.DeprecatedNativeType, "pointer in array"), + context); const int NATIVE_TYPE_PTR = 0x10; prefix.WriteByte(NATIVE_TYPE_PTR); } @@ -3342,7 +3807,9 @@ public GrammarResult.FormattedBlob VisitNativeTypeElement(CILParser.NativeTypeEl CILParser.CompQstringContext[] strings = context.compQstring(); if (strings.Length == 4) { - // TODO: warn on deprecated 4-string form of custom marshaller. + ReportWarning(DiagnosticIds.DeprecatedCustomMarshaller, + DiagnosticMessageTemplates.DeprecatedCustomMarshaller, + context); blob.WriteSerializedString(VisitCompQstring(strings[0]).Value); blob.WriteSerializedString(VisitCompQstring(strings[1]).Value); blob.WriteSerializedString(VisitCompQstring(strings[2]).Value); @@ -3368,7 +3835,9 @@ public GrammarResult.FormattedBlob VisitNativeTypeElement(CILParser.NativeTypeEl blob.LinkSuffix(VisitNativeType(context.nativeType()).Value); break; case CILParser.VARIANT: - // TODO: warn on deprecated native type + ReportWarning(DiagnosticIds.DeprecatedNativeType, + string.Format(DiagnosticMessageTemplates.DeprecatedNativeType, "VARIANT"), + context); const int NATIVE_TYPE_VARIANT = 0xe; blob.WriteByte(NATIVE_TYPE_VARIANT); break; @@ -3378,12 +3847,16 @@ public GrammarResult.FormattedBlob VisitNativeTypeElement(CILParser.NativeTypeEl break; #pragma warning restore CS0618 // Type or member is obsolete case CILParser.SYSCHAR: - // TODO: warn on deprecated native type + ReportWarning(DiagnosticIds.DeprecatedNativeType, + string.Format(DiagnosticMessageTemplates.DeprecatedNativeType, "SYSCHAR"), + context); const int NATIVE_TYPE_SYSCHAR = 0xd; blob.WriteByte(NATIVE_TYPE_SYSCHAR); break; case CILParser.VOID: - // TODO: warn on deprecated native type + ReportWarning(DiagnosticIds.DeprecatedNativeType, + string.Format(DiagnosticMessageTemplates.DeprecatedNativeType, "VOID"), + context); const int NATIVE_TYPE_VOID = 0x1; blob.WriteByte(NATIVE_TYPE_VOID); break; @@ -3424,12 +3897,16 @@ public GrammarResult.FormattedBlob VisitNativeTypeElement(CILParser.NativeTypeEl blob.WriteByte((byte)UnmanagedType.U8); break; case CILParser.DECIMAL: - // TODO: warn on deprecated native type + ReportWarning(DiagnosticIds.DeprecatedNativeType, + string.Format(DiagnosticMessageTemplates.DeprecatedNativeType, "DECIMAL"), + context); const int NATIVE_TYPE_DECIMAL = 0x11; blob.WriteByte(NATIVE_TYPE_DECIMAL); break; case CILParser.DATE: - // TODO: warn on deprecated native type + ReportWarning(DiagnosticIds.DeprecatedNativeType, + string.Format(DiagnosticMessageTemplates.DeprecatedNativeType, "DATE"), + context); const int NATIVE_TYPE_DATE = 0x12; blob.WriteByte(NATIVE_TYPE_DATE); break; @@ -3446,7 +3923,9 @@ public GrammarResult.FormattedBlob VisitNativeTypeElement(CILParser.NativeTypeEl blob.WriteByte((byte)UnmanagedType.LPTStr); break; case CILParser.OBJECTREF: - // TODO: warn on deprecated native type + ReportWarning(DiagnosticIds.DeprecatedNativeType, + string.Format(DiagnosticMessageTemplates.DeprecatedNativeType, "OBJECTREF"), + context); const int NATIVE_TYPE_OBJECTREF = 0x18; blob.WriteByte(NATIVE_TYPE_OBJECTREF); break; @@ -3500,7 +3979,9 @@ public GrammarResult.FormattedBlob VisitNativeTypeElement(CILParser.NativeTypeEl blob.WriteByte((byte)UnmanagedType.SysUInt); break; case CILParser.NESTEDSTRUCT: - // TODO: warn on deprecated native type + ReportWarning(DiagnosticIds.DeprecatedNativeType, + string.Format(DiagnosticMessageTemplates.DeprecatedNativeType, "NESTEDSTRUCT"), + context); const int NATIVE_TYPE_NESTEDSTRUCT = 0x21; blob.WriteByte(NATIVE_TYPE_NESTEDSTRUCT); break; @@ -3777,8 +4258,9 @@ public GrammarResult.FormattedBlob VisitSecAttrSetBlob(CILParser.SecAttrSetBlobC { if (context.PERMISSION() is not null) { - // TODO: Report unsupported error - // Cannot convert individual SecurityAttribute-based permissions to a PermissionSet without a runtime. + ReportError(DiagnosticIds.UnsupportedSecurityDeclaration, + DiagnosticMessageTemplates.UnsupportedSecurityDeclaration, + context); return new(null); } DeclarativeSecurityAction action = VisitSecAction(context.secAction()).Value; diff --git a/src/tools/ilasm/src/ILAssembler/Instructions.g4 b/src/tools/ilasm/src/ILAssembler/Instructions.g4 deleted file mode 100644 index 0919ca5c91955d..00000000000000 --- a/src/tools/ilasm/src/ILAssembler/Instructions.g4 +++ /dev/null @@ -1,20 +0,0 @@ -/* -Licensed to the .NET Foundation under one or more agreements. -The .NET Foundation licenses this file to you under the MIT license. -*/ - -lexer grammar Instructions; - -INSTR_NONE: ('nop'|'break'|'ldarg.0'|'ldarg.1'|'ldarg.2'|'ldarg.3'|'ldloc.0'|'ldloc.1'|'ldloc.2'|'ldloc.3'|'stloc.0'|'stloc.1'|'stloc.2'|'stloc.3'|'ldnull'|'ldc.i4.m1'|'ldc.i4.0'|'ldc.i4.1'|'ldc.i4.2'|'ldc.i4.3'|'ldc.i4.4'|'ldc.i4.5'|'ldc.i4.6'|'ldc.i4.7'|'ldc.i4.8'|'dup'|'pop'|'ret'|'ldind.i1'|'ldind.u1'|'ldind.i2'|'ldind.u2'|'ldind.i4'|'ldind.u4'|'ldind.i8'|'ldind.i'|'ldind.r4'|'ldind.r8'|'ldind.ref'|'stind.ref'|'stind.i1'|'stind.i2'|'stind.i4'|'stind.i8'|'stind.r4'|'stind.r8'|'add'|'sub'|'mul'|'div'|'div.un'|'rem'|'rem.un'|'and'|'or'|'xor'|'shl'|'shr'|'shr.un'|'neg'|'not'|'conv.i1'|'conv.i2'|'conv.i4'|'conv.i8'|'conv.r4'|'conv.r8'|'conv.u4'|'conv.u8'|'conv.r.un'|'throw'|'conv.ovf.i1.un'|'conv.ovf.i2.un'|'conv.ovf.i4.un'|'conv.ovf.i8.un'|'conv.ovf.u1.un'|'conv.ovf.u2.un'|'conv.ovf.u4.un'|'conv.ovf.u8.un'|'conv.ovf.i.un'|'conv.ovf.u.un'|'ldlen'|'ldelem.i1'|'ldelem.u1'|'ldelem.i2'|'ldelem.u2'|'ldelem.i4'|'ldelem.u4'|'ldelem.i8'|'ldelem.i'|'ldelem.r4'|'ldelem.r8'|'ldelem.ref'|'stelem.i'|'stelem.i1'|'stelem.i2'|'stelem.i4'|'stelem.i8'|'stelem.r4'|'stelem.r8'|'stelem.ref'|'conv.ovf.i1'|'conv.ovf.u1'|'conv.ovf.i2'|'conv.ovf.u2'|'conv.ovf.i4'|'conv.ovf.u4'|'conv.ovf.i8'|'conv.ovf.u8'|'ckfinite'|'conv.u2'|'conv.u1'|'conv.i'|'conv.ovf.i'|'conv.ovf.u'|'add.ovf'|'add.ovf.un'|'mul.ovf'|'mul.ovf.un'|'sub.ovf'|'sub.ovf.un'|'endfinally'|'stind.i'|'conv.u'|'prefix7'|'prefix6'|'prefix5'|'prefix4'|'prefix3'|'prefix2'|'prefix1'|'prefixref'|'arglist'|'ceq'|'cgt'|'cgt.un'|'clt'|'clt.un'|'localloc'|'endfilter'|'volatile.'|'tail.'|'cpblk'|'initblk'|'rethrow'|'refanytype'|'readonly.'|'illegal'|'endmac'); -INSTR_VAR: ('ldarg.s'|'ldarga.s'|'starg.s'|'ldloc.s'|'ldloca.s'|'stloc.s'|'ldarg'|'ldarga'|'starg'|'ldloc'|'ldloca'|'stloc'); -INSTR_I: ('ldc.i4.s'|'ldc.i4'|'unaligned.'|'no.'); -INSTR_I8: ('ldc.i8'); -INSTR_R: ('ldc.r4'|'ldc.r8'); -INSTR_METHOD: ('jmp'|'call'|'callvirt'|'newobj'|'ldftn'|'ldvirtftn'); -INSTR_SIG: ('calli'); -INSTR_BRTARGET: ('br.s'|'brfalse.s'|'brtrue.s'|'beq.s'|'bge.s'|'bgt.s'|'ble.s'|'blt.s'|'bne.un.s'|'bge.un.s'|'bgt.un.s'|'ble.un.s'|'blt.un.s'|'br'|'brfalse'|'brtrue'|'beq'|'bge'|'bgt'|'ble'|'blt'|'bne.un'|'bge.un'|'bgt.un'|'ble.un'|'blt.un'|'leave'|'leave.s'); -INSTR_SWITCH: ('switch'); -INSTR_TYPE: ('cpobj'|'ldobj'|'castclass'|'isinst'|'unbox'|'stobj'|'box'|'newarr'|'ldelema'|'ldelem'|'stelem'|'unbox.any'|'refanyval'|'mkrefany'|'initobj'|'constrained.'|'sizeof'); -INSTR_STRING: ('ldstr'); -INSTR_FIELD: ('ldfld'|'ldflda'|'stfld'|'ldsfld'|'ldsflda'|'stsfld'); -INSTR_TOK: ('ldtoken'); diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs index a179998b4bff5b..725d4b99cb645d 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs @@ -411,9 +411,102 @@ .class extern NonExistentParent Assert.All(diagnostics, d => Assert.Equal(DiagnosticSeverity.Error, d.Severity)); } - // Note: Tests for ByteArrayTooShort (ILA0016), ArgumentNotFound (ILA0018), LocalNotFound (ILA0019), - // and LabelNotFound (ILA0017) require method body parsing which currently has a pre-existing - // bug in EntityRegistry.WriteContentTo. These tests are deferred until those bugs are fixed. + [Fact] + public void Diagnostic_ByteArrayTooShort() + { + // A bytearray that's too short for the data type being loaded + string source = """ + .assembly extern mscorlib { } + .assembly test { } + + .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object + { + .method public static float64 TestMethod() cil managed + { + ldc.r8 bytearray (AA BB) + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.ByteArrayTooShort, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_ArgumentNotFound() + { + // Reference an argument that doesn't exist + string source = """ + .assembly extern mscorlib { } + .assembly test { } + + .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object + { + .method public void TestMethod(int32 x) cil managed + { + ldarg NonExistentArg + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.ArgumentNotFound, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_LocalNotFound() + { + // Reference a local variable that doesn't exist + string source = """ + .assembly extern mscorlib { } + .assembly test { } + + .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object + { + .method public static void TestMethod() cil managed + { + .locals (int32 x) + ldloc NonExistentLocal + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.LocalNotFound, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_LabelNotFound() + { + // Reference an undefined label in a branch instruction + string source = """ + .assembly extern mscorlib { } + .assembly test { } + + .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object + { + .method public static void TestMethod() cil managed + { + br UndefinedLabel + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.LabelNotFound, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } [Fact] public void ClassLayout_PackAndSize() @@ -608,6 +701,349 @@ .field [8] public static float32 FloatField at FloatData Assert.NotEqual(byteRva, floatRva); } + [Fact] + public void LanguageDecl_DoesNotThrow() + { + string source = """ + .assembly test { } + .language "C#" "3.0" + .class public auto ansi beforefieldinit Test + { + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + Assert.Empty(diagnostics); + } + + [Fact] + public void LanguageDecl_MultipleParameters_DoesNotThrow() + { + string source = """ + .assembly test { } + .language "C#" "3.0" "vendor" + .class public auto ansi beforefieldinit Test + { + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + Assert.Empty(diagnostics); + } + + [Fact] + public void ExtSourceSpec_LineDirective_DoesNotThrow() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + .method public static void TestMethod() cil managed + { + .line 10 "test.cs" + nop + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + Assert.Empty(diagnostics); + } + + [Fact] + public void ExtSourceSpec_LineWithColumn_DoesNotThrow() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + .method public static void TestMethod() cil managed + { + .line 10 : 5 "test.cs" + nop + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + Assert.Empty(diagnostics); + } + + [Fact] + public void ExtSourceSpec_LineDirectiveHashLine_DoesNotThrow() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + .method public static void TestMethod() cil managed + { + #line 42 "program.cs" + nop + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + Assert.Empty(diagnostics); + } + + [Fact] + public void ExportHead_ObsoleteSyntax_DoesNotThrow() + { + string source = """ + .assembly test { } + .export [System.Object] + .class public auto ansi beforefieldinit Test + { + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + Assert.Empty(diagnostics); + } + + [Fact] + public void FieldInit_ByteArray_DoesNotThrow() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + .field static int32 field1 at 0 + } + .data data1 = bytearray (00 01 02 03) + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + Assert.Empty(diagnostics); + } + + [Fact] + public void Diagnostic_AbstractMethodNotInAbstractType() + { + string source = """ + .assembly test { } + .class public auto ansi Test + { + .method public abstract void AbstractMethod() cil managed + { + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.AbstractMethodNotInAbstractType, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_InvalidPInvokeSignature() + { + // P/Invoke with no module name triggers InvalidPInvokeSignature + string source = """ + .assembly test { } + .class public auto ansi Test + { + .method public static pinvokeimpl() void TestMethod() cil managed + { + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.InvalidPInvokeSignature, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_DeprecatedNativeType_Variant() + { + // Using deprecated VARIANT native type triggers warning + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void TestMethod(object marshal(variant) arg) cil managed + { + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var warning = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.DeprecatedNativeType, warning.Id); + Assert.Equal(DiagnosticSeverity.Warning, warning.Severity); + } + + [Fact] + public void Diagnostic_DeprecatedCustomMarshaller() + { + // Using 4-string custom marshaller syntax triggers warning + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void TestMethod(object marshal(custom("guid", "nativeType", "marshallerType", "cookie")) arg) cil managed + { + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var warning = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.DeprecatedCustomMarshaller, warning.Id); + Assert.Equal(DiagnosticSeverity.Warning, warning.Severity); + } + + [Fact] + public void Diagnostic_UnsupportedSecurityDeclaration() + { + // Using .permission instead of .permissionset triggers error + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void TestMethod() cil managed + { + .permission demand [mscorlib]System.Security.Permissions.SecurityPermissionAttribute + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.UnsupportedSecurityDeclaration, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_GenericParameterIndexOutOfRange() + { + // Referencing generic parameter index that doesn't exist + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void TestMethod() cil managed + { + .param type [99] + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.GenericParameterIndexOutOfRange, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_ParameterIndexOutOfRange() + { + // Referencing parameter index that doesn't exist + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void TestMethod(int32 x) cil managed + { + .param [99] + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.ParameterIndexOutOfRange, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_DuplicateMethod() + { + string source = """ + .assembly test { } + .class public auto ansi Test + { + .method public static void TestMethod() cil managed + { + ret + } + .method public static void TestMethod() cil managed + { + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = diagnostics.FirstOrDefault(d => d.Id == DiagnosticIds.DuplicateMethod); + Assert.NotNull(error); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Typedef_ResolvedInTypeContext() + { + // .typedef className as alias syntax + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .typedef [mscorlib]System.Object as MyObject + .class public auto ansi Test + { + .field public class MyObject obj + .method public static void TestMethod() cil managed + { + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + // Should compile without errors when typedef is resolved + Assert.Empty(diagnostics); + } + + [Fact] + public void Typedef_TypeBlob_Compiles() + { + // .typedef type as alias syntax + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .typedef int32 as MyInt + .class public auto ansi Test + { + .field public MyInt val + .method public static void TestMethod() cil managed + { + ret + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + // Typedef type blob resolution should compile + Assert.Empty(diagnostics); + } + private static PEReader CompileAndGetReader(string source, Options options) { var sourceText = new SourceText(source, "test.il"); @@ -635,5 +1071,76 @@ private static ImmutableArray CompileAndGetDiagnostics(string source }, _ => { Assert.Fail("Expected no resources"); return default; }, options); return diagnostics; } + + [Fact] + public void PdbGeneration_WithLineAndLanguageDirectives_CreatesValidEmbeddedPdb() + { + // Use C# language GUID + string csharpGuid = "{3F5162F8-07C6-11D3-9053-00C04FA302A1}"; + string source = $$""" + .assembly test { } + .language '{{csharpGuid}}' + .class public auto ansi beforefieldinit Test + { + .method public static void TestMethod() cil managed + { + .line 10 "test.cs" + nop + .line 15 "test.cs" + nop + .line 20 "test.cs" + ret + } + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + + // Verify debug directory exists with embedded PDB + var debugDirectory = pe.ReadDebugDirectory(); + Assert.NotEmpty(debugDirectory); + + var embeddedPdbEntry = debugDirectory.FirstOrDefault(e => e.Type == DebugDirectoryEntryType.EmbeddedPortablePdb); + Assert.NotEqual(default, embeddedPdbEntry); + + // Read the embedded PDB and verify contents + var pdbProvider = pe.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry); + var pdbReader = pdbProvider.GetMetadataReader(); + + // Verify document exists with correct name and language + Assert.NotEmpty(pdbReader.Documents); + var document = pdbReader.GetDocument(pdbReader.Documents.First()); + var docName = pdbReader.GetString(document.Name); + Assert.Contains("test.cs", docName); + + var languageGuid = pdbReader.GetGuid(document.Language); + Assert.Equal(Guid.Parse(csharpGuid), languageGuid); + + // Verify method debug info exists (sequence points were recorded) + Assert.NotEmpty(pdbReader.MethodDebugInformation); + } + + [Fact] + public void PdbGeneration_WithoutLineDirectives_NoPdbGenerated() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + .method public static void TestMethod() cil managed + { + nop + ret + } + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + + // Verify no embedded PDB when no debug directives + var debugDirectory = pe.ReadDebugDirectory(); + var embeddedPdbEntry = debugDirectory.FirstOrDefault(e => e.Type == DebugDirectoryEntryType.EmbeddedPortablePdb); + Assert.Equal(default, embeddedPdbEntry); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/ILAssembler.Tests.csproj b/src/tools/ilasm/tests/ILAssembler.Tests/ILAssembler.Tests.csproj index 53bf0b7dd22a92..eafd03a265498c 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/ILAssembler.Tests.csproj +++ b/src/tools/ilasm/tests/ILAssembler.Tests/ILAssembler.Tests.csproj @@ -9,6 +9,7 @@ +