diff --git a/CHANGELOG.md b/CHANGELOG.md index ee61e8f72..416b47c53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ### Added - Extension methods declared in non-framework assemblies are bound to the type they extend, so they can be called from the instance like in C# (e.g. `order_status.is_open()`). Those marked `[EditorBrowsable(EditorBrowsableState.Never)]`, or declared in a class marked so, keep their static form only +- Types with an implicit conversion to a numeric type support Python arithmetic, comparisons, `float()` and `int()` through that conversion, behaving like a Python `int` for integral conversions and a `float` otherwise, unless they declare or inherit the operator - Ability to instantiate new .NET arrays using `Array[T](dim1, dim2, ...)` syntax - Python operator method will call C# operator method for supported binary and unary operators ([#1324][p1324]). - Add GetPythonThreadID and Interrupt methods in PythonEngine diff --git a/src/embed_tests/TestImplicitNumericOperators.cs b/src/embed_tests/TestImplicitNumericOperators.cs new file mode 100644 index 000000000..cd9de7470 --- /dev/null +++ b/src/embed_tests/TestImplicitNumericOperators.cs @@ -0,0 +1,367 @@ +using System; +using NUnit.Framework; +using Python.Runtime; + +namespace Python.EmbeddingTest +{ + public class ConvertibleFee + { + public decimal Amount { get; } + + public ConvertibleFee(decimal amount) + { + Amount = amount; + } + + public static implicit operator decimal(ConvertibleFee fee) => fee.Amount; + } + + public class ConvertibleRatio + { + public double Value { get; } + + public ConvertibleRatio(double value) + { + Value = value; + } + + public static implicit operator double(ConvertibleRatio ratio) => ratio.Value; + } + + public class ConvertibleMoney + { + public decimal Amount { get; } + + public ConvertibleMoney(decimal amount) + { + Amount = amount; + } + + public static implicit operator decimal(ConvertibleMoney money) => money.Amount; + + // the type's own operator must keep winning over the synthesized one + public static ConvertibleMoney operator +(ConvertibleMoney a, ConvertibleMoney b) => new(a.Amount + b.Amount); + } + + public class ConvertibleCount + { + public long Value { get; } + + public ConvertibleCount(long value) + { + Value = value; + } + + public static implicit operator long(ConvertibleCount count) => count.Value; + } + + public class ConvertibleBase + { + // inherited by ConvertibleDerived, it must keep winning over the synthesized one as in C# + public static string operator +(ConvertibleBase a, ConvertibleBase b) => "base"; + } + + public class ConvertibleDerived : ConvertibleBase + { + public decimal Amount { get; } + + public ConvertibleDerived(decimal amount) + { + Amount = amount; + } + + public static implicit operator decimal(ConvertibleDerived derived) => derived.Amount; + } + + public class PreciseQuantity : IComparable + { + public decimal Value { get; } + + public PreciseQuantity(decimal value) + { + Value = value; + } + + // equal to 1 once converted to double, only the type's own ordering tells them apart + public static PreciseQuantity AboveOne() => new(1.0000000000000000000000000001m); + + public int CompareTo(PreciseQuantity other) => Value.CompareTo(other.Value); + + public static implicit operator decimal(PreciseQuantity quantity) => quantity.Value; + } + + public class NotConvertible + { + } + + public class TestImplicitNumericOperators + { + private static dynamic module; + + private const string TestModule = @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * + +fee = ConvertibleFee(2) +other = ConvertibleFee(3) + +def arithmetic(): + return (fee + other, fee + 1, 1 + fee, fee - other, fee - 1, 10 - fee, + fee * other, fee * 2, 2 * fee, other / fee, fee / 2, 10 / fee, -fee) + +def aggregates(): + return sum([fee, other]), max(fee, other) is other, sorted([other, fee])[0] is fee + +def comparisons(): + return (fee < other, fee > other, fee <= other, fee >= other, + fee > 0, fee < 2, fee <= 2, fee >= 2, 1 < fee, 2 >= fee) + +def conversions(): + return float(fee), int(ConvertibleRatio(1.75)), float(ConvertibleRatio(1.75)) + +def own_operator_wins(): + total = ConvertibleMoney(2) + ConvertibleMoney(3) + return type(total).__name__, total.Amount, ConvertibleMoney(2) - ConvertibleMoney(1) + +def not_convertible(): + return hasattr(NotConvertible(), '__add__'), hasattr(NotConvertible(), '__float__') + +count = ConvertibleCount(2) +big = ConvertibleCount(2**53 + 1) + +def integer_arithmetic(): + results = (count + 1, 1 + count, count + count, count - 5, 10 - count, count * 3, 3 * count, -count, count + True) + return all(type(result) is int for result in results), results + +def integer_with_floats(): + results = (count + 0.5, 0.5 + count, count * 1.5, count / 2, count / count, 3 / count) + return all(type(result) is float for result in results), results + +def large_integers(): + exact = 2**53 + 1 + return (int(big) == exact, big > 2**53, big - 1 == 2**53, big * big == exact * exact, + -big == -exact, big >= exact, big + 2**70 == exact + 2**70) + +def int_conversions(): + errors = [] + for value in (float('nan'), float('inf')): + try: + int(ConvertibleRatio(value)) + except Exception as e: + errors.append(type(e).__name__) + return errors, int(ConvertibleRatio(-1.75)), int(ConvertibleFee(12345678901234567891)) == 12345678901234567891 + +def division_by_zero(): + errors = [] + for operation in (lambda: fee / 0, lambda: 1 / ConvertibleFee(0), lambda: count / 0.0, lambda: fee / ConvertibleFee(0)): + try: + operation() + except ZeroDivisionError: + errors.append(True) + return len(errors) + +def other_numeric_operands(): + margin = ConvertibleRatio(0.5) + return fee + 2**40, count + 2**70, fee + margin, count * margin, count < 2**64 + +def unsupported_operands(): + messages = [] + for operation in (lambda: count + 'x', lambda: fee * None, lambda: fee < 'x', lambda: NotConvertible() - fee): + try: + operation() + messages.append(None) + except TypeError as e: + messages.append(str(e)) + return messages + +def inherited_operator_wins(): + derived = ConvertibleDerived(2) + return derived + derived, derived - ConvertibleDerived(1), float(derived) + +def own_ordering_wins(): + one, above = PreciseQuantity(1), PreciseQuantity.AboveOne() + return above > one, one < above, above >= one, one <= above, above < one, sorted([above, one])[0] is one +"; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + PythonEngine.Initialize(); + using var _ = Py.GIL(); + module = PyModule.FromString("implicit_numeric_operators", TestModule); + } + + [OneTimeTearDown] + public void Dispose() + { + PythonEngine.Shutdown(); + } + + [Test] + public void ArithmeticDelegatesToTheImplicitConversion() + { + using var _ = Py.GIL(); + var result = module.arithmetic(); + var expected = new[] { 5.0, 3.0, 3.0, -1.0, 1.0, 8.0, 6.0, 4.0, 4.0, 1.5, 1.0, 5.0, -2.0 }; + for (var i = 0; i < expected.Length; i++) + { + Assert.AreEqual(expected[i], (double)result[i], $"index {i}"); + } + } + + [Test] + public void AggregatesWork() + { + using var _ = Py.GIL(); + var result = module.aggregates(); + Assert.AreEqual(5.0, (double)result[0]); + Assert.IsTrue((bool)result[1]); + Assert.IsTrue((bool)result[2]); + } + + [Test] + public void ComparisonsDelegateToTheImplicitConversion() + { + using var _ = Py.GIL(); + var result = module.comparisons(); + var expected = new[] { true, false, true, false, true, false, true, true, true, true }; + for (var i = 0; i < expected.Length; i++) + { + Assert.AreEqual(expected[i], (bool)result[i], $"index {i}"); + } + } + + [Test] + public void FloatAndIntConversions() + { + using var _ = Py.GIL(); + var result = module.conversions(); + Assert.AreEqual(2.0, (double)result[0]); + Assert.AreEqual(1, (int)result[1]); + Assert.AreEqual(1.75, (double)result[2]); + } + + [Test] + public void TheTypesOwnOperatorsKeepWinning() + { + using var _ = Py.GIL(); + var result = module.own_operator_wins(); + Assert.AreEqual("ConvertibleMoney", (string)result[0]); + Assert.AreEqual(5m, (decimal)result[1]); + // subtraction is not declared by the type, so the synthesized one applies + Assert.AreEqual(1.0, (double)result[2]); + } + + [Test] + public void IntegralConversionsBehaveLikeInts() + { + using var _ = Py.GIL(); + var result = module.integer_arithmetic(); + Assert.IsTrue((bool)result[0], "every result must be an int"); + var expected = new long[] { 3, 3, 4, -3, 8, 6, 6, -2, 3 }; + for (var i = 0; i < expected.Length; i++) + { + Assert.AreEqual(expected[i], (long)result[1][i], $"index {i}"); + } + } + + [Test] + public void IntegralConversionsGiveFloatsWherePythonWould() + { + using var _ = Py.GIL(); + var result = module.integer_with_floats(); + Assert.IsTrue((bool)result[0], "every result must be a float"); + var expected = new[] { 2.5, 2.5, 3.0, 1.0, 1.0, 1.5 }; + for (var i = 0; i < expected.Length; i++) + { + Assert.AreEqual(expected[i], (double)result[1][i], $"index {i}"); + } + } + + [Test] + public void LargeIntegralValuesStayExact() + { + using var _ = Py.GIL(); + var result = module.large_integers(); + for (var i = 0; i < 7; i++) + { + Assert.IsTrue((bool)result[i], $"index {i}"); + } + } + + [Test] + public void IntConversionFollowsPython() + { + using var _ = Py.GIL(); + var result = module.int_conversions(); + CollectionAssert.AreEqual(new[] { "ValueError", "OverflowError" }, ((PyObject)result[0]).As()); + Assert.AreEqual(-1, (int)result[1]); + Assert.IsTrue((bool)result[2], "decimal conversions must truncate exactly"); + } + + [Test] + public void DivisionByZeroRaisesLikePython() + { + using var _ = Py.GIL(); + Assert.AreEqual(4, (int)module.division_by_zero()); + } + + [Test] + public void LargeIntsAndOtherNumbersAreOperands() + { + using var _ = Py.GIL(); + var result = module.other_numeric_operands(); + Assert.AreEqual(2.0 + Math.Pow(2, 40), (double)result[0]); + Assert.AreEqual("1180591620717411303426", result[1].ToString()); + // another type with an implicit numeric conversion is read through its __float__ + Assert.AreEqual(2.5, (double)result[2]); + Assert.AreEqual(1.0, (double)result[3]); + Assert.IsTrue((bool)result[4]); + } + + [Test] + public void UnsupportedOperandsRaisePythonsTypeError() + { + using var _ = Py.GIL(); + var messages = ((PyObject)module.unsupported_operands()).As(); + StringAssert.Contains("unsupported operand type(s) for +", messages[0]); + StringAssert.Contains("unsupported operand type(s) for *", messages[1]); + StringAssert.Contains("'<' not supported", messages[2]); + StringAssert.Contains("unsupported operand type(s) for -", messages[3]); + } + + [Test] + public void InheritedOperatorsKeepWinning() + { + using var _ = Py.GIL(); + var result = module.inherited_operator_wins(); + Assert.AreEqual("base", (string)result[0]); + // subtraction is declared nowhere in the hierarchy, so the synthesized one applies + Assert.AreEqual(1.0, (double)result[1]); + Assert.AreEqual(2.0, (double)result[2]); + } + + [Test] + public void TheTypesOwnOrderingWinsBetweenItsInstances() + { + using var _ = Py.GIL(); + var result = module.own_ordering_wins(); + var expected = new[] { true, true, true, true, false, true }; + for (var i = 0; i < expected.Length; i++) + { + Assert.AreEqual(expected[i], (bool)result[i], $"index {i}"); + } + } + + [Test] + public void TypesWithoutAConversionAreUntouched() + { + using var _ = Py.GIL(); + var result = module.not_convertible(); + Assert.IsFalse((bool)result[0]); + Assert.IsFalse((bool)result[1]); + } + } +} diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index 835f0ec82..b4aefacc2 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -415,6 +415,16 @@ private static ClassInfo GetClassInfo(Type type, ClassBase impl) info = info.Concat(opsImpl.GetMethods(OpsHelper.BindingFlags)).ToArray(); } } + else if (ImplicitNumericOps.IsSupported(type)) + { + // Types with an implicit numeric conversion get Python's number protocol, keeping the operators they declare or inherit + var ops = ImplicitNumericOps.GetMethods(type); + foreach (var op in ops) + { + local.Add(op.Name); + } + info = info.Concat(ops).ToArray(); + } // Extension methods are bound as members so they can be called from the instance. A real member with the same name wins, as in C# var extensionMethods = ExtensionMethods.GetMethodsToBind(type).Where(em => !originalMemberNames.Contains(em.Name)).ToArray(); diff --git a/src/runtime/Native/ITypeOffsets.cs b/src/runtime/Native/ITypeOffsets.cs index fb65e76f8..bae8636c5 100644 --- a/src/runtime/Native/ITypeOffsets.cs +++ b/src/runtime/Native/ITypeOffsets.cs @@ -22,6 +22,7 @@ interface ITypeOffsets int nb_true_divide { get; } int nb_and { get; } int nb_int { get; } + int nb_float { get; } int nb_or { get; } int nb_xor { get; } int nb_lshift { get; } diff --git a/src/runtime/Native/TypeOffset.cs b/src/runtime/Native/TypeOffset.cs index c94447f37..76196bcad 100644 --- a/src/runtime/Native/TypeOffset.cs +++ b/src/runtime/Native/TypeOffset.cs @@ -31,6 +31,7 @@ static partial class TypeOffset internal static int nb_or { get; private set; } internal static int nb_xor { get; private set; } internal static int nb_int { get; private set; } + internal static int nb_float { get; private set; } internal static int nb_lshift { get; private set; } internal static int nb_rshift { get; private set; } internal static int nb_remainder { get; private set; } diff --git a/src/runtime/Types/OperatorMethod.cs b/src/runtime/Types/OperatorMethod.cs index 12f79ab10..462f69382 100644 --- a/src/runtime/Types/OperatorMethod.cs +++ b/src/runtime/Types/OperatorMethod.cs @@ -56,6 +56,7 @@ static OperatorMethod() ["op_UnaryPlus"] = new SlotDefinition("__pos__", TypeOffset.nb_positive), ["__int__"] = new SlotDefinition("__int__", TypeOffset.nb_int), + ["__float__"] = new SlotDefinition("__float__", TypeOffset.nb_float), }; ComparisonOpMap = new Dictionary { @@ -177,6 +178,11 @@ static IEnumerable GetOperatorCandidates(Type clrType) .Concat(typeof(FlagEnumOps<>).MakeGenericType(clrType).GetMethods(flags)); } + if (ImplicitNumericOps.IsSupported(clrType)) + { + return clrType.GetMethods(flags).Concat(ImplicitNumericOps.GetMethods(clrType)); + } + return clrType.GetMethods(flags); } diff --git a/src/runtime/Util/ImplicitNumericOps.cs b/src/runtime/Util/ImplicitNumericOps.cs new file mode 100644 index 000000000..d53259f14 --- /dev/null +++ b/src/runtime/Util/ImplicitNumericOps.cs @@ -0,0 +1,334 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Numerics; +using System.Reflection; + +namespace Python.Runtime +{ + /// + /// Locates the implicit numeric conversion of a CLR type. Python operators never consult implicit + /// conversions, so types declaring one get the number protocol through . + /// + internal static class ImplicitNumericOps + { + private static readonly Type[] NumericTypes = + { + typeof(decimal), typeof(double), typeof(float), typeof(long), typeof(int), typeof(short), typeof(sbyte), + typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) + }; + + internal static MethodInfo? GetConversion(Type type) + { + if (type.IsEnum || type.IsPrimitive || type == typeof(decimal) || type.ContainsGenericParameters) + { + return null; + } + + return type.GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(method => method.Name == "op_Implicit" + && NumericTypes.Contains(method.ReturnType) + && method.GetParameters().Length == 1 + && method.GetParameters()[0].ParameterType == type); + } + + internal static bool IsSupported(Type type) => GetConversion(type) != null; + + /// + /// Gets the operators to graft onto the type. The ones it declares or inherits keep winning, as in C# + /// + internal static MethodInfo[] GetMethods(Type type) + { + var existing = new HashSet(type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy).Select(method => method.Name)); + return typeof(ImplicitNumericOps<>).MakeGenericType(type).GetMethods(OpsHelper.BindingFlags) + .Where(method => !existing.Contains(method.Name)) + .ToArray(); + } + } + + /// + /// The number protocol of a type with an implicit numeric conversion, behaving like the Python number the + /// conversion maps to: an int for integral conversions (exact, results are ints unless Python would give a + /// float) and a float for floating point and decimal ones. Operands that are not numbers return + /// NotImplemented, so Python can try the other operand and raise its usual TypeError. + /// + [Ops] + internal static class ImplicitNumericOps + { + private static readonly MethodInfo Conversion = ImplicitNumericOps.GetConversion(typeof(T)) + ?? throw new InvalidOperationException($"{typeof(T)} has no implicit numeric conversion"); + private static readonly bool IntegralConversion = Type.GetTypeCode(Conversion.ReturnType) is not (TypeCode.Double or TypeCode.Single or TypeCode.Decimal); + private static readonly Func ToDouble = BuildConversion(); + // double to BigInteger throws on NaN and infinity, so floating point conversions go through ToDouble + private static readonly Func? ToInteger = IntegralConversion || Conversion.ReturnType == typeof(decimal) + ? BuildConversion() + : null; + private static readonly bool Comparable = typeof(IComparable).IsAssignableFrom(typeof(T)) || typeof(IComparable).IsAssignableFrom(typeof(T)); + +#pragma warning disable IDE1006 // Naming Styles - must match Python + [ForbidPythonThreads] + public static double __float__(T value) => ToDouble(value); + + [ForbidPythonThreads] + public static object __int__(T value) + { + using (Py.GIL()) + { + if (ToInteger != null) + { + return ToPython(ToInteger(value)); + } + + var number = ToDouble(value); + if (double.IsNaN(number)) + { + throw RaisePython(Exceptions.ValueError, "cannot convert float NaN to integer"); + } + if (double.IsInfinity(number)) + { + throw RaisePython(Exceptions.OverflowError, "cannot convert float infinity to integer"); + } + return ToPython(new BigInteger(number)); + } + } +#pragma warning restore IDE1006 + + public static object op_UnaryNegation(T value) + { + using (Py.GIL()) + { + var number = ToNumber(value); + return number.IsInteger ? ToPython(-number.Integer) : -number.Float; + } + } + + public static object op_Addition(T a, PyObject b) => Arithmetic(a, b, reverse: false, BigInteger.Add, (x, y) => x + y); + public static object op_Addition(PyObject a, T b) => Arithmetic(b, a, reverse: true, BigInteger.Add, (x, y) => x + y); + + public static object op_Subtraction(T a, PyObject b) => Arithmetic(a, b, reverse: false, BigInteger.Subtract, (x, y) => x - y); + public static object op_Subtraction(PyObject a, T b) => Arithmetic(b, a, reverse: true, BigInteger.Subtract, (x, y) => x - y); + + public static object op_Multiply(T a, PyObject b) => Arithmetic(a, b, reverse: false, BigInteger.Multiply, (x, y) => x * y); + public static object op_Multiply(PyObject a, T b) => Arithmetic(b, a, reverse: true, BigInteger.Multiply, (x, y) => x * y); + + // true division: a float even between ints, and like Python it raises ZeroDivisionError instead of returning infinity + public static object op_Division(T a, PyObject b) => Arithmetic(a, b, reverse: false, null, Divide); + public static object op_Division(PyObject a, T b) => Arithmetic(b, a, reverse: true, null, Divide); + + // Python reflects a reversed comparison onto the other operand, so only the forward overloads are needed + public static object op_LessThan(T a, PyObject b) => Compare(a, b, Runtime.Py_LT); + public static object op_GreaterThan(T a, PyObject b) => Compare(a, b, Runtime.Py_GT); + public static object op_LessThanOrEqual(T a, PyObject b) => Compare(a, b, Runtime.Py_LE); + public static object op_GreaterThanOrEqual(T a, PyObject b) => Compare(a, b, Runtime.Py_GE); + + private static object Arithmetic(T self, PyObject? other, bool reverse, + Func? integerOperation, Func floatOperation) + { + // the binder released the GIL to call us + using (Py.GIL()) + { + if (!TryGetOperand(other, out var right)) + { + return Runtime.PyNotImplemented; + } + + var left = ToNumber(self); + if (reverse) + { + (left, right) = (right, left); + } + + if (integerOperation != null && left.IsInteger && right.IsInteger) + { + return ToPython(integerOperation(left.Integer, right.Integer)); + } + return floatOperation(left.AsDouble, right.AsDouble); + } + } + + private static double Divide(double dividend, double divisor) + { + if (divisor == 0) + { + throw RaisePython(Exceptions.ZeroDivisionError, "division by zero"); + } + return dividend / divisor; + } + + private static object Compare(T self, PyObject? other, int op) + { + int order; + using (Py.GIL()) + { + // the type's own ordering wins between its instances, as it did before the number protocol was added + if (Comparable && TryGetInstance(other, out var same)) + { + order = Comparer.Default.Compare(self, same); + } + else + { + if (!TryGetOperand(other, out var right)) + { + return Runtime.PyNotImplemented; + } + + var left = ToNumber(self); + if (!left.IsInteger || !right.IsInteger) + { + // IEEE comparisons, false against NaN as in Python + double x = left.AsDouble, y = right.AsDouble; + return op switch + { + Runtime.Py_LT => x < y, + Runtime.Py_LE => x <= y, + Runtime.Py_GT => x > y, + _ => x >= y, + }; + } + order = left.Integer.CompareTo(right.Integer); + } + } + + return op switch + { + Runtime.Py_LT => order < 0, + Runtime.Py_LE => order <= 0, + Runtime.Py_GT => order > 0, + _ => order >= 0, + }; + } + + private static Number ToNumber(T value) => IntegralConversion ? new Number(ToInteger!(value)) : new Number(ToDouble(value)); + + private static bool TryGetInstance(PyObject? value, out T instance) + { + if (value != null && ManagedType.GetManagedObject(value.Reference) is CLRObject { inst: T clrInstance }) + { + instance = clrInstance; + return true; + } + instance = default!; + return false; + } + + /// + /// Reads a Python operand as a number: instances of , ints (bool included) and + /// floats, and any other object implementing __index__ or __float__ (numpy scalars, other CLR types with + /// an implicit numeric conversion). Requires the GIL. + /// + private static bool TryGetOperand(PyObject? value, out Number number) + { + number = default; + if (value == null) + { + return false; + } + if (TryGetInstance(value, out var instance)) + { + number = ToNumber(instance); + return true; + } + + var reference = value.Reference; + if (Runtime.PyInt_Check(reference)) + { + number = new Number(ToBigInteger(reference)); + return true; + } + if (Runtime.PyObject_TypeCheck(reference, Runtime.PyFloatType)) + { + number = new Number(Runtime.PyFloat_AsDouble(reference)); + return true; + } + if (Runtime.PyObject_HasAttrString(reference, "__index__") != 0) + { + using var integer = Runtime.PyNumber_Long(reference); + if (!integer.IsNull()) + { + number = new Number(ToBigInteger(integer.Borrow())); + return true; + } + } + else if (Runtime.PyObject_HasAttrString(reference, "__float__") != 0) + { + var real = Runtime.PyFloat_AsDouble(reference); + if (real != -1 || !Exceptions.ErrorOccurred()) + { + number = new Number(real); + return true; + } + } + + // a failed probe must not leak, the operand is just not a number + Exceptions.Clear(); + return false; + } + + private static BigInteger ToBigInteger(BorrowedReference integer) + { + var value = Runtime.PyLong_AsLongLong(integer); + if (value.HasValue) + { + return value.Value; + } + + // beyond long, parse its digits + Exceptions.Clear(); + using var digits = Runtime.PyObject_Str(integer); + return BigInteger.Parse(Runtime.GetManagedString(digits.Borrow())!, CultureInfo.InvariantCulture); + } + + /// + /// Converts an integer result to a Python int, which is unbounded. Requires the GIL beyond long. + /// + private static object ToPython(BigInteger value) + { + if (value >= long.MinValue && value <= long.MaxValue) + { + return (long)value; + } + return new PyInt(value.ToString(CultureInfo.InvariantCulture)); + } + + /// + /// Raises a Python exception from a call made by the binder, which restores it to the caller. Requires the GIL. + /// + private static Exception RaisePython(PyObject type, string message) + { + Exceptions.SetError(type.Reference, message); + return PythonException.ThrowLastAsClrException(); + } + + private static Func BuildConversion() + { + var value = Expression.Parameter(typeof(T), "value"); + var body = Expression.Convert(Expression.Call(Conversion, value), typeof(TResult)); + return Expression.Lambda>(body, value).Compile(); + } + + private readonly struct Number + { + public readonly bool IsInteger; + public readonly BigInteger Integer; + public readonly double Float; + + public Number(BigInteger value) + { + IsInteger = true; + Integer = value; + Float = 0; + } + + public Number(double value) + { + IsInteger = false; + Integer = default; + Float = value; + } + + public double AsDouble => IsInteger ? (double)Integer : Float; + } + } +}