diff --git a/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs b/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs index 1755fcc15920bf..cb1a2a8a51eb1d 100644 --- a/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs +++ b/src/tools/ilasm/src/ILAssembler/EntityRegistry.cs @@ -17,7 +17,7 @@ internal sealed class EntityRegistry private readonly Dictionary> _seenEntities = new(); private readonly Dictionary<(TypeDefinitionEntity? ContainingType, string Namespace, string Name), TypeDefinitionEntity> _seenTypeDefs = new(); private readonly Dictionary<(EntityBase ResolutionScope, string Namespace, string Name), TypeReferenceEntity> _seenTypeRefs = new(); - private readonly Dictionary _seenAssemblyRefs = new(); + private readonly Dictionary _seenAssemblyRefs = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _seenModuleRefs = new(); private readonly Dictionary _seenTypeSpecs = new(new BlobBuilderContentEqualityComparer()); private readonly Dictionary _seenStandaloneSignatures = new(new BlobBuilderContentEqualityComparer()); @@ -92,7 +92,7 @@ private IReadOnlyList GetSeenEntities(TableIndex table) return Array.Empty(); } - public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream) + public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream, IReadOnlyDictionary mappedFieldDataNames) { // Now that we've seen all of the entities, we can write them out in the correct order. // Record the entities in the correct order so they are assigned handles. @@ -187,7 +187,14 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream) (TypeDefinitionHandle)type.Handle, (PropertyDefinitionHandle)GetHandleForList(type.Properties, GetSeenEntities(TableIndex.TypeDef), type => ((TypeDefinitionEntity)type).Properties, i, TableIndex.Property)); - // TODO: ClassLayout + if (type.PackingSize is not null || type.ClassSize is not null) + { + builder.AddTypeLayout( + (TypeDefinitionHandle)type.Handle, + (ushort)(type.PackingSize ?? 0), + (uint)(type.ClassSize ?? 0)); + } + if (type.ContainingType is not null) { builder.AddNestedType((TypeDefinitionHandle)type.Handle, (TypeDefinitionHandle)type.ContainingType.Handle); @@ -201,7 +208,16 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream) builder.GetOrAddString(fieldDef.Name), fieldDef.Signature!.Count == 0 ? default : builder.GetOrAddBlob(fieldDef.Signature)); - // TODO: FieldLayout, FieldRVA + if (fieldDef.Offset is not null) + { + builder.AddFieldLayout((FieldDefinitionHandle)fieldDef.Handle, fieldDef.Offset.Value); + } + + if (fieldDef.DataDeclarationName is not null && mappedFieldDataNames.TryGetValue(fieldDef.DataDeclarationName, out int dataOffset)) + { + builder.AddFieldRelativeVirtualAddress((FieldDefinitionHandle)fieldDef.Handle, dataOffset); + } + if (fieldDef.MarshallingDescriptor is not null) { builder.AddMarshallingDescriptor(fieldDef.Handle, builder.GetOrAddBlob(fieldDef.MarshallingDescriptor)); @@ -310,6 +326,17 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream) builder.AddModuleReference(builder.GetOrAddString(moduleRef.Name)); } + foreach (AssemblyReferenceEntity asmRef in GetSeenEntities(TableIndex.AssemblyRef)) + { + builder.AddAssemblyReference( + builder.GetOrAddString(asmRef.Name), + asmRef.Version ?? new Version(), + asmRef.Culture is null ? default : builder.GetOrAddString(asmRef.Culture), + asmRef.PublicKeyOrToken is null ? default : builder.GetOrAddBlob(asmRef.PublicKeyOrToken), + asmRef.Flags, + asmRef.Hash is null ? default : builder.GetOrAddBlob(asmRef.Hash)); + } + foreach (TypeSpecificationEntity typeSpec in GetSeenEntities(TableIndex.TypeSpec)) { builder.AddTypeSpecification(builder.GetOrAddBlob(typeSpec.Signature)); @@ -338,8 +365,8 @@ public void WriteContentTo(MetadataBuilder builder, BlobBuilder ilStream) { builder.AddExportedType( exportedType.Attributes, - builder.GetOrAddString(exportedType.Name), builder.GetOrAddString(exportedType.Namespace), + builder.GetOrAddString(exportedType.Name), exportedType.Implementation?.Handle ?? default, exportedType.TypeDefinitionId); } @@ -439,8 +466,13 @@ public TypeEntity SystemEnumType private TypeReferenceEntity ResolveFromCoreAssembly(string typeName) { - // TODO: System.Private.CoreLib as the core assembly? - var coreAsmRef = GetOrCreateAssemblyReference("mscorlib", new Version(4, 0), culture: null, publicKeyOrToken: null, 0, ProcessorArchitecture.None); + // Match native ilasm behavior: check for assembly refs in order of preference, + // then fall back to creating mscorlib if none found + AssemblyReferenceEntity coreAsmRef = FindAssemblyReference("System.Private.CoreLib") + ?? FindAssemblyReference("System.Runtime") + ?? FindAssemblyReference("mscorlib") + ?? FindAssemblyReference("netstandard") + ?? GetOrCreateAssemblyReference("mscorlib", new Version(4, 0), culture: null, publicKeyOrToken: null, 0, ProcessorArchitecture.None); return GetOrCreateTypeReference(coreAsmRef, new TypeName(null, typeName)); } @@ -500,7 +532,7 @@ public TypeDefinitionEntity GetOrCreateTypeDefinition(TypeDefinitionEntity? cont public AssemblyReferenceEntity GetOrCreateAssemblyReference(string name, Action onCreateAssemblyReference) { - return GetOrCreateEntity(new(name), TableIndex.AssemblyRef, _seenAssemblyRefs, _ => new(name), onCreateAssemblyReference); + return GetOrCreateEntity(name, TableIndex.AssemblyRef, _seenAssemblyRefs, _ => new(name), onCreateAssemblyReference); } public ModuleReferenceEntity GetOrCreateModuleReference(string name, Action onCreateModuleReference) @@ -573,8 +605,14 @@ public TypeReferenceEntity GetOrCreateTypeReference(EntityBase resolutionContext builder.AppendFormat("{0}.{1}", typeRef.Namespace, typeRef.Name); if (resolutionContext is AssemblyReferenceEntity asmRef) { - // TODO: Do full assembly name here - builder.Append(asmRef.Name); + var assemblyNameInfo = new AssemblyNameInfo( + asmRef.Name, + asmRef.Version, + string.IsNullOrEmpty(asmRef.Culture) ? null : asmRef.Culture, + asmRef.PublicKeyOrToken is null ? AssemblyNameFlags.None : AssemblyNameFlags.PublicKey, + asmRef.PublicKeyOrToken?.ToImmutableArray() ?? []); + builder.Append(", "); + builder.Append(assemblyNameInfo.FullName); } typeRef.ReflectionNotation = builder.ToString(); }); @@ -757,7 +795,13 @@ public BlobOrHandle GetModifiedType(BlobOrHandle modifier, BlobOrHandle unmodifi unmodifiedType.WriteBlobTo(builder); return builder; } - public BlobOrHandle GetPinnedType(BlobOrHandle elementType) => throw new NotImplementedException(); + public BlobOrHandle GetPinnedType(BlobOrHandle elementType) + { + var builder = new BlobBuilder(); + builder.WriteByte((byte)SignatureTypeCode.Pinned); + elementType.WriteBlobTo(builder); + return builder; + } public BlobOrHandle GetPointerType(BlobOrHandle elementType) { var paramEncoder = new ParameterTypeEncoder(new BlobBuilder()); @@ -929,25 +973,16 @@ public FileEntity GetOrCreateFile(string name, bool hasMetadata, BlobBuilder? ha public AssemblyReferenceEntity? FindAssemblyReference(string name) { - if (_seenAssemblyRefs.TryGetValue(new AssemblyName(name), out var file)) + if (_seenAssemblyRefs.TryGetValue(name, out var asmRef)) { - return file; + return asmRef; } return null; } public AssemblyReferenceEntity GetOrCreateAssemblyReference(string name, Version version, string? culture, BlobBuilder? publicKeyOrToken, AssemblyFlags flags, ProcessorArchitecture architecture) { - AssemblyName key = new AssemblyName(name) - { - Version = version, - CultureName = culture, - Flags = (AssemblyNameFlags)flags, -#pragma warning disable SYSLIB0037 // ProcessorArchitecture is obsolete - ProcessorArchitecture = architecture -#pragma warning restore SYSLIB0037 // ProcessorArchitecture is obsolete - }; - return GetOrCreateEntity(key, TableIndex.AssemblyRef, _seenAssemblyRefs, (value) => new AssemblyReferenceEntity(name), entity => + return GetOrCreateEntity(name, TableIndex.AssemblyRef, _seenAssemblyRefs, _ => new AssemblyReferenceEntity(name), entity => { entity.Version = version; entity.Culture = culture; @@ -1096,6 +1131,10 @@ static string CreateReflectionNotation(TypeDefinitionEntity typeDefinition) public List InterfaceImplementations { get; } = new(); public string ReflectionNotation { get; } + + // ClassLayout table fields + public int? PackingSize { get; set; } + public int? ClassSize { get; set; } } public sealed class TypeReferenceEntity(EntityBase resolutionScope, string @namespace, string name) : TypeEntity, IHasReflectionNotation @@ -1251,6 +1290,9 @@ public sealed class FieldDefinitionEntity(FieldAttributes attributes, TypeDefini public BlobBuilder? MarshallingDescriptor { get; set; } public string? DataDeclarationName { get; set; } + + // FieldLayout table field (explicit field offset) + public int? Offset { get; set; } } public sealed class InterfaceImplementationEntity(TypeDefinitionEntity type, TypeEntity interfaceType) : EntityBase diff --git a/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs b/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs index f27e348632b9bb..c75506b2af541b 100644 --- a/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs +++ b/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs @@ -130,7 +130,7 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC } BlobBuilder ilStream = new(); - _entityRegistry.WriteContentTo(_metadataBuilder, ilStream); + _entityRegistry.WriteContentTo(_metadataBuilder, ilStream, _mappedFieldDataNames); MetadataRootBuilder rootBuilder = new(_metadataBuilder); PEHeaderBuilder header = new( fileAlignment: _alignment, @@ -389,6 +389,11 @@ public GrammarResult.Literal VisitCallConv(CILParser.CallConvContext conte GrammarResult ICILVisitor.VisitCallKind(CILParser.CallKindContext context) => VisitCallKind(context); public GrammarResult.Literal VisitCallKind(CILParser.CallKindContext context) { + // callKind can be empty (/* EMPTY */) - return Default in that case + if (context.ChildCount == 0) + { + return new(SignatureCallingConvention.Default); + } int childType = context.GetChild(context.ChildCount - 1).Symbol.Type; return new(childType switch { @@ -558,6 +563,24 @@ public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) { _ = VisitFieldDecl(fieldDecl); } + else if (context.int32() is {} int32) + { + // .pack or .size + string keyword = context.GetChild(0).GetText(); + int value = VisitInt32(int32).Value; + var currentType = _currentTypeDefinition.PeekOrDefault(); + if (currentType is not null) + { + if (keyword == ".pack") + { + currentType.PackingSize = value; + } + else if (keyword == ".size") + { + currentType.ClassSize = value; + } + } + } return GrammarResult.SentinelValue.Result; } @@ -1609,7 +1632,7 @@ public static GrammarResult.Flag VisitExptAttr(CILParser.ExptAtt if (declarations[i].mdtoken() is { } mdToken) { var entity = VisitMdtoken(mdToken).Value; - if (entity is null) + if (entity is null or EntityRegistry.FakeTypeEntity) { ReportError(DiagnosticIds.InvalidMetadataToken, DiagnosticMessageTemplates.InvalidMetadataToken, declarations[i]); } @@ -1617,7 +1640,7 @@ public static GrammarResult.Flag VisitExptAttr(CILParser.ExptAtt continue; } string kind = declarations[i].GetText(); - if (kind == ".file") + if (kind.StartsWith(".file")) { string fileName = VisitDottedName(declarations[i].dottedName()).Value; implementationEntity = _entityRegistry.FindFile(fileName); @@ -1626,7 +1649,7 @@ public static GrammarResult.Flag VisitExptAttr(CILParser.ExptAtt ReportError(DiagnosticIds.FileNotFound, string.Format(DiagnosticMessageTemplates.FileNotFound, fileName), declarations[i]); } } - else if (kind == ".assembly") + else if (kind.StartsWith(".assembly")) { string assemblyName = VisitDottedName(declarations[i].dottedName()).Value; implementationEntity = _entityRegistry.FindAssemblyReference(assemblyName); @@ -1635,7 +1658,7 @@ public static GrammarResult.Flag VisitExptAttr(CILParser.ExptAtt ReportError(DiagnosticIds.AssemblyNotFound, string.Format(DiagnosticMessageTemplates.AssemblyNotFound, assemblyName), declarations[i]); } } - else if (kind == ".class") + else if (kind.StartsWith(".class")) { if (declarations[i].int32() is CILParser.Int32Context int32) { @@ -1804,6 +1827,7 @@ public GrammarResult VisitFieldDecl(CILParser.FieldDeclContext context) var marshalBlob = marshalBlobs.Length > 0 ? VisitMarshalBlob(marshalBlobs[marshalBlobs.Length - 1]).Value : null; var name = VisitDottedName(context.dottedName()).Value; var rvaOffset = VisitAtOpt(context.atOpt()).Value; + var fieldOffset = VisitRepeatOpt(context.repeatOpt()).Value; _ = VisitInitOpt(context.initOpt()); var signature = new BlobEncoder(new BlobBuilder()); @@ -1816,6 +1840,7 @@ public GrammarResult VisitFieldDecl(CILParser.FieldDeclContext context) { field.MarshallingDescriptor = marshalBlob; field.DataDeclarationName = rvaOffset; + field.Offset = fieldOffset; } return GrammarResult.SentinelValue.Result; @@ -2706,7 +2731,15 @@ public GrammarResult.FormattedBlob VisitMarshalBlob(CILParser.MarshalBlobContext } GrammarResult ICILVisitor.VisitMarshalClause(CILParser.MarshalClauseContext context) => VisitMarshalClause(context); - public GrammarResult.FormattedBlob VisitMarshalClause(CILParser.MarshalClauseContext context) => VisitMarshalBlob(context.marshalBlob()); + public GrammarResult.FormattedBlob VisitMarshalClause(CILParser.MarshalClauseContext context) + { + if (context.ChildCount == 0) + { + return new(new BlobBuilder(0)); + } + + return VisitMarshalBlob(context.marshalBlob()); + } GrammarResult ICILVisitor.VisitMdtoken(ILAssembler.CILParser.MdtokenContext context) => VisitMdtoken(context); public GrammarResult.Literal VisitMdtoken(CILParser.MdtokenContext context) @@ -3990,6 +4023,8 @@ public GrammarResult.Literal VisitSimpleType(CILParser.Simple CILParser.INT16 => SignatureTypeCode.Int16, CILParser.INT32_ => SignatureTypeCode.Int32, CILParser.INT64_ => SignatureTypeCode.Int64, + CILParser.FLOAT32 => SignatureTypeCode.Single, + CILParser.FLOAT64_ => SignatureTypeCode.Double, CILParser.UINT8 => SignatureTypeCode.Byte, CILParser.UINT16 => SignatureTypeCode.UInt16, CILParser.UINT32 => SignatureTypeCode.UInt32, @@ -4085,9 +4120,18 @@ public static GrammarResult.Literal VisitTruefalse(CILParser.TruefalseCont } GrammarResult ICILVisitor.VisitTyBound(CILParser.TyBoundContext context) => VisitTyBound(context); - public GrammarResult.Sequence VisitTyBound(CILParser.TyBoundContext context) + public GrammarResult.Sequence VisitTyBound(CILParser.TyBoundContext? context) { - return new(VisitTypeList(context.typeList()).Value.Select(EntityRegistry.CreateGenericConstraint).ToImmutableArray()); + // context or typeList can be null when there are no constraints + if (context?.typeList() is not CILParser.TypeListContext typeList) + { + return new(ImmutableArray.Empty); + } + // Filter out null types (from unresolved type parameters) before creating constraints + return new(VisitTypeList(typeList).Value + .Where(t => t is not null) + .Select(EntityRegistry.CreateGenericConstraint) + .ToImmutableArray()); } GrammarResult ICILVisitor.VisitTypar(CILParser.TyparContext context) => VisitTypar(context); diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs index f37d1aea0697c4..a179998b4bff5b 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs @@ -233,6 +233,381 @@ .typedef [.module NonExistentModule]SomeType as MyType Assert.Equal(DiagnosticSeverity.Error, error.Severity); } + [Fact] + public void MethodTypeParameterOutsideMethod_ReportsError() + { + // Using !!T (method type parameter by name) outside a method should report an error + string source = """ + .assembly extern System.Runtime { } + .class public auto ansi beforefieldinit Test + { + .field public !!T myField + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.MethodTypeParameterOutsideMethod, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_NoBaseType() + { + // Using .base when the current type has no base type (interface) + string source = """ + .assembly extern mscorlib { } + .class interface public abstract auto ansi Test + { + .class interface nested public abstract auto ansi Nested + implements .base + { + } + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.NoBaseType, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_UnsealedValueType() + { + // A value type that extends System.ValueType but is not sealed (warning, auto-sealed) + string source = """ + .assembly extern System.Runtime { } + .assembly test { } + .class public sequential ansi beforefieldinit MyStruct + extends [System.Runtime]System.ValueType + { + .field public int32 value + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.UnsealedValueType, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_GenericParameterNotFound() + { + // Referencing a non-existent type parameter by name in a field + string source = """ + .class public auto ansi beforefieldinit Test`1 + { + .field public !NonExistent field1 + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.GenericParameterNotFound, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_LiteralOutOfRange() + { + // An integer literal that overflows + string source = """ + .class public auto ansi beforefieldinit Test + { + .pack 99999999999999999999999999999999 + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.LiteralOutOfRange, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_InvalidMetadataToken() + { + // Reference an invalid token in an exported type declaration + // Uses an assembly reference instead of a file to avoid file entry point issues + string source = """ + .assembly extern mscorlib { } + .assembly extern ForwardedAssembly { } + .assembly test { } + .class extern public MyExportedType + { + .assembly extern ForwardedAssembly + mdtoken(0x99999999) + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.InvalidMetadataToken, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_FileNotFound() + { + // Reference a file that doesn't exist in an exported type declaration + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class extern public MyExportedType + { + .file NonExistentFile.dll + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.FileNotFound, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_AssemblyNotFound() + { + // Reference an assembly that doesn't exist in an exported type declaration + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class extern public MyExportedType + { + .assembly extern NonExistentAssembly + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + var error = Assert.Single(diagnostics); + Assert.Equal(DiagnosticIds.AssemblyNotFound, error.Id); + Assert.Equal(DiagnosticSeverity.Error, error.Severity); + } + + [Fact] + public void Diagnostic_ExportedTypeNotFound() + { + // Reference a nested exported type that doesn't exist + // Uses assembly references instead of files to avoid file entry point issues + string source = """ + .assembly extern mscorlib { } + .assembly extern ForwardedAssembly { } + .assembly test { } + .class extern public MyExportedType + { + .assembly extern ForwardedAssembly + } + .class extern public NestedType + { + .class extern NonExistentParent + } + """; + + var diagnostics = CompileAndGetDiagnostics(source, new Options()); + Assert.NotEmpty(diagnostics); + Assert.All(diagnostics, d => Assert.Equal(DiagnosticIds.ExportedTypeNotFound, d.Id)); + 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 ClassLayout_PackAndSize() + { + // Test .pack and .size directives for explicit struct layout + string source = """ + .class public sequential ansi sealed beforefieldinit TestStruct + extends [System.Runtime]System.ValueType + { + .pack 4 + .size 16 + .field public int32 field1 + } + .assembly extern System.Runtime { } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var typeHandle = reader.TypeDefinitions + .First(h => reader.GetString(reader.GetTypeDefinition(h).Name) == "TestStruct"); + + var layout = reader.GetTypeDefinition(typeHandle).GetLayout(); + Assert.Equal(4, layout.PackingSize); + Assert.Equal(16, (int)layout.Size); + } + + [Fact] + public void FieldLayout_ExplicitOffset() + { + // Test explicit field offset with [n] syntax + string source = """ + .class public explicit ansi sealed beforefieldinit UnionStruct + extends [System.Runtime]System.ValueType + { + .field [0] public int32 intValue + .field [0] public float32 floatValue + .field [0] public float64 doubleValue + } + .assembly extern System.Runtime { } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var typeHandle = reader.TypeDefinitions + .First(h => reader.GetString(reader.GetTypeDefinition(h).Name) == "UnionStruct"); + + var fields = reader.GetTypeDefinition(typeHandle).GetFields() + .Select(reader.GetFieldDefinition).ToArray(); + Assert.Equal(3, fields.Length); + + // All fields should have offset 0, creating a union + Assert.Equal(0, fields[0].GetOffset()); + Assert.Equal(0, fields[1].GetOffset()); + Assert.Equal(0, fields[2].GetOffset()); + } + + [Fact] + public void CoreAssemblyResolution_PrefersSystemRuntime() + { + // When System.Runtime is referenced, implicit base types should use it + // A class with no explicit extends clause implicitly extends System.Object + string source = """ + .assembly extern System.Runtime { } + .assembly test { } + .class public auto ansi beforefieldinit Test + { + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + // Verify System.Runtime is the only assembly reference (no mscorlib created) + var asmRefs = reader.AssemblyReferences.Select(reader.GetAssemblyReference).ToArray(); + Assert.Single(asmRefs); + Assert.Equal("System.Runtime", reader.GetString(asmRefs[0].Name)); + + // Verify System.Object is referenced from System.Runtime + var typeRefs = reader.TypeReferences.Select(reader.GetTypeReference).ToArray(); + var objectRef = typeRefs.Single(t => reader.GetString(t.Name) == "Object"); + Assert.Equal("System", reader.GetString(objectRef.Namespace)); + Assert.Equal(asmRefs[0].Name, reader.GetAssemblyReference((AssemblyReferenceHandle)objectRef.ResolutionScope).Name); + } + + [Fact] + public void CoreAssemblyResolution_PrefersSystemPrivateCoreLib() + { + // When System.Private.CoreLib is referenced, it should be preferred over System.Runtime + string source = """ + .assembly extern System.Private.CoreLib { } + .assembly extern System.Runtime { } + .assembly test { } + .class public auto ansi beforefieldinit Test + { + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + // Both assemblies should be referenced + var asmRefs = reader.AssemblyReferences.Select(reader.GetAssemblyReference) + .Select(a => reader.GetString(a.Name)).ToArray(); + Assert.Contains("System.Private.CoreLib", asmRefs); + Assert.Contains("System.Runtime", asmRefs); + + // Verify System.Object is referenced from System.Private.CoreLib (preferred) + var typeRefs = reader.TypeReferences.Select(reader.GetTypeReference).ToArray(); + var objectRef = typeRefs.Single(t => reader.GetString(t.Name) == "Object"); + var resolvedAsm = reader.GetAssemblyReference((AssemblyReferenceHandle)objectRef.ResolutionScope); + Assert.Equal("System.Private.CoreLib", reader.GetString(resolvedAsm.Name)); + } + + [Fact] + public void CoreAssemblyResolution_FallsBackToMscorlib() + { + // When no core assembly is explicitly referenced, mscorlib should be created + // for implicit base type resolution. A class with no explicit extends clause + // implicitly extends System.Object. + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + // mscorlib should be created as fallback for System.Object base type + var asmRefs = reader.AssemblyReferences.Select(reader.GetAssemblyReference) + .Select(a => reader.GetString(a.Name)).ToArray(); + Assert.Contains("mscorlib", asmRefs); + + // Verify System.Object is referenced from mscorlib + var typeRefs = reader.TypeReferences.Select(reader.GetTypeReference).ToArray(); + var objectRef = typeRefs.Single(t => reader.GetString(t.Name) == "Object"); + Assert.Equal("System", reader.GetString(objectRef.Namespace)); + var resolvedAsm = reader.GetAssemblyReference((AssemblyReferenceHandle)objectRef.ResolutionScope); + Assert.Equal("mscorlib", reader.GetString(resolvedAsm.Name)); + } + + [Fact] + public void FieldRVA_MultipleDataSections() + { + // Test multiple .data declarations with different data types + string source = """ + .assembly extern mscorlib { } + .assembly test { } + + .data IntData = int32(0x12345678) + .data ByteData = bytearray (AA BB CC DD EE FF) + .data FloatData = float32(3.14159) + + .class public explicit ansi sealed beforefieldinit DataHolder extends [mscorlib]System.ValueType + { + .size 16 + .field [0] public static int32 IntField at IntData + .field [4] public static int32 ByteField at ByteData + .field [8] public static float32 FloatField at FloatData + } + """; + + using var pe = CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var testType = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .First(t => reader.GetString(t.Name) == "DataHolder"); + + var fields = testType.GetFields() + .Select(reader.GetFieldDefinition) + .ToDictionary(f => reader.GetString(f.Name)); + + // Verify IntField RVA and data (little-endian: 0x12345678 = 78 56 34 12) + int intRva = fields["IntField"].GetRelativeVirtualAddress(); + Assert.NotEqual(0, intRva); + + // Verify ByteField RVA + int byteRva = fields["ByteField"].GetRelativeVirtualAddress(); + Assert.NotEqual(0, byteRva); + + // Verify FloatField has an RVA + int floatRva = fields["FloatField"].GetRelativeVirtualAddress(); + Assert.NotEqual(0, floatRva); + + // Each field should point to different data locations + Assert.NotEqual(intRva, byteRva); + Assert.NotEqual(intRva, floatRva); + Assert.NotEqual(byteRva, floatRva); + } + private static PEReader CompileAndGetReader(string source, Options options) { var sourceText = new SourceText(source, "test.il");