3
3
use crate :: mem:: { self , ManuallyDrop , MaybeUninit } ;
4
4
use crate :: slice:: sort:: shared:: FreezeMarker ;
5
5
use crate :: { intrinsics, ptr, slice} ;
6
+ use safety:: { ensures, requires} ;
7
+
8
+ #[ cfg( kani) ]
9
+ use crate :: { kani, ub_checks} ;
6
10
7
11
// It's important to differentiate between SMALL_SORT_THRESHOLD performance for
8
12
// small slices and small-sort performance sorting small sub-slices as part of
@@ -62,6 +66,26 @@ impl<T: FreezeMarker> StableSmallSortTypeImpl for T {
62
66
}
63
67
}
64
68
69
+ // This wrapper contract function exists because;
70
+ // - kani contract attribute macros doesnt't work with `default fn`
71
+ // - we cannot specify the trait member function with `proof_for_contract`
72
+ #[ cfg( kani) ]
73
+ #[ kani:: modifies( v) ]
74
+ #[ ensures( |_| {
75
+ let mut is_less = is_less. clone( ) ;
76
+ v. is_sorted_by( |a, b| !is_less( b, a) )
77
+ } ) ]
78
+ pub ( crate ) fn _stable_small_sort_type_impl_small_sort < T , F > (
79
+ v : & mut [ T ] ,
80
+ scratch : & mut [ MaybeUninit < T > ] ,
81
+ is_less : & mut F ,
82
+ )
83
+ where
84
+ F : FnMut ( & T , & T ) -> bool + Clone ,
85
+ {
86
+ <T as StableSmallSortTypeImpl >:: small_sort ( v, scratch, is_less) ;
87
+ }
88
+
65
89
/// Using a trait allows us to specialize on `Freeze` which in turn allows us to make safe
66
90
/// abstractions.
67
91
pub ( crate ) trait UnstableSmallSortTypeImpl : Sized {
@@ -102,6 +126,25 @@ impl<T: FreezeMarker> UnstableSmallSortTypeImpl for T {
102
126
}
103
127
}
104
128
129
+ // This wrapper contract function exists because;
130
+ // - kani contract attribute macros doesnt't work with `default fn`
131
+ // - we cannot specify the trait member function with `proof_for_contract`
132
+ #[ cfg( kani) ]
133
+ #[ kani:: modifies( v) ]
134
+ #[ ensures( |_| {
135
+ let mut is_less = is_less. clone( ) ;
136
+ v. is_sorted_by( |a, b| !is_less( b, a) )
137
+ } ) ]
138
+ pub ( crate ) fn _unstable_small_sort_type_impl_small_sort < T , F > (
139
+ v : & mut [ T ] ,
140
+ is_less : & mut F ,
141
+ )
142
+ where
143
+ F : FnMut ( & T , & T ) -> bool + Clone ,
144
+ {
145
+ <T as UnstableSmallSortTypeImpl >:: small_sort ( v, is_less) ;
146
+ }
147
+
105
148
/// FIXME(const_trait_impl) use original ipnsort approach with choose_unstable_small_sort,
106
149
/// as found here <https://github.com/Voultapher/sort-research-rs/blob/438fad5d0495f65d4b72aa87f0b62fc96611dff3/ipnsort/src/smallsort.rs#L83C10-L83C36>.
107
150
pub ( crate ) trait UnstableSmallSortFreezeTypeImpl : Sized + FreezeMarker {
@@ -170,6 +213,26 @@ impl<T: FreezeMarker + CopyMarker> UnstableSmallSortFreezeTypeImpl for T {
170
213
}
171
214
}
172
215
216
+ // This wrapper contract function exists because;
217
+ // - kani contract attribute macros doesnt't work with `default fn`
218
+ // - we cannot specify the trait member function with `proof_for_contract`
219
+ #[ cfg( kani) ]
220
+ #[ kani:: modifies( v) ]
221
+ #[ ensures( |_| {
222
+ let mut is_less = is_less. clone( ) ;
223
+ v. is_sorted_by( |a, b| !is_less( b, a) )
224
+ } ) ]
225
+ pub ( crate ) fn _unstable_small_sort_freeze_type_impl_small_sort < T , F > (
226
+ v : & mut [ T ] ,
227
+ is_less : & mut F ,
228
+ )
229
+ where
230
+ T : FreezeMarker + CopyMarker ,
231
+ F : FnMut ( & T , & T ) -> bool + Clone ,
232
+ {
233
+ <T as UnstableSmallSortFreezeTypeImpl >:: small_sort ( v, is_less) ;
234
+ }
235
+
173
236
/// Optimal number of comparisons, and good perf.
174
237
const SMALL_SORT_FALLBACK_THRESHOLD : usize = 16 ;
175
238
@@ -539,6 +602,27 @@ where
539
602
///
540
603
/// # Safety
541
604
/// begin < tail and p must be valid and initialized for all begin <= p <= tail.
605
+ #[ cfg_attr(
606
+ kani,
607
+ kani:: modifies(
608
+ ptr:: slice_from_raw_parts_mut( begin, tail. addr( ) . saturating_sub( tail. addr( ) ) ) )
609
+ )
610
+ ]
611
+ #[ requires( begin. addr( ) < tail. addr( ) && {
612
+ let len = tail. addr( ) - begin. addr( ) ;
613
+ let is_less: & mut F = unsafe { mem:: transmute( & is_less) } ;
614
+ ( 0 ..=len) . into_iter( ) . all( |i| {
615
+ let p = begin. add( i) ;
616
+ ub_checks:: can_dereference( p as * const _) &&
617
+ ub_checks:: can_write( p) &&
618
+ ub_checks:: same_allocation( begin as * const _, p as * const _)
619
+ } ) && ( 0 ..( len - 1 ) ) . into_iter( ) . all( |i| !is_less( & * begin. add( i + 1 ) , & * begin. add( i) ) )
620
+ } ) ]
621
+ #[ ensures( |_| {
622
+ let len = tail. addr( ) - begin. addr( ) ;
623
+ let is_less: & mut F = unsafe { mem:: transmute( & is_less) } ;
624
+ ( 0 ..len) . into_iter( ) . all( |i| !is_less( & * begin. add( i + 1 ) , & * begin. add( i) ) )
625
+ } ) ]
542
626
unsafe fn insert_tail < T , F : FnMut ( & T , & T ) -> bool > ( begin : * mut T , tail : * mut T , is_less : & mut F ) {
543
627
// SAFETY: see individual comments.
544
628
unsafe {
@@ -556,7 +640,13 @@ unsafe fn insert_tail<T, F: FnMut(&T, &T) -> bool>(begin: *mut T, tail: *mut T,
556
640
let tmp = ManuallyDrop :: new ( tail. read ( ) ) ;
557
641
let mut gap_guard = CopyOnDrop { src : & * tmp, dst : tail, len : 1 } ;
558
642
559
- loop {
643
+ #[ safety:: loop_invariant(
644
+ sift. addr( ) >= begin. addr( ) && sift. addr( ) < tail. addr( )
645
+ ) ]
646
+ // FIXME: This was `loop` but kani's loop contract doesn't support `loop`.
647
+ // Once it is supported, replace `while true` with the original `loop`
648
+ #[ allow( while_true) ]
649
+ while true {
560
650
// SAFETY: we move sift into the gap (which is valid), and point the
561
651
// gap guard destination at sift, ensuring that if a panic occurs the
562
652
// gap is once again filled.
@@ -577,6 +667,15 @@ unsafe fn insert_tail<T, F: FnMut(&T, &T) -> bool>(begin: *mut T, tail: *mut T,
577
667
}
578
668
579
669
/// Sort `v` assuming `v[..offset]` is already sorted.
670
+ #[ cfg_attr( kani, kani:: modifies( v) ) ]
671
+ #[ requires( offset != 0 && offset <= v. len( ) && {
672
+ let is_less: & mut F = unsafe { mem:: transmute( & is_less) } ;
673
+ v[ ..offset] . is_sorted_by( |a, b| !is_less( b, a) )
674
+ } ) ]
675
+ #[ ensures( |_| {
676
+ let is_less: & mut F = unsafe { mem:: transmute( & is_less) } ;
677
+ v. is_sorted_by( |a, b| !is_less( b, a) )
678
+ } ) ]
580
679
pub fn insertion_sort_shift_left < T , F : FnMut ( & T , & T ) -> bool > (
581
680
v : & mut [ T ] ,
582
681
offset : usize ,
@@ -597,6 +696,14 @@ pub fn insertion_sort_shift_left<T, F: FnMut(&T, &T) -> bool>(
597
696
let v_end = v_base. add ( len) ;
598
697
let mut tail = v_base. add ( offset) ;
599
698
while tail != v_end {
699
+ // FIXME: This should be loop contract but sadly, making this into
700
+ // loop invariant causes OOM
701
+ #[ cfg( kani) ]
702
+ kani:: assert (
703
+ tail. addr ( ) > v_base. addr ( ) && tail. addr ( ) <= v_end. addr ( ) ,
704
+ "loop invariants" ,
705
+ ) ;
706
+
600
707
// SAFETY: v_base and tail are both valid pointers to elements, and
601
708
// v_base < tail since we checked offset != 0.
602
709
insert_tail ( v_base, tail, is_less) ;
@@ -609,6 +716,28 @@ pub fn insertion_sort_shift_left<T, F: FnMut(&T, &T) -> bool>(
609
716
610
717
/// SAFETY: The caller MUST guarantee that `v_base` is valid for 4 reads and
611
718
/// `dst` is valid for 4 writes. The result will be stored in `dst[0..4]`.
719
+ #[ cfg_attr( kani, kani:: modifies( dst, dst. add( 1 ) , dst. add( 2 ) , dst. add( 3 ) ) ) ]
720
+ #[ requires(
721
+ ( 0 ..4 ) . into_iter( ) . all( |i| {
722
+ let p = v_base. add( i) ;
723
+ ub_checks:: can_dereference( p) &&
724
+ ub_checks:: same_allocation( v_base, p)
725
+ } )
726
+ ) ]
727
+ #[ requires(
728
+ ( 0 ..4 ) . into_iter( ) . all( |i| {
729
+ let p = dst. add( i) ;
730
+ ub_checks:: can_write( p) &&
731
+ ub_checks:: same_allocation( dst, p)
732
+ } )
733
+ ) ]
734
+ #[ ensures( |_| {
735
+ let is_less: & mut F = unsafe { mem:: transmute( & is_less) } ;
736
+ ( 0 ..3 ) . into_iter( ) . all( |i| !is_less(
737
+ & * dst. add( i + 1 ) ,
738
+ & * dst. add( i) ,
739
+ ) )
740
+ } ) ]
612
741
pub unsafe fn sort4_stable < T , F : FnMut ( & T , & T ) -> bool > (
613
742
v_base : * const T ,
614
743
dst : * mut T ,
@@ -870,3 +999,136 @@ pub(crate) const fn has_efficient_in_place_swap<T>() -> bool {
870
999
// Heuristic that holds true on all tested 64-bit capable architectures.
871
1000
mem:: size_of :: < T > ( ) <= 8 // mem::size_of::<u64>()
872
1001
}
1002
+
1003
+ #[ cfg( kani) ]
1004
+ #[ unstable( feature = "kani" , issue = "none" ) ]
1005
+ mod verify {
1006
+ use super :: * ;
1007
+
1008
+ // The maximum length of the slice that `insertion_sort_shift_left`
1009
+ // is called upon.
1010
+ // The value is from the following line;
1011
+ // https://github.com/model-checking/verify-rust-std/blob/1a38674ad6753e3a78e0181d1fe613f3b25ebacd/library/core/src/slice/sort/shared/smallsort.rs#L330
1012
+ const INSERTION_SORT_MAX_LEN : usize = 17 ;
1013
+
1014
+ #[ kani:: proof]
1015
+ pub fn check_swap_if_less ( ) {
1016
+ let mut array: [ u8 ; SMALL_SORT_GENERAL_THRESHOLD ] = kani:: any ( ) ;
1017
+ let a_pos = kani:: any_where ( |x : & usize | * x < SMALL_SORT_GENERAL_THRESHOLD ) ;
1018
+ let b_pos = kani:: any_where ( |x : & usize | * x < SMALL_SORT_GENERAL_THRESHOLD ) ;
1019
+ let mut is_less = |x : & u8 , y : & u8 | x < y;
1020
+ let expected = {
1021
+ let mut array = array. clone ( ) ;
1022
+ let a: u8 = array[ a_pos] ;
1023
+ let b: u8 = array[ b_pos] ;
1024
+ if is_less ( & b, & a) {
1025
+ array[ a_pos] = b;
1026
+ array[ b_pos] = a;
1027
+ }
1028
+ array
1029
+ } ;
1030
+ unsafe {
1031
+ swap_if_less ( array. as_mut_ptr ( ) , a_pos, b_pos, & mut is_less) ;
1032
+ }
1033
+ kani:: assert (
1034
+ array == expected,
1035
+ "swapped slice is different from the expectation"
1036
+ ) ;
1037
+ }
1038
+
1039
+ #[ kani:: proof_for_contract( insert_tail) ]
1040
+ #[ kani:: unwind( 17 ) ]
1041
+ pub fn check_insert_tail ( ) {
1042
+ let mut array: [ u8 ; INSERTION_SORT_MAX_LEN ] = kani:: any ( ) ;
1043
+ let tail = kani:: any_where ( |x : & usize | * x < INSERTION_SORT_MAX_LEN ) ;
1044
+ let mut is_less = |x : & u8 , y : & u8 | x < y;
1045
+ unsafe {
1046
+ let begin = array. as_mut_ptr ( ) ;
1047
+ let tail = begin. add ( tail) ;
1048
+ insert_tail ( begin, tail, & mut is_less) ;
1049
+ }
1050
+ }
1051
+
1052
+ // FIXME: Ideally, this should be `proof_for_contract(insertion_sort_shift_left)`,
1053
+ // but this failes with OOM due to `proof_for_contract`'s perf issue.
1054
+ //
1055
+ // See https://github.com/model-checking/kani/issues/3797
1056
+ #[ kani:: proof]
1057
+ #[ kani:: stub_verified( insert_tail) ]
1058
+ #[ kani:: unwind( 17 ) ]
1059
+ pub fn check_insertion_sort_shift_left ( ) {
1060
+ let slice_len = kani:: any_where ( |x : & usize | {
1061
+ * x != 0 && * x <= INSERTION_SORT_MAX_LEN
1062
+ } ) ;
1063
+ let offset = kani:: any_where ( |x : & usize | * x != 0 && * x <= slice_len) ;
1064
+ let mut is_less = |x : & u8 , y : & u8 | x < y;
1065
+ let mut array = kani:: any_where ( |x : & [ u8 ; INSERTION_SORT_MAX_LEN ] | {
1066
+ x[ ..offset] . is_sorted_by ( |a, b| !is_less ( b, a) )
1067
+ } ) ;
1068
+ insertion_sort_shift_left ( & mut array[ ..slice_len] , offset, & mut is_less) ;
1069
+ kani:: assert (
1070
+ array[ ..slice_len] . is_sorted_by ( |a, b| !is_less ( b, a) ) ,
1071
+ "slice is not sorted" ,
1072
+ ) ;
1073
+ }
1074
+
1075
+ #[ kani:: proof_for_contract( sort4_stable) ]
1076
+ pub fn check_sort4_stable ( ) {
1077
+ let src: [ u8 ; 4 ] = kani:: any ( ) ;
1078
+ let mut dst = MaybeUninit :: < [ u8 ; 4 ] > :: uninit ( ) ;
1079
+ let mut is_less = |x : & u8 , y : & u8 | x < y;
1080
+ unsafe {
1081
+ sort4_stable ( src. as_ptr ( ) , dst. as_mut_ptr ( ) as * mut _ , & mut is_less) ;
1082
+ }
1083
+ }
1084
+
1085
+ #[ kani:: proof]
1086
+ pub fn check_sort4_stable_stability ( ) {
1087
+ let src: [ ( u8 , u8 ) ; 4 ] = [
1088
+ ( kani:: any ( ) , 0 ) ,
1089
+ ( kani:: any ( ) , 1 ) ,
1090
+ ( kani:: any ( ) , 2 ) ,
1091
+ ( kani:: any ( ) , 3 ) ,
1092
+ ] ;
1093
+ let mut dst = MaybeUninit :: < [ ( u8 , u8 ) ; 4 ] > :: uninit ( ) ;
1094
+ let mut is_less = |x : & ( u8 , u8 ) , y : & ( u8 , u8 ) | x. 0 < y. 0 ;
1095
+ unsafe {
1096
+ sort4_stable ( src. as_ptr ( ) , dst. as_mut_ptr ( ) as * mut _ , & mut is_less) ;
1097
+ }
1098
+ let dst = unsafe { dst. assume_init ( ) } ;
1099
+ let mut is_stably_less = |x : & ( u8 , u8 ) , y : & ( u8 , u8 ) | {
1100
+ if x. 0 == y. 0 {
1101
+ x. 1 < y. 1
1102
+ } else {
1103
+ x. 0 < y. 0
1104
+ }
1105
+ } ;
1106
+ kani:: assert (
1107
+ dst. is_sorted_by ( |a, b| !is_stably_less ( b, a) ) ,
1108
+ "slice is not stably sorted" ,
1109
+ ) ;
1110
+ }
1111
+
1112
+ #[ kani:: proof]
1113
+ pub fn check_has_efficient_in_place_swap ( ) {
1114
+ // There aren't much to verify as the function just calls `mem::size_of`.
1115
+ // So, just brought some tests written by the author of smallsort,
1116
+ // from Voultapher/sort-research-rs
1117
+ //
1118
+ // https://github.com/Voultapher/sort-research-rs/blob/c0cb46214a8d6e10ae5f4e0c363c2dbcbf71966f/ipnsort/src/smallsort.rs#L758-L771
1119
+ assert ! ( has_efficient_in_place_swap:: <i32 >( ) ) ;
1120
+ assert ! ( has_efficient_in_place_swap:: <u64 >( ) ) ;
1121
+ assert ! ( !has_efficient_in_place_swap:: <u128 >( ) ) ;
1122
+ }
1123
+
1124
+ #[ kani:: proof_for_contract( _stable_small_sort_type_impl_small_sort) ]
1125
+ #[ kani:: stub_verified( insertion_sort_shift_left) ]
1126
+ pub fn check_stable_small_sort_type_impl_small_sort ( ) {
1127
+ let mut array: [ u8 ; SMALL_SORT_FALLBACK_THRESHOLD ] = kani:: any ( ) ;
1128
+ let len = kani:: any_where ( |x : & usize | * x <= SMALL_SORT_FALLBACK_THRESHOLD ) ;
1129
+ let mut scratch: [ MaybeUninit < u8 > ; SMALL_SORT_GENERAL_SCRATCH_LEN ]
1130
+ = [ const { MaybeUninit :: uninit ( ) } ; SMALL_SORT_GENERAL_SCRATCH_LEN ] ;
1131
+ let mut is_less = |x : & u8 , y : & u8 | x < y;
1132
+ _stable_small_sort_type_impl_small_sort ( & mut array[ ..len] , & mut scratch, & mut is_less) ;
1133
+ }
1134
+ }
0 commit comments