Skip to content

Commit 374c86b

Browse files
CAIMEOXbobzhang
authored andcommitted
feat: better arbitrary for BigInt
1 parent 63a0e05 commit 374c86b

4 files changed

Lines changed: 136 additions & 24 deletions

File tree

quickcheck/README.mbt.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ test "builtin types" {
240240
#| 8766027650639656979,
241241
#| 0.23986786603927612,
242242
#| 0.7917029935679342,
243-
#| 0,
243+
#| -2,
244244
#|)
245245
),
246246
)
@@ -262,11 +262,16 @@ full-width random bit patterns, common small values, values next to powers of
262262
two and ten, and type extrema. This keeps the entire scalar domain reachable
263263
while regularly exercising overflow, bit-width, and formatting boundaries.
264264
`Byte` remains uniformly distributed across all 256 bit patterns so `Bytes`
265-
and other byte-oriented workloads retain broad payload coverage. Fixed-width
266-
integer shrinkers try a midpoint first, then progressively finer candidates
267-
approaching the original value, with zero as the final fallback. With the
268-
greedy shrink driver, large failures still converge without linearly walking
269-
the numeric range while avoiding a likely rejected zero at every level.
265+
and other byte-oriented workloads retain broad payload coverage. `BigInt`
266+
uses a separate portfolio: 35% arbitrary bit patterns, 25% canonical small
267+
values, 25% neighbors of common powers of two, and 15% neighbors of common
268+
powers of ten. Its random branch grows with the size hint up to 32 64-bit
269+
chunks, while boundary branches regularly cover important widths regardless
270+
of size. Fixed-width integer shrinkers try a midpoint first, then progressively
271+
finer candidates approaching the original value, with zero as the final
272+
fallback. With the greedy shrink driver, large failures still converge without
273+
linearly walking the numeric range while avoiding a likely rejected zero at
274+
every level.
270275

271276
## Custom Types
272277

quickcheck/arbitrary.mbt

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,15 +152,6 @@ pub impl[X : Arbitrary] Arbitrary for Array[X] with fn arbitrary(size, rs) {
152152
Array::makei(len, x => X::arbitrary(x, rs))
153153
}
154154

