Browse Source

Merge pull request #4077 from icsharpcode/type-inference

Improvements for type inference
pull/4057/head
Daniel Grunwald 2 weeks ago committed by GitHub
parent
commit
05d274ff72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 84
      ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs
  2. 18
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullableRefTypes.cs
  3. 261
      ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs
  4. 6
      ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs
  5. 2
      ICSharpCode.Decompiler/Semantics/LambdaResolveResult.cs
  6. 21
      ICSharpCode.Decompiler/TypeSystem/ITypeParameter.cs
  7. 5
      ICSharpCode.Decompiler/TypeSystem/Implementation/DecoratedType.cs
  8. 5
      ICSharpCode.Decompiler/TypeSystem/Implementation/NullabilityAnnotatedType.cs
  9. 28
      ICSharpCode.Decompiler/TypeSystem/NormalizeTypeVisitor.cs

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

@ -23,6 +23,7 @@ using System.Collections.Immutable;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection.Metadata;
using ICSharpCode.Decompiler.CSharp.Resolver; using ICSharpCode.Decompiler.CSharp.Resolver;
using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Metadata;
@ -713,6 +714,15 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
ImmutableArray.CreateRange(elementNames)); ImmutableArray.CreateRange(elementNames));
} }
FunctionPointerType MakeFunctionPointerType(ICompilation comp, IType returnType)
{
return new FunctionPointerType(
(MetadataModule)comp.MainModule,
SignatureCallingConvention.Default, ImmutableArray<IType>.Empty,
returnType, returnIsRefReadOnly: false,
ImmutableArray<IType>.Empty, ImmutableArray<ReferenceKind>.Empty);
}
[Test] [Test]
public void BestCommonTypeMergesTupleElementNames() public void BestCommonTypeMergesTupleElementNames()
{ {
@ -730,6 +740,21 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
Assert.That(success); Assert.That(success);
} }
[Test]
public void BestCommonTypeMergesFunctionPointerTupleElementNames()
{
var comp = RefAssemblyCompilation.Instance;
var inference = new TypeInference(comp);
Assert.That(
inference.GetBestCommonType(new[] {
new ResolveResult(MakeFunctionPointerType(comp, MakeTupleType(comp, "a", "b"))),
new ResolveResult(MakeFunctionPointerType(comp, MakeTupleType(comp, "a", "c")))
}, out bool success),
Is.EqualTo(MakeFunctionPointerType(comp, MakeTupleType(comp, "a", null))));
Assert.That(success);
}
[Test] [Test]
public void FixingMergesTupleElementNamesOfExactAndLowerBounds() public void FixingMergesTupleElementNamesOfExactAndLowerBounds()
{ {
@ -854,25 +879,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]
@ -1245,6 +1269,42 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
Is.EqualTo(SpecialType.Dynamic)); Is.EqualTo(SpecialType.Dynamic));
Assert.That(success); 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);
}
[Test]
public void BestCommonTypeObjectAndNullableString()
{
Assert.That(
ti.GetBestCommonType([
new ResolveResult(compilation.FindType(KnownTypeCode.Object).ChangeNullability(Nullability.NotNullable)),
new ResolveResult(compilation.FindType(KnownTypeCode.String).ChangeNullability(Nullability.Nullable))
], out bool success),
Is.EqualTo(compilation.FindType(KnownTypeCode.Object).ChangeNullability(Nullability.Nullable)));
Assert.That(success);
}
[Test]
public void BestCommonTypeObjectAndNullableObject()
{
Assert.That(
ti.GetBestCommonType([
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
@ -1387,8 +1447,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

18
ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullableRefTypes.cs

@ -3,6 +3,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{ {
@ -207,4 +208,21 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
return default; return default;
} }
} }
public class T09_Linq
{
public IEnumerable<string> QueryWithNonNullableReferenceTypes(IEnumerable<string> strings)
{
return from s in strings
where s.Length > 0
select s.ToUpper();
}
public IEnumerable<string> QueryWithNullableReferenceTypes(IEnumerable<string?> strings)
{
return from s in strings
where s != null
select s.ToUpper();
}
}
} }

261
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
@ -1000,8 +1006,7 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
// Same Roslyn-style merge, over lower and upper bounds together as Roslyn's Fix does, // 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 // so bounds that differ only in tuple element names don't survive into
// FindTypesInBounds as distinct candidates. // FindTypesInBounds as distinct candidates.
var (lowerBounds, upperBounds) = MergeShapeEquivalentBounds(tp.LowerBounds, tp.UpperBounds); var types = CreateNestedInstance().FindTypesInBounds(tp.LowerBounds, tp.UpperBounds);
var types = CreateNestedInstance().FindTypesInBounds(lowerBounds, upperBounds);
Log.Unindent(); Log.Unindent();
if (algorithm == TypeInferenceAlgorithm.ImprovedReturnAllResults) if (algorithm == TypeInferenceAlgorithm.ImprovedReturnAllResults)
{ {
@ -1018,23 +1023,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))
{ {
@ -1050,7 +1055,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);
@ -1064,72 +1073,124 @@ 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);
} }
return null; if (a is ByReferenceType refA && b is ByReferenceType refB)
} {
var mergedElem = MergeSimilarTypes(
/// <summary> refA.ElementType, refB.ElementType,
/// Collapses bounds that are equal modulo tuple element names (possibly nested) into a variance.Combine(VarianceModifier.Invariant));
/// single merged type via <see cref="MergeSimilarTypes"/>, across both bound sets. Bounds if (mergedElem == null)
/// without a shape-equivalent partner are returned as-is. return null;
/// </summary> return new ByReferenceType(mergedElem);
static (IReadOnlyList<IType> LowerBounds, IReadOnlyList<IType> UpperBounds) MergeShapeEquivalentBounds( }
IReadOnlyCollection<IType> lowerBounds, IReadOnlyCollection<IType> upperBounds) if (a is PointerType ptrA && b is PointerType ptrB)
{ {
if (lowerBounds.Count + upperBounds.Count < 2) var mergedElem = MergeSimilarTypes(
return (lowerBounds.ToArray(), upperBounds.ToArray()); ptrA.ElementType, ptrB.ElementType,
var mergedBounds = new List<IType>(); variance.Combine(VarianceModifier.Invariant));
bool anyNamesMerged = false; if (mergedElem == null)
foreach (var bound in lowerBounds.Concat(upperBounds)) return null;
{ return new PointerType(mergedElem);
bool absorbed = false; }
for (int i = 0; i < mergedBounds.Count && !absorbed; i++) if (a is FunctionPointerType fnPtrA && b is FunctionPointerType fnPtrB
&& fnPtrA.CallingConvention == fnPtrB.CallingConvention
&& fnPtrA.CustomCallingConventions.SequenceEqual(fnPtrB.CustomCallingConventions)
&& fnPtrA.ReturnIsRefReadOnly == fnPtrB.ReturnIsRefReadOnly
&& fnPtrA.ParameterTypes.Length == fnPtrB.ParameterTypes.Length
&& fnPtrA.ParameterReferenceKinds.SequenceEqual(fnPtrB.ParameterReferenceKinds))
{
var mergedReturn = MergeSimilarTypes(
fnPtrA.ReturnType, fnPtrB.ReturnType,
variance.Combine(VarianceModifier.Covariant));
if (mergedReturn == null)
return null;
var mergedParameters = ImmutableArray.CreateBuilder<IType>(fnPtrA.ParameterTypes.Length);
for (int i = 0; i < fnPtrA.ParameterTypes.Length; i++)
{ {
IType merged = MergeSimilarTypes(mergedBounds[i], bound); var mergedParameter = MergeSimilarTypes(
if (merged != null) fnPtrA.ParameterTypes[i], fnPtrB.ParameterTypes[i],
{ variance.Combine(VarianceModifier.Contravariant));
// Bounds that are already equal merge to the existing entry itself; if (mergedParameter == null)
// only a new type means element names were actually merged. return null;
anyNamesMerged |= !ReferenceEquals(merged, mergedBounds[i]); mergedParameters.Add(mergedParameter);
mergedBounds[i] = merged;
absorbed = true;
}
} }
if (!absorbed) return fnPtrA.WithSignature(mergedReturn, mergedParameters.MoveToImmutable());
mergedBounds.Add(bound); }
if (a is ModifiedType modA && b is ModifiedType modB
&& modA.Kind == modB.Kind
&& modA.Modifier.Equals(modB.Modifier))
{
var mergedElem = MergeSimilarTypes(modA.ElementType, modB.ElementType, variance);
if (mergedElem == null)
return null;
return new ModifiedType(modA.Modifier, mergedElem, modA.Kind == TypeKind.ModReq);
}
if (a is UnknownType unknownTypeA && b is UnknownType unknownTypeB
&& unknownTypeA.FullTypeName == unknownTypeB.FullTypeName)
{
if (unknownTypeA.IsReferenceType == unknownTypeB.IsReferenceType)
return unknownTypeA;
else
return unknownTypeA.WithoutReferenceTypeKnowledge();
} }
if (!anyNamesMerged) return null;
return (lowerBounds.ToArray(), upperBounds.ToArray());
return (MapToMergedBounds(lowerBounds, mergedBounds), MapToMergedBounds(upperBounds, mergedBounds));
} }
/// <summary> static Nullability MergeNullability(Nullability a, Nullability b, VarianceModifier variance)
/// 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 => MergeSimilarTypes(m, b) != null)).Distinct().ToArray(); // 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
@ -1169,7 +1230,7 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
/// <summary> /// <summary>
/// Finds a type that satisfies the given lower and upper bounds. /// Finds a type that satisfies the given lower and upper bounds.
/// </summary> /// </summary>
public IType FindTypeInBounds(IReadOnlyList<IType> lowerBounds, IReadOnlyList<IType> upperBounds) public IType FindTypeInBounds(IReadOnlyCollection<IType> lowerBounds, IReadOnlyCollection<IType> upperBounds)
{ {
if (lowerBounds == null) if (lowerBounds == null)
throw new ArgumentNullException(nameof(lowerBounds)); throw new ArgumentNullException(nameof(lowerBounds));
@ -1189,13 +1250,13 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
} }
} }
static IType GetFirstTypePreferNonInterfaces(IReadOnlyList<IType> result) static IType GetFirstTypePreferNonInterfaces(IReadOnlyCollection<IType> result)
{ {
return result.FirstOrDefault(c => c.Kind != TypeKind.Interface) return result.FirstOrDefault(c => c.Kind != TypeKind.Interface)
?? result.FirstOrDefault() ?? SpecialType.UnknownType; ?? result.FirstOrDefault() ?? SpecialType.UnknownType;
} }
IReadOnlyList<IType> FindTypesInBounds(IReadOnlyList<IType> lowerBounds, IReadOnlyList<IType> upperBounds) IReadOnlyCollection<IType> FindTypesInBounds(IReadOnlyCollection<IType> lowerBounds, IReadOnlyCollection<IType> upperBounds)
{ {
// If there's only a single type; return that single type. // If there's only a single type; return that single type.
// If both inputs are empty, return the empty list. // If both inputs are empty, return the empty list.
@ -1210,8 +1271,57 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
Log.WriteCollection("FindTypesInBound, LowerBounds=", lowerBounds); Log.WriteCollection("FindTypesInBound, LowerBounds=", lowerBounds);
Log.WriteCollection("FindTypesInBound, UpperBounds=", upperBounds); Log.WriteCollection("FindTypesInBound, UpperBounds=", upperBounds);
// Deduplicate types. This also merges types that differ only in tuple
// element names and/or object/dynamic.
// The specification doesn't mention this step, but Roslyn does it,
// and it's crucial for tuple element names and nested nullabilities in otherwise
// equivalent types.
Nullability? topLevelNullability = null;
var candidateMergeDict = new Dictionary<IType, IType>();
void AddCandidates(IReadOnlyCollection<IType> bounds, VarianceModifier variance)
{
// This helper function works like Roslyn's MethodTypeInference.AddAllCandidates().
// It deduplicates similar types and merges them into a single candidate type,
// handling differences in nullability, tuple element names, and object/dynamic,
// but only if the type is otherwise completely identical.
foreach (var bound in bounds)
{
if (topLevelNullability.HasValue)
{
topLevelNullability = MergeNullability(topLevelNullability.Value, bound.Nullability, variance);
}
else
{
topLevelNullability = bound.Nullability;
}
var key = bound.AcceptVisitor(NormalizeTypeVisitor.KeyForTypeMerging);
if (candidateMergeDict.TryGetValue(key, out var existing))
{
var merged = MergeSimilarTypes(existing, bound, variance);
Log.WriteLine(" Merged similar types " + existing + " and " + bound + " 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, bound);
}
}
}
AddCandidates(lowerBounds, VarianceModifier.Covariant);
AddCandidates(upperBounds, VarianceModifier.Contravariant);
Log.WriteCollection("FindTypesInBound, Merged types from bounds=", candidateMergeDict.Values);
// First try the Fixing algorithm from the C# spec (§12.6.3.13) // First try the Fixing algorithm from the C# spec (§12.6.3.13)
List<IType> candidateTypes = lowerBounds.Union(upperBounds) List<IType> candidateTypes = candidateMergeDict.Values
.Where(c => lowerBounds.All(b => conversions.ImplicitConversion(b, c).IsValid)) .Where(c => lowerBounds.All(b => conversions.ImplicitConversion(b, c).IsValid))
.Where(c => upperBounds.All(b => conversions.ImplicitConversion(c, b).IsValid)) .Where(c => upperBounds.All(b => conversions.ImplicitConversion(c, b).IsValid))
.ToList(); // evaluate the query only once .ToList(); // evaluate the query only once
@ -1224,6 +1334,18 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
c => candidateTypes.All(o => conversions.ImplicitConversion(o, c).IsValid) c => candidateTypes.All(o => conversions.ImplicitConversion(o, c).IsValid)
).ToList(); ).ToList();
// Apply the merged top-level nullability:
Debug.Assert(topLevelNullability.HasValue);
// Roslyn has a different approach in MergeOrRemoveCandidates, which can
// differ in behavior when there's both lower+upper bounds
// -- e.g. `static void M<T>(T x, Action<T> a)` called with `M("s", (object? o) => {}))`
// is inferred as `T = object?` by Roslyn, but `T = object` by us.
// To match Roslyn exactly, we'd need to handle the topLevelNullability per-candidate.
for (int i = 0; i < candidateTypes.Count; i++)
{
candidateTypes[i] = candidateTypes[i].ChangeNullability(topLevelNullability.Value);
}
// If the specified algorithm produces a single candidate, we return // If the specified algorithm produces a single candidate, we return
// that candidate. // that candidate.
// We also return the whole candidate list if we're not using the improved // We also return the whole candidate list if we're not using the improved
@ -1240,10 +1362,11 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
if (lowerBounds.Count > 0) if (lowerBounds.Count > 0)
{ {
// Find candidates by using the lower bounds: // Find candidates by using the lower bounds:
var hashSet = new HashSet<ITypeDefinition>(lowerBounds[0].GetAllBaseTypeDefinitions()); var lowerBoundsList = lowerBounds.ToList();
for (int i = 1; i < lowerBounds.Count; i++) var hashSet = new HashSet<ITypeDefinition>(lowerBoundsList[0].GetAllBaseTypeDefinitions());
for (int i = 1; i < lowerBoundsList.Count; i++)
{ {
hashSet.IntersectWith(lowerBounds[i].GetAllBaseTypeDefinitions()); hashSet.IntersectWith(lowerBoundsList[i].GetAllBaseTypeDefinitions());
} }
candidateTypeDefinitions = hashSet.ToList(); candidateTypeDefinitions = hashSet.ToList();
} }

6
ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs

@ -142,6 +142,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
MemberReferenceExpression? mre = invocation.Target as MemberReferenceExpression; MemberReferenceExpression? mre = invocation.Target as MemberReferenceExpression;
if (mre == null || IsNullConditional(mre.Target)) if (mre == null || IsNullConditional(mre.Target))
return null; return null;
if (mre.TypeArguments.Count > 0)
return null;
switch (mre.MemberName) switch (mre.MemberName)
{ {
case "Select": case "Select":
@ -346,7 +348,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
static bool IsComplexQuery(MemberReferenceExpression mre) static bool IsComplexQuery(MemberReferenceExpression mre)
{ {
return ((mre.Target is InvocationExpression && mre.Parent is InvocationExpression) || mre.Parent?.Parent is QueryClause); return (mre.Target is InvocationExpression && mre.Parent is InvocationExpression) || mre.Parent?.Parent is QueryClause;
} }
QueryFromClause MakeFromClause(ParameterDeclaration parameter, Expression body) QueryFromClause MakeFromClause(ParameterDeclaration parameter, Expression body)
@ -412,6 +414,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return false; return false;
if (parameter.Name != expectedParameterName) if (parameter.Name != expectedParameterName)
return false; return false;
if (mre.TypeArguments.Count > 0)
return false;
if (mre.MemberName == "OrderBy" || mre.MemberName == "OrderByDescending") if (mre.MemberName == "OrderBy" || mre.MemberName == "OrderByDescending")
return !IsNullConditional(mre.Target); return !IsNullConditional(mre.Target);

2
ICSharpCode.Decompiler/Semantics/LambdaResolveResult.cs

@ -165,7 +165,7 @@ namespace ICSharpCode.Decompiler.Semantics
return Conversion.None; return Conversion.None;
for (int i = 0; i < parameterTypes.Length; ++i) for (int i = 0; i < parameterTypes.Length; ++i)
{ {
if (parameterTypes[i].Equals(this.Parameters[i].Type)) if (NormalizeTypeVisitor.IgnoreNullability.EquivalentTypes(parameterTypes[i], this.Parameters[i].Type))
{ {
continue; continue;
} }

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);
}
}
} }

5
ICSharpCode.Decompiler/TypeSystem/Implementation/DecoratedType.cs

@ -60,7 +60,12 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
public abstract IType AcceptVisitor(TypeVisitor visitor); public abstract IType AcceptVisitor(TypeVisitor visitor);
public abstract override int GetHashCode();
public abstract bool Equals(IType other); public abstract bool Equals(IType other);
public sealed override bool Equals(object obj)
{
return Equals(obj as IType);
}
IEnumerable<IMethod> IType.GetAccessors(Predicate<IMethod> filter, GetMemberOptions options) IEnumerable<IMethod> IType.GetAccessors(Predicate<IMethod> filter, GetMemberOptions options)
{ {

5
ICSharpCode.Decompiler/TypeSystem/Implementation/NullabilityAnnotatedType.cs

@ -52,6 +52,11 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
return visitor.VisitNullabilityAnnotatedType(this); return visitor.VisitNullabilityAnnotatedType(this);
} }
public override int GetHashCode()
{
return baseType.GetHashCode() ^ nullability.GetHashCode();
}
public override bool Equals(IType other) public override bool Equals(IType other)
{ {
return other is NullabilityAnnotatedType nat return other is NullabilityAnnotatedType nat

28
ICSharpCode.Decompiler/TypeSystem/NormalizeTypeVisitor.cs

@ -18,7 +18,10 @@
#nullable enable #nullable enable
using System.Linq;
using ICSharpCode.Decompiler.TypeSystem.Implementation; using ICSharpCode.Decompiler.TypeSystem.Implementation;
using ICSharpCode.Decompiler.Util;
namespace ICSharpCode.Decompiler.TypeSystem namespace ICSharpCode.Decompiler.TypeSystem
{ {
@ -34,6 +37,7 @@ namespace ICSharpCode.Decompiler.TypeSystem
DynamicAndObject = true, DynamicAndObject = true,
IntPtrToNInt = true, IntPtrToNInt = true,
TupleToUnderlyingType = true, TupleToUnderlyingType = true,
RemoveTupleElementNames = false,
RemoveModOpt = true, RemoveModOpt = true,
RemoveModReq = true, RemoveModReq = true,
RemoveNullability = true, RemoveNullability = true,
@ -45,17 +49,32 @@ namespace ICSharpCode.Decompiler.TypeSystem
DynamicAndObject = false, DynamicAndObject = false,
IntPtrToNInt = false, IntPtrToNInt = false,
TupleToUnderlyingType = true, TupleToUnderlyingType = true,
RemoveTupleElementNames = false,
RemoveModOpt = true, RemoveModOpt = true,
RemoveModReq = true, RemoveModReq = true,
RemoveNullability = 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 = true,
};
internal static readonly NormalizeTypeVisitor IgnoreNullability = new NormalizeTypeVisitor { internal static readonly NormalizeTypeVisitor IgnoreNullability = new NormalizeTypeVisitor {
ReplaceClassTypeParametersWithDummy = false, ReplaceClassTypeParametersWithDummy = false,
ReplaceMethodTypeParametersWithDummy = false, ReplaceMethodTypeParametersWithDummy = false,
DynamicAndObject = false, DynamicAndObject = false,
IntPtrToNInt = false, IntPtrToNInt = false,
TupleToUnderlyingType = false, TupleToUnderlyingType = false,
RemoveTupleElementNames = false,
RemoveModOpt = true, RemoveModOpt = true,
RemoveModReq = true, RemoveModReq = true,
RemoveNullability = true, RemoveNullability = true,
@ -75,6 +94,7 @@ namespace ICSharpCode.Decompiler.TypeSystem
public bool DynamicAndObject = true; public bool DynamicAndObject = true;
public bool IntPtrToNInt = true; public bool IntPtrToNInt = true;
public bool TupleToUnderlyingType = true; public bool TupleToUnderlyingType = true;
public bool RemoveTupleElementNames = true;
public bool RemoveNullability = true; public bool RemoveNullability = true;
public override IType VisitTypeParameter(ITypeParameter type) public override IType VisitTypeParameter(ITypeParameter type)
@ -129,6 +149,14 @@ namespace ICSharpCode.Decompiler.TypeSystem
{ {
return type.UnderlyingType.AcceptVisitor(this); 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 else
{ {
return base.VisitTupleType(type); return base.VisitTupleType(type);

Loading…
Cancel
Save