Browse Source

Merge tuple element names during type-inference fixing

When fixing a type parameter, Roslyn merges the tuple element names of
bounds that are identical apart from those names: names are kept where
all bounds agree and dropped where they conflict (MergeTupleNames in
Roslyn's MethodTypeInference.cs). The C# standard does not describe
this step. Without it, fixing either kept the first bound's names
verbatim or, with two exact bounds differing only in names, failed
outright - so inferred tuple types could carry names csc would not
produce. All merged-name expectations are csc-verified.

Nullability is deliberately not merged: Roslyn derives it from the
variance of the position, which this implementation does not track, so
bounds that differ in it stay distinct and fixing fails as before
rather than inventing an annotation.

Assisted-by: Claude:claude-fable-5:Claude Code
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
pull/4005/head
Siegfried Pammer 2 months ago committed by Christoph Wille
parent
commit
01895fbab7
  1. 87
      ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs
  2. 136
      ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs

87
ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs

@ -798,7 +798,92 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -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<T>(ref T, ref T) with (int a, string b)/(int a, string c) compiles, T = (int a, string)).")]
public void FixingMergesTupleElementNamesAcrossLowerAndUpperBounds()
{
// Signature: M<T>(T x, Action<T> y)
// Invocation: M(listOfAB, actionOfListOfAC); -> T = IList<(int a, string)>
// Action<in T> 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>(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>(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<T>(ref T x, ref T y)

136
ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs

@ -18,6 +18,7 @@ @@ -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 @@ -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 @@ -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 @@ -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 @@ -992,6 +1014,112 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
return types.Count == 1;
}
}
/// <summary>
/// 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
/// <c>null</c> if the types differ in anything else.
/// </summary>
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<IType>(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<string>(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;
}
/// <summary>
/// Collapses bounds that are equal modulo tuple element names (possibly nested) into a
/// single merged type via <see cref="MergeTupleNames"/>, across both bound sets. Bounds
/// without a shape-equivalent partner are returned as-is.
/// </summary>
static (IReadOnlyList<IType> LowerBounds, IReadOnlyList<IType> UpperBounds) MergeShapeEquivalentBounds(
IReadOnlyCollection<IType> lowerBounds, IReadOnlyCollection<IType> upperBounds)
{
if (lowerBounds.Count + upperBounds.Count < 2)
return (lowerBounds.ToArray(), upperBounds.ToArray());
var mergedBounds = new List<IType>();
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));
}
/// <summary>
/// Replaces each bound with the entry of <paramref name="mergedBounds"/> 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.
/// </summary>
static IType[] MapToMergedBounds(IEnumerable<IType> bounds, List<IType> 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

Loading…
Cancel
Save