diff --git a/benches/decimal/bench_root.mojo b/benches/decimal/bench_root.mojo index a7c145e..e5216c6 100644 --- a/benches/decimal/bench_root.mojo +++ b/benches/decimal/bench_root.mojo @@ -133,7 +133,7 @@ fn run_benchmark( mojo_time = 1 # Prevent division by zero # Benchmark Python implementation (if possible) - var python_time: Float64 = 0 + var python_time: Float64 if not is_negative_odd_root and not ( py_result is Python.evaluate("None") ): # Correct way to check for None diff --git a/docs/changelog.md b/docs/changelog.md index 31b22a8..bee459a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,7 +8,7 @@ Version 0.4.1 of DeciMojo introduces implicit type conversion between built-in i ### ⭐️ New -Now DeciMojo supports implicit type conversion between built-in integeral types (`Int`, `UInt`, `Int8`, `UInt8`, `Int16`, `UInt16`, `Int32`, `UInt32`, `Int64`, `UInt64`, `Int128`,`UInt128`, `Int256`, and `UInt256`) and the arbitrary-precision integer types (`BigUInt`, `BigInt`, and `BigDecimal`). This allows you to use these built-in types directly in arithmetic operations with `BigInt` and `BigUInt` without explicit conversion. The merged type will always be the most compatible one (PR #89, PR #90). +Now DeciMojo supports implicit type conversion between built-in integeral types (`Int`, `UInt`, `Int8`, `UInt8`, `Int16`, `UInt16`, `Int32`, `UInt32`, `Int64`, `UInt64`, `Int128`,`UInt128`, `Int256`, and `UInt256`) and the arbitrary-precision types (`BigUInt`, `BigInt`, and `BigDecimal`). This allows you to use these built-in types directly in arithmetic operations with `BigInt` and `BigUInt` without explicit conversion. The merged type will always be the most compatible one (PR #89, PR #90). For example, you can now do the following: diff --git a/docs/todo.md b/docs/todo.md index 2d9ae3e..6f8f063 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -5,4 +5,5 @@ This is a to-do list for Yuhao's personal use. - [x] (#31) The `exp()` function performs slower than Python's counterpart in specific cases. Detailed investigation reveals the bottleneck stems from multiplication operations between decimals with significant fractional components. These operations currently rely on UInt256 arithmetic, which introduces performance overhead. Optimization of the `multiply()` function is required to address these performance bottlenecks, particularly for high-precision decimal multiplication with many digits after the decimal point. - [ ] When Mojo supports global variables, implement a global variable for the `BigDecimal` class to store the precision of the decimal number. This will allow users to set the precision globally, rather than having to set it for each function of the `BigDecimal` class. - [ ] Implement different methods for augmented arithmetic assignments to improve memeory-efficiency and performance. +- [ ] Implement different methods for adding decimojo types with `Int` types so that an implicit conversion is not required. - [ ] Implement a method `remove_trailing_zeros` for `BigUInt`. diff --git a/pixi.toml b/pixi.toml index 8bce78e..1942995 100644 --- a/pixi.toml +++ b/pixi.toml @@ -27,8 +27,8 @@ clean = "rm tests/decimojo.mojopkg && rm benches/decimojo.mojopkg && rm tests/to c = "clear && pixi run clean" # tests (use the mojo testing tool) -test = "pixi run package && pixi run mojo test tests --filter" -t = "clear && pixi run package && pixi run mojo test tests --filter" +test = "pixi run package && pixi run mojo test tests -D ASSERT=all --filter" +t = "clear && pixi run package && pixi run mojo test tests -D ASSERT=all --filter" # benches bench_decimal = "clear && pixi run package && cd benches/decimal && pixi run mojo -I ../ bench.mojo && cd ../.. && pixi run clean" diff --git a/src/decimojo/bigdecimal/arithmetics.mojo b/src/decimojo/bigdecimal/arithmetics.mojo index cc18709..f64744c 100644 --- a/src/decimojo/bigdecimal/arithmetics.mojo +++ b/src/decimojo/bigdecimal/arithmetics.mojo @@ -76,10 +76,8 @@ fn add(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: # Handle addition based on signs if x1.sign == x2.sign: # Same sign: Add coefficients, keep sign - var result_coef = coef1 + coef2 - return BigDecimal( - coefficient=result_coef^, scale=max_scale, sign=x1.sign - ) + coef1.add_inplace(coef2) + return BigDecimal(coefficient=coef1^, scale=max_scale, sign=x1.sign) # Different signs: Subtract smaller coefficient from larger if coef1 > coef2: # |x1| > |x2|, result sign is x1's sign @@ -143,10 +141,8 @@ fn subtract(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: # Handle subtraction based on signs if x1.sign != x2.sign: # Different signs: x1 - (-x2) = x1 + x2, or (-x1) - x2 = -(x1 + x2) - var result_coef = coef1 + coef2 - return BigDecimal( - coefficient=result_coef^, scale=max_scale, sign=x1.sign - ) + coef1.add_inplace(coef2) + return BigDecimal(coefficient=coef1^, scale=max_scale, sign=x1.sign) # Same signs: Must perform actual subtraction if coef1 > coef2: diff --git a/src/decimojo/bigdecimal/bigdecimal.mojo b/src/decimojo/bigdecimal/bigdecimal.mojo index 1b38f14..12a71eb 100644 --- a/src/decimojo/bigdecimal/bigdecimal.mojo +++ b/src/decimojo/bigdecimal/bigdecimal.mojo @@ -829,7 +829,7 @@ struct BigDecimal( ) @always_inline - fn internal_representation(self) raises: + fn print_internal_representation(self) raises: """Prints the internal representation of the BigDecimal.""" var line_width = 30 var string_of_number = self.to_string(line_width=line_width).split("\n") diff --git a/src/decimojo/bigint/arithmetics.mojo b/src/decimojo/bigint/arithmetics.mojo index 33cc7a5..f36d87e 100644 --- a/src/decimojo/bigint/arithmetics.mojo +++ b/src/decimojo/bigint/arithmetics.mojo @@ -63,9 +63,11 @@ fn add_inplace(mut x1: BigInt, x2: BigInt) raises -> None: # If signs are different, delegate to `subtract` if x1.sign != x2.sign: x1 = subtract(x1, -x2) + return # Same sign: add magnitudes in place - x1.magnitude += x2.magnitude + else: + x1.magnitude += x2.magnitude fn subtract(x1: BigInt, x2: BigInt) raises -> BigInt: diff --git a/src/decimojo/bigint/bigint.mojo b/src/decimojo/bigint/bigint.mojo index 342a5bf..2f02397 100644 --- a/src/decimojo/bigint/bigint.mojo +++ b/src/decimojo/bigint/bigint.mojo @@ -523,7 +523,7 @@ struct BigInt(Absable, IntableRaising, Representable, Stringable, Writable): @always_inline fn __iadd__(mut self, other: Int) raises: # Optimize the case `i += 1` - if other == 1: + if (self >= 0) and (other == 1): self.magnitude.add_inplace_by_1() else: decimojo.bigint.arithmetics.add_inplace(self, other) @@ -712,7 +712,7 @@ struct BigInt(Absable, IntableRaising, Representable, Stringable, Writable): # Internal methods # ===------------------------------------------------------------------=== # - fn internal_representation(self) raises: + fn print_internal_representation(self) raises: """Prints the internal representation details of a BigInt.""" var string_of_number = self.to_string(line_width=30).split("\n") print("\nInternal Representation Details of BigInt") diff --git a/src/decimojo/biguint/arithmetics.mojo b/src/decimojo/biguint/arithmetics.mojo index 91c3b45..47bc7f3 100644 --- a/src/decimojo/biguint/arithmetics.mojo +++ b/src/decimojo/biguint/arithmetics.mojo @@ -59,7 +59,7 @@ from decimojo.rounding_mode import RoundingMode # ===----------------------------------------------------------------------=== # -fn add(x1: BigUInt, x2: BigUInt) raises -> BigUInt: +fn add(x1: BigUInt, x2: BigUInt) -> BigUInt: """Returns the sum of two unsigned integers. Args: @@ -69,35 +69,27 @@ fn add(x1: BigUInt, x2: BigUInt) raises -> BigUInt: Returns: The sum of the two unsigned integers. """ + # Short circuit cases - if len(x1.words) == 1: - if x1.is_zero(): - return x2 - if x1.is_one(): - # Optimized case for adding 1 - var result = x2 - add_inplace_by_1(result) - return result^ - if len(x2.words) == 1: - var value = x1.words[0] + x2.words[0] - if value <= 999_999_999: - return BigUInt(List[UInt32](value)) - else: - return BigUInt( - List[UInt32]( - value % UInt32(1_000_000_000), - value // UInt32(1_000_000_000), - ) - ) - if len(x2.words) == 1: - if x2.is_zero(): - return x1 - if x2.is_one(): - # Optimized case for adding 1 - var result = x1 - add_inplace_by_1(result) - return result^ + # Zero cases + if x1.is_zero(): + return x2 + + if x2.is_zero(): + return x1 + + # If both numbers are single-word, we can handle them with UInt32 + if len(x1.words) == 1 and len(x2.words) == 1: + return BigUInt.from_uint32(x1.words[0] + x2.words[0]) + + # If both numbers are double-word, we can handle them with UInt64 + if len(x1.words) <= 2 and len(x2.words) <= 2: + return BigUInt.from_unsigned_integral_scalar( + x1.to_uint64_with_first_2_words() + + x2.to_uint64_with_first_2_words() + ) + # Normal cases # The result will have at most one more word than the longer operand var words = List[UInt32](capacity=max(len(x1.words), len(x2.words)) + 1) @@ -118,8 +110,8 @@ fn add(x1: BigUInt, x2: BigUInt) raises -> BigUInt: sum_of_words += x2.words[ith] # Compute new word and carry - carry = UInt32(sum_of_words // 1_000_000_000) - words.append(UInt32(sum_of_words % 1_000_000_000)) + carry = sum_of_words // UInt32(1_000_000_000) + words.append(sum_of_words % UInt32(1_000_000_000)) ith += 1 @@ -130,7 +122,7 @@ fn add(x1: BigUInt, x2: BigUInt) raises -> BigUInt: return BigUInt(words=words^) -fn add_inplace(mut x1: BigUInt, x2: BigUInt) raises -> None: +fn add_inplace(mut x1: BigUInt, x2: BigUInt) -> None: """Increments a BigUInt number by another BigUInt number in place. Args: @@ -152,38 +144,30 @@ fn add_inplace(mut x1: BigUInt, x2: BigUInt) raises -> None: x1.words[0] = value % UInt32(1_000_000_000) x1.words.append(value // UInt32(1_000_000_000)) return + else: + pass + if len(x2.words) == 1: - if x2.is_zero(): - return - if x2.is_one(): + if x2.words[0] == 0: + return # No change needed + elif x2.words[0] == 1: # Optimized case for adding 1 add_inplace_by_1(x1) return - var carry: UInt32 = 0 - var ith: Int = 0 - var sum_of_words: UInt32 - var x1_len = len(x1.words) - - while ith < x1_len or ith < len(x2.words): - sum_of_words = carry - - # Add x1's word if available - if ith < len(x1.words): - sum_of_words += x1.words[ith] + # Normal cases + if len(x1.words) < len(x2.words): + x1.words.resize(new_size=len(x2.words), value=UInt32(0)) - # Add x2's word if available - if ith < len(x2.words): - sum_of_words += x2.words[ith] + var carry: UInt32 = 0 - # Compute new word and carry - carry = UInt32(sum_of_words // 1_000_000_000) - if ith < len(x1.words): - x1.words[ith] = UInt32(sum_of_words % 1_000_000_000) + for i in range(len(x1.words)): + if i < len(x2.words): + x1.words[i] += carry + x2.words[i] else: - x1.words.append(UInt32(sum_of_words % 1_000_000_000)) - - ith += 1 + x1.words[i] += carry + carry = x1.words[i] // UInt32(1_000_000_000) + x1.words[i] %= UInt32(1_000_000_000) # Handle final carry if it exists if carry > 0: @@ -192,7 +176,7 @@ fn add_inplace(mut x1: BigUInt, x2: BigUInt) raises -> None: return -fn add_inplace_by_1(mut x: BigUInt) raises -> None: +fn add_inplace_by_1(mut x: BigUInt) -> None: """Increments a BigUInt number by 1.""" var i = 0 while i < len(x.words): diff --git a/src/decimojo/biguint/biguint.mojo b/src/decimojo/biguint/biguint.mojo index ebf5466..3b069d8 100644 --- a/src/decimojo/biguint/biguint.mojo +++ b/src/decimojo/biguint/biguint.mojo @@ -63,6 +63,8 @@ struct BigUInt(Absable, IntableRaising, Stringable, Writable): alias ZERO = Self.zero() alias ONE = Self.one() + alias MAX_UINT64 = Self(709551615, 446744073, 18) + alias MAX_UINT128 = Self(768211455, 374607431, 938463463, 282366920, 340) @always_inline @staticmethod @@ -139,11 +141,18 @@ struct BigUInt(Absable, IntableRaising, Stringable, Writable): @implicit fn __init__(out self, value: UInt): - """Initializes a BigUInt from an Int. + """Initializes a BigUInt from an UInt. See `from_uint()` for more information. """ self = Self.from_uint(value) + @implicit + fn __init__(out self, value: UInt32): + """Initializes a BigUInt from an UInt32. + See `from_uint32()` for more information. + """ + self = Self.from_uint32(value) + @implicit fn __init__(out self, value: Scalar): """Initializes a BigUInt from an unsigned integral scalar. @@ -163,7 +172,7 @@ struct BigUInt(Absable, IntableRaising, Stringable, Writable): # from_list(owned words: List[UInt32]) -> Self # from_words(*words: UInt32) -> Self # from_int(value: Int) -> Self - # from_scalar[dtype: DType](value: Scalar[dtype]) -> Self + # from_unsigned_integral_scalar[dtype: DType](value: Scalar[dtype]) -> Self # from_string(value: String) -> Self # ===------------------------------------------------------------------=== # @@ -268,6 +277,26 @@ struct BigUInt(Absable, IntableRaising, Stringable, Writable): return Self(list_of_words^) + @staticmethod + fn from_uint32(value: UInt32) -> Self: + """Creates a BigUInt from an `UInt32` object. + + Notes: + UInt32 is special, so we have a separate method for it. + """ + # One word is enough + if value <= 999_999_999: + return Self(words=List[UInt32](value)) + + # Two words are needed + else: + return Self( + words=List[UInt32]( + value % UInt32(1_000_000_000), + value // UInt32(1_000_000_000), + ) + ) + @staticmethod fn from_unsigned_integral_scalar[ dtype: DType, // @@ -292,11 +321,7 @@ struct BigUInt(Absable, IntableRaising, Stringable, Writable): ]() @parameter - if ( - (dtype == DType.uint8) - or (dtype == DType.uint16) - or (dtype == DType.uint32) - ): + if (dtype == DType.uint8) or (dtype == DType.uint16): return Self(words=List[UInt32](UInt32(value))) if value == 0: @@ -548,24 +573,137 @@ struct BigUInt(Absable, IntableRaising, Stringable, Writable): Raises: Error: If the number is too large or too small to fit in Int. """ - - if len(self.words) > 3: + if self.is_uint64_overflow(): raise Error( - "Error in `BigUInt.to_int()`: The number exceeds the size" - " of UInt64" + "`BigUInt.to_int()`: The number exceeds the size" + " of UInt64 (18446744073709551615)" ) - var value: UInt128 = 0 - for i in range(len(self.words)): - value += UInt128(self.words[i]) * UInt128(1_000_000_000) ** i + if len(self.words) == 1: + return self.words.data.load[width=1]().cast[DType.uint64]() + elif len(self.words) == 2: + return ( + self.words.data.load[width=2]().cast[DType.uint64]() + * SIMD[DType.uint64, 2](1, 1_000_000_000) + ).reduce_add() + else: + return ( + self.words.data.load[width=4]().cast[DType.uint64]() + * SIMD[DType.uint64, 4]( + 1, + 1_000_000_000, + 1_000_000_000_000_000_000, + 0, + ) + ).reduce_add() - if value > UInt128(UInt64.MAX): - raise Error( - "Error in `BigUInt.to_uint64()`: The number exceeds the size" - " of UInt64" - ) + fn to_uint64_with_first_2_words(self) -> UInt64: + """Convert the first two words of the BigUInt to UInt64. + + Notes: + This method quickly convert BigUInt with 2 words into UInt64. + """ + if len(self.words) == 1: + return self.words.data.load[width=1]().cast[DType.uint64]() + else: # len(self.words) == 2 + return ( + self.words.data.load[width=2]().cast[DType.uint64]() + * SIMD[DType.uint64, 2](1, 1_000_000_000) + ).reduce_add() + + fn to_uint128(self) -> UInt128: + """Returns the number as UInt128. + **UNSAFE** You need to ensure that the number of words is less than 5. + + Returns: + The number as UInt128. + """ + + # FIXME: Due to an unknown bug in Mojo, + # The returned value changed in the caller when we use raises + # So I have to comment out the raises part + # In the future, we need to fix this bug and add raises back + # + # if self.is_uint128_overflow(): + # raise Error( + # "`BigUInt.to_int()`: The number exceeds the size" + # " of UInt128 (340282366920938463463374607431768211455)" + # ) + + var result: UInt128 = 0 + + if len(self.words) == 1: + result = self.words.data.load[width=1]().cast[DType.uint128]() + elif len(self.words) == 2: + result = ( + self.words.data.load[width=2]().cast[DType.uint128]() + * SIMD[DType.uint128, 2](1, 1_000_000_000) + ).reduce_add() + elif len(self.words) == 3: + result = ( + self.words.data.load[width=4]().cast[DType.uint128]() + * SIMD[DType.uint128, 4]( + 1, 1_000_000_000, 1_000_000_000_000_000_000, 0 + ) + ).reduce_add() + elif len(self.words) == 4: + result = ( + self.words.data.load[width=4]().cast[DType.uint128]() + * SIMD[DType.uint128, 4]( + 1, + 1_000_000_000, + 1_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000_000, + ) + ).reduce_add() + else: + result = ( + self.words.data.load[width=8]().cast[DType.uint128]() + * SIMD[DType.uint128, 8]( + 1, + 1_000_000_000, + 1_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000_000_000_000_000, + 0, + 0, + 0, + ) + ).reduce_add() + + return result - return UInt64(value) + fn to_uint128_with_first_4_words(self) -> UInt128: + """Convert the first four words of the BigUInt to UInt128. + + Notes: + This method quickly convert BigUInt with 4 words into UInt128. + """ + + if len(self.words) == 1: + return self.words.data.load[width=1]().cast[DType.uint128]() + elif len(self.words) == 2: + return ( + self.words.data.load[width=2]().cast[DType.uint128]() + * SIMD[DType.uint128, 2](1, 1_000_000_000) + ).reduce_add() + elif len(self.words) == 3: + return ( + self.words.data.load[width=4]().cast[DType.uint128]() + * SIMD[DType.uint128, 4]( + 1, 1_000_000_000, 1_000_000_000_000_000_000, 0 + ) + ).reduce_add() + else: # len(self.words) == 4 + return ( + self.words.data.load[width=4]().cast[DType.uint128]() + * SIMD[DType.uint128, 4]( + 1, + 1_000_000_000, + 1_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000_000, + ) + ).reduce_add() fn to_string(self, line_width: Int = 0) -> String: """Returns string representation of the BigUInt. @@ -629,6 +767,10 @@ struct BigUInt(Absable, IntableRaising, Stringable, Writable): return result^ + # ===------------------------------------------------------------------=== # + # Type-conversion methods that are unsafe + # ===------------------------------------------------------------------=== # + # ===------------------------------------------------------------------=== # # Basic unary operation dunders # neg @@ -810,6 +952,14 @@ struct BigUInt(Absable, IntableRaising, Stringable, Writable): # Mathematical methods that do not implement a trait (not a dunder) # ===------------------------------------------------------------------=== # + @always_inline + fn add_inplace(mut self, other: Self) raises: + """Adds `other` to this number in place. + It is equal to `self += other`. + See `add_inplace()` for more information. + """ + decimojo.biguint.arithmetics.add_inplace(self, other) + @always_inline fn add_inplace_by_1(mut self) raises: """Adds 1 to this number in place. @@ -943,7 +1093,7 @@ struct BigUInt(Absable, IntableRaising, Stringable, Writable): # Other methods # ===------------------------------------------------------------------=== # - fn internal_representation(self) raises: + fn print_internal_representation(self) raises: """Prints the internal representation details of a BigUInt.""" var string_of_number = self.to_string(line_width=30).split("\n") print("\nInternal Representation Details of BigUInt") @@ -1009,6 +1159,54 @@ struct BigUInt(Absable, IntableRaising, Stringable, Writable): """Returns True if the BigUInt is uninitialized.""" return len(self.words) == 0 + @always_inline + fn is_uint64_overflow(self) -> Bool: + """Returns True if the BigUInt larger than UInt64.MAX.""" + # UInt64.MAX: 18_446_744_073_709_551_615 + # word 0: 709551615 + # word 1: 446744073 + # word 2: 18 + if len(self.words) > 3: + return True + elif len(self.words) == 3: + if self.words[2] > UInt32(18): + return True + elif self.words[2] == 18: + if self.words[1] > UInt32(446744073): + return True + elif self.words[1] == UInt32(446744073): + if self.words[0] > UInt32(709551615): + return True + return False + + @always_inline + fn is_uint128_overflow(self) -> Bool: + """Returns True if the BigUInt larger than UInt128.MAX.""" + # UInt128.MAX: 340_282_366_920_938_463_463_374_607_431_768_211_455 + # word 0: 768211455 + # word 1: 374607431 + # word 2: 938463463 + # word 3: 282366920 + # word 4: 340 + if len(self.words) > 5: + return True + elif len(self.words) == 4: + if self.words[4] > UInt32(340): + return True + elif self.words[4] == UInt32(340): + if self.words[3] > UInt32(282366920): + return True + elif self.words[3] == UInt32(282366920): + if self.words[2] > UInt32(938463463): + return True + elif self.words[2] == UInt32(938463463): + if self.words[1] > UInt32(374607431): + return True + elif self.words[1] == UInt32(374607431): + if self.words[0] > UInt32(768211455): + return True + return False + @always_inline fn ith_digit(self, i: Int) raises -> UInt8: """Returns the ith least significant digit of the BigUInt. diff --git a/src/decimojo/decimal/decimal.mojo b/src/decimojo/decimal/decimal.mojo index fcb93b2..c426fcd 100644 --- a/src/decimojo/decimal/decimal.mojo +++ b/src/decimojo/decimal/decimal.mojo @@ -1574,7 +1574,7 @@ struct Decimal( return result - fn internal_representation(self): + fn print_internal_representation(self): """Prints the internal representation details of a Decimal.""" print("\nInternal Representation Details:") print("--------------------------------") diff --git a/tests/biguint/test_biguint_arithmetics.mojo b/tests/biguint/test_biguint_arithmetics.mojo index a7e5531..34cf50a 100644 --- a/tests/biguint/test_biguint_arithmetics.mojo +++ b/tests/biguint/test_biguint_arithmetics.mojo @@ -28,6 +28,19 @@ fn test_biguint_arithmetics() raises: ) print("BigUInt addition tests passed!") + print("------------------------------------------------------") + print("Testing BigUInt inplace addition...") + test_cases = load_test_cases(toml, "addition_tests") + for test_case in test_cases: + var result = BigUInt(test_case.a) + result += BigUInt(test_case.b) + testing.assert_equal( + lhs=String(result), + rhs=test_case.expected, + msg=test_case.description, + ) + print("BigUInt addition tests passed!") + print("------------------------------------------------------") print("Testing BigUInt subtraction...") test_cases = load_test_cases(toml, "subtraction_tests") @@ -39,19 +52,6 @@ fn test_biguint_arithmetics() raises: msg=test_case.description, ) - # Special case: Test underflow handling - print("Testing underflow behavior (smaller - larger)...") - test_cases = load_test_cases(toml, "subtraction_underflow") - for test_case in test_cases: - try: - var result = BigUInt(test_case.a) - BigUInt(test_case.b) - print( - "Implementation allows underflow, result is: " + String(result) - ) - except: - print("Implementation correctly throws error on underflow") - print("BigUInt subtraction tests passed!") - print("------------------------------------------------------") print("Testing BigUInt multiplication...") @@ -66,6 +66,19 @@ fn test_biguint_arithmetics() raises: ) print("BigUInt multiplication tests passed!") + # Special case: Test underflow handling + print("Testing underflow behavior (smaller - larger)...") + test_cases = load_test_cases(toml, "subtraction_underflow") + for test_case in test_cases: + try: + var result = BigUInt(test_case.a) - BigUInt(test_case.b) + print( + "Implementation allows underflow, result is: " + String(result) + ) + except: + print("Implementation correctly throws error on underflow") + print("BigUInt subtraction tests passed!") + fn main() raises: print("Running BigUInt arithmetic tests")