@@ -149,26 +149,28 @@ impl<'a, C: GeoConfig<Diff>> GeoDiffCount<'a, C> {
149149
150150 /// Builds a sort key from the most significant bits of the masked filter.
151151 ///
152- /// The key packs the largest bucket positions of the masked filter into a single `u64`,
153- /// most significant position first. Because masking distributes over the xor used by
154- /// [`Self::cmp_masked`], comparing two keys numerically yields the same ordering as
155- /// [`Self::cmp_masked`] whenever the keys differ. When two keys are equal the ordering is
156- /// undetermined and the caller must fall back to [`Self::cmp_masked`], e.g.
152+ /// The key packs as many complete bucket positions as fit into a `u64`, most significant
153+ /// position first. Positions use the smallest width that can hold the configuration's
154+ /// bucket positions, as reported by [`GeoConfig::bucket_position_bits`]. Any remaining key bits
155+ /// contain the most-significant portion of the next position. Because masking distributes over
156+ /// the xor used by
157+ /// [`Self::cmp_masked`] and the partial position is an order-preserving prefix, comparing two
158+ /// keys numerically yields the same ordering as [`Self::cmp_masked`] whenever the keys differ.
159+ /// When two keys are equal the ordering is undetermined and the caller must fall back to
160+ /// [`Self::cmp_masked`], e.g.
157161 /// `a_key.cmp(&b_key).then_with(|| a.cmp_masked(b, mask, mask_size))`.
158- ///
159- /// Each position occupies `C::BucketType::BITS` bits, so the key holds
160- /// `64 / C::BucketType::BITS` positions (4 for `u16`, 2 for `u32`).
161162 pub fn masked_sort_key ( & self , mask : u64 , mask_size : usize ) -> u64 {
162163 assert ! (
163164 ( 1 ..u64 :: BITS as usize ) . contains( & mask_size) ,
164165 "mask_size must be in 1..=63 (got {mask_size})"
165166 ) ;
166- let bits = C :: BucketType :: BITS ;
167- debug_assert ! (
168- ( 1 ..=32 ) . contains( & bits ) && u64 :: BITS % bits == 0 ,
169- "sort key packing requires a bucket type of at most 32 bits "
167+ let position_bits = self . config . bucket_position_bits ( ) ;
168+ assert ! (
169+ ( 1 ..=u64 :: BITS ) . contains( & position_bits ) ,
170+ "bucket position width must be in 1..=64 (got {position_bits}) "
170171 ) ;
171- let per_word = ( u64:: BITS / bits) as usize ;
172+ let complete_positions = ( u64:: BITS / position_bits) as usize ;
173+ let remaining_bits = u64:: BITS % position_bits;
172174
173175 // The most significant bits are stored sparsely and sorted from largest to smallest, so we
174176 // can test each of them against the periodic mask directly, avoiding the more expensive
@@ -188,8 +190,21 @@ impl<'a, C: GeoConfig<Diff>> GeoDiffCount<'a, C> {
188190 let mut positions = msb. chain ( lsb) ;
189191
190192 let mut key = 0u64 ;
191- for _ in 0 ..per_word {
192- key = ( key << bits) | positions. next ( ) . unwrap_or ( 0 ) ;
193+ for _ in 0 ..complete_positions {
194+ let position = positions. next ( ) . unwrap_or ( 0 ) ;
195+ debug_assert ! (
196+ position_bits == u64 :: BITS || position < 1u64 << position_bits,
197+ "bucket position {position} exceeds configured width {position_bits}"
198+ ) ;
199+ key = ( key << position_bits) | position;
200+ }
201+ if remaining_bits > 0 {
202+ let position = positions. next ( ) . unwrap_or ( 0 ) ;
203+ debug_assert ! (
204+ position_bits == u64 :: BITS || position < 1u64 << position_bits,
205+ "bucket position {position} exceeds configured width {position_bits}"
206+ ) ;
207+ key = ( key << remaining_bits) | ( position >> ( position_bits - remaining_bits) ) ;
193208 }
194209 key
195210 }
@@ -1062,7 +1077,7 @@ mod tests {
10621077
10631078 #[ test]
10641079 fn test_masked_sort_key ( ) {
1065- let masks : & [ ( u64 , usize ) ] = & [
1080+ let fixed_masks : & [ ( u64 , usize ) ] = & [
10661081 ( 0b1 , 1 ) , // keeps every bit, i.e. a full comparison
10671082 ( 0b10 , 2 ) , // keeps every other bit
10681083 ( 0b110 , 3 ) , // keeps two out of every three bits
@@ -1071,42 +1086,117 @@ mod tests {
10711086 ] ;
10721087
10731088 fn check < C : GeoConfig < Diff > + Default > ( rnd : & mut ChaCha12Rng , masks : & [ ( u64 , usize ) ] ) {
1074- let mut build = || {
1075- let mut f = GeoDiffCount :: < C > :: new ( C :: default ( ) ) ;
1076- for _ in 0 ..1000 {
1077- f. push_hash ( rnd. next_u64 ( ) ) ;
1078- }
1079- f
1080- } ;
1081- let a = build ( ) ;
1082- let b = build ( ) ;
1083- for & ( mask, mask_size) in masks {
1084- let ka = a. masked_sort_key ( mask, mask_size) ;
1085- let kb = b. masked_sort_key ( mask, mask_size) ;
1086- let expected = a. cmp_masked ( & b, mask, mask_size) ;
1087- // The key comparison plus fall back must always agree with the exact comparison.
1088- assert_eq ! (
1089- ka. cmp( & kb) . then_with( || a. cmp_masked( & b, mask, mask_size) ) ,
1090- expected,
1091- "keyed comparison mismatch for mask {mask:b}/{mask_size}" ,
1092- ) ;
1093- // Whenever the keys already differ, they alone must yield the exact order.
1094- if ka != kb {
1095- assert_eq ! (
1096- ka. cmp( & kb) ,
1097- expected,
1098- "key ordering mismatch for mask {mask:b}/{mask_size}" ,
1099- ) ;
1089+ let filters = ( 0 ..8 )
1090+ . map ( |_| {
1091+ let mut f = GeoDiffCount :: < C > :: new ( C :: default ( ) ) ;
1092+ let items = 250 + rnd. next_u64 ( ) as usize % 1500 ;
1093+ for _ in 0 ..items {
1094+ f. push_hash ( rnd. next_u64 ( ) ) ;
1095+ }
1096+ f
1097+ } )
1098+ . collect_vec ( ) ;
1099+
1100+ for a_index in 0 ..filters. len ( ) {
1101+ for b_index in ( a_index + 1 ) ..filters. len ( ) {
1102+ let a = & filters[ a_index] ;
1103+ let b = & filters[ b_index] ;
1104+ for & ( mask, mask_size) in masks {
1105+ let ka = a. masked_sort_key ( mask, mask_size) ;
1106+ let kb = b. masked_sort_key ( mask, mask_size) ;
1107+ let expected = a. cmp_masked ( b, mask, mask_size) ;
1108+ // The key comparison plus fall back must always agree with the exact
1109+ // comparison.
1110+ assert_eq ! (
1111+ ka. cmp( & kb) . then_with( || a. cmp_masked( b, mask, mask_size) ) ,
1112+ expected,
1113+ "keyed comparison mismatch for mask {mask:b}/{mask_size}" ,
1114+ ) ;
1115+ // Whenever the keys already differ, they alone must yield the exact order.
1116+ if ka != kb {
1117+ assert_eq ! (
1118+ ka. cmp( & kb) ,
1119+ expected,
1120+ "key ordering mismatch for mask {mask:b}/{mask_size}" ,
1121+ ) ;
1122+ }
1123+ }
11001124 }
11011125 }
11021126 }
11031127
11041128 prng_test_harness ( 20 , |rnd| {
1105- check :: < GeoDiffConfig7 > ( rnd, masks) ;
1106- check :: < GeoDiffConfig13 > ( rnd, masks) ;
1129+ let mut masks = fixed_masks. to_vec ( ) ;
1130+ for _ in 0 ..12 {
1131+ let mask_size = 1 + rnd. next_u64 ( ) as usize % 63 ;
1132+ let mask = ( rnd. next_u64 ( ) & ( u64:: MAX >> ( 64 - mask_size) ) ) | 1 ;
1133+ masks. push ( ( mask, mask_size) ) ;
1134+ }
1135+ check :: < GeoDiffConfig7 > ( rnd, & masks) ;
1136+ check :: < GeoDiffConfig10 > ( rnd, & masks) ;
1137+ check :: < GeoDiffConfig13 > ( rnd, & masks) ;
11071138 } ) ;
11081139 }
11091140
1141+ #[ test]
1142+ fn test_masked_sort_key_packing_boundaries ( ) {
1143+ fn check < C : GeoConfig < Diff > + Default > (
1144+ expected_bits : u32 ,
1145+ expected_complete : usize ,
1146+ expected_remaining : u32 ,
1147+ ) {
1148+ let config = C :: default ( ) ;
1149+ let max_position = 65 * config. bits_per_level ( ) - 1 ;
1150+ let position_bits = config. bucket_position_bits ( ) ;
1151+ assert_eq ! ( position_bits, expected_bits) ;
1152+ assert_eq ! ( ( u64 :: BITS / position_bits) as usize , expected_complete) ;
1153+ assert_eq ! ( u64 :: BITS % position_bits, expected_remaining) ;
1154+
1155+ let positions = ( 0 ..expected_complete + 2 )
1156+ . map ( |offset| C :: BucketType :: from_usize ( max_position - offset) )
1157+ . collect_vec ( ) ;
1158+ let filter = GeoDiffCount :: < C > :: from_ones ( positions. iter ( ) . copied ( ) ) ;
1159+ let actual = filter. masked_sort_key ( 1 , 1 ) ;
1160+
1161+ let mut expected = 0 ;
1162+ for & position in positions. iter ( ) . take ( expected_complete) {
1163+ expected = ( expected << position_bits) | position. into_usize ( ) as u64 ;
1164+ }
1165+ if expected_remaining > 0 {
1166+ expected = ( expected << expected_remaining)
1167+ | ( positions[ expected_complete] . into_usize ( ) as u64
1168+ >> ( position_bits - expected_remaining) ) ;
1169+ }
1170+ assert_eq ! ( actual, expected) ;
1171+
1172+ let common = positions[ ..expected_complete] . iter ( ) . copied ( ) ;
1173+ let prefix_step = 1usize << ( position_bits - expected_remaining) ;
1174+ let lower = C :: BucketType :: from_usize ( prefix_step - 1 ) ;
1175+ let higher = C :: BucketType :: from_usize ( prefix_step) ;
1176+ let lower_filter = GeoDiffCount :: < C > :: from_ones ( common. clone ( ) . chain ( [ lower] ) ) ;
1177+ let higher_filter = GeoDiffCount :: < C > :: from_ones ( common. chain ( [ higher] ) ) ;
1178+ let lower_key = lower_filter. masked_sort_key ( 1 , 1 ) ;
1179+ let higher_key = higher_filter. masked_sort_key ( 1 , 1 ) ;
1180+ assert_eq ! ( lower_key. cmp( & higher_key) , Ordering :: Less ) ;
1181+ assert_eq ! (
1182+ lower_key. cmp( & higher_key) ,
1183+ lower_filter. cmp_masked( & higher_filter, 1 , 1 )
1184+ ) ;
1185+ }
1186+
1187+ check :: < GeoDiffConfig7 > ( 14 , 4 , 8 ) ;
1188+ check :: < GeoDiffConfig10 > ( 17 , 3 , 13 ) ;
1189+ check :: < GeoDiffConfig13 > ( 20 , 3 , 4 ) ;
1190+
1191+ let empty = GeoDiffCount10 :: default ( ) ;
1192+ let bucket_zero = GeoDiffCount10 :: from_ones ( [ 0 ] ) ;
1193+ assert_eq ! (
1194+ empty. masked_sort_key( 1 , 1 ) ,
1195+ bucket_zero. masked_sort_key( 1 , 1 )
1196+ ) ;
1197+ assert_eq ! ( empty. cmp_masked( & bucket_zero, 1 , 1 ) , Ordering :: Less ) ;
1198+ }
1199+
11101200 #[ test]
11111201 fn test_bit_chunks ( ) {
11121202 prng_test_harness ( 100 , |rnd| {
0 commit comments