Skip to content

Mimic BigInteger members on Int32 #1399

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Src/IronPython/Lib/iptest/test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
is_net50 = False
is_net60 = False
is_mono = False
is_netstandard = False
if is_ironpython:
import clr
is_netcoreapp = clr.IsNetCoreApp
Expand All @@ -30,10 +31,11 @@
is_net50 = clr.FrameworkDescription.startswith(".NET 5.0")
is_net60 = clr.FrameworkDescription.startswith(".NET 6.0")
is_mono = clr.IsMono
is_netstandard = clr.TargetFramework.startswith(".NETStandard")

#--The bittedness of the Python implementation
is_cli32, is_cli64 = False, False
if is_ironpython:
if is_ironpython:
import System
is_cli32, is_cli64 = (System.IntPtr.Size == 4), (System.IntPtr.Size == 8)

Expand Down
154 changes: 146 additions & 8 deletions Src/IronPython/Runtime/Operations/IntOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,7 @@ public static string __format__(CodeContext/*!*/ context, int self, [NotDynamicN
// So use FormattingHelper.ToCultureString for that support.
if (spec.Fill.HasValue && spec.Fill.Value == '0' && width > 1) {
digits = FormattingHelper.ToCultureString(self, culture.NumberFormat, spec);
}
else {
} else {
digits = self.ToString("N0", culture);
}
break;
Expand All @@ -246,8 +245,7 @@ public static string __format__(CodeContext/*!*/ context, int self, [NotDynamicN
// so use FormattingHelper.ToCultureString for that support.
if (spec.Fill.HasValue && spec.Fill.Value == '0' && width > 1) {
digits = FormattingHelper.ToCultureString(self, FormattingHelper.InvariantCommaNumberInfo, spec);
}
else {
} else {
digits = self.ToString("#,0", CultureInfo.InvariantCulture);
}
} else {
Expand Down Expand Up @@ -299,8 +297,7 @@ public static string __format__(CodeContext/*!*/ context, int self, [NotDynamicN
} else if (spec.ThousandsComma) {
// Handle the common case in 'd'.
goto case 'd';
}
else {
} else {
digits = self.ToString(CultureInfo.InvariantCulture);
}
break;
Expand Down Expand Up @@ -391,7 +388,7 @@ internal static string ToBinary(int self) {
if (self == Int32.MinValue) {
return "-0b10000000000000000000000000000000";
}

string res = ToBinary(self, true);
if (self < 0) {
res = "-" + res;
Expand Down Expand Up @@ -422,14 +419,155 @@ private static string ToBinary(int self, bool includeType) {
} else {
digits = "10000000000000000000000000000000";
}

if (includeType) {
digits = "0b" + digits;
}
return digits;
}

#endregion

#region Mimic BigInteger members
// Ideally only on instances

#region Properties

[SpecialName, PropertyMethod, PythonHidden]
public static bool GetIsEven(int self) => (self & 1) == 0;

[SpecialName, PropertyMethod, PythonHidden]
public static bool GetIsOne(int self) => self == 1;

[SpecialName, PropertyMethod, PythonHidden]
public static bool GetIsPowerOfTwo(int self) => self > 0 && (self & (self - 1)) == 0;

[SpecialName, PropertyMethod, PythonHidden]
public static bool GetIsZero(int self) => self == 0;

[SpecialName, PropertyMethod, PythonHidden]
public static int GetSign(int self) => self == 0 ? 0 : self > 0 ? 1 : -1;

[PythonHidden]
public static object Zero => ScriptingRuntimeHelpers.Int32ToObject(0);

[PythonHidden]
public static object One => ScriptingRuntimeHelpers.Int32ToObject(1);

[PythonHidden]
public static object MinusOne => ScriptingRuntimeHelpers.Int32ToObject(-1);

#endregion

#region Methods

[PythonHidden]
public static byte[] ToByteArray(int self) => new BigInteger(self).ToByteArray();

#if NETCOREAPP
[PythonHidden]
public static byte[] ToByteArray(int self, bool isUnsigned = false, bool isBigEndian = false) => new BigInteger(self).ToByteArray(isUnsigned, isBigEndian);

[PythonHidden]
public static int GetByteCount(int self, bool isUnsigned = false) => new BigInteger(self).GetByteCount(isUnsigned);

[PythonHidden]
public static bool TryWriteBytes(int self, Span<byte> destination, out int bytesWritten, bool isUnsigned = false, bool isBigEndian = false)
=> new BigInteger(self).TryWriteBytes(destination, out bytesWritten, isUnsigned, isBigEndian);

#elif NETSTANDARD
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These feel a bit icky since depending on what runtime is using the .NET Standard assemblies you will get different methods. For example, if the .NET Standard assemblies are used with .NET Framework you'll might get extra methods on Int32 based int which won't appear when backed by BigInteger.

Anyway, I don't have a good solution to propose right now so this is probably fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, but I too have no better ideas. However I am not too concerned about this case: from the perspective of the Python type system consistency it is totally acceptable for some instances to have some additional attributes that other instances or their type don't have.

On the other hand, it is not OK if a type has attributes which are missing on (some) instances (without any explicit deletion, that is). Unfortunately, this is now the case on Mono. After spending an hour or so trying to fix it, I decided to simply fix the tests on Mono and will submit an issue report once this PR is merged. Maybe it will be picked up at some point in the future (or Mono becomes obsolete by .NET vNext). My enthusiasm to work around Mono quirks is quite limited, and in any case, this particular issue is not on the top of the list.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to simply fix the tests on Mono and will submit an issue report once this PR is merged.

Fine by me. Mono is less relevant these days with .NET having good cross-platform support.

[PythonHidden]
public static byte[] ToByteArray(int self, bool isUnsigned = false, bool isBigEndian = false) {
if (self < 0 && isUnsigned) throw new OverflowException("Negative values do not have an unsigned representation.");
byte[] bytes = ToByteArray(self);
int count = bytes.Length;
if (isUnsigned && count > 1 && bytes[count - 1] == 0) Array.Resize(ref bytes, count - 1);
if (isBigEndian) Array.Reverse(bytes);
return bytes;
}

[PythonHidden]
public static int GetByteCount(int self, bool isUnsigned = false) => ToByteArray(self, isUnsigned).Length;

[PythonHidden]
public static bool TryWriteBytes(int self, Span<byte> destination, out int bytesWritten, bool isUnsigned = false, bool isBigEndian = false) {
bytesWritten = 0;
byte[] bytes = ToByteArray(self, isUnsigned, isBigEndian);
if (bytes.Length > destination.Length) return false;

bytes.AsSpan().CopyTo(destination);
bytesWritten = bytes.Length;
return true;
}
#endif

#if NET || NETSTANDARD
[PythonHidden]
public static long GetBitLength(int self) {
int length = MathUtils.BitLength(self);
if (self < 0 && (self == int.MinValue || GetIsPowerOfTwo(-self))) length--;
return length;
}
#endif

#endregion

#region Static Extension Methods

[StaticExtensionMethod, PythonHidden]
public static BigInteger Compare(BigInteger left, BigInteger right) => BigInteger.Compare(left, right);

[StaticExtensionMethod, PythonHidden]
public static BigInteger Min(BigInteger left, BigInteger right) => BigInteger.Min(left, right);

[StaticExtensionMethod, PythonHidden]
public static BigInteger Max(BigInteger left, BigInteger right) => BigInteger.Max(left, right);

[StaticExtensionMethod, PythonHidden]
public static double Log(BigInteger value) => BigInteger.Log(value);

[StaticExtensionMethod, PythonHidden]
public static double Log(BigInteger value, double baseValue) => BigInteger.Log(value, baseValue);

[StaticExtensionMethod, PythonHidden]
public static double Log10(BigInteger value) => BigInteger.Log10(value);

[StaticExtensionMethod, PythonHidden]
public static object Pow(BigInteger value, int exponent) => BigInteger.Pow(value, exponent);

[StaticExtensionMethod, PythonHidden]
public static object ModPow(BigInteger value, BigInteger exponent, BigInteger modulus) => BigInteger.ModPow(value, exponent, modulus);

[StaticExtensionMethod, PythonHidden]
public static BigInteger Negate(BigInteger value) => BigInteger.Negate(value);

[StaticExtensionMethod, PythonHidden]
public static BigInteger Abs(BigInteger value) => BigInteger.Abs(value);

[StaticExtensionMethod, PythonHidden]
public static BigInteger Add(BigInteger left, BigInteger right) => BigInteger.Add(left, right);

[StaticExtensionMethod, PythonHidden]
public static BigInteger Subtract(BigInteger left, BigInteger right) => BigInteger.Subtract(left, right);

[StaticExtensionMethod, PythonHidden]
public static BigInteger Multiply(BigInteger left, BigInteger right) => BigInteger.Multiply(left, right);

[StaticExtensionMethod, PythonHidden]
public static BigInteger Divide(BigInteger left, BigInteger right) => BigInteger.Divide(left, right);

[StaticExtensionMethod, PythonHidden]
public static BigInteger Remainder(BigInteger dividend, BigInteger divisor) => BigInteger.Remainder(dividend, divisor);

[StaticExtensionMethod, PythonHidden]
public static BigInteger DivRem(BigInteger dividend, BigInteger divisor, out BigInteger remainder) => BigInteger.DivRem(dividend, divisor, out remainder);

[StaticExtensionMethod, PythonHidden]
public static BigInteger GreatestCommonDivisor(BigInteger left, BigInteger right) => BigInteger.GreatestCommonDivisor(left, right);

#endregion

#endregion
}

public static partial class Int64Ops {
Expand Down
65 changes: 65 additions & 0 deletions Tests/test_cliclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -1420,6 +1420,71 @@ def test_clr_dir(self):
self.assertTrue('IndexOf' not in clr.Dir('abc'))
self.assertTrue('IndexOf' in clr.DirClr('abc'))

def test_int32_bigint_equivalence(self):
import math

# properties
for i in range(-10, 10):
bi = big(i)
self.assertEqual(i.IsEven, bi.IsEven)
self.assertEqual(i.IsOne, bi.IsOne)
self.assertEqual(i.IsPowerOfTwo, bi.IsPowerOfTwo)
self.assertEqual(i.IsZero, bi.IsZero)
self.assertEqual(i.Sign, bi.Sign)
# static properties
self.assertEqual(i.Zero, bi.Zero)
self.assertEqual(i.One, bi.One)
self.assertEqual(i.MinusOne, bi.MinusOne)

test_values = [0, 1, 2, 4, 8, 10, 255, 256, 7<<30, 1<<31, (1<<31)-1, (1<<31)+1, 1<<32, (1<<32)-1, (1<<32)+1]
# methods
for i in test_values:
for i2 in [i, -i]:
ii = int(i2) # convert to Int32 if possible
bi = big(i2)
self.assertEqual(ii.ToByteArray(), bi.ToByteArray())
if hasattr(ii, 'GetByteCount') and hasattr(bi, 'GetByteCount'):
self.assertEqual(ii.GetByteCount(), bi.GetByteCount())
if hasattr(ii, 'GetBitLength') and hasattr(bi, 'GetBitLength'):
self.assertEqual(ii.GetBitLength(), bi.GetBitLength())

# static methods
for i in test_values:
for i2 in [i, -i]:
ii = int(i2) # convert to Int32 if possible
bi = big(i2)
self.assertEqual((1).Negate(ii), int.Negate(bi))
self.assertEqual((1).Abs(ii), int.Abs(bi))
self.assertEqual((1).Pow(ii, 5), int.Pow(bi, 5))
self.assertEqual((1).ModPow(ii, 5, 3), int.ModPow(bi, 5, 3))
if ii >= 0:
self.assertEqual((1).Log(ii), int.Log(bi))
self.assertEqual((1).Log10(ii), int.Log10(bi))
self.assertEqual((1).Log(ii, 7.2), int.Log(bi, 7.2))
else:
self.assertTrue(math.isnan((1).Log(ii)))
self.assertTrue(math.isnan(int.Log(bi)))
self.assertTrue(math.isnan((1).Log10(ii)))
self.assertTrue(math.isnan(int.Log10(bi)))
self.assertTrue(math.isnan((1).Log(ii, 7.2)))
self.assertTrue(math.isnan(int.Log(bi, 7.2)))

for j in test_values:
for j2 in [j, -j]:
jj = int(j2) # convert to Int32 if possible
bj = big(j2)
self.assertEqual((1).Compare(ii, jj), int.Compare(bi, bj))
self.assertEqual((1).Min(ii, jj), int.Min(bi, bj))
self.assertEqual((1).Max(ii, jj), int.Max(bi, bj))
self.assertEqual((1).Add(ii, jj), int.Add(bi, bj))
self.assertEqual((1).Subtract(ii, jj), int.Subtract(bi, bj))
self.assertEqual((1).Multiply(ii, jj), int.Multiply(bi, bj))
self.assertEqual((1).GreatestCommonDivisor(ii, jj), int.GreatestCommonDivisor(bi, bj))
if jj != 0:
self.assertEqual((1).Divide(ii, jj), int.Divide(bi, bj))
self.assertEqual((1).DivRem(ii, jj), int.DivRem(bi, bj))
self.assertEqual((1).Remainder(ii, jj), int.Remainder(bi, bj))

def test_array_contains(self):
if is_mono: # for whatever reason this is defined on Mono
System.Array[str].__dict__['__contains__']
Expand Down
29 changes: 16 additions & 13 deletions Tests/test_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
# See the LICENSE file in the project root for more information.

import sys
import unittest

from iptest import IronPythonTestCase, is_cli, big, myint, skipUnlessIronPython, run_test
from iptest import IronPythonTestCase, is_cli, is_netstandard, is_mono, big, myint, skipUnlessIronPython, run_test

class IntNoClrTest(IronPythonTestCase):
"""Must be run before IntTest because it depends on CLR API not being visible."""
Expand All @@ -21,17 +20,21 @@ def test_instance_set(self):
j = big(1)
from System import Int32

self.assertSetEqual(set(dir(i)) - set(dir(j)), {'MaxValue', 'MinValue'})
self.assertSetEqual(set(dir(Int32)) - set(dir(int)), {'MaxValue', 'MinValue'})

@unittest.expectedFailure
def test_instance_set_todo(self):
i = 1
j = big(1)
from System import Int32

self.assertSetEqual(set(dir(j)) - set(dir(i)), set())
self.assertSetEqual(set(dir(int)) - set(dir(Int32)), set())
if not is_mono:
self.assertSetEqual(set(dir(j)) - set(dir(i)), set())
self.assertSetEqual(set(dir(int)) - set(dir(Int32)), set())
else:
self.assertSetEqual(set(dir(j)) - set(dir(i)), {'GetByteCount', 'TryWriteBytes'})
self.assertSetEqual(set(dir(int)) - set(dir(Int32)), {'GetByteCount', 'TryWriteBytes'})

# these two assertions fail on IronPython compiled for .NET Standard
if not is_netstandard:
self.assertSetEqual(set(dir(i)) - set(dir(j)), {'MaxValue', 'MinValue'})
self.assertSetEqual(set(dir(Int32)) - set(dir(int)), {'MaxValue', 'MinValue'})

# weaker assertions that should always hold
self.assertTrue((set(dir(i)) - set(dir(j))).issubset({'MaxValue', 'MinValue', 'GetByteCount', 'TryWriteBytes', 'GetBitLength'}))
self.assertTrue((set(dir(Int32)) - set(dir(int))).issubset({'MaxValue', 'MinValue', 'GetByteCount', 'TryWriteBytes', 'GetBitLength'}))

def test_from_bytes(self):
self.assertEqual(type(int.from_bytes(b"abc", "big")), int)
Expand Down