From f4dd43a38cd32c14356deaafd412b7d280a3a62d Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 1 Jul 2026 11:51:06 -0400 Subject: [PATCH 1/8] cDAC: HFA / HVA support + enable ARGITER + zero-known-issues on ARM/ARM64 Fills in the CdacTypeHandle.IsHomogeneousAggregate() and GetHomogeneousAggregateElementSize() paths so the shared ArgIterator port produces correct GCRefMap blobs on FEATURE_HFA targets (ARM, ARM64), then wires the ARGITER stress sub-check and the zero-known-issues GCREFS assertion on Windows ARM64 + Linux ARM/ARM64. ## Contract additions (IRuntimeTypeSystem) Single new API: bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize); Returns 4 / 8 / 16 for HFA / HVA types, false otherwise. Self-gates on target arch (FEATURE_HFA = ARM/ARM64) so callers don't need to. Mirrors MethodTable::GetHFAType (src/coreclr/vm/class.cpp:1867): - At each recursion level, first checks GetVectorHFAElementSize for an intrinsic Vector shape (Vector64/Vector128/System.Numerics.Vector). - Then walks the first non-static instance field. R4 -> 4, R8 -> 8, ValueType -> recurse via existing GetFieldDescApproxTypeHandle. - HVA detection uses enum_flag_IsIntrinsicType (WFLAGS2_ENUM.IsIntrinsicType = 0x0020) plus TypeDef name+namespace match, with T-must-be-numerical verification (CorIsNumericalType: I1..R8 || I || U). The MethodTable flag WFLAGS_LOW.IsHFA = 0x00000800 is added to MethodTableFlags_1.cs. That bit is repurposed on UNIX_AMD64_ABI as enum_flag_IsRegStructPassed, hence the arch self-gate in the API. ## CdacTypeHandle (thin adapter) IsHomogeneousAggregate() -> Rts.TryGetHFAElementSize(_, out _) GetHomogeneousAggregateElementSize() -> Arm shortcut: RequiresAlign8()?8:4 Else: Rts.TryGetHFAElementSize (returns size) The ARM shortcut avoids a field walk: on ARM there is no HVA, and RequiresAlign8 mirrors CheckForHFA's element-type-driven alignment choice. ## Test coverage CallSignatures debuggee gains an HfaCategory (~100 LOC) exercising: - Scalar HFAs: Float2/3/4, Double2/3/4 (all legal HFA arities) - Nested HFAs: NestedFloat3, NestedDouble4 (first-field recursion) - Mixed args: HfaThenRef / RefThenHfa / TwoFloatHfas (FP-reg + int-reg interaction; validates GCRefMap tokens for trailing ref args after FP-reg consumption) - HFA returns: ReturnFloat3, ReturnDouble2 (FP-register return path, no HasRetBufArg) - HVA args: Vector64, Vector128, System.Numerics.Vector (ARM64-only classification; benign on other targets) - Negative shapes: Float5 (>4 fields), MixedR4R8 (mixed FP) -- runtime correctly declines to flag as HFA, cDAC matches Same test compiles/passes everywhere; only exercises the new HFA walker on ARM/ARM64. ## Test-gate expansion Both gates now match: Windows {x86, x64, ARM64} + Linux {ARM, ARM64} - CdacStressTests.ArgIterStress_AllVerificationsPass: admits the new ARM/ARM64 platforms. - CdacStressTestBase.AssertAllPassed.requiresZeroKnownIssues: extended from Windows x86/x64 to include the same set. Any deferred Frame on these platforms is now a hard failure. - GcScanner.PromoteCallerStack: supportedByCallingConvention gate lifted to match, so on ARM/ARM64 the caller-stack refs are enumerated via ICallingConvention.TryComputeArgGCRefMapBlob instead of falling through to RecordDeferredFrame. Still deferred: Linux/macOS x64 (SystemV-AMD64 eightbyte classifier), RISC-V/LoongArch64 (FP struct classifier), WASM32 (GetFieldAlignment). Tracked under https://github.com/dotnet/runtime/issues/130008. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../design/datacontracts/RuntimeTypeSystem.md | 23 +++ .../Contracts/IRuntimeTypeSystem.cs | 14 ++ .../CallingConvention/CdacTypeHandle.cs | 24 +-- .../Contracts/RuntimeTypeSystem_1.cs | 142 ++++++++++++++++++ .../Contracts/StackWalk/GC/GcScanner.cs | 15 +- .../MethodTableFlags_1.cs | 10 ++ .../tests/StressTests/CdacStressTestBase.cs | 8 +- .../cdac/tests/StressTests/CdacStressTests.cs | 22 ++- .../Debuggees/CallSignatures/Program.cs | 102 ++++++++++++- 9 files changed, 334 insertions(+), 26 deletions(-) diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index c9d32f319f8fc7..296b10505f896f 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -75,6 +75,15 @@ partial interface IRuntimeTypeSystem : IContract public virtual bool ContainsGCPointers(TypeHandle typeHandle); // True if the MethodTable represents a byref-like value type (Span, ReadOnlySpan, any ref struct). public virtual bool IsByRefLike(TypeHandle typeHandle); + // Returns true and sets elementSize when the type is classified as a + // Homogeneous Floating-point / Vector Aggregate: + // 4 -> R4 (float) HFA + // 8 -> R8 (double) HFA, Vector64, or System.Numerics.Vector at 8 bytes + // 16 -> Vector128, or System.Numerics.Vector at 16 bytes + // Returns false (and elementSize=0) on any non-HFA type or on any target + // architecture that doesn't define FEATURE_HFA (callers don't need to gate + // themselves). Mirrors MethodTable::GetHFAType in src/coreclr/vm/class.cpp. + public virtual bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize); // True if the type requires 8-byte alignment on platforms that don't 8-byte align by default (FEATURE_64BIT_ALIGNMENT) public virtual bool RequiresAlign8(TypeHandle typeHandle); // True if the MethodTable represents a continuation type used by the async continuation feature @@ -339,6 +348,9 @@ internal partial struct RuntimeTypeSystem_1 IsByRefLike = 0x00001000, // value type that may contain managed pointers (e.g. Span, ReadOnlySpan) + IsHFA = 0x00000800, // ARM/ARM64 only (FEATURE_HFA): Homogeneous Floating-point Aggregate. + // On UNIX_AMD64_ABI this bit is reused as IsRegStructPassed. + StringArrayValues = GenericsMask_NonGeneric, } @@ -371,6 +383,7 @@ internal partial struct RuntimeTypeSystem_1 internal enum WFLAGS2_ENUM : uint { DynamicStatics = 0x0002, + IsIntrinsicType = 0x0020, } // Encapsulates the MethodTable flags v1 uses @@ -410,10 +423,12 @@ internal partial struct RuntimeTypeSystem_1 public bool RequiresAlign8 => GetFlag(WFLAGS_HIGH.RequiresAlign8) != 0; public bool IsCollectible => GetFlag(WFLAGS_HIGH.Collectible) != 0; public bool IsDynamicStatics => GetFlag(WFLAGS2_ENUM.DynamicStatics) != 0; + public bool IsIntrinsicType => GetFlag(WFLAGS2_ENUM.IsIntrinsicType) != 0; public bool IsTrackedReferenceWithFinalizer => GetFlag(WFLAGS_HIGH.IsTrackedReferenceWithFinalizer) != 0; public bool IsGenericTypeDefinition => TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_TypicalInstantiation); public bool IsSharedByGenericInstantiations => TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_SharedInst); public bool IsByRefLike => TestFlagWithMask(WFLAGS_LOW.IsByRefLike, WFLAGS_LOW.IsByRefLike); + public bool IsHFA => TestFlagWithMask(WFLAGS_LOW.IsHFA, WFLAGS_LOW.IsHFA); } [Flags] @@ -680,6 +695,14 @@ Contracts used: public bool IsByRefLike(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _methodTables[typeHandle.Address].Flags.IsByRefLike; + // Mirrors MethodTable::GetHFAType in src/coreclr/vm/class.cpp. Returns true + // and sets elementSize to 4 / 8 / 16 for HFA / HVA types; returns false + // otherwise. HVA detection (Vector64/Vector128/ + // System.Numerics.Vector) is performed at every recursion level via + // the private GetVectorHFAElementSize helper. Self-gates on target + // architecture so callers don't need to. + public bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize) { ... } + public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8; public bool IsCanonicalMethodTable(TypeHandle typeHandle) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index e5c6750e1a5406..c7f2c7f59e2810 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -152,6 +152,20 @@ public interface IRuntimeTypeSystem : IContract bool ContainsGCPointers(TypeHandle typeHandle) => throw new NotImplementedException(); // True if MethodTable represents a byreflike value (Span, ReadOnlySpan, etc.). bool IsByRefLike(TypeHandle typeHandle) => throw new NotImplementedException(); + // Returns true and sets elementSize when the type is classified as a + // Homogeneous Floating-point / Vector Aggregate: + // 4 -> R4 (float) HFA + // 8 -> R8 (double) HFA, Vector64, or System.Numerics.Vector at 8 bytes + // 16 -> Vector128, or System.Numerics.Vector at 16 bytes + // Returns false (and elementSize=0) on any non-HFA / non-HVA type, or on + // any target architecture that doesn't define FEATURE_HFA (so callers + // don't need to gate themselves). Mirrors MethodTable::GetHFAType in + // src/coreclr/vm/class.cpp. + bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize) + { + elementSize = 0; + throw new NotImplementedException(); + } // True if the type requires 8-byte alignment on platforms that don't 8-byte align by default (FEATURE_64BIT_ALIGNMENT) bool RequiresAlign8(TypeHandle typeHandle) => throw new NotImplementedException(); // True if the MethodTable represents a continuation subtype that has no metadata of its own diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs index a5b1c8882badcc..6315dccc3e7b17 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs @@ -111,21 +111,23 @@ public bool RequiresAlign8() public bool IsHomogeneousAggregate() { - if (Arch is not RuntimeInfoArchitecture.Arm and not RuntimeInfoArchitecture.Arm64) - return false; - - // TODO(hfa): Implement HFA detection for ARM/ARM64. - // See crossgen2 TypeHandle.IsHomogeneousAggregate(). - throw new NotImplementedException("HFA detection for ARM/ARM64 is not yet implemented."); + // Rts.TryGetHFAElementSize handles target-arch gating internally. + return !_typeHandle.IsNull && Rts.TryGetHFAElementSize(_typeHandle, out _); } public int GetHomogeneousAggregateElementSize() { - if (Arch is not RuntimeInfoArchitecture.Arm and not RuntimeInfoArchitecture.Arm64) - return 0; - - // TODO(hfa): Return 4 for float HFA, 8 for double HFA, 16 for Vector128 HFA. - throw new NotImplementedException("HFA element size for ARM/ARM64 is not yet implemented."); + // ARM has no HVA; the element is either float (4) or double (8), and + // RequiresAlign8 mirrors the runtime's choice between the two (set by + // CheckForHFA based on the resolved HFA element type). The shortcut + // avoids a field walk. + if (Arch == RuntimeInfoArchitecture.Arm) + return RequiresAlign8() ? 8 : 4; + + // ARM64 (and any future FEATURE_HFA target) uses the full classifier + // in RTS, which also detects HVA shapes (Vector64/Vector128/ + // System.Numerics.Vector). Returns 0 on non-FEATURE_HFA targets. + return _typeHandle.IsNull || !Rts.TryGetHFAElementSize(_typeHandle, out int size) ? 0 : size; } public void GetSystemVAmd64PassStructInRegisterDescriptor(out SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR descriptor) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index cf079229ee4e22..9806f1e7168907 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -614,6 +614,148 @@ public TargetPointer GetWellKnownMethodTable(WellKnownMethodTable kind) public bool ContainsGCPointers(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.ContainsGCPointers; public bool IsByRefLike(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _methodTables[typeHandle.Address].Flags.IsByRefLike; + + // True iff the target runtime is built with FEATURE_HFA (ARM, ARM64). On + // non-FEATURE_HFA targets the enum_flag_IsHFA bit (0x800 in MTFlags low) + // is either unused (Windows x64, x86) or repurposed (UNIX_AMD64_ABI uses + // it as enum_flag_IsRegStructPassed); reading the bit as HFA is incorrect. + private bool IsFeatureHfaTarget() + { + RuntimeInfoArchitecture arch = _target.Contracts.RuntimeInfo.GetTargetArchitecture(); + return arch is RuntimeInfoArchitecture.Arm or RuntimeInfoArchitecture.Arm64; + } + + // Mirrors MethodTable::GetHFAType in src/coreclr/vm/class.cpp. At each + // recursion level first checks for an intrinsic Vector shape via + // GetVectorHFAElementSize (covers Vector64/Vector128/ + // System.Numerics.Vector); if that doesn't match, walks the first + // instance field. R4 -> 4, R8 -> 8, ValueType -> recurse via + // GetFieldDescApproxTypeHandle. Returns false when the type is not + // classified as an HFA / HVA, or when the target arch doesn't define + // FEATURE_HFA. + public bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize) + { + elementSize = 0; + + if (!IsFeatureHfaTarget()) + return false; + + if (!typeHandle.IsMethodTable() || !_methodTables[typeHandle.Address].Flags.IsHFA) + return false; + + TypeHandle current = typeHandle; + for (int depth = 0; depth < 16; depth++) + { + int vectorElem = GetVectorHFAElementSize(current); + if (vectorElem != 0) + { + elementSize = vectorElem; + return true; + } + + TargetPointer firstField = TargetPointer.Null; + foreach (TargetPointer fd in ((IRuntimeTypeSystem)this).GetFieldDescList(current)) + { + if (((IRuntimeTypeSystem)this).IsFieldDescStatic(fd)) + continue; + firstField = fd; + break; + } + + if (firstField == TargetPointer.Null) + return false; + + CorElementType ft = ((IRuntimeTypeSystem)this).GetFieldDescType(firstField); + switch (ft) + { + case CorElementType.R4: + elementSize = 4; + return true; + case CorElementType.R8: + elementSize = 8; + return true; + case CorElementType.ValueType: + current = ((IRuntimeTypeSystem)this).GetFieldDescApproxTypeHandle(firstField); + if (current.IsNull) + return false; + continue; + default: + return false; + } + } + + return false; + } + + // Mirrors MethodTable::GetVectorHFA in src/coreclr/vm/class.cpp. Detects + // intrinsic Vector shapes by TypeDef name+namespace, then validates the + // generic argument is a numerical primitive. + private int GetVectorHFAElementSize(TypeHandle typeHandle) + { + if (!typeHandle.IsMethodTable() || !_methodTables[typeHandle.Address].Flags.IsIntrinsicType) + return 0; + + TargetPointer modulePtr = ((IRuntimeTypeSystem)this).GetModule(typeHandle); + if (modulePtr == TargetPointer.Null) + return 0; + + ModuleHandle moduleHandle = _target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); + MetadataReader? reader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + if (reader is null) + return 0; + + uint typeDefToken = ((IRuntimeTypeSystem)this).GetTypeDefToken(typeHandle); + if (EcmaMetadataUtils.GetRowId(typeDefToken) == 0) + return 0; + + TypeDefinitionHandle tdHandle = (TypeDefinitionHandle)MetadataTokens.Handle((int)typeDefToken); + TypeDefinition typeDef = reader.GetTypeDefinition(tdHandle); + string className = reader.GetString(typeDef.Name); + string namespaceName = reader.GetString(typeDef.Namespace); + + int elemSize; + if (className == "Vector`1" && namespaceName == "System.Numerics") + { + // System.Numerics.Vector is size-dependent. + elemSize = ((IRuntimeTypeSystem)this).GetNumInstanceFieldBytes(typeHandle) switch + { + 8 => 8, + 16 => 16, + _ => 0, + }; + } + else if (className == "Vector128`1" && namespaceName == "System.Runtime.Intrinsics") + { + elemSize = 16; + } + else if (className == "Vector64`1" && namespaceName == "System.Runtime.Intrinsics") + { + elemSize = 8; + } + else + { + return 0; + } + + if (elemSize == 0) + return 0; + + // T must be a numerical primitive (CorIsNumericalType in cor.h): I1..R8, I, U. + ReadOnlySpan instantiation = ((IRuntimeTypeSystem)this).GetInstantiation(typeHandle); + if (instantiation.Length < 1) + return 0; + + CorElementType argType = ((IRuntimeTypeSystem)this).GetSignatureCorElementType(instantiation[0]); + if (!IsCorNumericalType(argType)) + return 0; + + return elemSize; + } + + private static bool IsCorNumericalType(CorElementType t) + => (t >= CorElementType.I1 && t <= CorElementType.R8) + || t == CorElementType.I + || t == CorElementType.U; public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8; public bool IsContinuationWithoutMetadata(TypeHandle typeHandle) => typeHandle.IsMethodTable() && ContinuationMethodTablePointer != TargetPointer.Null diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index 9b165587267952..d62d01e68bc1d4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -340,12 +340,17 @@ private void PromoteCallerStack(TargetPointer frameAddress, GcScanContext scanCo IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo; RuntimeInfoArchitecture arch = runtimeInfo.GetTargetArchitecture(); RuntimeInfoOperatingSystem os = runtimeInfo.GetTargetOperatingSystem(); - // TODO(https://github.com/dotnet/runtime/issues/130008): extend ICallingConvention.TryComputeArgGCRefMapBlob - // coverage to non-Windows / ARM targets (SystemV-AMD64 / ARM64 struct-in-register classification, ARM32 ABI - // port) so this path is taken on those targets too instead of deferring to RecordDeferredFrame. + // Matches the ARGITER stress-test gate in CdacStressTests.cs. Platforms + // still missing calling-convention coverage (SystemV-AMD64 / RISC-V / + // LoongArch / WASM) fall through to RecordDeferredFrame. + // TODO(https://github.com/dotnet/runtime/issues/130008): extend + // ICallingConvention.TryComputeArgGCRefMapBlob coverage to the + // remaining targets so this path fires everywhere. bool supportedByCallingConvention = - os is RuntimeInfoOperatingSystem.Windows - && arch is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64; + (os is RuntimeInfoOperatingSystem.Windows + && arch is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64 or RuntimeInfoArchitecture.Arm64) + || (os is RuntimeInfoOperatingSystem.Unix + && arch is RuntimeInfoArchitecture.Arm or RuntimeInfoArchitecture.Arm64); if (!supportedByCallingConvention) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs index e60f24bc90960e..69ff8c68f69bb2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs @@ -28,6 +28,13 @@ internal enum WFLAGS_LOW : uint IsByRefLike = 0x00001000, // value type that may contain managed pointers (e.g. Span, ReadOnlySpan) + // ARM / ARM64 (FEATURE_HFA): set after layout when CheckForHFA classifies + // the type as a Homogeneous Floating-point Aggregate. On UNIX_AMD64_ABI + // runtimes the same 0x800 bit is reused as enum_flag_IsRegStructPassed, + // so callers MUST gate on the target architecture before treating this + // bit as HFA. The cDAC gates at CdacTypeHandle.IsHomogeneousAggregate. + IsHFA = 0x00000800, + StringArrayValues = GenericsMask_NonGeneric | 0, @@ -62,6 +69,7 @@ internal enum WFLAGS_HIGH : uint internal enum WFLAGS2_ENUM : uint { DynamicStatics = 0x0002, + IsIntrinsicType = 0x0020, } public uint MTFlags { get; init; } @@ -110,9 +118,11 @@ private bool TestFlagWithMask(WFLAGS2_ENUM mask, WFLAGS2_ENUM flag) public bool IsCollectible => GetFlag(WFLAGS_HIGH.Collectible) != 0; public bool IsTrackedReferenceWithFinalizer => GetFlag(WFLAGS_HIGH.IsTrackedReferenceWithFinalizer) != 0; public bool IsDynamicStatics => GetFlag(WFLAGS2_ENUM.DynamicStatics) != 0; + public bool IsIntrinsicType => GetFlag(WFLAGS2_ENUM.IsIntrinsicType) != 0; public bool IsGenericTypeDefinition => TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_TypicalInstantiation); public bool IsSharedByGenericInstantiations => TestFlagWithMask(WFLAGS_LOW.GenericsMask, WFLAGS_LOW.GenericsMask_SharedInst); public bool IsByRefLike => TestFlagWithMask(WFLAGS_LOW.IsByRefLike, WFLAGS_LOW.IsByRefLike); + public bool IsHFA => TestFlagWithMask(WFLAGS_LOW.IsHFA, WFLAGS_LOW.IsHFA); public bool ContainsGenericVariables => GetFlag(WFLAGS_HIGH.ContainsGenericVariables) != 0; internal static EEClassOrCanonMTBits GetEEClassOrCanonMTBits(TargetPointer eeClassOrCanonMTPtr) diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs index 02d1631d07a49d..b490417c0bef2b 100644 --- a/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs @@ -186,16 +186,20 @@ internal static void AssertAllPassed(CdacStressResults results, string debuggeeN // On supported targets every Frame's caller-arg refs are enumerated via // the GCRefMap blob synthesized by ICallingConvention -- there should be // no deferred frames at all, so any KnownIssue count is a regression. + // Keep this gate in sync with the ARGITER-support gate in + // ArgIterStress_AllVerificationsPass below and with the + // supportedByCallingConvention gate in GcScanner.PromoteCallerStack. GetTargetPlatform(out OSPlatform os, out Architecture arch); bool requiresZeroKnownIssues = - os == OSPlatform.Windows && arch is Architecture.X86 or Architecture.X64; + (os == OSPlatform.Windows && arch is Architecture.X86 or Architecture.X64 or Architecture.Arm64) + || (os == OSPlatform.Linux && arch is Architecture.Arm or Architecture.Arm64); if (requiresZeroKnownIssues && results.KnownIssues > 0) { string analysis = results.AnalyzeFailures(maxFailures: 3); Assert.Fail( $"GCREFS stress test '{debuggeeName}' had {results.KnownIssues} known issue(s) " + $"out of {results.TotalVerifications} verifications. " + - "Windows x86 / x64 are expected to enumerate every transition Frame's " + + "This platform is expected to enumerate every transition Frame's " + "caller-stack refs via ICallingConvention.TryComputeArgGCRefMapBlob with no " + "deferred frames. A non-zero KnownIssues count indicates the encoder declined " + "a method it should support (e.g. a regression in ComputeArgGCRefMapBlobCore " + diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs index d71fc7fe901250..7cd1a151ca8f44 100644 --- a/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs @@ -74,14 +74,22 @@ public async Task ArgIterStress_AllVerificationsPass(Debuggee debuggee) if (debuggee.WindowsOnly && os != OSPlatform.Windows) throw new SkipTestException($"{debuggee.Name} debuggee is Windows-only."); - // Scope of this PR: ARGITER is validated on Windows x86 / x64 - // only. Other architectures hit known gaps that need follow-up - // work (SystemV-AMD64 / ARM64 struct-in-register classification, - // arm32 ABI port). Skip there until those land. - if (os != OSPlatform.Windows || arch is not (Architecture.X86 or Architecture.X64)) + // ARGITER stress requires a CdacTypeHandle whose calling-convention + // helpers are filled in for the target ABI. Currently validated: + // - Windows x86 / x64 (TransitionBlock + IsTrivialPointerSizedStruct) + // - Windows ARM64 (HFA via MethodTable enum_flag_IsHFA) + // - Linux ARM / ARM64 (HFA, same path) + // Still skipped: + // - Linux / macOS x64 (SystemV-AMD64 eightbyte classifier not ported) + // - RISC-V / LoongArch64 (FP struct classifier not ported) + // - WASM32 (GetFieldAlignment not ported) + bool argIterSupported = + (os == OSPlatform.Windows && arch is Architecture.X86 or Architecture.X64 or Architecture.Arm64) + || (os == OSPlatform.Linux && arch is Architecture.Arm or Architecture.Arm64); + if (!argIterSupported) throw new SkipTestException( - "ARGITER stress is validated for windows-x86 / windows-x64 in this PR; " + - "other targets need follow-up work (SystemV / ARM64 struct-in-registers, ARM32 ABI port)."); + "ARGITER stress: needs follow-up work for this platform " + + "(SystemV-AMD64 / RISC-V / LoongArch / WASM struct classifiers)."); CdacStressResults results = await RunArgIterStressAsync(debuggee.Name); AssertAllArgIterPassed(results, debuggee.Name); diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs index 83b3c4e5b645ad..82b40fbb26d034 100644 --- a/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs @@ -3,8 +3,10 @@ using System; using System.IO; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; /// /// Exhaustive cdacstress ArgIterator debuggee. Covers a wide variety of @@ -26,6 +28,8 @@ /// - Generic value-type instance methods (interface dispatch) /// - Enum arguments (Int32-, Int64-, byte-backed) /// - Large-struct return (HasRetBuffArg) +/// - HFA / HVA: float, double, Vector64, Vector128, System.Numerics.Vector +/// plus negative shapes (too many fields, mixed FP types) /// - Mutually-recursive deep stack /// /// __arglist / vararg coverage lives in the dedicated VarArgs debuggee @@ -90,6 +94,7 @@ private static void Drive() GenericCategory(); EnumCategory(); ReturnCategory(); + HfaCategory(); DeepStackCategory(); } @@ -437,7 +442,102 @@ private static void ReturnCategory() [MethodImpl(MethodImplOptions.NoInlining)] private static BigStruct ReturnLarge() { AllocBurst(); return new BigStruct { A = 1 }; } [MethodImpl(MethodImplOptions.NoInlining)] private static Span ReturnSpan(Span s) { AllocBurst(); return s; } - // ===== Category 12: deep stack ===== + // ===== Category 12: HFA / HVA ===== + // HFAs (Homogeneous Floating-point Aggregates) and HVAs (Homogeneous Vector + // Aggregates) are classified at MethodTable layout time on FEATURE_HFA + // targets (ARM / ARM64). The ArgIterator code path for HFA/HVA args is + // distinct from regular structs, so these debuggee methods exist primarily + // to exercise the cDAC's TryGetHFAElementSize walker on ARM/ARM64. On + // Windows x86/x64 the runtime does not classify these types as HFAs, so + // they go through the regular struct path -- the cDAC matches that, and + // the ARGITER stress assertion still passes. + // + // Negative shapes (Float5, MixedR4R8) are included so the walker correctly + // returns "not an HFA" -- the runtime never sets the IsHFA flag on these. + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void HfaCategory() + { + // Float HFAs of legal arity (1..4 elements). + PassFloat2(default); + PassFloat3(default); + PassFloat4(default); + + // Double HFAs of legal arity. + PassDouble2(default); + PassDouble3(default); + PassDouble4(default); + + // Nested HFAs: still classified as HFAs because the recursive + // first-field walker reaches R4/R8. + PassNestedFloat3(default); + PassNestedDouble4(default); + + // Mixed-in args: HFA in register, refs elsewhere, ensures + // ArgIterator correctly accounts for FP register consumption + // when emitting GCRefMap tokens for the trailing INTEGER args. + HfaThenRef(default, "x"); + RefThenHfa("x", default); + TwoFloatHfas(default, default); + + // Returns: HFA returns use FP-register return paths on ARM/ARM64, + // not the integer-return path, so HasRetBuffArg should be false. + _ = ReturnFloat3(); + _ = ReturnDouble2(); + + // HVAs (ARM64 only at the runtime level; classification falls + // through to "not HFA" on other targets). All passed as default. + Vec64FloatArg(default); + Vec128FloatArg(default); + VecNumericsFloatArg(default); + + // Negative cases -- runtime does not flag these as HFAs. + PassFloat5(default); + PassMixedR4R8(default); + } + + // --- HFA struct shapes --- + private struct Float2 { public float A, B; } + private struct Float3 { public float A, B, C; } + private struct Float4 { public float A, B, C, D; } + private struct Float5 { public float A, B, C, D, E; } // >4 fields: not HFA + private struct Double2 { public double A, B; } + private struct Double3 { public double A, B, C; } + private struct Double4 { public double A, B, C, D; } + private struct MixedR4R8 { public float A; public double B; } // mixed FP: not HFA + + // Nested HFA: first field is itself an HFA, total still <=4 R4/R8 elements. + private struct NestedFloat3 { public Float2 Inner; public float C; } + private struct NestedDouble4 { public Double2 First; public Double2 Second; } + + // --- HFA arg / return helpers --- + [MethodImpl(MethodImplOptions.NoInlining)] private static void PassFloat2(Float2 s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void PassFloat3(Float3 s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B + s.C)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void PassFloat4(Float4 s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B + s.C + s.D)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void PassFloat5(Float5 s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.E)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void PassDouble2(Double2 s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void PassDouble3(Double3 s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B + s.C)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void PassDouble4(Double4 s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B + s.C + s.D)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void PassMixedR4R8(MixedR4R8 s) { AllocBurst(); GC.KeepAlive((object)(s.A + s.B)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void PassNestedFloat3(NestedFloat3 s) { AllocBurst(); GC.KeepAlive((object)(s.Inner.A + s.C)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void PassNestedDouble4(NestedDouble4 s) { AllocBurst(); GC.KeepAlive((object)(s.First.A + s.Second.B)); } + + [MethodImpl(MethodImplOptions.NoInlining)] private static void HfaThenRef(Float3 s, string r) { AllocBurst(); GC.KeepAlive((object)(s.A)); GC.KeepAlive(r); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void RefThenHfa(string r, Float3 s) { AllocBurst(); GC.KeepAlive(r); GC.KeepAlive((object)(s.A)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void TwoFloatHfas(Float2 a, Float2 b) { AllocBurst(); GC.KeepAlive((object)(a.A + b.A)); } + + [MethodImpl(MethodImplOptions.NoInlining)] private static Float3 ReturnFloat3() { AllocBurst(); return default; } + [MethodImpl(MethodImplOptions.NoInlining)] private static Double2 ReturnDouble2() { AllocBurst(); return default; } + + // --- HVA shapes (intrinsic Vector types). On non-FEATURE_HFA targets + // these go through the regular struct path; on ARM64 they hit the + // GetVectorHFAElementSize TypeDef-name match (Vector64/128 in + // System.Runtime.Intrinsics, Vector in System.Numerics). --- + [MethodImpl(MethodImplOptions.NoInlining)] private static void Vec64FloatArg(Vector64 v) { AllocBurst(); GC.KeepAlive((object)v.GetElement(0)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void Vec128FloatArg(Vector128 v) { AllocBurst(); GC.KeepAlive((object)v.GetElement(0)); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void VecNumericsFloatArg(Vector v) { AllocBurst(); GC.KeepAlive((object)v[0]); } + + // ===== Category 13: deep stack ===== // Mutually-recursive chains of methods with mixed signatures. At any // given allocation trigger many frames are simultaneously live, so a // single stack-walk verification run touches multiple MDs across From 72b92e79b0521fe1748576b2428bcffbbf2302a0 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 1 Jul 2026 12:44:03 -0400 Subject: [PATCH 2/8] Address Copilot PR review comments - GetVectorHFAElementSize: defensively wrap the metadata decode in try/catch and validate HandleKind before casting the token, mirroring GetFieldDescApproxTypeHandle. A bad TypeDef token or malformed metadata now returns 0 (treated as "not an HVA") instead of throwing during GC-scan / stack-walking. - CdacStressTests ArgIter skip message: mention the Windows ARM32 ABI port gap alongside the SystemV / RISC-V / LoongArch / WASM classifiers, since the gate now also skips ARM32. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/RuntimeTypeSystem_1.cs | 107 ++++++++++-------- .../cdac/tests/StressTests/CdacStressTests.cs | 4 +- 2 files changed, 64 insertions(+), 47 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index 9806f1e7168907..308ab71722d053 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -690,66 +690,81 @@ public bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize) // Mirrors MethodTable::GetVectorHFA in src/coreclr/vm/class.cpp. Detects // intrinsic Vector shapes by TypeDef name+namespace, then validates the // generic argument is a numerical primitive. + // Mirrors MethodTable::GetVectorHFA in src/coreclr/vm/class.cpp. Detects + // intrinsic Vector shapes by TypeDef name+namespace, then validates the + // generic argument is a numerical primitive. Any metadata decode failure + // (bad token, missing metadata, corrupt image) is swallowed; the caller + // treats a 0 return as "not an HVA" and falls through to the field walk. private int GetVectorHFAElementSize(TypeHandle typeHandle) { if (!typeHandle.IsMethodTable() || !_methodTables[typeHandle.Address].Flags.IsIntrinsicType) return 0; - TargetPointer modulePtr = ((IRuntimeTypeSystem)this).GetModule(typeHandle); - if (modulePtr == TargetPointer.Null) - return 0; + try + { + TargetPointer modulePtr = ((IRuntimeTypeSystem)this).GetModule(typeHandle); + if (modulePtr == TargetPointer.Null) + return 0; - ModuleHandle moduleHandle = _target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); - MetadataReader? reader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); - if (reader is null) - return 0; + ModuleHandle moduleHandle = _target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePtr); + MetadataReader? reader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle); + if (reader is null) + return 0; - uint typeDefToken = ((IRuntimeTypeSystem)this).GetTypeDefToken(typeHandle); - if (EcmaMetadataUtils.GetRowId(typeDefToken) == 0) - return 0; + uint typeDefToken = ((IRuntimeTypeSystem)this).GetTypeDefToken(typeHandle); + if (EcmaMetadataUtils.GetRowId(typeDefToken) == 0) + return 0; - TypeDefinitionHandle tdHandle = (TypeDefinitionHandle)MetadataTokens.Handle((int)typeDefToken); - TypeDefinition typeDef = reader.GetTypeDefinition(tdHandle); - string className = reader.GetString(typeDef.Name); - string namespaceName = reader.GetString(typeDef.Namespace); + EntityHandle handle = (EntityHandle)MetadataTokens.Handle((int)typeDefToken); + if (handle.Kind != HandleKind.TypeDefinition) + return 0; - int elemSize; - if (className == "Vector`1" && namespaceName == "System.Numerics") - { - // System.Numerics.Vector is size-dependent. - elemSize = ((IRuntimeTypeSystem)this).GetNumInstanceFieldBytes(typeHandle) switch + TypeDefinition typeDef = reader.GetTypeDefinition((TypeDefinitionHandle)handle); + string className = reader.GetString(typeDef.Name); + string namespaceName = reader.GetString(typeDef.Namespace); + + int elemSize; + if (className == "Vector`1" && namespaceName == "System.Numerics") { - 8 => 8, - 16 => 16, - _ => 0, - }; - } - else if (className == "Vector128`1" && namespaceName == "System.Runtime.Intrinsics") - { - elemSize = 16; - } - else if (className == "Vector64`1" && namespaceName == "System.Runtime.Intrinsics") - { - elemSize = 8; - } - else - { - return 0; - } + // System.Numerics.Vector is size-dependent. + elemSize = ((IRuntimeTypeSystem)this).GetNumInstanceFieldBytes(typeHandle) switch + { + 8 => 8, + 16 => 16, + _ => 0, + }; + } + else if (className == "Vector128`1" && namespaceName == "System.Runtime.Intrinsics") + { + elemSize = 16; + } + else if (className == "Vector64`1" && namespaceName == "System.Runtime.Intrinsics") + { + elemSize = 8; + } + else + { + return 0; + } - if (elemSize == 0) - return 0; + if (elemSize == 0) + return 0; - // T must be a numerical primitive (CorIsNumericalType in cor.h): I1..R8, I, U. - ReadOnlySpan instantiation = ((IRuntimeTypeSystem)this).GetInstantiation(typeHandle); - if (instantiation.Length < 1) - return 0; + // T must be a numerical primitive (CorIsNumericalType in cor.h): I1..R8, I, U. + ReadOnlySpan instantiation = ((IRuntimeTypeSystem)this).GetInstantiation(typeHandle); + if (instantiation.Length < 1) + return 0; - CorElementType argType = ((IRuntimeTypeSystem)this).GetSignatureCorElementType(instantiation[0]); - if (!IsCorNumericalType(argType)) - return 0; + CorElementType argType = ((IRuntimeTypeSystem)this).GetSignatureCorElementType(instantiation[0]); + if (!IsCorNumericalType(argType)) + return 0; - return elemSize; + return elemSize; + } + catch + { + return 0; + } } private static bool IsCorNumericalType(CorElementType t) diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs index 7cd1a151ca8f44..47d3132c39640e 100644 --- a/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs @@ -89,7 +89,9 @@ public async Task ArgIterStress_AllVerificationsPass(Debuggee debuggee) if (!argIterSupported) throw new SkipTestException( "ARGITER stress: needs follow-up work for this platform " + - "(SystemV-AMD64 / RISC-V / LoongArch / WASM struct classifiers)."); + "(SystemV-AMD64 struct classifier for linux/macOS x64, " + + "Windows ARM32 ABI port, RISC-V / LoongArch FP struct classifier, " + + "or WASM GetFieldAlignment)."); CdacStressResults results = await RunArgIterStressAsync(debuggee.Name); AssertAllArgIterPassed(results, debuggee.Name); From 3842a16d584a6beb98dc006bcdb0aba220cf1d5d Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 1 Jul 2026 16:18:16 -0400 Subject: [PATCH 3/8] Simplify TryGetHFAElementSize doc comment; add pseudocode in RTS.md The full 4/8/16 element-size breakdown belonged in the impl-section`ndoc, not on every declaration. Interface comment now just says`n'HFA (or HVA on ARM64), returns 4/8/16 element size'; RTS.md`nimpl section grows a pseudocode block covering the recursive`nfield-walk + GetVectorHFAElementSize intrinsic name match.`n`nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../design/datacontracts/RuntimeTypeSystem.md | 44 +++++++++++++------ .../Contracts/IRuntimeTypeSystem.cs | 11 ++--- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 296b10505f896f..7c4949ddcb51d3 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -75,14 +75,10 @@ partial interface IRuntimeTypeSystem : IContract public virtual bool ContainsGCPointers(TypeHandle typeHandle); // True if the MethodTable represents a byref-like value type (Span, ReadOnlySpan, any ref struct). public virtual bool IsByRefLike(TypeHandle typeHandle); - // Returns true and sets elementSize when the type is classified as a - // Homogeneous Floating-point / Vector Aggregate: - // 4 -> R4 (float) HFA - // 8 -> R8 (double) HFA, Vector64, or System.Numerics.Vector at 8 bytes - // 16 -> Vector128, or System.Numerics.Vector at 16 bytes - // Returns false (and elementSize=0) on any non-HFA type or on any target - // architecture that doesn't define FEATURE_HFA (callers don't need to gate - // themselves). Mirrors MethodTable::GetHFAType in src/coreclr/vm/class.cpp. + // If the type is an HFA (or HVA on ARM64), returns true and sets elementSize + // to 4, 8, or 16. Returns false otherwise (including on targets that don't + // define FEATURE_HFA). Mirrors MethodTable::GetHFAType in + // src/coreclr/vm/class.cpp. public virtual bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize); // True if the type requires 8-byte alignment on platforms that don't 8-byte align by default (FEATURE_64BIT_ALIGNMENT) public virtual bool RequiresAlign8(TypeHandle typeHandle); @@ -695,12 +691,32 @@ Contracts used: public bool IsByRefLike(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _methodTables[typeHandle.Address].Flags.IsByRefLike; - // Mirrors MethodTable::GetHFAType in src/coreclr/vm/class.cpp. Returns true - // and sets elementSize to 4 / 8 / 16 for HFA / HVA types; returns false - // otherwise. HVA detection (Vector64/Vector128/ - // System.Numerics.Vector) is performed at every recursion level via - // the private GetVectorHFAElementSize helper. Self-gates on target - // architecture so callers don't need to. + // Mirrors MethodTable::GetHFAType in src/coreclr/vm/class.cpp. Pseudocode: + // + // TryGetHFAElementSize(th): + // if targetArch is not ARM/ARM64: return false + // if !th.Flags.IsHFA: return false + // mt = th + // loop (bounded depth): + // if (elem = GetVectorHFAElementSize(mt)): return (true, elem) + // field = first non-static field of mt + // if field is null: return false + // switch field.ElementType: + // R4: return (true, 4) + // R8: return (true, 8) + // ValueType: mt = GetFieldDescApproxTypeHandle(field); continue + // default: return false + // + // GetVectorHFAElementSize(mt): // detects HVA shapes + // if !mt.Flags.IsIntrinsicType: return 0 + // (ns, name) = typedef name+namespace via EcmaMetadata + // elem = match on (ns, name): + // "System.Numerics", "Vector`1": NumInstanceFieldBytes (8 or 16, else 0) + // "System.Runtime.Intrinsics", "Vector128`1": 16 + // "System.Runtime.Intrinsics", "Vector64`1": 8 + // _: return 0 + // if !CorIsNumericalType(GetInstantiation(mt)[0]): return 0 + // return elem public bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize) { ... } public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index c7f2c7f59e2810..253e4e2f0f3e6a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -152,14 +152,9 @@ public interface IRuntimeTypeSystem : IContract bool ContainsGCPointers(TypeHandle typeHandle) => throw new NotImplementedException(); // True if MethodTable represents a byreflike value (Span, ReadOnlySpan, etc.). bool IsByRefLike(TypeHandle typeHandle) => throw new NotImplementedException(); - // Returns true and sets elementSize when the type is classified as a - // Homogeneous Floating-point / Vector Aggregate: - // 4 -> R4 (float) HFA - // 8 -> R8 (double) HFA, Vector64, or System.Numerics.Vector at 8 bytes - // 16 -> Vector128, or System.Numerics.Vector at 16 bytes - // Returns false (and elementSize=0) on any non-HFA / non-HVA type, or on - // any target architecture that doesn't define FEATURE_HFA (so callers - // don't need to gate themselves). Mirrors MethodTable::GetHFAType in + // If the type is an HFA (or HVA on ARM64), returns true and sets elementSize + // to 4, 8, or 16. Returns false otherwise (including on targets that don't + // define FEATURE_HFA). Mirrors MethodTable::GetHFAType in // src/coreclr/vm/class.cpp. bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize) { From b704b8d35fae117453085605d71a25be90b9560e Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 1 Jul 2026 16:21:16 -0400 Subject: [PATCH 4/8] Trim redundant HFA/HVA comments in RTS_1.cs, CdacTypeHandle.cs, MethodTableFlags_1.cs Algorithm now lives in RuntimeTypeSystem.md pseudocode; the impl`nfiles just cross-reference the runtime source. Removed re-explanations`nof:`n- FEATURE_HFA arch-gate rationale (now a 1-line note)`n- TryGetHFAElementSize algorithm walkthrough (see RTS.md)`n- Duplicate GetVectorHFA comment block (accidental)`n- Redundant IsHFA bit note (already in the enum value line)`n- CdacTypeHandle wrapper explanations (self-evident from Rts. calls)`n`nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CallingConvention/CdacTypeHandle.cs | 14 ++------- .../Contracts/RuntimeTypeSystem_1.cs | 29 +++++-------------- .../MethodTableFlags_1.cs | 8 +---- 3 files changed, 11 insertions(+), 40 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs index 6315dccc3e7b17..85a56665686850 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs @@ -110,23 +110,15 @@ public bool RequiresAlign8() } public bool IsHomogeneousAggregate() - { - // Rts.TryGetHFAElementSize handles target-arch gating internally. - return !_typeHandle.IsNull && Rts.TryGetHFAElementSize(_typeHandle, out _); - } + => !_typeHandle.IsNull && Rts.TryGetHFAElementSize(_typeHandle, out _); public int GetHomogeneousAggregateElementSize() { - // ARM has no HVA; the element is either float (4) or double (8), and - // RequiresAlign8 mirrors the runtime's choice between the two (set by - // CheckForHFA based on the resolved HFA element type). The shortcut - // avoids a field walk. + // ARM has no HVA; RequiresAlign8 encodes the R4-vs-R8 choice, so we + // can skip the field walk that TryGetHFAElementSize would do. if (Arch == RuntimeInfoArchitecture.Arm) return RequiresAlign8() ? 8 : 4; - // ARM64 (and any future FEATURE_HFA target) uses the full classifier - // in RTS, which also detects HVA shapes (Vector64/Vector128/ - // System.Numerics.Vector). Returns 0 on non-FEATURE_HFA targets. return _typeHandle.IsNull || !Rts.TryGetHFAElementSize(_typeHandle, out int size) ? 0 : size; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index 308ab71722d053..c0a7f149af5467 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -615,24 +615,16 @@ public TargetPointer GetWellKnownMethodTable(WellKnownMethodTable kind) public bool ContainsGCPointers(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.ContainsGCPointers; public bool IsByRefLike(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _methodTables[typeHandle.Address].Flags.IsByRefLike; - // True iff the target runtime is built with FEATURE_HFA (ARM, ARM64). On - // non-FEATURE_HFA targets the enum_flag_IsHFA bit (0x800 in MTFlags low) - // is either unused (Windows x64, x86) or repurposed (UNIX_AMD64_ABI uses - // it as enum_flag_IsRegStructPassed); reading the bit as HFA is incorrect. + // FEATURE_HFA targets. On non-FEATURE_HFA targets the enum_flag_IsHFA + // bit is either unused or reused (UNIX_AMD64_ABI IsRegStructPassed). private bool IsFeatureHfaTarget() { RuntimeInfoArchitecture arch = _target.Contracts.RuntimeInfo.GetTargetArchitecture(); return arch is RuntimeInfoArchitecture.Arm or RuntimeInfoArchitecture.Arm64; } - // Mirrors MethodTable::GetHFAType in src/coreclr/vm/class.cpp. At each - // recursion level first checks for an intrinsic Vector shape via - // GetVectorHFAElementSize (covers Vector64/Vector128/ - // System.Numerics.Vector); if that doesn't match, walks the first - // instance field. R4 -> 4, R8 -> 8, ValueType -> recurse via - // GetFieldDescApproxTypeHandle. Returns false when the type is not - // classified as an HFA / HVA, or when the target arch doesn't define - // FEATURE_HFA. + // See RuntimeTypeSystem.md for the pseudocode. Mirrors + // MethodTable::GetHFAType in src/coreclr/vm/class.cpp. public bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize) { elementSize = 0; @@ -687,14 +679,8 @@ public bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize) return false; } - // Mirrors MethodTable::GetVectorHFA in src/coreclr/vm/class.cpp. Detects - // intrinsic Vector shapes by TypeDef name+namespace, then validates the - // generic argument is a numerical primitive. - // Mirrors MethodTable::GetVectorHFA in src/coreclr/vm/class.cpp. Detects - // intrinsic Vector shapes by TypeDef name+namespace, then validates the - // generic argument is a numerical primitive. Any metadata decode failure - // (bad token, missing metadata, corrupt image) is swallowed; the caller - // treats a 0 return as "not an HVA" and falls through to the field walk. + // Mirrors MethodTable::GetVectorHFA in src/coreclr/vm/class.cpp. Any + // metadata decode failure returns 0 (treated as "not an HVA"). private int GetVectorHFAElementSize(TypeHandle typeHandle) { if (!typeHandle.IsMethodTable() || !_methodTables[typeHandle.Address].Flags.IsIntrinsicType) @@ -726,7 +712,6 @@ private int GetVectorHFAElementSize(TypeHandle typeHandle) int elemSize; if (className == "Vector`1" && namespaceName == "System.Numerics") { - // System.Numerics.Vector is size-dependent. elemSize = ((IRuntimeTypeSystem)this).GetNumInstanceFieldBytes(typeHandle) switch { 8 => 8, @@ -750,7 +735,6 @@ private int GetVectorHFAElementSize(TypeHandle typeHandle) if (elemSize == 0) return 0; - // T must be a numerical primitive (CorIsNumericalType in cor.h): I1..R8, I, U. ReadOnlySpan instantiation = ((IRuntimeTypeSystem)this).GetInstantiation(typeHandle); if (instantiation.Length < 1) return 0; @@ -767,6 +751,7 @@ private int GetVectorHFAElementSize(TypeHandle typeHandle) } } + // Mirrors CorIsNumericalType in src/coreclr/inc/cor.h. private static bool IsCorNumericalType(CorElementType t) => (t >= CorElementType.I1 && t <= CorElementType.R8) || t == CorElementType.I diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs index 69ff8c68f69bb2..a7b7e8e5c437fd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs @@ -27,13 +27,7 @@ internal enum WFLAGS_LOW : uint GenericsMask_TypicalInstantiation = 0x00000030, // the type instantiated at its formal parameters, e.g. List IsByRefLike = 0x00001000, // value type that may contain managed pointers (e.g. Span, ReadOnlySpan) - - // ARM / ARM64 (FEATURE_HFA): set after layout when CheckForHFA classifies - // the type as a Homogeneous Floating-point Aggregate. On UNIX_AMD64_ABI - // runtimes the same 0x800 bit is reused as enum_flag_IsRegStructPassed, - // so callers MUST gate on the target architecture before treating this - // bit as HFA. The cDAC gates at CdacTypeHandle.IsHomogeneousAggregate. - IsHFA = 0x00000800, + IsHFA = 0x00000800, // ARM/ARM64 only (FEATURE_HFA); reused on UNIX_AMD64_ABI as IsRegStructPassed StringArrayValues = GenericsMask_NonGeneric | From 7188be67a189e44e9d5725f47d956a043b2c1776 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 1 Jul 2026 18:17:49 -0400 Subject: [PATCH 5/8] CdacTypeHandle.GetHomogeneousAggregateElementSize: Debug.Assert HFA invariant Mirrors the Debug.Assert(IsHomogeneousAggregate()) check in the shared ReadyToRun TypeHandle implementation. Catches a class of call-site mistakes on the ARM shortcut path where a non-HFA type would otherwise silently receive a bogus 4/8 result from `RequiresAlign8() ? 8 : 4`. Addresses Copilot PR review feedback on #130090. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/CallingConvention/CdacTypeHandle.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs index 85a56665686850..b9860d79d08a33 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; using Internal.CallingConvention; using Internal.JitInterface; @@ -114,6 +115,8 @@ public bool IsHomogeneousAggregate() public int GetHomogeneousAggregateElementSize() { + Debug.Assert(IsHomogeneousAggregate()); + // ARM has no HVA; RequiresAlign8 encodes the R4-vs-R8 choice, so we // can skip the field walk that TryGetHFAElementSize would do. if (Arch == RuntimeInfoArchitecture.Arm) From 4b47f9f6bc59c4ddfdc0d074ed0a17c10c5d3d02 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 1 Jul 2026 18:31:15 -0400 Subject: [PATCH 6/8] Move ARM HFA-element-size shortcut from CdacTypeHandle into RTS CdacTypeHandle.GetHomogeneousAggregateElementSize now unconditionally delegates to Rts.TryGetHFAElementSize, becoming a pure adapter. The RequiresAlign8-based ARM shortcut (which avoids the field walk by reading the alignment flag that CheckForHFA sets based on the resolved R4/R8 element type) is now baked into TryGetHFAElementSize itself, so any future consumer of the RTS API automatically benefits without having to know the trick. IsFeatureHfaTarget now returns the resolved architecture via `out` so the shortcut branch can dispatch without re-querying RuntimeInfo. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/RuntimeTypeSystem.md | 1 + .../Contracts/CallingConvention/CdacTypeHandle.cs | 8 +------- .../Contracts/RuntimeTypeSystem_1.cs | 15 ++++++++++++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 7c4949ddcb51d3..d61fdbbf281a8f 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -696,6 +696,7 @@ Contracts used: // TryGetHFAElementSize(th): // if targetArch is not ARM/ARM64: return false // if !th.Flags.IsHFA: return false + // if targetArch is ARM: return (true, th.Flags.RequiresAlign8 ? 8 : 4) // mt = th // loop (bounded depth): // if (elem = GetVectorHFAElementSize(mt)): return (true, elem) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs index b9860d79d08a33..41988ead99c5a7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs @@ -116,13 +116,7 @@ public bool IsHomogeneousAggregate() public int GetHomogeneousAggregateElementSize() { Debug.Assert(IsHomogeneousAggregate()); - - // ARM has no HVA; RequiresAlign8 encodes the R4-vs-R8 choice, so we - // can skip the field walk that TryGetHFAElementSize would do. - if (Arch == RuntimeInfoArchitecture.Arm) - return RequiresAlign8() ? 8 : 4; - - return _typeHandle.IsNull || !Rts.TryGetHFAElementSize(_typeHandle, out int size) ? 0 : size; + return Rts.TryGetHFAElementSize(_typeHandle, out int size) ? size : 0; } public void GetSystemVAmd64PassStructInRegisterDescriptor(out SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR descriptor) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index c0a7f149af5467..637151c901d361 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -617,9 +617,9 @@ public TargetPointer GetWellKnownMethodTable(WellKnownMethodTable kind) // FEATURE_HFA targets. On non-FEATURE_HFA targets the enum_flag_IsHFA // bit is either unused or reused (UNIX_AMD64_ABI IsRegStructPassed). - private bool IsFeatureHfaTarget() + private bool IsFeatureHfaTarget(out RuntimeInfoArchitecture arch) { - RuntimeInfoArchitecture arch = _target.Contracts.RuntimeInfo.GetTargetArchitecture(); + arch = _target.Contracts.RuntimeInfo.GetTargetArchitecture(); return arch is RuntimeInfoArchitecture.Arm or RuntimeInfoArchitecture.Arm64; } @@ -629,12 +629,21 @@ public bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize) { elementSize = 0; - if (!IsFeatureHfaTarget()) + if (!IsFeatureHfaTarget(out RuntimeInfoArchitecture arch)) return false; if (!typeHandle.IsMethodTable() || !_methodTables[typeHandle.Address].Flags.IsHFA) return false; + // ARM shortcut: no HVA, and RequiresAlign8 encodes the R4-vs-R8 choice + // (CheckForHFA sets the alignment flag based on the resolved element + // type). Avoids walking fields. + if (arch == RuntimeInfoArchitecture.Arm) + { + elementSize = _methodTables[typeHandle.Address].Flags.RequiresAlign8 ? 8 : 4; + return true; + } + TypeHandle current = typeHandle; for (int depth = 0; depth < 16; depth++) { From 9d9fac041c2eec51063477874bee0c0390bd22d1 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 1 Jul 2026 18:32:24 -0400 Subject: [PATCH 7/8] Drop redundant TryGetHFAElementSize header comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/RuntimeTypeSystem_1.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index 637151c901d361..a34784b071b338 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -623,8 +623,6 @@ private bool IsFeatureHfaTarget(out RuntimeInfoArchitecture arch) return arch is RuntimeInfoArchitecture.Arm or RuntimeInfoArchitecture.Arm64; } - // See RuntimeTypeSystem.md for the pseudocode. Mirrors - // MethodTable::GetHFAType in src/coreclr/vm/class.cpp. public bool TryGetHFAElementSize(TypeHandle typeHandle, out int elementSize) { elementSize = 0; From b1b9ad99e852f6f14c76f40608805b37786688af Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 1 Jul 2026 18:33:25 -0400 Subject: [PATCH 8/8] Drop redundant IsFeatureHfaTarget header comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/RuntimeTypeSystem_1.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index a34784b071b338..e1a72c20e8eb27 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -615,8 +615,6 @@ public TargetPointer GetWellKnownMethodTable(WellKnownMethodTable kind) public bool ContainsGCPointers(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.ContainsGCPointers; public bool IsByRefLike(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _methodTables[typeHandle.Address].Flags.IsByRefLike; - // FEATURE_HFA targets. On non-FEATURE_HFA targets the enum_flag_IsHFA - // bit is either unused or reused (UNIX_AMD64_ABI IsRegStructPassed). private bool IsFeatureHfaTarget(out RuntimeInfoArchitecture arch) { arch = _target.Contracts.RuntimeInfo.GetTargetArchitecture();