Skip to content

Commit 84319ad

Browse files
authored
Implement to_bytes for all integer types (#1392)
* Fix BigIntegerOps.to_bytes * Optimize BigIntegerOps.to_bytes * Optimize Int32Ops.to_bytes * Implement to_bytes for all integral types * Cleanup test * Use new fast BigInteger methods on .NET * Cleanup whitespace * Simplify test condition
1 parent 7726955 commit 84319ad

5 files changed

Lines changed: 230 additions & 45 deletions

File tree

Src/IronPython/Runtime/Operations/BigIntegerOps.cs

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -715,8 +715,17 @@ public static BigInteger Getdenominator(BigInteger self) {
715715
return 1;
716716
}
717717

718-
public static int bit_length(BigInteger self) {
718+
public static object bit_length(BigInteger self) {
719+
#if NET
720+
long length = self.Sign switch {
721+
0 => 0,
722+
> 0 => self.GetBitLength(),
723+
< 0 => BigInteger.Abs(self).GetBitLength(),
724+
};
725+
return length <= int.MaxValue ? (int)length : (BigInteger)length;
726+
#else
719727
return MathUtils.BitLength(self);
728+
#endif
720729
}
721730

722731
[PythonHidden]
@@ -740,7 +749,7 @@ public static BigInteger ToBigInteger(BigInteger self) {
740749
val = -self;
741750
}
742751
string digits;
743-
752+
744753
switch (spec.Type) {
745754
case 'n':
746755
CultureInfo culture = context.LanguageContext.NumericCulture;
@@ -853,24 +862,50 @@ public static BigInteger ToBigInteger(BigInteger self) {
853862

854863
public static Bytes to_bytes(BigInteger value, int length, [NotDynamicNull] string byteorder, bool signed = false) {
855864
// TODO: signed should be a keyword only argument
856-
// TODO: should probably be moved to IntOps.Generated and included in all types
857865

858-
if (length < 0) throw PythonOps.ValueError("length argument must be non-negative");
859-
if (!signed && value < 0) throw PythonOps.OverflowError("can't convert negative int to unsigned");
860-
861-
bool isLittle = byteorder == "little";
866+
bool isLittle = (byteorder == "little");
862867
if (!isLittle && byteorder != "big") throw PythonOps.ValueError("byteorder must be either 'little' or 'big'");
863868

864-
var reqLength = (bit_length(value) + (signed ? 1 : 0)) / 8;
865-
if (reqLength > length) throw PythonOps.OverflowError("int too big to convert");
869+
if (length < 0) throw PythonOps.ValueError("length argument must be non-negative");
870+
if (!signed && value.Sign < 0) throw PythonOps.OverflowError("can't convert negative int to unsigned");
871+
872+
if (value.IsZero) return Bytes.Make(new byte[length]);
866873

874+
#if NETCOREAPP
875+
var bytes = new byte[length];
876+
int start = isLittle? 0 : length - value.GetByteCount(isUnsigned: !signed);
877+
if (start < 0) ThrowOverflow();
878+
if (!value.TryWriteBytes(bytes.AsSpan(start), out int written, isUnsigned: !signed, isBigEndian: !isLittle)) {
879+
ThrowOverflow();
880+
}
881+
if (written < length && value.Sign < 0) {
882+
if (isLittle) {
883+
bytes.AsSpan(written).Fill(0xFF);
884+
} else {
885+
bytes.AsSpan(0, length - written).Fill(0xFF);
886+
}
887+
}
888+
#else
867889
var bytes = value.ToByteArray();
868-
IEnumerable<byte> res = bytes;
869-
if (length > bytes.Length) res = res.Concat(Enumerable.Repeat<byte>((value < 0) ? (byte)0xff : (byte)0, length - bytes.Length));
870-
else if (length < bytes.Length) res = res.Take(length);
871-
if (!isLittle) res = res.Reverse();
890+
if (length > bytes.Length) {
891+
int top = bytes.Length;
892+
Array.Resize(ref bytes, length);
893+
if (value.IsNegative()) {
894+
for (int i = top; i < length; i++) {
895+
bytes[i] = 0xFF;
896+
}
897+
}
898+
} else if (length == bytes.Length - 1 && !signed && bytes[length] == 0) {
899+
Array.Resize(ref bytes, length);
900+
} else if (length != bytes.Length) {
901+
ThrowOverflow();
902+
}
903+
if (!isLittle) Array.Reverse(bytes);
904+
#endif
905+
906+
return Bytes.Make(bytes);
872907

873-
return Bytes.Make(res.ToArray());
908+
static void ThrowOverflow() => throw PythonOps.OverflowError("int too big to convert");
874909
}
875910

876911
[ClassMethod, StaticExtensionMethod]
@@ -1045,7 +1080,7 @@ private static string ToOctal(BigInteger val, bool lowercase) {
10451080
return ToDigits(val, 8, lowercase);
10461081
}
10471082

1048-
internal static string ToBinary(BigInteger val) {
1083+
internal static string ToBinary(BigInteger val) {
10491084
string res = ToBinary(BigInteger.Abs(val), true, true);
10501085
if (val.IsNegative()) {
10511086
res = "-" + res;
@@ -1057,7 +1092,7 @@ private static string ToBinary(BigInteger val, bool includeType, bool lowercase)
10571092
Debug.Assert(!val.IsNegative());
10581093

10591094
string digits = ToDigits(val, 2, lowercase);
1060-
1095+
10611096
if (includeType) {
10621097
digits = (lowercase ? "0b" : "0B") + digits;
10631098
}
@@ -1084,7 +1119,7 @@ private static string ToBinary(BigInteger val, bool includeType, bool lowercase)
10841119
for (int i = str.Length - 1; i >= 0; i--) {
10851120
res.Append(str[i]);
10861121
}
1087-
1122+
10881123
return res.ToString();
10891124
}
10901125

Src/IronPython/Runtime/Operations/IntOps.Generated.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
using IronPython.Runtime.Types;
1414

15+
using NotDynamicNullAttribute = Microsoft.Scripting.Runtime.NotNullAttribute;
16+
1517
#pragma warning disable 675
1618

1719
namespace IronPython.Runtime.Operations {
@@ -294,6 +296,11 @@ public static int bit_length(SByte value) {
294296
return MathUtils.BitLength((int)value);
295297
}
296298

299+
public static Bytes to_bytes(SByte value, int length, [NotDynamicNull] string byteorder, bool signed = false) {
300+
// TODO: signed should be a keyword only argument
301+
return Int64Ops.to_bytes(value, length, byteorder, signed);
302+
}
303+
297304
#endregion
298305
}
299306

@@ -652,6 +659,11 @@ public static int bit_length(Byte value) {
652659
return MathUtils.BitLength((int)value);
653660
}
654661

662+
public static Bytes to_bytes(Byte value, int length, [NotDynamicNull] string byteorder, bool signed = false) {
663+
// TODO: signed should be a keyword only argument
664+
return UInt64Ops.to_bytes(value, length, byteorder, signed);
665+
}
666+
655667
#endregion
656668
}
657669

@@ -933,6 +945,11 @@ public static int bit_length(Int16 value) {
933945
return MathUtils.BitLength((int)value);
934946
}
935947

948+
public static Bytes to_bytes(Int16 value, int length, [NotDynamicNull] string byteorder, bool signed = false) {
949+
// TODO: signed should be a keyword only argument
950+
return Int64Ops.to_bytes(value, length, byteorder, signed);
951+
}
952+
936953
#endregion
937954
}
938955

@@ -1301,6 +1318,11 @@ public static int bit_length(UInt16 value) {
13011318
return MathUtils.BitLength((int)value);
13021319
}
13031320

1321+
public static Bytes to_bytes(UInt16 value, int length, [NotDynamicNull] string byteorder, bool signed = false) {
1322+
// TODO: signed should be a keyword only argument
1323+
return UInt64Ops.to_bytes(value, length, byteorder, signed);
1324+
}
1325+
13041326
#endregion
13051327
}
13061328

@@ -1557,6 +1579,11 @@ public static int bit_length(Int32 value) {
15571579
return MathUtils.BitLength(value);
15581580
}
15591581

1582+
public static Bytes to_bytes(Int32 value, int length, [NotDynamicNull] string byteorder, bool signed = false) {
1583+
// TODO: signed should be a keyword only argument
1584+
return Int64Ops.to_bytes(value, length, byteorder, signed);
1585+
}
1586+
15601587
#endregion
15611588
}
15621589

@@ -1925,6 +1952,11 @@ public static int bit_length(UInt32 value) {
19251952
return MathUtils.BitLengthUnsigned(value);
19261953
}
19271954

1955+
public static Bytes to_bytes(UInt32 value, int length, [NotDynamicNull] string byteorder, bool signed = false) {
1956+
// TODO: signed should be a keyword only argument
1957+
return UInt64Ops.to_bytes(value, length, byteorder, signed);
1958+
}
1959+
19281960
#endregion
19291961
}
19301962

Src/IronPython/Runtime/Operations/IntOps.cs

Lines changed: 90 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -338,34 +338,11 @@ public static string __format__(CodeContext/*!*/ context, int self, [NotDynamicN
338338
return spec.AlignNumericText(digits, self == 0, self > 0);
339339
}
340340

341-
public static Bytes to_bytes(Int32 value, int length, [NotDynamicNull] string byteorder, bool signed=false) {
342-
// TODO: signed should be a keyword only argument
343-
// TODO: should probably be moved to IntOps.Generated and included in all types
344-
345-
if (length < 0) throw PythonOps.ValueError("length argument must be non-negative");
346-
if (!signed && value < 0) throw PythonOps.OverflowError("can't convert negative int to unsigned");
347-
348-
bool isLittle = byteorder == "little";
349-
if (!isLittle && byteorder != "big") throw PythonOps.ValueError("byteorder must be either 'little' or 'big'");
350-
351-
var reqLength = (bit_length(value) + (value > 0 && signed ? 1 : 0) + 7) / 8;
352-
if (reqLength > length) throw PythonOps.OverflowError("int too big to convert");
353-
354-
var bytes = new BigInteger(value).ToByteArray();
355-
IEnumerable<byte> res = bytes;
356-
if (length > bytes.Length) res = res.Concat(Enumerable.Repeat<byte>((value < 0) ? (byte)0xff : (byte)0, length - bytes.Length));
357-
else if (length < bytes.Length) res = res.Take(length);
358-
if (!isLittle) res = res.Reverse();
359-
360-
return Bytes.Make(res.ToArray());
361-
}
362-
363341
[ClassMethod, StaticExtensionMethod]
364342
public static object from_bytes(CodeContext context, PythonType type, object bytes, [NotDynamicNull] string byteorder, bool signed = false)
365343
// TODO: signed should be a keyword only argument
366344
=> BigIntegerOps.from_bytes(context, type, bytes, byteorder, signed);
367345

368-
369346
#endregion
370347

371348
#region Helpers
@@ -454,4 +431,94 @@ private static string ToBinary(int self, bool includeType) {
454431

455432
#endregion
456433
}
434+
435+
public static partial class Int64Ops {
436+
437+
#region Public API - Bytes
438+
439+
public static Bytes to_bytes(Int64 value, int length, [NotDynamicNull] string byteorder, bool signed = false) {
440+
// TODO: signed should be a keyword only argument
441+
bool isLittle = (byteorder == "little");
442+
if (!isLittle && byteorder != "big") throw PythonOps.ValueError("byteorder must be either 'little' or 'big'");
443+
444+
if (length < 0) throw PythonOps.ValueError("length argument must be non-negative");
445+
if (!signed && value < 0) throw PythonOps.OverflowError("can't convert negative int to unsigned");
446+
447+
if (value == 0) return Bytes.Make(new byte[length]);
448+
449+
var bytes = new byte[length];
450+
int cur, end, step;
451+
if (isLittle) {
452+
cur = 0; end = length; step = 1;
453+
} else {
454+
cur = length - 1; end = -1; step = -1;
455+
}
456+
457+
if (!signed || value >= 0) {
458+
ulong uvalue = unchecked((ulong)value);
459+
do {
460+
if (cur == end) ThrowOverflow();
461+
bytes[cur] = (byte)(uvalue & 0xFF);
462+
uvalue >>= 8;
463+
cur += step;
464+
} while (uvalue != 0);
465+
} else {
466+
byte curbyte;
467+
do {
468+
if (cur == end) ThrowOverflow();
469+
bytes[cur] = curbyte = (byte)(value & 0xFF);
470+
value >>= 8;
471+
cur += step;
472+
} while (value != -1 || (curbyte & 0x80) == 0);
473+
474+
while (cur != end) {
475+
bytes[cur] = 0xFF;
476+
cur += step;
477+
}
478+
}
479+
480+
return Bytes.Make(bytes);
481+
482+
static void ThrowOverflow() => throw PythonOps.OverflowError("int too big to convert");
483+
}
484+
485+
#endregion
486+
}
487+
488+
public static partial class UInt64Ops {
489+
490+
#region Public API - Bytes
491+
492+
public static Bytes to_bytes(UInt64 value, int length, [NotDynamicNull] string byteorder, bool signed = false) {
493+
bool isLittle = (byteorder == "little");
494+
if (!isLittle && byteorder != "big") throw PythonOps.ValueError("byteorder must be either 'little' or 'big'");
495+
496+
if (length < 0) throw PythonOps.ValueError("length argument must be non-negative");
497+
498+
if (value == 0) return Bytes.Make(new byte[length]);
499+
500+
var bytes = new byte[length];
501+
int cur, end, step;
502+
if (isLittle) {
503+
cur = 0; end = length; step = 1;
504+
} else {
505+
cur = length - 1; end = -1; step = -1;
506+
}
507+
508+
do {
509+
if (cur == end) ThrowOverflow();
510+
bytes[cur] = (byte)(value & 0xFF);
511+
value >>= 8;
512+
cur += step;
513+
} while (value != 0);
514+
515+
if (signed && (bytes[end - step] & 0x80) == 0x80) ThrowOverflow();
516+
517+
return Bytes.Make(bytes);
518+
519+
static void ThrowOverflow() => throw PythonOps.OverflowError("int too big to convert");
520+
}
521+
522+
#endregion
523+
}
457524
}

Src/Scripts/generate_alltypes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,13 @@ def gen_api(cw, ty):
474474
cw.write('return MathUtils.%s(%svalue);' % (counter, cast))
475475
cw.exit_block()
476476

477+
if ty.name not in ['Int64', 'UInt64']:
478+
cw.writeline()
479+
cw.enter_block('public static Bytes to_bytes(%s value, int length, [NotDynamicNull] string byteorder, bool signed = false)' % ty.name)
480+
cw.write('// TODO: signed should be a keyword only argument')
481+
cw.write('return %s.to_bytes(value, length, byteorder, signed);' % ("Int64Ops" if ty.is_signed else "UInt64Ops"))
482+
cw.exit_block()
483+
477484
cw.writeline()
478485
cw.writeline("#endregion")
479486

0 commit comments

Comments
 (0)