155-
///|
156-
pub impl Arbitrary for @bigint.BigInt with fn arbitrary(size, rs) {
157-
if size == 0 {
158-
0
159-
} else {
160-
rs.next_int64() |> @bigint.BigInt::from_int64
161-
}
162-
}
163-
164155
///|
165156
pub impl[X : Arbitrary] Arbitrary for @ref.Ref[X] with fn arbitrary(size, rs) {
166157
@ref.new(X::arbitrary(size, rs))

quickcheck/arbitrary_bigint.mbt

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2026 International Digital Economy Academy
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
///|
16+
fn[T] @splitmix.RandomState::pick(
17+
self : @splitmix.RandomState,
18+
values : ReadOnlyArray[T],
19+
) -> T {
20+
let index = self.next_uint(limit=values.length().reinterpret_as_uint())
21+
values[index.reinterpret_as_int()]
22+
}
23+
24+
///|
25+
fn with_random_sign(
26+
magnitude : @bigint.BigInt,
27+
rs : @splitmix.RandomState,
28+
) -> @bigint.BigInt {
29+
if magnitude.is_zero() || rs.next_uint(limit=2U) == 0U {
30+
magnitude
31+
} else {
32+
-magnitude
33+
}
34+
}
35+
36+
///|
37+
fn around(base : @bigint.BigInt, rs : @splitmix.RandomState) -> @bigint.BigInt {
38+
with_random_sign(base + rs.pick([-1N, 0N, 1N]), rs)
39+
}
40+
41+
///|
42+
fn random_bigint(size : Int, rs : @splitmix.RandomState) -> @bigint.BigInt {
43+
let chunk_limit = size.clamp(min=0, max=31) + 1
44+
let chunk_count = for count in 1..<chunk_limit {
45+
guard rs.next_uint(limit=2U) == 0U else { break count }
46+
} nobreak {
47+
chunk_limit
48+
}
49+
let bytes = Bytes::makei(chunk_count * 8, _ => {
50+
rs.next_uint(limit=256U).to_byte()
51+
})
52+
with_random_sign(@bigint.BigInt::from_octets(bytes), rs)
53+
}
54+
55+
///|
56+
/// 35% arbitrary multi-word bit patterns, 25% canonical small values,
57+
/// 25% values next to common powers of two, and 15% values next to common
58+
/// powers of ten. `size` caps the random branch at one to 32 64-bit words;
59+
/// boundary branches deliberately exercise important widths independently.
60+
pub impl Arbitrary for @bigint.BigInt with fn arbitrary(size, rs) {
61+
match rs.next_uint(limit=100U) {
62+
0..<35 => random_bigint(size, rs)
63+
35..<60 => rs.pick([0N, 1N, -1N, 2N, -2N, 10N, -10N])
64+
60..<85 =>
65+
around(
66+
1N <<
67+
rs.pick([0, 1, 7, 8, 15, 16, 31, 32, 52, 53, 63, 64, 127, 128, 255, 256]),
68+
rs,
69+
)
70+
_ => {
71+
let exponent = rs.pick([1, 2, 3, 6, 9, 18, 19, 38, 76, 100])
72+
around(
73+
(0 : Int).until(exponent).fold(init=1N, (power, _) => power * 10N),
74+
rs,
75+
)
76+
}
77+
}
78+
}

quickcheck/arbitrary_type_test.mbt

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,66 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
///|
16+
fn is_power_of_two_bigint(value : @bigint.BigInt) -> Bool {
17+
value > 0N && (value & (value - 1N)) == 0N
18+
}
19+
20+
///|
21+
fn is_noncanonical_binary_boundary_bigint(value : @bigint.BigInt) -> Bool {
22+
let magnitude = if value < 0N { -value } else { value }
23+
magnitude > 10N &&
24+
[-1N, 0N, 1N].any(offset => is_power_of_two_bigint(magnitude + offset))
25+
}
26+
1527
///|
1628
test "Arbitrary for BigInt" {
1729
let samples : Array[@bigint.BigInt] = @quickcheck.samples(10)
1830
debug_inspect(
1931
samples,
2032
content=(
2133
#|[
34+
#| 8957778123143436869,
35+
#| 224568372937935354752176555220923494950,
36+
#| 4294967296,
2237
#| 0,
23-
#| 2236702871533800590,
24-
#| 4899806414470401660,
25-
#| -5076203457455444144,
26-
#| -3206590268553584789,
27-
#| -8738668918601119554,
28-
#| -5822442221117384418,
29-
#| 4293417177517987002,
30-
#| -174397816515637542,
31-
#| -7574984457386592187,
38+
#| 9007199254740991,
39+
#| -330974765789023695575794723562474317865,
40+
#| 1321161328954558061203775241612665678662169762445949217122616206433285005712430002148382207261259,
41+
#| -14497849068316585071,
42+
#| 1000,
43+
#| -2,
3244
#|]
3345
),
3446
)
3547
}
3648

49+
///|
50+
test "Arbitrary for BigInt favors boundaries across multiple limbs" {
51+
let samples : Array[@bigint.BigInt] = @quickcheck.samples(256)
52+
let canonical : Array[@bigint.BigInt] = [0N, 1N, -1N, 2N, -2N, 10N, -10N]
53+
let count : ((@bigint.BigInt) -> Bool) -> Int = predicate => {
54+
samples.iter().filter(predicate).count()
55+
}
56+
let is_wide : (@bigint.BigInt) -> Bool = value => value.bit_length() > 64
57+
let canonical_count = count(value => canonical.contains(value))
58+
let binary_boundary_count = count(is_noncanonical_binary_boundary_bigint)
59+
let wide_count = count(is_wide)
60+
let positive_wide_count = count(value => value > 0N && is_wide(value))
61+
let negative_wide_count = count(value => value < 0N && is_wide(value))
62+
let max_bit_length = samples.fold(init=0, (maximum, value) => {
63+
maximum.max(value.bit_length())
64+
})
65+
// canonical, binary boundary, wide, positive wide, negative wide, maximum bits
66+
debug_inspect(
67+
(
68+
canonical_count, binary_boundary_count, wide_count, positive_wide_count, negative_wide_count,
69+
max_bit_length,
70+
),
71+
content="(73, 56, 64, 34, 30, 445)",
72+
)
73+
}
74+
3775
///|
3876
test "Arbitrary for builtin Map" {
3977
let samples : Array[Map[Int, Bool]] = @quickcheck.samples(20)

0 commit comments

Comments
 (0)