From 354dc79255e76925f63629000c3a6a1365cc9326 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sat, 29 Aug 2026 13:09:26 +0200 Subject: [PATCH] Move MergeSimilarTypes into FindTypesInBounds. This way, we don't need the MapToMergedBounds logic to split the merged list back into lower/upper. Also, this commit avoids the quadratic merge-everything-with-everything else -- instead we use a dictionary to compare only types that are equivalent to begin with. This is the same approach as Roslyn MethodTypeInference.Fix/AddAllCandidates. --- .../Semantics/TypeInferenceTests.cs | 12 +++ .../CSharp/Resolver/TypeInference.cs | 92 ++++++++----------- .../Implementation/DecoratedType.cs | 5 + .../NullabilityAnnotatedType.cs | 5 + .../TypeSystem/NormalizeTypeVisitor.cs | 28 ++++++ 5 files changed, 87 insertions(+), 55 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs b/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs index 0b40a02a1..0770355eb 100644 --- a/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs +++ b/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs @@ -1245,6 +1245,18 @@ namespace ICSharpCode.Decompiler.Tests.Semantics Is.EqualTo(SpecialType.Dynamic)); Assert.That(success); } + + [Test] + public void BestCommonTypeDynamicAndObject() + { + Assert.That( + ti.GetBestCommonType(new[] { + new ResolveResult(SpecialType.Dynamic), + new ResolveResult(compilation.FindType(KnownTypeCode.Object)) + }, out bool success), + Is.EqualTo(SpecialType.Dynamic)); + Assert.That(success); + } #endregion #region FindTypeInBounds diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs b/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs index 62e80ddc8..41aa50324 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs @@ -1000,9 +1000,7 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver // Same Roslyn-style merge, over lower and upper bounds together as Roslyn's Fix does, // so bounds that differ only in tuple element names don't survive into // FindTypesInBounds as distinct candidates. - var (lowerBounds, upperBounds) = MergeShapeEquivalentBounds(tp.LowerBounds, tp.UpperBounds); - var types = CreateNestedInstance().FindTypesInBounds(lowerBounds, upperBounds); - Log.Unindent(); + var types = CreateNestedInstance().FindTypesInBounds(tp.LowerBounds, tp.UpperBounds); if (algorithm == TypeInferenceAlgorithm.ImprovedReturnAllResults) { tp.FixedTo = IntersectionType.Create(types); @@ -1086,51 +1084,6 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver } return null; } - - /// - /// Collapses bounds that are equal modulo tuple element names (possibly nested) into a - /// single merged type via , across both bound sets. Bounds - /// without a shape-equivalent partner are returned as-is. - /// - static (IReadOnlyList LowerBounds, IReadOnlyList UpperBounds) MergeShapeEquivalentBounds( - IReadOnlyCollection lowerBounds, IReadOnlyCollection upperBounds) - { - if (lowerBounds.Count + upperBounds.Count < 2) - return (lowerBounds.ToArray(), upperBounds.ToArray()); - var mergedBounds = new List(); - bool anyNamesMerged = false; - foreach (var bound in lowerBounds.Concat(upperBounds)) - { - bool absorbed = false; - for (int i = 0; i < mergedBounds.Count && !absorbed; i++) - { - IType merged = MergeSimilarTypes(mergedBounds[i], bound); - if (merged != null) - { - // Bounds that are already equal merge to the existing entry itself; - // only a new type means element names were actually merged. - anyNamesMerged |= !ReferenceEquals(merged, mergedBounds[i]); - mergedBounds[i] = merged; - absorbed = true; - } - } - if (!absorbed) - mergedBounds.Add(bound); - } - if (!anyNamesMerged) - return (lowerBounds.ToArray(), upperBounds.ToArray()); - return (MapToMergedBounds(lowerBounds, mergedBounds), MapToMergedBounds(upperBounds, mergedBounds)); - } - - /// - /// Replaces each bound with the entry of it was merged into. - /// Merging only changes element names, never the shape, so every bound is still - /// shape-equivalent to exactly one of those entries. - /// - static IType[] MapToMergedBounds(IEnumerable bounds, List mergedBounds) - { - return bounds.Select(b => mergedBounds.First(m => MergeSimilarTypes(m, b) != null)).Distinct().ToArray(); - } #endregion #region Finding the best common type of a set of expressions @@ -1169,7 +1122,7 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver /// /// Finds a type that satisfies the given lower and upper bounds. /// - public IType FindTypeInBounds(IReadOnlyList lowerBounds, IReadOnlyList upperBounds) + public IType FindTypeInBounds(IReadOnlyCollection lowerBounds, IReadOnlyCollection upperBounds) { if (lowerBounds == null) throw new ArgumentNullException(nameof(lowerBounds)); @@ -1189,13 +1142,13 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver } } - static IType GetFirstTypePreferNonInterfaces(IReadOnlyList result) + static IType GetFirstTypePreferNonInterfaces(IReadOnlyCollection result) { return result.FirstOrDefault(c => c.Kind != TypeKind.Interface) ?? result.FirstOrDefault() ?? SpecialType.UnknownType; } - IReadOnlyList FindTypesInBounds(IReadOnlyList lowerBounds, IReadOnlyList upperBounds) + IReadOnlyCollection FindTypesInBounds(IReadOnlyCollection lowerBounds, IReadOnlyCollection upperBounds) { // If there's only a single type; return that single type. // If both inputs are empty, return the empty list. @@ -1210,8 +1163,36 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver Log.WriteCollection("FindTypesInBound, LowerBounds=", lowerBounds); Log.WriteCollection("FindTypesInBound, UpperBounds=", upperBounds); + // Deduplicate types. This also merges types that differ only in tuple + // element names and/or object/dynamic. + var candidateMergeDict = new Dictionary(); + foreach (var candidate in lowerBounds.Concat(upperBounds)) + { + var key = candidate.AcceptVisitor(NormalizeTypeVisitor.KeyForTypeMerging); + if (candidateMergeDict.TryGetValue(key, out var existing)) + { + var merged = MergeSimilarTypes(existing, candidate); + Log.WriteLine(" Merged similar types " + existing + " and " + candidate + " into " + merged); + if (merged != null) + { + candidateMergeDict[key] = merged; + } + else + { + Debug.Fail("MergeSimilarTypes should always be able to merge;" + + " is the KeyForTypeMerging visitor misconfigured?"); + } + } + else + { + candidateMergeDict.Add(key, candidate); + } + } + + Log.WriteCollection("FindTypesInBound, Merged types from bounds=", candidateMergeDict.Values); + // First try the Fixing algorithm from the C# spec (ยง12.6.3.13) - List candidateTypes = lowerBounds.Union(upperBounds) + List candidateTypes = candidateMergeDict.Values .Where(c => lowerBounds.All(b => conversions.ImplicitConversion(b, c).IsValid)) .Where(c => upperBounds.All(b => conversions.ImplicitConversion(c, b).IsValid)) .ToList(); // evaluate the query only once @@ -1240,10 +1221,11 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver if (lowerBounds.Count > 0) { // Find candidates by using the lower bounds: - var hashSet = new HashSet(lowerBounds[0].GetAllBaseTypeDefinitions()); - for (int i = 1; i < lowerBounds.Count; i++) + var lowerBoundsList = lowerBounds.ToList(); + var hashSet = new HashSet(lowerBoundsList[0].GetAllBaseTypeDefinitions()); + for (int i = 1; i < lowerBoundsList.Count; i++) { - hashSet.IntersectWith(lowerBounds[i].GetAllBaseTypeDefinitions()); + hashSet.IntersectWith(lowerBoundsList[i].GetAllBaseTypeDefinitions()); } candidateTypeDefinitions = hashSet.ToList(); } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/DecoratedType.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/DecoratedType.cs index e5f1c4996..5b00d4e99 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/DecoratedType.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/DecoratedType.cs @@ -60,7 +60,12 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation public abstract IType AcceptVisitor(TypeVisitor visitor); + public abstract override int GetHashCode(); public abstract bool Equals(IType other); + public sealed override bool Equals(object obj) + { + return Equals(obj as IType); + } IEnumerable IType.GetAccessors(Predicate filter, GetMemberOptions options) { diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/NullabilityAnnotatedType.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/NullabilityAnnotatedType.cs index 1b94f2e39..cdef4349a 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/NullabilityAnnotatedType.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/NullabilityAnnotatedType.cs @@ -52,6 +52,11 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation return visitor.VisitNullabilityAnnotatedType(this); } + public override int GetHashCode() + { + return baseType.GetHashCode() ^ nullability.GetHashCode(); + } + public override bool Equals(IType other) { return other is NullabilityAnnotatedType nat diff --git a/ICSharpCode.Decompiler/TypeSystem/NormalizeTypeVisitor.cs b/ICSharpCode.Decompiler/TypeSystem/NormalizeTypeVisitor.cs index a1eafe028..14c4fe91a 100644 --- a/ICSharpCode.Decompiler/TypeSystem/NormalizeTypeVisitor.cs +++ b/ICSharpCode.Decompiler/TypeSystem/NormalizeTypeVisitor.cs @@ -18,7 +18,10 @@ #nullable enable +using System.Linq; + using ICSharpCode.Decompiler.TypeSystem.Implementation; +using ICSharpCode.Decompiler.Util; namespace ICSharpCode.Decompiler.TypeSystem { @@ -34,6 +37,7 @@ namespace ICSharpCode.Decompiler.TypeSystem DynamicAndObject = true, IntPtrToNInt = true, TupleToUnderlyingType = true, + RemoveTupleElementNames = false, RemoveModOpt = true, RemoveModReq = true, RemoveNullability = true, @@ -45,17 +49,32 @@ namespace ICSharpCode.Decompiler.TypeSystem DynamicAndObject = false, IntPtrToNInt = false, TupleToUnderlyingType = true, + RemoveTupleElementNames = false, RemoveModOpt = true, RemoveModReq = true, RemoveNullability = true, }; + // Used in type inference to group together similar types that can be merged. + internal static readonly NormalizeTypeVisitor KeyForTypeMerging = new NormalizeTypeVisitor { + ReplaceClassTypeParametersWithDummy = false, + ReplaceMethodTypeParametersWithDummy = false, + DynamicAndObject = true, + IntPtrToNInt = false, + TupleToUnderlyingType = false, + RemoveTupleElementNames = true, + RemoveModOpt = false, + RemoveModReq = false, + RemoveNullability = false, + }; + internal static readonly NormalizeTypeVisitor IgnoreNullability = new NormalizeTypeVisitor { ReplaceClassTypeParametersWithDummy = false, ReplaceMethodTypeParametersWithDummy = false, DynamicAndObject = false, IntPtrToNInt = false, TupleToUnderlyingType = false, + RemoveTupleElementNames = false, RemoveModOpt = true, RemoveModReq = true, RemoveNullability = true, @@ -75,6 +94,7 @@ namespace ICSharpCode.Decompiler.TypeSystem public bool DynamicAndObject = true; public bool IntPtrToNInt = true; public bool TupleToUnderlyingType = true; + public bool RemoveTupleElementNames = true; public bool RemoveNullability = true; public override IType VisitTypeParameter(ITypeParameter type) @@ -129,6 +149,14 @@ namespace ICSharpCode.Decompiler.TypeSystem { return type.UnderlyingType.AcceptVisitor(this); } + else if (RemoveTupleElementNames && type.ElementNames.Any(name => name != null)) + { + return new TupleType( + type.Compilation, + type.ElementTypes.SelectImmutableArray(t => t.AcceptVisitor(this)), + default, + type.GetDefinition()?.ParentModule); + } else { return base.VisitTupleType(type);