rust icon indicating copy to clipboard operation
rust copied to clipboard

Implement partial_sort_unstable for slice

Open tisonkun opened this issue 3 weeks ago • 18 comments

This refers to https://github.com/rust-lang/rust/issues/149046.

tisonkun avatar Nov 25 '25 15:11 tisonkun

r? @scottmcm

rustbot has assigned @scottmcm. They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

rustbot avatar Nov 25 '25 15:11 rustbot

The job aarch64-gnu-llvm-20-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
[RUSTC-TIMING] build_script_build test:false 0.311
error[E0618]: expected function, found `R`
    --> library/core/src/slice/mod.rs:3267:36
     |
3261 |       pub fn partial_sort_unstable_by<F, R>(&mut self, range: R, mut compare: F)
     |                                                        ----- `range` has type `R`
...
3267 |           let Range { start, end } = range(range, ..len);
     |                                      ^^^^^--------------
     |                                      |
     |                                      call expression requires function
     |
    ::: library/core/src/slice/index.rs:908:1
     |
 908 | / pub const fn range<R>(range: R, bounds: ops::RangeTo<usize>) -> ops::Range<usize>
 909 | | where
 910 | |     R: [const] ops::RangeBounds<usize> + [const] Destruct,
     | |__________________________________________________________- this function of the same name is available here, but it's shadowed by the local binding

For more information about this error, try `rustc --explain E0618`.
[RUSTC-TIMING] core test:false 23.486
error: could not compile `core` (lib) due to 1 previous error

rust-log-analyzer avatar Nov 25 '25 15:11 rust-log-analyzer

The job x86_64-gnu-gcc failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
[RUSTC-TIMING] profiler_builtins test:false 0.024
error[E0382]: use of moved value: `is_less`
    --> library/core/src/slice/mod.rs:3299:76
     |
3291 |         let (_, _, slice) = sort::select::partition_at_index(self, index, is_less);
     |                                                                           ------- value moved here
...
3299 |         let (slice, _, _) = sort::select::partition_at_index(slice, index, is_less);
     |                                                                            ^^^^^^^ value used here after move
     |
note: closure cannot be moved more than once as it is not `Copy` due to moving the variable `compare` out of its environment
    --> library/core/src/slice/mod.rs:3288:38
     |
3288 |         let is_less = |a: &T, b: &T| compare(a, b) == Less;
     |                                      ^^^^^^^
help: consider mutably borrowing `is_less`
     |
3291 |         let (_, _, slice) = sort::select::partition_at_index(self, index, &mut is_less);
     |                                                                           ++++

error[E0382]: borrow of moved value: `is_less`
    --> library/core/src/slice/mod.rs:3300:37
     |
3299 |         let (slice, _, _) = sort::select::partition_at_index(slice, index, is_less);
     |                                                                            ------- value moved here
3300 |         sort::unstable::sort(slice, &mut is_less);
     |                                     ^^^^^^^^^^^^ value borrowed here after move
     |
note: closure cannot be moved more than once as it is not `Copy` due to moving the variable `compare` out of its environment
    --> library/core/src/slice/mod.rs:3288:38
     |
3288 |         let is_less = |a: &T, b: &T| compare(a, b) == Less;
     |                                      ^^^^^^^
help: consider mutably borrowing `is_less`
     |
3299 |         let (slice, _, _) = sort::select::partition_at_index(slice, index, &mut is_less);
     |                                                                            ++++

error[E0596]: cannot borrow `is_less` as mutable, as it is not declared as mutable
    --> library/core/src/slice/mod.rs:3300:37
     |
3288 |         let is_less = |a: &T, b: &T| compare(a, b) == Less;
     |                                      ------- calling `is_less` requires mutable binding due to mutable borrow of `compare`
...
3300 |         sort::unstable::sort(slice, &mut is_less);
     |                                     ^^^^^^^^^^^^ cannot borrow as mutable
     |
help: consider changing this to be mutable
     |
3288 |         let mut is_less = |a: &T, b: &T| compare(a, b) == Less;
     |             +++

Some errors have detailed explanations: E0382, E0596.
For more information about an error, try `rustc --explain E0382`.
[RUSTC-TIMING] core test:false 14.205

For more information how to resolve CI failures of this job, visit this link.

rust-log-analyzer avatar Nov 25 '25 16:11 rust-log-analyzer

cc @orlp

tisonkun avatar Nov 25 '25 17:11 tisonkun

I do note a lack of tests?

Doc tests cover most branches. I don't find a dedicated file to cover its cousin sort_unstable. If you can point me to one, I'm glad to add cases there.

tisonkun avatar Nov 26 '25 02:11 tisonkun

The examples can change at any time. And you didn't test, for example, the post-condition that all elements ..start are less than or equal to the elements start..end and that those are less than or equal to the elements end.., including for the zero-length case.

orlp avatar Nov 26 '25 07:11 orlp

The examples can change at any time. And you didn't test, for example, the post-condition that all elements ..start are less than or equal to the elements start..end and that those are less than or equal to the elements end.., including for the zero-length case.

Thanks and yes. Do you know where the unit tests of sort/sort_unstable locate?

tisonkun avatar Nov 26 '25 07:11 tisonkun

I believe the bulk is found in https://github.com/rust-lang/rust/blob/main/library/alloctests/tests/sort/tests.rs.

orlp avatar Nov 26 '25 07:11 orlp

What I suggested in the ACP was a sketch implementation, I did some more thinking and I think the following handles all corner cases nicely:

