Skip to content

Conversation

kornelski
Copy link
Contributor

@kornelski kornelski commented Oct 5, 2025

This reduces amount of unsafe code in partition(), while generating essentially the same code.

partition() and functions calling it can only run into out-of-bounds issues if the is_less function is buggy, so I've used cold panic_on_ord_violation() to replace various other panics/aborts. This slightly reduces code size, and gives a more relevant error message.

Related to #144327

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Oct 5, 2025
@rustbot
Copy link
Collaborator

rustbot commented Oct 5, 2025

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

@workingjubilee
Copy link
Member

cc @orlp @Voultapher you may find this interesting (but you don't have to)

@Voultapher
Copy link
Contributor

Voultapher commented Oct 5, 2025

partition() and functions calling it can only run into out-of-bounds issues if the is_less function is buggy,

I'm not sure that's true. Looking at for example this condition if len == 0 -> abort it doesn't happen because is_less is buggy. It can only happen if future versions of the code calling the function break assumptions. IIRC the only place that can be caused by a buggy is_less is the one currently calling panic_on_ord_violation.

I'm all for reducing unsafe and a smaller code-size, with this PR as is, it might lead to confusing error messages and incorrect understanding of the code in the future. Maybe we can achieve a similar goal using the Option approach but still call abort?

@kornelski
Copy link
Contributor Author

I mean only is_less can trigger panic_on_ord_violation under assumption that the implementation itself isn't buggy. is_less is used indirectly via partition_lomuto_branchless_cyclic, which is non-trivial and could be buggy itself, so there's never going to be a case that is 100% certain. But it could be helpful in practice.

The additional checks piggyback on returning None, because that's the simplest implementation. Distinguishing between checks in partition would require a Result with a custom Error type and/or moving checks around. That's more code, potentially more branches, and I doubt it's worth it. I could add more debug_asserts?

I still think it's an improvement over the current version of partition, where if len == 0 case happened, it could make partition_at_index_loop just return without reporting any issue, while failures most likely caused by is_less just abort() without explaining why.

@Voultapher
Copy link
Contributor

Voultapher commented Oct 6, 2025

The arguments you present don't make sense to me.

  • It's not just guess work what happens with buggy is_less implementations. The Rust CI an local development has been fuzzing the code for over a year now [1] and I've yet to hear of any aborts.

  • While the code is certainly complex it's important to not overgeneralize. len == 0 is a bug caused by the caller of partition not partition itself. So the complexity of the underlying partition implementation isn't relevant. The other part where it could theoretically return a partition index >= len is related to the partition implementation. But in practice for each slice element processed the partition index can be incremented by zero or one. So it is guaranteed < len. What elements are processed does not depend on the outcome of is_less so whether or not the implementation is buggy doesn't play any role here [2]. The simplified variant shows this more clearly, but it's true for both. Nothing other than bidirectional_merge and merge have loop bounds depending on the outcome of is_less. insert_tail has an early exit, but other than that everything else has a loop bound independent of is_less and can be verified accordingly.

  • If some future change of the code introduces bugs that would trigger one of the current aborts I don't see how telling our users that "hey your comparison function is buggy" when it's not would help. It's more likely to conceal and suppress reports of it.

@rustbot
Copy link
Collaborator

rustbot commented Oct 6, 2025

This PR was rebased onto a different master 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.

@kornelski
Copy link
Contributor Author

ok, if reporting is_less is not a concern, then I've simplified it significantly to just aborting.

Copy link
Contributor

@Voultapher Voultapher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
let (pivot, v_without_pivot) = v.split_at_mut(1);
v.swap(0, pivot);
let (pivot, v_without_pivot) = v.split_first_mut().unwrap_or_else(|| intrinsics::abort());
Copy link
Contributor

@Voultapher Voultapher Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe .unwrap_or_else(intrinsics::abort);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting:

.unwrap_or_else(intrinsics::abort);
^^^^^^^^^^^^^^^^^ expected (&mut T, &mut [T]), found !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants