Skip to content

SortComparator: materialise comparator Sequence before reusing it for sort (#874)#2009

Open
wadetregaskis wants to merge 1 commit into
swiftlang:mainfrom
wadetregaskis:SortComparator
Open

SortComparator: materialise comparator Sequence before reusing it for sort (#874)#2009
wadetregaskis wants to merge 1 commit into
swiftlang:mainfrom
wadetregaskis:SortComparator

Conversation

@wadetregaskis

Copy link
Copy Markdown
Contributor

SortComparator shouldn't assume it can iterate its Sequence of SortComparators multiple times.

Motivation:

The public Sequence.sorted(using: S) overload that takes a Sequence of SortComparators forwards each pair of elements through comparators.compare(_:_:), which itself iterates the entire sequence. That works for Array (or any multi-pass collection) but Sequence does not promise non-destructive iteration: a single-use sequence (e.g. AnySequence wrapping an AnyIterator) is exhausted after the first comparison, so every subsequent call to compare walks an empty iterator and returns .orderedSame — silently leaving the bulk of the input unsorted.

I don't know how common it is to do this - I imagine the concrete type used is just an Array 99% of the time, which happens to work just fine - but this is easy to fix and the kind of bug that's quite hard to find if you stumble into it unwittingly, as a user of this API.

Modifications:

This patch fixes that by snapshotting the comparator sequence into an Array before sorting so it can be replayed safely.

Result:

Sorting works as the caller intended, irrespective of what type of Sequence they use.

Testing:

New unit test added.

…or sort (swiftlang#874).

The public `Sequence.sorted(using: S)` overload that takes a `Sequence` of
SortComparators forwards each pair of elements through `comparators.compare(_:_:)`, which itself iterates the entire sequence.
 That works for `Array` (or any multi-pass collection) but `Sequence` does not promise non-destructive iteration: a single-use sequence (e.g. `AnySequence` wrapping an `AnyIterator`) is exhausted after the first comparison, so every subsequent call to `compare` walks an empty iterator and returns `.orderedSame` — silently leaving the bulk of the input unsorted.

This patch fixes that by snapshotting the comparator sequence into an `Array` before sorting so it can be replayed safely.
@wadetregaskis wadetregaskis requested a review from a team as a code owner May 31, 2026 16:06
// non-destructive multi-pass iteration. A single-use sequence (e.g. one
// backed by `AnyIterator`) would otherwise be exhausted after the first
// comparison and silently produce an unsorted result.
let comparators = Array(comparators)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Have you measured the performance impact of requiring the comparators to be copied into a heap allocation for every call site, even those that could have been iterated twice?

We also don't typically wrap comments in this repo, nor do I think we need a long comment here - a short comment would suffice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants