Skip to content

Commit 0590e99

Browse files
authored
Improve method resolution for numerics (#1456)
* Reorganise Converter.HastNarrowingConversion * Improve overload resolution rules * Review int tests in test_methodbinder2 * Resolve numerical extensibles like their underlyings * Prevent cast from float to int (#828) for all integer types * Review integer tests of methodbinder2 * Move rules for BigInt to Int64/32 from narrowing level to PythonOverloadResolver * Add tests using Extensible<double> * Increase test coverage for binding cases involving BigInteger * Correct comments
1 parent b16734a commit 0590e99

7 files changed

Lines changed: 399 additions & 130 deletions

File tree

Src/IronPython/Runtime/Binding/PythonOverloadResolver.cs

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,64 @@ public override Candidate SelectBestConversionFor(DynamicMetaObject arg, Paramet
7272
return basePreferred;
7373
}
7474

75-
if (Converter.IsNumeric(arg.LimitType)) {
76-
if (Converter.IsFloatingPoint(arg.LimitType)) {
75+
// Work around the choice made in Converter.PreferConvert
76+
// This cannot be done using NarrowingLevel rules because it would confuse rules for selecting custom operators
77+
if (level >= PythonNarrowing.IndexOperator && Converter.IsPythonBigInt(arg.LimitType)) {
78+
TypeCode c1tc = candidateOne.Type.GetTypeCode();
79+
TypeCode c2tc = candidateTwo.Type.GetTypeCode();
80+
if (c1tc is TypeCode.UInt32 && c2tc is TypeCode.Int32) return Candidate.Two;
81+
if (c1tc is TypeCode.Int32 && c2tc is TypeCode.UInt32) return Candidate.One;
82+
if (c1tc is TypeCode.UInt64 && c2tc is TypeCode.Int64) return Candidate.Two;
83+
if (c1tc is TypeCode.Int64 && c2tc is TypeCode.UInt64) return Candidate.One;
84+
}
85+
86+
// This block codifies the following set of rules for Python numerics (i.e. numeric types and any derived types):
87+
// 1. Prefer the parameter that is of the matching kind to the argument type (i.e. floating-point to floating-point, or integer to integer).
88+
// 2. If both parameters are of the matching kind, break the tie by prefering the one that is wider, if any (so the chance of overflow is lower).
89+
// "Wider" here means being able to represent all values of the narrower type; e.g. in this sense UInt64 is not wider than SByte.
90+
// There are the following exceptions to these rules:
91+
// * Rule 1 is not applied to user-defined subtypes (i.e. subclasses of Extensible<T>). This is to allow such types to bind to parameters
92+
// of type object or an interface on Extensible<T> (rule from the DLR). It is because a user-defined type may have user-defined operators
93+
// that may need to be inspected by the overload and provide additional functionality above a simple numeric value.
94+
// * Rule 2 is not applied on narrowing level None, so in a case of a widening cast or a perfect type match, the most efficient (narrowest)
95+
// type is selected (rule from the DLR).
96+
// * If one of the parameters is Boolean and the other is numeric, the Boolean parameter is treated as a 1-bit-wide numeric (thus becomes a case for Rule 2).
97+
// This makes the preference for numeric types over Boolean consistent with the one encoded using narrowing levels and conversion sequence.
98+
// TODO: Increase test coverage for cases involving Complex, Decimal, Half
99+
if (Converter.IsPythonNumeric(arg.LimitType)) {
100+
if (Converter.IsPythonFloatingPoint(arg.LimitType)) {
77101
if (Converter.IsFloatingPoint(candidateOne.Type)) {
78102
if (!Converter.IsFloatingPoint(candidateTwo.Type)) {
79-
return Candidate.One;
103+
if (!Converter.IsExtensibleNumeric(arg.LimitType)) {
104+
return Candidate.One;
105+
}
106+
} else if (level > PythonNarrowing.None) { // both params are floating point
107+
Candidate preferred = SelectWiderNumericType(candidateOne.Type, candidateTwo.Type);
108+
if (preferred != Candidate.Ambiguous) {
109+
return preferred;
110+
}
80111
}
81112
} else if (Converter.IsFloatingPoint(candidateTwo.Type)) {
82-
return Candidate.Two;
113+
if (!Converter.IsExtensibleNumeric(arg.LimitType)) {
114+
return Candidate.Two;
115+
}
83116
}
84117
} else { // arg is integer
85118
if (Converter.IsInteger(candidateOne.Type)) {
86119
if (!Converter.IsInteger(candidateTwo.Type)) {
87-
return Candidate.One;
120+
if (!Converter.IsExtensibleNumeric(arg.LimitType) || (Converter.IsBoolean(candidateTwo.Type) && level > PythonNarrowing.None)) {
121+
return Candidate.One;
122+
}
123+
} else if (level > PythonNarrowing.None) { // both params are integer
124+
Candidate preferred = SelectWiderNumericType(candidateOne.Type, candidateTwo.Type);
125+
if (preferred != Candidate.Ambiguous) {
126+
return preferred;
127+
}
88128
}
89129
} else if (Converter.IsInteger(candidateTwo.Type)) {
90-
return Candidate.Two;
130+
if (!Converter.IsExtensibleNumeric(arg.LimitType) || (Converter.IsBoolean(candidateOne.Type) && level > PythonNarrowing.None)) {
131+
return Candidate.Two;
132+
}
91133
}
92134
}
93135
}
@@ -179,22 +221,57 @@ public override Expression Convert(DynamicMetaObject metaObject, Type restricted
179221

180222
public override Expression GetDynamicConversion(Expression value, Type type) {
181223
return DynamicExpression.Dynamic(
182-
Binder.Context.Convert(type, ConversionResultKind.ExplicitCast),
183-
type,
224+
Binder.Context.Convert(type, ConversionResultKind.ExplicitCast),
225+
type,
184226
value);
185227
}
186228

187-
public override Type GetGenericInferenceType(DynamicMetaObject dynamicObject) {
229+
public override Type GetGenericInferenceType(DynamicMetaObject dynamicObject) {
188230
Type res = PythonTypeOps.GetFinalSystemType(dynamicObject.LimitType);
189231
if (res == typeof(ExtensibleString) ||
190-
res == typeof(ExtensibleComplex) ||
232+
res == typeof(ExtensibleComplex) ||
191233
(res.IsGenericType && res.GetGenericTypeDefinition() == typeof(Extensible<>))) {
192234
return typeof(object);
193235
}
194236

195237
return res;
196238
}
197239

240+
private static Candidate SelectWiderNumericType(Type candidateOneType, Type candidateTwoType) {
241+
if (GetUnmanagedNumericTypeWidth(candidateOneType) is not int candidateOneWidth) return Candidate.Ambiguous;
242+
if (GetUnmanagedNumericTypeWidth(candidateTwoType) is not int candidateTwoWidth) return Candidate.Ambiguous;
243+
244+
Candidate preferred = Comparer<int>.Default.Compare(candidateOneWidth, candidateTwoWidth) switch {
245+
1 => Candidate.One,
246+
0 => Candidate.Equivalent,
247+
-1 => Candidate.Two,
248+
_ => throw new InvalidOperationException()
249+
};
250+
251+
if (preferred == Candidate.One && Converter.IsUnsignedInt(candidateOneType) && !Converter.IsUnsignedInt(candidateTwoType)) {
252+
preferred = Candidate.Ambiguous;
253+
} else if (preferred == Candidate.Two && Converter.IsUnsignedInt(candidateTwoType) && !Converter.IsUnsignedInt(candidateOneType)) {
254+
preferred = Candidate.Ambiguous;
255+
}
256+
return preferred;
257+
}
258+
259+
private static int? GetUnmanagedNumericTypeWidth(Type type) {
260+
return type.GetTypeCode() switch {
261+
TypeCode.SByte => sizeof(sbyte),
262+
TypeCode.Int16 => sizeof(short),
263+
TypeCode.Int32 => sizeof(int),
264+
TypeCode.Int64 => sizeof(long),
265+
TypeCode.Byte => sizeof(byte),
266+
TypeCode.UInt16 => sizeof(ushort),
267+
TypeCode.UInt32 => sizeof(uint),
268+
TypeCode.UInt64 => sizeof(ulong),
269+
TypeCode.Single => sizeof(float),
270+
TypeCode.Double => sizeof(double),
271+
_ => null
272+
};
273+
}
274+
198275
private bool IsBytesLikeParameter(ParameterWrapper parameter) {
199276
return parameter.ParameterInfo?.IsDefined(typeof(BytesLikeAttribute), inherit: false) ?? false;
200277
}

Src/IronPython/Runtime/Converter.cs

Lines changed: 77 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,9 @@ private static Exception MakeTypeError(string expectedType, object o) {
415415
private static readonly Type BooleanType = typeof(bool);
416416
private static readonly Type BigIntegerType = typeof(BigInteger);
417417
private static readonly Type ComplexType = typeof(Complex);
418+
private static readonly Type ExtensibleBigIntegerType = typeof(Extensible<BigInteger>);
419+
private static readonly Type ExtensibleDoubleType = typeof(Extensible<double>);
420+
private static readonly Type ExtensibleComplexType = typeof(Extensible<Complex>);
418421
private static readonly Type DelegateType = typeof(Delegate);
419422
private static readonly Type IEnumerableType = typeof(IEnumerable);
420423
private static readonly Type TypeType = typeof(Type);
@@ -690,7 +693,6 @@ private static bool HasImplicitNumericConversion(Type fromType, Type toType) {
690693
public static Candidate PreferConvert(Type t1, Type t2) {
691694
if (t1 == typeof(bool) && t2 == typeof(int)) return Candidate.Two;
692695
if (t1 == typeof(decimal) && t2 == typeof(BigInteger)) return Candidate.Two;
693-
//if (t1 == typeof(int) && t2 == typeof(BigInteger)) return Candidate.Two;
694696

695697
switch (t1.GetTypeCode()) {
696698
case TypeCode.SByte:
@@ -732,35 +734,25 @@ public static Candidate PreferConvert(Type t1, Type t2) {
732734
}
733735

734736
private static bool HasNarrowingConversion(Type fromType, Type toType, NarrowingLevel allowNarrowing) {
735-
if (allowNarrowing == PythonNarrowing.IndexOperator) {
736-
if (toType == CharType && fromType == StringType) return true;
737-
if (toType == StringType && fromType == CharType) return true;
738-
//if (toType == Int32Type && fromType == BigIntegerType) return true;
739-
//if (IsIntegral(fromType) && IsIntegral(toType)) return true;
740737

741-
//Check if there is an implicit convertor defined on fromType to toType
742-
if (HasImplicitConversion(fromType, toType)) {
738+
if (allowNarrowing >= PythonNarrowing.All) {
739+
if (IsPythonNumeric(fromType) && IsPythonNumeric(toType)) {
740+
if (IsPythonFloatingPoint(fromType) && !IsPythonFloatingPoint(toType)) return false;
743741
return true;
744742
}
743+
if (toType == Int32Type && HasPythonProtocol(fromType, "__int__")) return true;
744+
if (toType == DoubleType && HasPythonProtocol(fromType, "__float__")) return true;
745+
if (toType == BigIntegerType && HasPythonProtocol(fromType, "__int__")) return true;
745746
}
746747

747-
if (toType == DoubleType || toType == SingleType) {
748-
if (IsNumeric(fromType) && fromType != ComplexType) return true;
749-
}
750-
751-
if (toType.IsArray) {
752-
return typeof(PythonTuple).IsAssignableFrom(fromType);
753-
}
754-
755-
if (allowNarrowing == PythonNarrowing.IndexOperator) {
756-
if (IsNumeric(fromType) && IsNumeric(toType)) {
757-
if (fromType != typeof(float) && fromType != typeof(double) && fromType != typeof(decimal) && fromType != typeof(Complex)) {
758-
return true;
759-
}
748+
if (allowNarrowing >= PythonNarrowing.IndexOperator) {
749+
if (IsNumeric(toType)) {
750+
if (IsInteger(fromType)) return true;
751+
if (fromType == BooleanType) return true; // bool is a subclass of int in Python
760752
}
761-
if (fromType == typeof(bool) && IsNumeric(toType)) return true;
762753

763754
if (toType == CharType && fromType == StringType) return true;
755+
if (toType == StringType && fromType == CharType) return true;
764756
if (toType == Int32Type && fromType == BooleanType) return true;
765757

766758
// Everything can convert to Boolean in Python
@@ -780,36 +772,43 @@ private static bool HasNarrowingConversion(Type fromType, Type toType, Narrowing
780772
if (IsPythonType(fromType)) return true;
781773
}
782774
}
783-
}
784775

785-
if (allowNarrowing == PythonNarrowing.All) {
786-
//__int__, __float__; __long__ is obsolete
787-
if (IsNumeric(fromType) && IsNumeric(toType)) {
788-
if ((toType == Int32Type || toType == BigIntegerType) &&
789-
(fromType == DoubleType || typeof(Extensible<double>).IsAssignableFrom(fromType))) return false;
776+
// Check if there is an implicit converter defined on fromType to toType
777+
if (HasImplicitConversion(fromType, toType)) {
790778
return true;
791779
}
792-
if (toType == Int32Type && HasPythonProtocol(fromType, "__int__")) return true;
793-
if (toType == DoubleType && HasPythonProtocol(fromType, "__float__")) return true;
794-
if (toType == BigIntegerType && HasPythonProtocol(fromType, "__int__")) return true;
795780
}
796781

797-
if (toType.IsGenericType) {
798-
Type genTo = toType.GetGenericTypeDefinition();
799-
if (genTo == IListOfTType) {
800-
return IListOfObjectType.IsAssignableFrom(fromType);
801-
} else if (genTo == NullableOfTType) {
802-
if (fromType == typeof(DynamicNull) || CanConvertFrom(fromType, toType.GetGenericArguments()[0], allowNarrowing)) {
803-
return true;
782+
if (allowNarrowing >= PythonNarrowing.BinaryOperator) {
783+
if (toType == SingleType) {
784+
if (IsNumeric(fromType) && fromType != ComplexType) return true;
785+
}
786+
787+
if (toType.IsGenericType) {
788+
Type genTo = toType.GetGenericTypeDefinition();
789+
if (genTo == IListOfTType) {
790+
return IListOfObjectType.IsAssignableFrom(fromType);
791+
} else if (genTo == NullableOfTType) {
792+
if (fromType == typeof(DynamicNull) || CanConvertFrom(fromType, toType.GetGenericArguments()[0], allowNarrowing)) {
793+
return true;
794+
}
795+
} else if (genTo == IDictOfTType) {
796+
return IDictionaryOfObjectType.IsAssignableFrom(fromType);
804797
}
805-
} else if (genTo == IDictOfTType) {
806-
return IDictionaryOfObjectType.IsAssignableFrom(fromType);
798+
}
799+
800+
if (toType.IsArray) {
801+
return typeof(PythonTuple).IsAssignableFrom(fromType);
807802
}
808803
}
809804

810-
if (fromType == BigIntegerType && toType == Int64Type) return true;
811-
if (fromType == BigIntegerType && toType == UInt64Type) return true;
812-
if (toType.IsEnum && fromType == Enum.GetUnderlyingType(toType)) return true;
805+
if (allowNarrowing >= PythonNarrowing.Minimal) {
806+
if (toType.IsEnum && fromType == Enum.GetUnderlyingType(toType)) return true;
807+
808+
if (IsFloatingPoint(toType) && toType != SingleType) {
809+
if (IsNumeric(fromType) && fromType != ComplexType) return true;
810+
}
811+
}
813812

814813
return false;
815814
}
@@ -896,6 +895,41 @@ internal static bool IsInteger(Type t) {
896895
return IsNumeric(t) && !IsFloatingPoint(t);
897896
}
898897

898+
internal static bool IsUnsignedInt(Type t) {
899+
if (t.IsEnum) return false;
900+
901+
switch (t.GetTypeCode()) {
902+
case TypeCode.Byte:
903+
case TypeCode.UInt16:
904+
case TypeCode.UInt32:
905+
case TypeCode.UInt64:
906+
return true;
907+
}
908+
909+
return false;
910+
}
911+
912+
internal static bool IsBoolean(Type t)
913+
=> t == BooleanType;
914+
915+
internal static bool IsExtensibleNumeric(Type t) {
916+
return ExtensibleBigIntegerType.IsAssignableFrom(t)
917+
|| ExtensibleDoubleType.IsAssignableFrom(t)
918+
|| ExtensibleComplexType.IsAssignableFrom(t);
919+
}
920+
921+
internal static bool IsPythonNumeric(Type t)
922+
=> IsNumeric(t) || IsExtensibleNumeric(t);
923+
924+
internal static bool IsPythonFloatingPoint(Type t) {
925+
return IsFloatingPoint(t)
926+
|| ExtensibleDoubleType.IsAssignableFrom(t)
927+
|| ExtensibleComplexType.IsAssignableFrom(t);
928+
}
929+
930+
internal static bool IsPythonBigInt(Type t)
931+
=> t == BigIntegerType || ExtensibleBigIntegerType.IsAssignableFrom(t);
932+
899933
private static bool IsPythonType(Type t) {
900934
return t.FullName.StartsWith("IronPython.", StringComparison.Ordinal); //!!! this and the check below are hacks
901935
}

Src/IronPython/Runtime/PythonNarrowing.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ internal static class PythonNarrowing {
1515
public const NarrowingLevel None = NarrowingLevel.None;
1616

1717
/// <summary>
18-
/// Double/Single to Decimal
18+
/// Underlying integer to enum
19+
/// Numeric (except Complex) to floating point (except Simple)
20+
/// </summary>
21+
public const NarrowingLevel Minimal = NarrowingLevel.One;
22+
23+
/// <summary>
1924
/// PythonTuple to Array
2025
/// Generic conversions
21-
/// BigInteger to Int64
26+
/// Numeric (except Complex) to Simple
2227
/// </summary>
2328
public const NarrowingLevel BinaryOperator = NarrowingLevel.Two;
2429

@@ -27,6 +32,7 @@ internal static class PythonNarrowing {
2732
/// Boolean conversions
2833
/// Delegate conversions
2934
/// Enumeration conversions
35+
/// Implicit conversion operators
3036
/// </summary>
3137
public const NarrowingLevel IndexOperator = NarrowingLevel.Three;
3238

0 commit comments

Comments
 (0)