pub fn partial_sort<T, F, R>(mut v: &mut [T], range: R, is_less: &mut F)
where
    F: FnMut(&T, &T) -> bool,
    R: RangeBounds<usize>,
{
    let len = v.len();
    let Range { start, end } = slice::range(range, ..len);
    
    if end - start <= 1 {
        // Can be resolved in at most a single partition_at_index call, without
        // further sorting. Do nothing if it is an empty range at start or end.
        if start != len && end != 0 {
            sort::select::partition_at_index(v, start, is_less);
        }
        return;
    }
    
    // Don't bother reducing the slice to sort if it eliminates fewer than 8 elements.
    if end + 8 <= len {
        v = sort::select::partition_at_index(v, end - 1, is_less).0;
    }
    if start >= 8 {
        v = sort::select::partition_at_index(v, start, is_less).2;
    }
    sort::unstable::sort(v, is_less);
}

And to formalize the post-conditions, I think the following should hold after a call to v.partial_sort_unstable(b..e):

for i in 0..b {
    for j in b..n {
        assert!(v[i] <= v[j]);
    }
}
for i in 0..e {
    for j in e..n {
        assert!(v[i] <= v[j]);
    }
}
for i in b..e {
    for j in i..e {
        assert!(v[i] <= v[j]);
    }
}

orlp avatar Nov 26 '25 09:11 orlp

And to formalize the post-conditions, I think the following should hold after a call to v.partial_sort_unstable(b..e):

A lot of those individual comparisons are implied by transitivity of the ordering, so it can be reduced to choosing the maximum of the prefix (if any), the minimum of the suffix (if any), and then asserting that the concatenation is sorted.

Informally, max(v[..b]) <= v[b] <= v[b + 1] <= ... <= v[e-1] <= min(v[e..]), or in code:

let max_before = v[..b].iter().max().into_iter();
let sorted_range = v[b..e].iter();
let min_after = v[e..].iter().min().into_iter();
let seq = max_before.chain(sorted_range).chain(min_after);
assert!(seq.is_sorted());

That's pretty much what you said in https://github.com/rust-lang/libs-team/issues/685#issuecomment-3494102871 , just using transitivity of the comparison. Without assuming that, the implementation couldn't guarantee the universally quantified property anyway.

quaternic avatar Nov 28 '25 05:11 quaternic

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

rustbot avatar Dec 01 '25 04:12 rustbot

Pushed a new implementation.

I'm writing tests but perhaps we'd have a new mod under library/alloctests/tests/partial_sort rather than patch the existing library/alloctests/tests/sort/tests.rs. Since the sort tests are heavily depending on macros while partial sorts assertions are quite different.

tisonkun avatar Dec 01 '25 04:12 tisonkun

Pushed a new implementation.

I'm writing tests but perhaps we'd have a new mod under library/alloctests/tests/partial_sort rather than patch the existing library/alloctests/tests/sort/tests.rs. Since the sort tests are heavily depending on macros while partial sorts assertions are quite different.

cc @Amanieu for early review for the direction and advice on where to organize the tests.

tisonkun avatar Dec 01 '25 04:12 tisonkun

The job pr-check-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)

stderr:

thread 'main' (143592) panicked at library/core/src/slice/mod.rs:16:4:
assertion failed: v[i] <= v[2]
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::panicking::panic
   3: rust_out::main::_doctest_main_library_core_src_slice_mod_rs_3335_0
   4: rust_out::main
   5: <fn() as core::ops::function::FnOnce<()>>::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

---- library/core/src/slice/mod.rs - slice::[T]::partial_sort_unstable_by (line 3335) stdout end ----

rust-log-analyzer avatar Dec 01 '25 04:12 rust-log-analyzer

The job aarch64-gnu-llvm-20-1 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
[RUSTC-TIMING] build_script_build test:false 0.310
error[E0308]: mismatched types
   --> library/core/src/slice/sort/unstable/mod.rs:104:13
    |
 67 | pub fn partial_sort<T, F, R>(v: &mut [T], range: R, mut is_less: F)
    |                        - found this type parameter
...
104 |     sort(v, is_less);
    |     ----    ^^^^^^^ expected `&mut _`, found type parameter `F`
    |     |
    |     arguments to this function are incorrect
    |
    = note: expected mutable reference `&mut _`
                  found type parameter `F`
note: function defined here
   --> library/core/src/slice/sort/unstable/mod.rs:23:8
    |
 23 | pub fn sort<T, F>(v: &mut [T], is_less: &mut F)
    |        ^^^^                    ---------------
help: consider mutably borrowing here
    |
104 |     sort(v, &mut is_less);
    |             ++++

For more information about this error, try `rustc --explain E0308`.
[RUSTC-TIMING] core test:false 24.596
error: could not compile `core` (lib) due to 1 previous error

rust-log-analyzer avatar Dec 01 '25 11:12 rust-log-analyzer

Regarding the tests I'm happy with either a separate file or as part of the existing tests as you see fit.

Amanieu avatar Dec 02 '25 03:12 Amanieu

Pushed some basic test cases at https://github.com/rust-lang/rust/pull/149318/commits/01bfcc05322896da4cbc24835aa61e244fa3e391.

The existing sort tests have many assumptions, like checked_sort always sorts the whole range (for sure), and this helper is tightly coupled in cases. I can hardly insert the range concept in the current sort tests set up.

I added the basic test cases and will try to leverage the pattern functions in the sort module to generate more cases. But this patch itself should be mergeable as long as the implementation looks good, since now we have the test structure, which can be improved continuously.

tisonkun avatar Dec 04 '25 06:12 tisonkun

r? libs

scottmcm avatar Dec 06 '25 08:12 scottmcm