GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — [DRAFT] Use Rust's `sort_by` instead of `timsort::try_sort_by_gt` by jackoconnordev · Pull Request #6096 · RustPython/RustPython Conversation
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the ✨ Finishing Touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| }; | ||
| let cmp = |a: &PyObjectRef, b: &PyObjectRef| a.rich_compare_bool(b, op, vm); | ||
| let cmp = |a: &PyObjectRef, b: &PyObjectRef| { | ||
| let res = a.rich_compare_bool(b, op, vm).unwrap(); |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai can you think of a good way I can unwrap this and correctly handle the PyRef<PyBaseException> in the error case?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I need to examine how similar error handling is done in the RustPython codebase, particularly around sorting operations that can fail.
🏁 Script executed:
#!/bin/bash
# Look for similar sorting patterns in the codebase that handle PyResult errors
rg -n -A5 -B5 "sort.*rich_compare" --type=rust
# Also look for other uses of rich_compare_bool to see how errors are typically handled
rg -n -A3 -B3 "rich_compare_bool.*unwrap\|unwrap.*rich_compare_bool" --type=rust
# Check for any existing fallible sorting utilities or patterns
rg -n -A5 -B5 "sort.*PyResult\|PyResult.*sort" --type=rust
Length of output: 192
🏁 Script executed:
#!/bin/bash
# Let's look for any existing fallible comparison patterns in the codebase
rg -n -A5 -B5 "rich_compare_bool" --type=rust
# Look for error handling patterns in comparison operations
rg -n -A3 -B3 "PyResult.*bool" --type=rust
# Check for any try_sort or fallible sorting patterns
rg -n "try_sort\|fallible.*sort\|sort.*error" --type=rust
Length of output: 73743
🏁 Script executed:
#!/bin/bash
# Let's look at the specific sort function implementation and see how other similar functions handle errors
rg -n -A10 -B5 "sort_by" vm/src/builtins/list.rs
# Look for other comparison functions that handle PyResult errors properly
rg -n -A5 -B5 "PyResult.*Ordering\|Ordering.*PyResult" --type=rust
# Check if there are any existing error-handling patterns for fallible comparisons in sorting
ast-grep --pattern 'fn $_($$_) -> PyResult<$_> {
$$$
sort_by($$$)
$$$
}'
Length of output: 971
@jackoconnordev Looking at the code, I can see the issue clearly. The rich_compare_bool method returns PyResult<bool>, but sort_by expects an infallible comparison function that returns std::cmp::Ordering.
From the search results, I notice there are commented lines showing timsort::try_sort_by_gt was previously used, which suggests there was a fallible sorting implementation before. Here are the best approaches to handle this properly:
fn do_sort( vm: &VirtualMachine, values: &mut Vec<PyObjectRef>, key_func: Option<PyObjectRef>, reverse: bool, ) -> PyResult<()> { let op = if reverse { PyComparisonOp::Lt } else { PyComparisonOp::Gt }; // Create a fallible comparison function let cmp_result = |a: &PyObjectRef, b: &PyObjectRef| -> PyResult<Ordering> { let res = a.rich_compare_bool(b, op, vm)?; Ok(if res { Ordering::Greater } else { Ordering::Less }) }; if let Some(ref key_func) = key_func { let mut items = values .iter() .map(|x| Ok((x.clone(), key_func.call((x.clone(),), vm)?))) .collect::<Result<Vec<_>, _>>()?; // Use try_sort pattern items.try_sort_by(|a, b| cmp_result(&a.1, &b.1))?; *values = items.into_iter().map(|(val, _)| val).collect(); } else { values.try_sort_by(|a, b| cmp_result(a, b))?; } Ok(()) }
However, Rust's standard library doesn't have try_sort_by. So here's the practical solution:
use std::cell::RefCell; fn do_sort( vm: &VirtualMachine, values: &mut Vec<PyObjectRef>, key_func: Option<PyObjectRef>, reverse: bool, ) -> PyResult<()> { let op = if reverse { PyComparisonOp::Lt } else { PyComparisonOp::Gt }; // Use RefCell to capture comparison errors let comparison_error: RefCell<Option<PyRef<PyBaseException>>> = RefCell::new(None); let cmp = |a: &PyObjectRef, b: &PyObjectRef| { // If we already have an error, just return some consistent ordering if comparison_error.borrow().is_some() { return Ordering::Equal; } match a.rich_compare_bool(b, op, vm) { Ok(res) => { if res { Ordering::Greater } else { Ordering::Less } } Err(err) => { *comparison_error.borrow_mut() = Some(err); Ordering::Equal // Return something to continue sorting } } }; if let Some(ref key_func) = key_func { let mut items = values .iter() .map(|x| Ok((x.clone(), key_func.call((x.clone(),), vm)?))) .collect::<Result<Vec<_>, _>>()?; items.sort_by(|a, b| cmp(&a.1, &b.1)); // Check if any comparison failed if let Some(err) = comparison_error.into_inner() { return Err(err); } *values = items.into_iter().map(|(val, _)| val).collect(); } else { values.sort_by(|a, b| cmp(a, b)); // Check if any comparison failed if let Some(err) = comparison_error.into_inner() { return Err(err); } } Ok(()) }
If you prefer a simpler approach, you could also pre-validate that all comparisons will work, but this would be less efficient for large lists.
The Option 2 approach is what I'd recommend as it properly handles the PyRef<PyBaseException> error case while maintaining the performance characteristics of the sort operation.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| .collect::<Result<Vec<_>, _>>()?; | ||
| timsort::try_sort_by_gt(&mut items, |a, b| cmp(&a.1, &b.1))?; | ||
| // timsort::try_sort_by_gt(&mut items, |a, b| cmp(&a.1, &b.1))?; | ||
| items.sort_by(|a, b| cmp(&a.1, &b.1)); |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai is rust's builtin sort_by a stable sorting algorithm if only Ordering::Less and Ordering::Greater are returned by cmp?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
@jackoconnordev No, Rust's builtin sort_by is not a stable sorting algorithm, regardless of what the comparison function returns.
Here are the key points:
Inherently Unstable: Rust's sort_by uses an unstable sorting algorithm (pattern-defeating quicksort/pdqsort) by design. If you need stability, you should use sort_by_stable instead.
Impact of Never Returning Ordering::Equal: When your comparison function only returns Ordering::Less and Ordering::Greater, you're effectively telling the algorithm that no two elements are equal. This has several implications:
Performance Considerations: By never returning Ordering::Equal, you're preventing the sorting algorithm from taking advantage of optimizations for equal elements, which could result in suboptimal performance.
In the context of your code, if you need stable sorting for Python's list.sort() (which should be stable according to Python's specification), you should consider:
sort_by_stable instead of sort_byOrdering::Equal when elements are truly equalThe current implementation may not match Python's expected stable sorting behavior.
🌐 Web query:
Rust sort_by algorithm stability when comparison function only returns Ordering::Less and Ordering::Greater never Ordering::Equal
💡 Result:
Short answer: sort_by is stable, but stability only matters for elements that compare equal. If your comparator never returns Ordering::Equal for any two distinct elements, then there are no “ties” to preserve — stability has no observable effect. However, your comparator still must implement a total order (in particular compare(x, x) must return Equal) or sorting may panic / produce unspecified results. (doc.rust-lang.org, rust-lang.github.io)
Details and notes
Example patterns
If you want, paste the comparator you’re using and I’ll check whether it properly implements a total order and whether you need to worry about stability or correctness.
Citations:
🌐 Web query:
Rust std::slice::sort_by algorithm pdqsort stable unstable behavior documentation
💡 Result:
Short answer
Details and practical notes
If you want
Citations:
@jackoconnordev I need to correct my previous response - I made an error about Rust's sorting algorithms.
Rust's sort_by IS stable. Here's the accurate information:
sort_by is stable: It uses a stable sorting algorithm (currently driftsort) and preserves the relative order of equal elements.
sort_unstable_by is unstable: This is the one that uses pdqsort and may reorder equal elements.
Impact of never returning Ordering::Equal: When your comparison function only returns Less and Greater for distinct elements, stability has no observable effect because there are no "equal" elements to preserve the order of.
Critical correctness issue: However, your comparator must still implement a total order. Specifically, compare(x, x) must return Ordering::Equal, or the sort may panic or produce unspecified results.
Looking at your code:
let cmp = |a: &PyObjectRef, b: &PyObjectRef| { let res = a.rich_compare_bool(b, op, vm).unwrap(); if res { Ordering::Greater } else { Ordering::Less } };
This comparator never returns Ordering::Equal, even when comparing an element to itself. This violates the total order requirement and could cause the sort to panic or behave incorrectly.
Since Python's list.sort() should be stable, using sort_by is the right choice, but the comparison function should properly handle equal elements by returning Ordering::Equal when appropriate.
Learnt from: jackoconnordev
PR: RustPython/RustPython#6086
File: benches/microbenchmarks/sort.py:7-7
Timestamp: 2025-08-09T22:55:10.512Z
Learning: In RustPython benchmarks, when testing sorting performance, benchmarking only `sorted()` is sufficient since it internally uses the same sorting logic as `list.sort()`. The only difference is that `sorted()` creates a copy first, so testing both would be redundant for evaluating the sorting algorithm's performance.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Do you know what the True and False return values of rich_compare_bool correspond to semantically?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
🏁 Script executed:
#!/bin/bash
# Search for the definition of rich_compare_bool to understand its semantics
rg -n "rich_compare_bool" --type=rust -A 5 -B 5
Length of output: 12093
@jackoconnordev Yes! Based on the codebase analysis, rich_compare_bool returns:
true if the comparison operation is true (e.g., if you call a.rich_compare_bool(b, PyComparisonOp::Gt, vm), it returns true when a > b)false if the comparison operation is false (e.g., false when a <= b)Looking at the current sorting code:
let op = if reverse { PyComparisonOp::Lt // When reverse=true, we use Lt } else { PyComparisonOp::Gt // When reverse=false, we use Gt }; let cmp = |a: &PyObjectRef, b: &PyObjectRef| { let res = a.rich_compare_bool(b, op, vm).unwrap(); if res { Ordering::Greater // If comparison is true } else { Ordering::Less // If comparison is false } };
This logic has a critical flaw: when rich_compare_bool returns false, it doesn't distinguish between "less than" and "equal to". For example:
reverse=false, op=Gt, and a.rich_compare_bool(b, Gt, vm) returns false, it could mean either a < b OR a == bOrdering::LessFor a correct total order, you need to handle equality properly. The fix should be something like:
let cmp = |a: &PyObjectRef, b: &PyObjectRef| { if a.rich_compare_bool(b, PyComparisonOp::Gt, vm).unwrap() { Ordering::Greater } else if a.rich_compare_bool(b, PyComparisonOp::Lt, vm).unwrap() { Ordering::Less } else { Ordering::Equal } };
This ensures the comparator implements a proper total order where compare(x, x) == Ordering::Equal.
Snippet 1: do_sort function (uses rich_compare_bool to implement sorting comparator)
Content:
fn do_sort(
vm: &VirtualMachine,
values: &mut Vec,
key_func: Option,
reverse: bool,
) -> PyResult<()> {
let op = if reverse {
PyComparisonOp::Lt
} else {
PyComparisonOp::Gt
};
let cmp = |a: &PyObjectRef, b: &PyObjectRef| {
let res = a.rich_compare_bool(b, op, vm).unwrap();
if res {
Ordering::Greater
} else {
Ordering::Less
}
};
if let Some(ref key_func) = key_func {
let mut items = values
.iter()
.map(|x| Ok((x.clone(), key_func.call((x.clone(),), vm)?)))
.collect::<Result<Vec<_>, _>>()?;
// timsort::try_sort_by_gt(&mut items, |a, b| cmp(&a.1, &b.1))?;
items.sort_by(|a, b| cmp(&a.1, &b.1));
*values = items.into_iter().map(|(val, _)| val).collect();
} else {
// timsort::try_sort_by_gt(values, cmp)?;
values.sort_by(|a, b| cmp(a, b));
}
Ok(())
}
Snippet 2: PyList cmp implementation (uses rich comparison across elements)
Content:
impl Comparable for PyList {
fn cmp(
zelf: &Py,
other: &PyObject,
op: PyComparisonOp,
vm: &VirtualMachine,
) -> PyResult {
if let Some(res) = op.identical_optimization(zelf, other) {
return Ok(res.into());
}
let other = class_or_notimplemented!(Self, other);
let a = &*zelf.borrow_vec();
let b = &*other.borrow_vec();
a.iter()
.richcompare(b.iter(), op, vm)
.map(PyComparisonValue::Implemented)
}
}
Learnt from: jackoconnordev
PR: RustPython/RustPython#6086
File: benches/microbenchmarks/sort.py:7-7
Timestamp: 2025-08-09T22:55:10.512Z
Learning: In RustPython benchmarks, when testing sorting performance, benchmarking only `sorted()` is sufficient since it internally uses the same sorting logic as `list.sort()`. The only difference is that `sorted()` creates a copy first, so testing both would be redundant for evaluating the sorting algorithm's performance.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| }; | ||
| let cmp = |a: &PyObjectRef, b: &PyObjectRef| a.rich_compare_bool(b, op, vm); | ||
| let cmp = |a: &PyObjectRef, b: &PyObjectRef| { | ||
| let res = a.rich_compare_bool(b, op, vm).unwrap(); |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
| let res = a.rich_compare_bool(b, op, vm).unwrap(); | |
| let res = a.rich_compare_bool(b, op, vm)?; |
Does this do the trick?
if not you can do it with:
let res = match a.rich_compare_bool(b, op, vm) { OK(v)=> v, Err(e) => return e, };
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
There were compiler errors for both versions to do with returning a value that isn't an Ordering :-/
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
There were compiler errors for both versions to do with returning a value that isn't an
Ordering:-/
Not so sure why it fails and what's the issue but you can try with:
| let res = a.rich_compare_bool(b, op, vm).unwrap(); | |
| let res = a.rich_compare_bool(b, op, vm).unwrap_or(Ordering::Equal); |
And hope that the CI passes
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@jackoconnordev I've dug a bit more and it seems that we need a failable sorting mechanism. maybe try to use something like https://docs.rs/try-partialord/latest/try_partialord/ |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Also use sort_unstable_by since we only get Less/Greater from the rich compare bool function anyway. Returning Equal in the error case will allow the sort to not panic.
|
I think if this was Rust < 1.81.0 this might actually pass https://users.rust-lang.org/t/panic-on-sort-of-f32-s-with-f32-total-cmp-in-1-81-0/117675 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| if res { | ||
| Ordering::Greater | ||
| } else { | ||
| Ordering::Less | ||
| } |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
The meaning of true/false will be different by op
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I'd like to pick this up — planning to continue from #6096 . |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Go for it! I'd love to learn from a working solution and the speed up is well worth the effort |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
At least 1 approving review is required to merge this pull request.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.