diff --git a/bigint/bigint_default.mbt b/bigint/bigint_default.mbt index 0f9b5f5e6..1bb1b0cd5 100644 --- a/bigint/bigint_default.mbt +++ b/bigint/bigint_default.mbt @@ -1109,29 +1109,61 @@ fn BigInt::to_string_radix(self : BigInt, radix : Int) -> String { match pow2_shift(radix) { Some(shift) => self.to_string_radix_pow2(shift) None => { - let is_negative = self.sign == Negative - let base = BigInt::from_int(radix) - let value = if is_negative { -self } else { self } - let digits = [] - for v = value { - if v > zero { - let (q, r) = BigInt::grade_school_div(v, base) - digits.push(char_from_digit(r.to_int())) - continue q - } else { - break + // Same limb-by-limb conversion as the radix-10 path in to_string: + // convert to base chunk = radix^chunk_len using only Int64 division, + // then emit chunk_len digits per slot. This replaces one full BigInt + // division per output digit with one Int64 division per digit. + let radix64 = radix.to_int64() + // Largest radix^chunk_len such that (slot << RADIX_BIT_LEN) | limb + // still fits in Int64 (slots stay below 2^(63 - RADIX_BIT_LEN)). + let chunk_limit = 0x7FFF_FFFF_FFFF_FFFFL >> RADIX_BIT_LEN + let mut chunk = radix64 + let mut chunk_len = 1 + while chunk <= chunk_limit / radix64 { + chunk = chunk * radix64 + chunk_len += 1 + } + // Digits in radix >= 3 never exceed the bit count, so this bounds the + // number of slots. + let slots = self.len * RADIX_BIT_LEN / chunk_len + 2 + let v = Array::make(slots, 0L) + let mut v_idx = 0 + for i in self.len>..0 { + let mut x = self.limbs[i].to_int64() + for j in 0.. 0L { + v[v_idx] = x % chunk + v_idx += 1 + x /= chunk } } - let builder = StringBuilder( - size_hint=digits.length() + (if is_negative { 1 } else { 0 }), - ) - if is_negative { - builder.write_char('-') + let cap = v_idx * chunk_len + 1 // +1 for an optional sign + let chars = FixedArray::make(cap, '0') + let mut pos = cap + // Lower slots each contribute exactly chunk_len digits, zero-padded. + for i in 0..<(v_idx - 1) { + let mut x = v[i] + for _ in 0.. 0L; x = x / radix64 { + pos -= 1 + chars[pos] = char_from_digit((x % radix64).to_int()) } - for i in digits.length()>..0 { - builder.write_char(digits[i]) + if self.sign == Negative { + pos -= 1 + chars[pos] = '-' } - builder.to_string() + String::from_array(chars[pos:]) } } } diff --git a/bigint/bigint_test.mbt b/bigint/bigint_test.mbt index 1ad0b965a..5365ef069 100644 --- a/bigint/bigint_test.mbt +++ b/bigint/bigint_test.mbt @@ -1393,3 +1393,31 @@ test "BigInt modpow negative base normalization" { // (-3)^3 = -27, -27 mod 10 = 3 (canonical non-negative) inspect((-3N).pow(3N, modulus=10N), content="3") } + +///| +test "to_string non-power-of-two radixes round-trip and match known values" { + // known small values + inspect(255N.to_string(radix=3), content="100110") + inspect((-255N).to_string(radix=3), content="-100110") + inspect(35N.to_string(radix=36), content="z") + inspect(36N.to_string(radix=36), content="10") + inspect(6N.to_string(radix=6), content="10") + // round-trip large values through every non-power-of-two radix + let big = (@bigint.BigInt::from_string("123456789123456789") << 700) + + @bigint.BigInt::from_string("987654321987654321") + for radix in [3, 5, 6, 7, 11, 12, 15, 20, 33, 36] { + let s = big.to_string(radix~) + assert_eq(@bigint.BigInt::from_string(s, radix~), big) + let s_neg = (-big).to_string(radix~) + assert_eq(@bigint.BigInt::from_string(s_neg, radix~), -big) + } + // boundary around a chunk: radix 3 uses 19-digit chunks; exercise values + // spanning one and several slots + for e in [1, 18, 19, 20, 37, 38, 39] { + let p = @bigint.BigInt::from_string("3").pow( + @bigint.BigInt::from_string(e.to_string()), + ) + assert_eq(@bigint.BigInt::from_string(p.to_string(radix=3), radix=3), p) + inspect(p.to_string(radix=3).length(), content=(e + 1).to_string()) + } +} diff --git a/bigint/to_string_radix_bench_test.mbt b/bigint/to_string_radix_bench_test.mbt new file mode 100644 index 000000000..7c0f127fc --- /dev/null +++ b/bigint/to_string_radix_bench_test.mbt @@ -0,0 +1,38 @@ +// Copyright 2026 International Digital Economy Academy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +///| +fn radix_bench_value() -> @bigint.BigInt { + // ~4000 bits + (@bigint.BigInt::from_string("7") << 4000) + + @bigint.BigInt::from_string("12345678901234567890") +} + +///| +test "bench bigint to_string radix=7 (4000 bits)" (it : @bench.T) { + let v = radix_bench_value() + it.bench(fn() { it.keep(v.to_string(radix=7)) }) +} + +///| +test "bench bigint to_string radix=36 (4000 bits)" (it : @bench.T) { + let v = radix_bench_value() + it.bench(fn() { it.keep(v.to_string(radix=36)) }) +} + +///| +test "bench bigint to_string radix=10 reference (4000 bits)" (it : @bench.T) { + let v = radix_bench_value() + it.bench(fn() { it.keep(v.to_string()) }) +}