Skip to content

Commit 2d80f71

Browse files
bobzhangclaude
andcommitted
fix(bigint): treat empty octets as zero in BigInt::from_octets
An empty big-endian magnitude naturally denotes zero, and mainstream bigint APIs agree: Java's BigInteger(signum, magnitude) permits a zero-length magnitude for any signum, Python's int.from_bytes(b"") returns 0, and Rust's num-bigint normalizes Sign::Minus with an empty magnitude to zero. Previously MoonBit was the outlier, aborting with "empty octet string" whenever signum != 0, while still accepting the morally identical all-zero magnitude (e.g. b"\x00" with signum=-1). Relax the panic on both backends to return zero, update the docs, and replace the panic tests with value tests covering every signum. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 318a6ee commit 2d80f71

5 files changed

Lines changed: 13 additions & 21 deletions

File tree

bigint/bigint_js.mbt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,15 @@ extern "js" fn hex2(b : Byte) -> String =
199199
/// Parameters:
200200
///
201201
/// * `octets` : A sequence of bytes representing the magnitude of the number in
202-
/// big-endian order. It must not be empty unless `signum` is 0.
202+
/// big-endian order. An empty sequence represents a zero magnitude.
203203
/// * `signum` : The sign of the resulting number. A negative value creates a
204204
/// negative number, zero returns zero, and a positive value creates a positive
205205
/// number. Defaults to 1.
206206
///
207207
/// Returns a `BigInt` value represented by the byte sequence and sign.
208+
///
209+
/// An empty byte sequence yields zero for any `signum`, matching the behavior
210+
/// of Java's `BigInteger`, Python's `int.from_bytes`, and Rust's `num-bigint`.
208211
pub fn BigInt::from_octets(octets : BytesView, signum? : Int = 1) -> BigInt {
209212
if signum < 0 {
210213
return -1N * BigInt::from_octets(octets, signum=1)
@@ -213,7 +216,7 @@ pub fn BigInt::from_octets(octets : BytesView, signum? : Int = 1) -> BigInt {
213216
return 0N
214217
}
215218
if octets.is_empty() {
216-
abort("empty octet string")
219+
return 0N
217220
}
218221
let str = StringBuilder()
219222
for octet in octets {

bigint/bigint_nonjs.mbt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,15 +1428,16 @@ pub fn BigInt::pow(self : BigInt, exp : BigInt, modulus? : BigInt) -> BigInt {
14281428
/// Parameters:
14291429
///
14301430
/// * `bytes` : A sequence of bytes representing the magnitude of the number in
1431-
/// big-endian order. The sequence must not be empty unless `sign` is 0.
1431+
/// big-endian order. An empty sequence represents a zero magnitude.
14321432
/// * `sign` : An integer specifying the sign of the resulting number (default:
14331433
/// 1). A value of 1 creates a positive number, -1 creates a negative number, and
14341434
/// 0 returns zero regardless of the input bytes.
14351435
///
14361436
/// Returns a `BigInt` value representing the number encoded in the byte sequence
14371437
/// with the specified sign.
14381438
///
1439-
/// Throws a panic if the input byte sequence is empty and the sign is not 0.
1439+
/// An empty byte sequence yields zero for any `sign`, matching the behavior of
1440+
/// Java's `BigInteger`, Python's `int.from_bytes`, and Rust's `num-bigint`.
14401441
///
14411442
/// Example:
14421443
///
@@ -1447,6 +1448,7 @@ pub fn BigInt::pow(self : BigInt, exp : BigInt, modulus? : BigInt) -> BigInt {
14471448
/// let negative = @bigint.BigInt::from_octets(bytes, signum=-1)
14481449
/// inspect(positive, content="66051")
14491450
/// inspect(negative, content="-66051")
1451+
/// inspect(@bigint.BigInt::from_octets(b""), content="0")
14501452
/// }
14511453
/// ```
14521454
pub fn BigInt::from_octets(input : BytesView, signum? : Int = 1) -> BigInt {
@@ -1457,7 +1459,7 @@ pub fn BigInt::from_octets(input : BytesView, signum? : Int = 1) -> BigInt {
14571459
return -BigInt::from_octets(input)
14581460
}
14591461
if len == 0 {
1460-
abort("empty octet string")
1462+
return zero
14611463
}
14621464
let div = len * 8 / RADIX_BIT_LEN
14631465
let mod = len * 8 % RADIX_BIT_LEN // number of bits in the first limb

bigint/bigint_test.mbt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ test "to_octets pads larger length" {
6767
}
6868

6969
///|
70-
test "from_octets accepts empty input with zero signum" {
70+
test "from_octets treats empty input as zero" {
71+
inspect(@bigint.BigInt::from_octets(b""), content="0")
7172
inspect(@bigint.BigInt::from_octets(b"", signum=0), content="0")
73+
inspect(@bigint.BigInt::from_octets(b"", signum=-1), content="0")
7274
}
7375

7476
///|

bigint/panic_test.mbt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,6 @@ test "panic pow negative exponent" {
8585
base.pow(exp) |> ignore
8686
}
8787

88-
///|
89-
test "panic from_octets empty" {
90-
@bigint.BigInt::from_octets(Bytes::new(0)) |> ignore
91-
}
92-
93-
///|
94-
test "panic from_octets empty negative signum" {
95-
@bigint.BigInt::from_octets(Bytes::new(0), signum=-1) |> ignore
96-
}
97-
9888
///|
9989
test "panic to_octets negative" {
10090
(-1N).to_octets() |> ignore

builtin/panic_test.mbt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,6 @@ test "panic to_octets coverage for negative number than required" {
158158
(-123456789N).to_octets() |> ignore
159159
}
160160

161-
///|
162-
test "panic from_octets coverage for empty octets" {
163-
BigInt::from_octets(b"") |> ignore
164-
}
165-
166161
///|
167162
test "panic sub_string with invalid byte_length" {
168163
let bytes = b"Hello, World!"

0 commit comments

Comments
 (0)