Browse Source

Add support for merging types with different nullability.

pull/4077/head
Daniel Grunwald 3 weeks ago
parent
commit
979f5c9f02
  1. 35
      ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs
  2. 128
      ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs
  3. 21
      ICSharpCode.Decompiler/TypeSystem/ITypeParameter.cs
  4. 2
      ICSharpCode.Decompiler/TypeSystem/NormalizeTypeVisitor.cs

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

@ -854,25 +854,24 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
} }
[Test] [Test]
public void FixingDoesNotMergeBoundsThatDifferInNullability() public void FixingMergesBoundsThatDifferInNullability()
{ {
// Signature: M<T>(T x, T y) // Signature: M<T>(T x, T y)
// Invocation: M(nullableArrayOfString, arrayOfString); // Invocation: M(nullableArrayOfString, arrayOfString);
// Merging nullability requires the variance of the position, which this // Merging nullability in this covariant position should result in T=string[]? (the nullable array type).
// implementation does not track, so such bounds stay distinct and fixing fails
// (csc infers string[]?).
var comp = RefAssemblyCompilation.Instance; var comp = RefAssemblyCompilation.Instance;
var T = new DefaultTypeParameter(comp, SymbolKind.Method, 0, "T"); var T = new DefaultTypeParameter(comp, SymbolKind.Method, 0, "T");
IType stringType = comp.FindType(KnownTypeCode.String); IType stringType = comp.FindType(KnownTypeCode.String);
new TypeInference(comp).InferTypeArguments(new ITypeParameter[] { T }, var result = new TypeInference(comp).InferTypeArguments([T],
new[] { [
new ResolveResult(new ArrayType(comp, stringType, 1, Nullability.Nullable)), new ResolveResult(new ArrayType(comp, stringType, 1, Nullability.Nullable)),
new ResolveResult(new ArrayType(comp, stringType)) new ResolveResult(new ArrayType(comp, stringType))
}, ],
new IType[] { T, T }, [T, T],
out bool success); out bool success);
Assert.That(success, Is.False); Assert.That(success, Is.True);
Assert.That(result, Is.EqualTo([new ArrayType(comp, stringType, 1, Nullability.Nullable)]));
} }
[Test] [Test]
@ -1257,6 +1256,18 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
Is.EqualTo(SpecialType.Dynamic)); Is.EqualTo(SpecialType.Dynamic));
Assert.That(success); Assert.That(success);
} }
[Test]
public void BestCommonTypeObjectAndNullableObject()
{
Assert.That(
ti.GetBestCommonType(new[] {
new ResolveResult(compilation.FindType(KnownTypeCode.Object).ChangeNullability(Nullability.NotNullable)),
new ResolveResult(compilation.FindType(KnownTypeCode.Object).ChangeNullability(Nullability.Nullable))
}, out bool success),
Is.EqualTo(compilation.FindType(KnownTypeCode.Object).ChangeNullability(Nullability.Nullable)));
Assert.That(success);
}
#endregion #endregion
#region FindTypeInBounds #region FindTypeInBounds
@ -1399,8 +1410,12 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
// ReadOnlyCollectionBuilder<T> appears because the test compilation includes // ReadOnlyCollectionBuilder<T> appears because the test compilation includes
// System.Core, which declares it as another public implementation of both // System.Core, which declares it as another public implementation of both
// IList and IList<T>. // IList and IList<T>.
var typesInBounds = FindAllTypesInBounds(Resolve(), Resolve(typeof(IEnumerable<ICloneable>), typeof(IEnumerable<IComparable>), typeof(IList)));
// As this finds all derived types, the result set contains compiler-generated types like <>z__ReadOnlyArray`1.
// We filter those out to make the test more robust against changes.
typesInBounds = typesInBounds.Where(t => !t.GetDefinition().IsCompilerGenerated()).ToArray();
Assert.That( Assert.That(
FindAllTypesInBounds(Resolve(), Resolve(typeof(IEnumerable<ICloneable>), typeof(IEnumerable<IComparable>), typeof(IList))), typesInBounds,
Is.EqualTo(Resolve(typeof(List<string>), typeof(List<Version>), typeof(Collection<string>), typeof(Collection<Version>), typeof(ReadOnlyCollection<string>), typeof(ReadOnlyCollection<Version>), typeof(System.Runtime.CompilerServices.ReadOnlyCollectionBuilder<string>), typeof(System.Runtime.CompilerServices.ReadOnlyCollectionBuilder<Version>)))); Is.EqualTo(Resolve(typeof(List<string>), typeof(List<Version>), typeof(Collection<string>), typeof(Collection<Version>), typeof(ReadOnlyCollection<string>), typeof(ReadOnlyCollection<Version>), typeof(System.Runtime.CompilerServices.ReadOnlyCollectionBuilder<string>), typeof(System.Runtime.CompilerServices.ReadOnlyCollectionBuilder<Version>))));
} }
#endregion #endregion

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

