fix: correct off-by-one in SortSlices equal-run check - #402
Conversation
sliceSorter.checkSort records the start of a run of mutually equal elements as i, but when !less(v[i-1], v[i]) the run begins at i-1. The loop's own comment says the check covers v[start:i], the whole run, and with start = i a two element run yields the empty slice v[i:i] in the panic message. Two consequences. The guard misses violations whose offending element is first in a run, so a non-transitive less function silently produces an order dependent result from cmp.Equal instead of the documented "incomparable values detected" panic. And when it does fire, the message prints a window missing its leading element. Adds a case where the incomparable element starts the run, which does not panic before this change.
fallenmi
left a comment
There was a problem hiding this comment.
Reviewed with Codex at exact head b455d2a7a6f49d14067ef105fe3070b4451a89b9.
The change restores the invariant stated by checkSort: when !less(v[i-1], v[i]) starts an equal run, its first member is i-1, so the later endpoint comparison must include it.
On exact base b133f1f1932e48f466f597a3346ce6f5a49a0dc1, an independent first-element-of-run NaN oracle fails because no panic occurs; on this head it passes. I also exhaustively checked all 729 length-six slices over three equivalence classes under a valid strict weak ordering, repeated the matrix 20 times with no false panic or mis-sort, ran go test ./..., repeated go test ./cmp/cmpopts 10 times, and verified gofmt, git diff --check, and byte-identical pre-existing go vet diagnostics. The base is current master, the CLA check is green, and the reviewed head is otherwise clean.
Approved.
The bug
sliceSorter.checkSorttracks the start of a run of mutually "equal" elements:When
!less(v[i-1], v[i])the run begins ati-1, noti, so the first element of every equal run is excluded from the check.The loop's own comment gives it away: it says the check covers
v[start:i], the whole run. Withstart = i, a two-element run producesv.Slice(i, i), an empty slice, in the panic message.Why it matters
checkSortexists to panic withincomparable values detectedwhen the supplied less function is not transitive over the data, because the sorted order and thereforecmp.Equal's answer is otherwise arbitrary. With the off-by-one, a violation whose offending element is first in a run goes undetected, andcmp.Equalquietly returns an order-dependent result instead of telling the caller their option is broken.The everyday case is a float slice containing
NaNwith the obvious less function, which is exactly the misuse this package already has awantPanictest for:Sorted, the run is
[0 NaN 1]andless(0, 1)is true, so it must panic. It does not, becausestartpoints atNaNrather than at0, andNaNcompares "equal" to1in both directions.When the check does fire, the message also prints the wrong window. On the existing test input it reports
[2 2 NaN 3 3 3 3]where the run is[2 2 2 NaN 3 3 3 3].mapSorter.checkSortis unaffected: it requires a total order and checks every adjacent pair, so it has no run-start concept.The change
start = i - 1, plus one case in the existingTestOptionstable where the incomparable element begins the run.Widening the window by one cannot cause a false panic: for a valid strict weak ordering every element of a run is mutually equal, so the added comparison is against an element already known to be equivalent.
Testing
Without the fix the new case fails:
With it,
./cmp/...passes in full.gofmt -l cmp/is clean, andgo vetoutput is byte-identical to the unmodified tree.One user-visible note: this changes the panic message on the existing
wantPaniccase from[2 2 NaN 3 3 3 3]to[2 2 2 NaN 3 3 3 3]. No test asserts that string.