diff --git a/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs b/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs index 900b7dcb9..2d0dfb4f9 100644 --- a/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs +++ b/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs @@ -798,7 +798,92 @@ namespace ICSharpCode.Decompiler.Tests.Semantics } [Test] - [Ignore("Not implemented: AddExactBound compares bounds with name-sensitive equality, so two exact bounds that differ only in tuple element names count as conflicting before any merging can happen; csc merges them (verified: M(ref T, ref T) with (int a, string b)/(int a, string c) compiles, T = (int a, string)).")] + public void FixingMergesTupleElementNamesAcrossLowerAndUpperBounds() + { + // Signature: M(T x, Action y) + // Invocation: M(listOfAB, actionOfListOfAC); -> T = IList<(int a, string)> + // Action is contravariant, so the second argument produces an upper bound + // while the first produces a lower bound. + var comp = tupleCompilation.Value; + var inference = new TypeInference(comp); + var T = new DefaultTypeParameter(comp, SymbolKind.Method, 0, "T"); + ITypeDefinition listType = comp.FindType(KnownTypeCode.IListOfT).GetDefinition(); + ITypeDefinition actionType = comp.FindType(typeof(Action<>)).GetDefinition(); + IType listOfAC = new ParameterizedType(listType, new[] { MakeTupleType(comp, "a", "c") }); + + bool success; + Assert.That( + inference.InferTypeArguments(new ITypeParameter[] { T }, + new[] { + new ResolveResult(new ParameterizedType(listType, new[] { MakeTupleType(comp, "a", "b") })), + new ResolveResult(new ParameterizedType(actionType, new[] { listOfAC })) + }, + new IType[] { + T, + new ParameterizedType(actionType, new IType[] { T }) + }, + out success), + Is.EqualTo(new[] { new ParameterizedType(listType, new[] { MakeTupleType(comp, "a", null) }) })); + Assert.That(success); + } + + [Test] + public void FixingMergesTupleElementNamesThroughEqualNullabilityAnnotations() + { + // Signature: M(T x, T y) + // Invocation: M(nullableListOfAB, nullableListOfAC); -> T = IList<(int a, string)>? + // M(nullableArrayOfAB, nullableArrayOfAC); -> T = (int a, string)[]? + var comp = tupleCompilation.Value; + ITypeDefinition listType = comp.FindType(KnownTypeCode.IListOfT).GetDefinition(); + + IType InferSingle(IType argType1, IType argType2) + { + var T = new DefaultTypeParameter(comp, SymbolKind.Method, 0, "T"); + var result = new TypeInference(comp).InferTypeArguments(new ITypeParameter[] { T }, + new[] { new ResolveResult(argType1), new ResolveResult(argType2) }, + new IType[] { T, T }, + out bool success); + Assert.That(success); + return result.Single(); + } + + IType NullableListOf(TupleType elementType) + => new ParameterizedType(listType, new[] { elementType }).ChangeNullability(Nullability.Nullable); + IType NullableArrayOf(TupleType elementType) + => new ArrayType(comp, elementType, 1, Nullability.Nullable); + + Assert.That( + InferSingle(NullableListOf(MakeTupleType(comp, "a", "b")), NullableListOf(MakeTupleType(comp, "a", "c"))), + Is.EqualTo(NullableListOf(MakeTupleType(comp, "a", null)))); + + Assert.That( + InferSingle(NullableArrayOf(MakeTupleType(comp, "a", "b")), NullableArrayOf(MakeTupleType(comp, "a", "c"))), + Is.EqualTo(NullableArrayOf(MakeTupleType(comp, "a", null)))); + } + + [Test] + public void FixingDoesNotMergeBoundsThatDifferInNullability() + { + // Signature: M(T x, T y) + // Invocation: M(nullableArrayOfString, arrayOfString); + // Merging nullability requires the variance of the position, which this + // implementation does not track, so such bounds stay distinct and fixing fails + // (csc infers string[]?). + var comp = tupleCompilation.Value; + var T = new DefaultTypeParameter(comp, SymbolKind.Method, 0, "T"); + IType stringType = comp.FindType(KnownTypeCode.String); + + new TypeInference(comp).InferTypeArguments(new ITypeParameter[] { T }, + new[] { + new ResolveResult(new ArrayType(comp, stringType, 1, Nullability.Nullable)), + new ResolveResult(new ArrayType(comp, stringType)) + }, + new IType[] { T, T }, + out bool success); + Assert.That(success, Is.False); + } + + [Test] public void FixingMergesTupleElementNamesOfMultipleExactBounds() { // Signature: M(ref T x, ref T y) diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs b/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs index e4d43cf4b..d579cfbdb 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; @@ -242,8 +243,18 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver // Exact bounds need to stored separately, not just as Lower+Upper bounds, // due to TypeInferenceTests.GenericArgumentImplicitlyConvertibleToAndFromAnotherTypeList (see #281) if (ExactBound == null) + { ExactBound = type; - else if (!ExactBound.Equals(type)) + return; + } + if (ExactBound.Equals(type)) + return; + // Two exact bounds that differ only in tuple element names are not conflicting; + // their names are merged instead (kept where both agree, dropped otherwise). + IType merged = MergeTupleNames(ExactBound, type); + if (merged != null) + ExactBound = merged; + else MultipleDifferentExactBounds = true; } @@ -968,8 +979,15 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver Debug.Assert(!tp.IsFixed); if (tp.ExactBound != null) { - // the exact bound will always be the result - tp.FixedTo = tp.ExactBound; + // Roslyn behavior (not in the C# standard): when a lower/upper bound has the + // same shape as the exact bound except for tuple element names, the names are + // merged - kept where both sides agree, dropped where they conflict. See + // MergeTupleNames in Roslyn's MethodTypeInference.cs. + IType fixedTo = tp.ExactBound; + foreach (var b in tp.LowerBounds.Concat(tp.UpperBounds)) + fixedTo = MergeTupleNames(fixedTo, b) ?? fixedTo; + // the exact bound determines the result, up to the merged element names + tp.FixedTo = fixedTo; // check validity if (tp.MultipleDifferentExactBounds) return false; @@ -977,7 +995,11 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver && tp.UpperBounds.All(b => conversions.ImplicitConversion(tp.FixedTo, b).IsValid); } Log.Indent(); - var types = CreateNestedInstance().FindTypesInBounds(tp.LowerBounds.ToArray(), tp.UpperBounds.ToArray()); + // 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(); if (algorithm == TypeInferenceAlgorithm.ImprovedReturnAllResults) { @@ -992,6 +1014,112 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver return types.Count == 1; } } + + /// + /// Merges the tuple element names of two types that are equal apart from those names: + /// a name is kept where both sides agree and dropped where they conflict. Returns + /// null if the types differ in anything else. + /// + static IType MergeTupleNames(IType a, IType b) + { + if (a.Equals(b)) + return a; + // Roslyn merges differing nullability based on the variance of the position; this + // implementation does not track that, so differently annotated types are left alone. + if (a.Nullability != b.Nullability) + return null; + if (a is NullabilityAnnotatedType na && b is NullabilityAnnotatedType nb) + { + return MergeTupleNames(na.TypeWithoutAnnotation, nb.TypeWithoutAnnotation) + ?.ChangeNullability(a.Nullability); + } + if (a is TupleType ta && b is TupleType tb + && ta.ElementTypes.Length == tb.ElementTypes.Length) + { + var mergedElements = ImmutableArray.CreateBuilder(ta.ElementTypes.Length); + for (int i = 0; i < ta.ElementTypes.Length; i++) + { + var merged = MergeTupleNames(ta.ElementTypes[i], tb.ElementTypes[i]); + if (merged == null) + return null; + mergedElements.Add(merged); + } + var mergedNames = ImmutableArray.CreateBuilder(ta.ElementNames.Length); + for (int i = 0; i < ta.ElementNames.Length; i++) + { + mergedNames.Add(ta.ElementNames[i] == tb.ElementNames[i] ? ta.ElementNames[i] : null); + } + return new TupleType(ta.Compilation, mergedElements.MoveToImmutable(), mergedNames.MoveToImmutable(), + ta.GetDefinition()?.ParentModule); + } + if (a is ParameterizedType pa && b is ParameterizedType pb + && pa.GenericType.Equals(pb.GenericType) + && pa.TypeArguments.Count == pb.TypeArguments.Count) + { + var mergedArgs = new IType[pa.TypeArguments.Count]; + for (int i = 0; i < pa.TypeArguments.Count; i++) + { + var merged = MergeTupleNames(pa.TypeArguments[i], pb.TypeArguments[i]); + if (merged == null) + return null; + mergedArgs[i] = merged; + } + return new ParameterizedType(pa.GenericType, mergedArgs); + } + if (a is ArrayType arrA && b is ArrayType arrB && arrA.Dimensions == arrB.Dimensions) + { + var mergedElem = MergeTupleNames(arrA.ElementType, arrB.ElementType); + if (mergedElem == null) + return null; + return new ArrayType(arrA.Compilation, mergedElem, arrA.Dimensions, arrA.Nullability); + } + 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 = MergeTupleNames(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 => MergeTupleNames(m, b) != null)).Distinct().ToArray(); + } #endregion #region Finding the best common type of a set of expressions