@ -251,7 +251,7 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
return; return;
// Two exact bounds that differ only in tuple element names are not conflicting; // Two exact bounds that differ only in tuple element names are not conflicting;
// their names are merged instead (kept where both agree, dropped otherwise). // their names are merged instead (kept where both agree, dropped otherwise).
IType merged = MergeSimilarTypes(ExactBound, type); IType merged = MergeSimilarTypes(ExactBound, type, VarianceModifier.Invariant);
if (merged != null) if (merged != null)
ExactBound = merged; ExactBound = merged;
else else
@ -986,8 +986,14 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
// merged - kept where both sides agree, dropped where they conflict. See // merged - kept where both sides agree, dropped where they conflict. See
// MergeTupleNames in Roslyn's MethodTypeInference.cs. // MergeTupleNames in Roslyn's MethodTypeInference.cs.
IType fixedTo = tp.ExactBound; IType fixedTo = tp.ExactBound;
foreach (var b in tp.LowerBounds.Concat(tp.UpperBounds)) foreach (var b in tp.LowerBounds)
fixedTo = MergeSimilarTypes(fixedTo, b) ?? fixedTo; {
fixedTo = MergeSimilarTypes(fixedTo, b, VarianceModifier.Covariant) ?? fixedTo;
}
foreach (var b in tp.UpperBounds)
{
fixedTo = MergeSimilarTypes(fixedTo, b, VarianceModifier.Contravariant) ?? fixedTo;
}
// the exact bound determines the result, up to the merged element names // the exact bound determines the result, up to the merged element names
tp.FixedTo = fixedTo; tp.FixedTo = fixedTo;
// check validity // check validity
@ -1016,23 +1022,23 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
} }
/// <summary> /// <summary>
/// Merges similar types that differ only in tuple element names and/or object/dynamic, recursively. /// Merges similar types that differ only in any of these aspects.
/// * for tuple element names, a name is kept where both sides agree and dropped where they conflict. /// * tuple element names: a name is kept where both sides agree and dropped where they conflict.
/// * for object/dynamic, dynamic is preferred over object. /// * object/dynamic: dynamic is preferred over object.
/// * nullability: depends on the variance of the position.
/// Returns <c>null</c> if the types differ in any other aspects. /// Returns <c>null</c> if the types differ in any other aspects.
/// </summary> /// </summary>
static IType MergeSimilarTypes(IType a, IType b) static IType MergeSimilarTypes(IType a, IType b, VarianceModifier variance)
{ {
if (a.Equals(b)) if (a.Equals(b))
return a; return a;
// Roslyn merges differing nullability based on the variance of the position; this if (a is NullabilityAnnotatedType || b is NullabilityAnnotatedType)
// 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 MergeSimilarTypes(na.TypeWithoutAnnotation, nb.TypeWithoutAnnotation) var nullability = MergeNullability(a.Nullability, b.Nullability, variance);
?.ChangeNullability(a.Nullability); var merged = MergeSimilarTypes(a.WithoutNullability(), b.WithoutNullability(), variance);
if (merged == null)
return null;
return merged.ChangeNullability(nullability);
} }
if (a.Kind == TypeKind.Dynamic && b.IsKnownType(KnownTypeCode.Object)) if (a.Kind == TypeKind.Dynamic && b.IsKnownType(KnownTypeCode.Object))
{ {
@ -1048,7 +1054,11 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
var mergedElements = ImmutableArray.CreateBuilder<IType>(ta.ElementTypes.Length); var mergedElements = ImmutableArray.CreateBuilder<IType>(ta.ElementTypes.Length);
for (int i = 0; i < ta.ElementTypes.Length; i++) for (int i = 0; i < ta.ElementTypes.Length; i++)
{ {
var merged = MergeSimilarTypes(ta.ElementTypes[i], tb.ElementTypes[i]); // Note: even though ValueTuple has invariant type parameters,
// Roslyn merges tuple element types in a covariant manner.
var merged = MergeSimilarTypes(
ta.ElementTypes[i], tb.ElementTypes[i],
variance.Combine(VarianceModifier.Covariant));
if (merged == null) if (merged == null)
return null; return null;
mergedElements.Add(merged); mergedElements.Add(merged);
@ -1062,28 +1072,75 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
ta.GetDefinition()?.ParentModule); ta.GetDefinition()?.ParentModule);
} }
if (a is ParameterizedType pa && b is ParameterizedType pb if (a is ParameterizedType pa && b is ParameterizedType pb
&& pa.GenericType.Equals(pb.GenericType)
&& pa.TypeArguments.Count == pb.TypeArguments.Count) && pa.TypeArguments.Count == pb.TypeArguments.Count)
{ {
var genericType = MergeSimilarTypes(pa.GenericType, pb.GenericType, variance);
if (genericType == null)
return null;
var mergedArgs = new IType[pa.TypeArguments.Count]; var mergedArgs = new IType[pa.TypeArguments.Count];
for (int i = 0; i < pa.TypeArguments.Count; i++) for (int i = 0; i < pa.TypeArguments.Count; i++)
{ {
var merged = MergeSimilarTypes(pa.TypeArguments[i], pb.TypeArguments[i]); var merged = MergeSimilarTypes(
pa.TypeArguments[i], pb.TypeArguments[i],
variance.Combine(pa.TypeParameters[i].Variance));
if (merged == null) if (merged == null)
return null; return null;
mergedArgs[i] = merged; mergedArgs[i] = merged;
} }
return new ParameterizedType(pa.GenericType, mergedArgs); return new ParameterizedType(genericType, mergedArgs);
} }
if (a is ArrayType arrA && b is ArrayType arrB && arrA.Dimensions == arrB.Dimensions) if (a is ArrayType arrA && b is ArrayType arrB && arrA.Dimensions == arrB.Dimensions)
{ {
var mergedElem = MergeSimilarTypes(arrA.ElementType, arrB.ElementType); // Roslyn ArrayTypeSymbol merges in a covariant manner.
var mergedElem = MergeSimilarTypes(
arrA.ElementType, arrB.ElementType,
variance.Combine(VarianceModifier.Covariant));
if (mergedElem == null) if (mergedElem == null)
return null; return null;
return new ArrayType(arrA.Compilation, mergedElem, arrA.Dimensions, arrA.Nullability); var nullability = MergeNullability(arrA.Nullability, arrB.Nullability, variance);
return new ArrayType(arrA.Compilation, mergedElem, arrA.Dimensions, nullability);
}
if (a is PointerType ptrA && b is PointerType ptrB)
{
var mergedElem = MergeSimilarTypes(
ptrA.ElementType, ptrB.ElementType,
variance.Combine(VarianceModifier.Invariant));
if (mergedElem == null)
return null;
return new PointerType(mergedElem);
} }
return null; return null;
} }
static Nullability MergeNullability(Nullability a, Nullability b, VarianceModifier variance)
{
// Like Roslyn's MergeNullableAnnotation()
return (variance, a, b) switch {
// Covariant merging rules: Nullable wins over Oblivious which wins over NotNullable.
(VarianceModifier.Covariant, Nullability.Nullable, _) => Nullability.Nullable,
(VarianceModifier.Covariant, _, Nullability.Nullable) => Nullability.Nullable,
(VarianceModifier.Covariant, Nullability.Oblivious, _) => Nullability.Oblivious,
(VarianceModifier.Covariant, _, Nullability.Oblivious) => Nullability.Oblivious,
(VarianceModifier.Covariant, Nullability.NotNullable, Nullability.NotNullable) => Nullability.NotNullable,
// Contravariant merging rules: NotNullable wins over Oblivious which wins over Nullable.
(VarianceModifier.Contravariant, Nullability.NotNullable, _) => Nullability.NotNullable,
(VarianceModifier.Contravariant, _, Nullability.NotNullable) => Nullability.NotNullable,
(VarianceModifier.Contravariant, Nullability.Oblivious, _) => Nullability.Oblivious,
(VarianceModifier.Contravariant, _, Nullability.Oblivious) => Nullability.Oblivious,
(VarianceModifier.Contravariant, Nullability.Nullable, Nullability.Nullable) => Nullability.Nullable,
// Invariant merging rules: NotNullable wins over Nullable which wins over Oblivious.
// Weird but that's what Roslyn does:
// static T M<T>(ref T x, ref T y) => x;
// M(ref nullableArray, nonNullableArray);
// T is inferred as int[] and then the first argument reports a "possible null reference assignment" warning.
(VarianceModifier.Invariant, Nullability.NotNullable, _) => Nullability.NotNullable,
(VarianceModifier.Invariant, _, Nullability.NotNullable) => Nullability.NotNullable,
(VarianceModifier.Invariant, Nullability.Nullable, _) => Nullability.Nullable,
(VarianceModifier.Invariant, _, Nullability.Nullable) => Nullability.Nullable,
(VarianceModifier.Invariant, Nullability.Oblivious, Nullability.Oblivious) => Nullability.Oblivious,
_ => throw new NotSupportedException("Unexpected nullability combination: " + a + ", " + b + " with variance " + variance)
};
}
#endregion #endregion
#region Finding the best common type of a set of expressions #region Finding the best common type of a set of expressions
@ -1166,28 +1223,33 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
// Deduplicate types. This also merges types that differ only in tuple // Deduplicate types. This also merges types that differ only in tuple
// element names and/or object/dynamic. // element names and/or object/dynamic.
var candidateMergeDict = new Dictionary<IType, IType>(); var candidateMergeDict = new Dictionary<IType, IType>();
foreach (var candidate in lowerBounds.Concat(upperBounds)) void AddCandidates(IEnumerable<IType> candidates, VarianceModifier variance)
{ {
var key = candidate.AcceptVisitor(NormalizeTypeVisitor.KeyForTypeMerging); foreach (var candidate in candidates)
if (candidateMergeDict.TryGetValue(key, out var existing))
{ {
var merged = MergeSimilarTypes(existing, candidate); var key = candidate.AcceptVisitor(NormalizeTypeVisitor.KeyForTypeMerging);
Log.WriteLine(" Merged similar types " + existing + " and " + candidate + " into " + merged); if (candidateMergeDict.TryGetValue(key, out var existing))
if (merged != null)
{ {
candidateMergeDict[key] = merged; var merged = MergeSimilarTypes(existing, candidate, variance);
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 else
{ {
Debug.Fail("MergeSimilarTypes should always be able to merge;" candidateMergeDict.Add(key, candidate);
+ " is the KeyForTypeMerging visitor misconfigured?");
} }
} }
else
{
candidateMergeDict.Add(key, candidate);
}
} }
AddCandidates(lowerBounds, VarianceModifier.Covariant);
AddCandidates(upperBounds, VarianceModifier.Contravariant);
Log.WriteCollection("FindTypesInBound, Merged types from bounds=", candidateMergeDict.Values); Log.WriteCollection("FindTypesInBound, Merged types from bounds=", candidateMergeDict.Values);

21
ICSharpCode.Decompiler/TypeSystem/ITypeParameter.cs

@ -129,19 +129,32 @@ namespace ICSharpCode.Decompiler.TypeSystem
/// <summary> /// <summary>
/// Represents the variance of a type parameter. /// Represents the variance of a type parameter.
/// </summary> /// </summary>
public enum VarianceModifier : byte public enum VarianceModifier : sbyte
{ {
/// <summary> /// <summary>
/// The type parameter is not variant. /// The type parameter is not variant.
/// </summary> /// </summary>
Invariant, Invariant = 0,
/// <summary> /// <summary>
/// The type parameter is covariant (used in output position). /// The type parameter is covariant (used in output position).
/// </summary> /// </summary>
Covariant, Covariant = 1,
/// <summary> /// <summary>
/// The type parameter is contravariant (used in input position). /// The type parameter is contravariant (used in input position).
/// </summary> /// </summary>
Contravariant Contravariant = -1
}; };
static class VarianceExtensions
{
/// <summary>
/// Combines variance modifiers.
/// It's like a multiplication where Invariant is 0, Covariant is +1 and Contravariant is -1.
/// </summary>
public static VarianceModifier Combine(this VarianceModifier a, VarianceModifier b)
{
// By picking matching enum values, we can actually implement this as multiplication.
return (VarianceModifier)((sbyte)a * (sbyte)b);
}
}
} }

2
ICSharpCode.Decompiler/TypeSystem/NormalizeTypeVisitor.cs

@ -65,7 +65,7 @@ namespace ICSharpCode.Decompiler.TypeSystem
RemoveTupleElementNames = true, RemoveTupleElementNames = true,
RemoveModOpt = false, RemoveModOpt = false,
RemoveModReq = false, RemoveModReq = false,
RemoveNullability = false, RemoveNullability = true,
}; };
internal static readonly NormalizeTypeVisitor IgnoreNullability = new NormalizeTypeVisitor { internal static readonly NormalizeTypeVisitor IgnoreNullability = new NormalizeTypeVisitor {

Loading…
Cancel
Save