-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Quick sort and related Arrays utilities #58
base: master
Are you sure you want to change the base?
Changes from all commits
2e62624
00e8667
fd12d32
d8f8cee
57e35ae
ac104b2
9af98ed
aab9de9
513d165
f44ea6f
71fe474
41faf37
33a43a5
5c8f06f
7da871a
62b9346
e8e5919
bb009fb
6d57e55
9038f2a
a2edede
1df340f
850e3d4
34e380a
730278a
f1f31ad
92a19bd
bc018cb
ae832a4
a508b99
03af996
467f587
9688843
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,375 @@ | ||
// RUN: %dafny /compile:0 "%s" | ||
|
||
/******************************************************************************* | ||
* Copyright by the contributors to the Dafny Project | ||
* SPDX-License-Identifier: MIT | ||
*******************************************************************************/ | ||
|
||
include "../Sets/Sets.dfy" | ||
include "../../Comparison.dfy" | ||
|
||
module Arrays{ | ||
|
||
import opened Sets | ||
import opened Comparison | ||
import opened Wrappers | ||
|
||
trait Sorter<T> { | ||
ghost const C := Comparison(Cmp) | ||
|
||
function method Cmp(t0: T, t1: T): Cmp | ||
|
||
twostate predicate UnchangedSlice(arr: array<T>, lo: int, hi: int) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just food for thought, or a potential learning exercise: It might make all of this more readable if we had an actual datatype ArraySlice_<T> = ArraySlice_(arr: array<T>, lo: int, hi: int) {
predicate Valid() {
0 <= lo <= hi <= arr.Length
}
twostate predicate Unchanged()
requires Valid()
reads arr
{
arr[lo..hi] == old(arr[lo..hi])
}
// etc.
}
type ArraySlice<T> = a: ArraySlice_<T> | a.Valid() witness * AFAICT this would be useful even if we only used it in ghost code to avoid any runtime cost (and even then it's possible we could optimize the wrapper away in the future, just as we now do for single-field datatypes). |
||
requires 0 <= lo <= hi <= arr.Length | ||
reads arr | ||
{ | ||
arr[lo..hi] == old(arr[lo..hi]) | ||
} | ||
|
||
twostate lemma UnchangedSliceSplit(arr: array<T>, lo: int, mid: int, hi: int) | ||
requires 0 <= lo <= mid <= hi <= arr.Length | ||
requires UnchangedSlice(arr, lo, hi) | ||
ensures UnchangedSlice(arr, lo, mid) | ||
ensures UnchangedSlice(arr, mid, hi) | ||
{ | ||
assert arr[lo..mid] == arr[lo..hi][..mid-lo]; | ||
} | ||
|
||
twostate lemma UnchangedSliceShuffled(arr: array<T>, lo: int, hi: int) | ||
requires 0 <= lo <= hi <= arr.Length | ||
requires UnchangedSlice(arr, lo, hi) | ||
ensures Shuffled(arr, lo, hi) | ||
{} | ||
|
||
twostate predicate UnchangedExceptSliceShuffled(arr: array<T>, lo: int, hi: int) | ||
reads arr | ||
requires 0 <= lo <= hi <= arr.Length | ||
{ | ||
&& UnchangedSlice(arr, 0, lo) | ||
&& Shuffled(arr, lo, hi) | ||
&& UnchangedSlice(arr, hi, arr.Length) | ||
} | ||
|
||
twostate lemma UnchangedExceptSliceShuffledExtend(arr: array<T>, lo: int, lo': int, hi': int, hi: int) | ||
requires 0 <= lo <= lo' <= hi' <= hi <= arr.Length | ||
requires UnchangedExceptSliceShuffled(arr, lo', hi') | ||
ensures UnchangedExceptSliceShuffled(arr, lo, hi) | ||
{ | ||
assert UnchangedExceptSliceShuffled(arr, lo', hi'); | ||
assert UnchangedSlice(arr, 0, lo') && Shuffled(arr, lo', hi') && UnchangedSlice(arr, hi', arr.Length); | ||
UnchangedSliceSplit(arr, 0, lo, lo'); UnchangedSliceSplit(arr, hi', hi, arr.Length); | ||
assert && UnchangedSlice(arr, 0, lo) && UnchangedSlice(arr, lo, lo') | ||
&& Shuffled(arr, lo', hi') | ||
&& UnchangedSlice(arr, hi', hi) && UnchangedSlice(arr, hi, arr.Length); | ||
UnchangedSliceShuffled(arr, lo, lo'); UnchangedSliceShuffled(arr, hi', hi); | ||
assert && UnchangedSlice(arr, 0, lo) && Shuffled(arr, lo, lo') | ||
&& Shuffled(arr, lo', hi') | ||
&& Shuffled(arr, hi', hi) && UnchangedSlice(arr, hi, arr.Length); | ||
ShuffledConcat(arr, lo, lo', hi'); ShuffledConcat(arr, lo, hi', hi); | ||
assert UnchangedSlice(arr, 0, lo) && Shuffled(arr, lo, hi) && UnchangedSlice(arr, hi, arr.Length); | ||
assert UnchangedExceptSliceShuffled(arr, lo, hi); | ||
} | ||
|
||
twostate predicate Shuffled(arr: array<T>, lo: int, hi: int) | ||
requires 0 <= lo <= hi <= arr.Length | ||
reads arr | ||
{ | ||
multiset(arr[lo..hi]) == old(multiset(arr[lo..hi])) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I remember adding this to almost every every function method in |
||
} | ||
|
||
predicate Sortable(arr: array<T>, lo: int, hi: int) | ||
requires 0 <= lo <= hi <= arr.Length | ||
reads arr | ||
{ | ||
reveal C.Valid(); // Leaks through | ||
C.Valid(Sets.OfSlice(arr, lo, hi)) | ||
} | ||
|
||
twostate lemma SetOfSliceShuffled(arr: array<T>, lo: int, hi: int) | ||
requires 0 <= lo <= hi <= arr.Length | ||
requires Shuffled(arr, lo, hi) | ||
ensures Sets.OfSlice(arr, lo, hi) == old(Sets.OfSlice(arr, lo, hi)) | ||
{ | ||
calc { | ||
old(Sets.OfSlice(arr, lo, hi)); | ||
set x | x in old(arr[lo..hi]); | ||
set x | x in old(multiset(arr[lo..hi])); | ||
set x | x in multiset(arr[lo..hi]); | ||
set x | x in arr[lo..hi]; | ||
Sets.OfSlice(arr, lo, hi); | ||
} | ||
} | ||
|
||
twostate lemma SortableUnchangedExceptSliceShuffled(arr: array<T>, lo: int, hi: int) | ||
requires 0 <= lo <= hi <= arr.Length | ||
requires old(Sortable(arr, lo, hi)) | ||
requires UnchangedExceptSliceShuffled(arr, lo, hi) | ||
ensures Sortable(arr, lo, hi) | ||
{ | ||
SetOfSliceShuffled(arr, lo, hi); | ||
} | ||
|
||
twostate lemma ShuffledConcat(arr: array<T>, lo: int, mid: int, hi: int) | ||
requires 0 <= lo <= mid <= hi <= arr.Length | ||
requires Shuffled(arr, lo, mid) | ||
requires Shuffled(arr, mid, hi) | ||
ensures Shuffled(arr, lo, hi) | ||
{ | ||
calc { | ||
old(multiset(arr[lo..hi])); | ||
{ assert old(arr[lo..hi] == arr[lo..mid] + arr[mid..hi]); } | ||
old(multiset(arr[lo..mid] + arr[mid..hi])); | ||
old(multiset(arr[lo..mid])) + old(multiset(arr[mid..hi])); | ||
multiset(arr[lo..mid]) + multiset(arr[mid..hi]); | ||
{ assert arr[lo..hi] == arr[lo..mid] + arr[mid..hi]; } | ||
multiset(arr[lo..hi]); | ||
} | ||
} | ||
|
||
lemma SortableSubslice(arr: array<T>, lo: int, hi: int, lo': int, hi': int) | ||
requires 0 <= lo <= lo' <= hi' <= hi <= arr.Length | ||
requires Sortable(arr, lo, hi) | ||
ensures Sortable(arr, lo', hi') | ||
{} | ||
|
||
predicate Sorted(arr: array<T>, lo: int, hi: int) | ||
reads arr | ||
requires 0 <= lo <= hi <= arr.Length | ||
{ | ||
C.Sorted(arr[lo..hi]) | ||
} | ||
|
||
lemma StripedInit(sq: seq<T>, pivot: T, lo: int, hi: int) | ||
requires 0 <= lo <= hi <= |sq| | ||
ensures C.Striped(sq, pivot, lo, lo, lo, hi, hi) | ||
{ | ||
reveal C.Striped(); | ||
} | ||
|
||
lemma StripedNonEmpty(sq: seq<T>, pivot: T, lo: int, left: int, mid: int, right: int, hi: int) | ||
requires 0 <= lo <= left <= mid <= right <= hi <= |sq| | ||
requires C.Striped(sq, pivot, lo, left, mid, right, hi) | ||
requires C.Valid(Sets.OfSeq(sq[lo..hi])) | ||
requires pivot in sq[lo..hi] | ||
ensures left < right | ||
{ | ||
reveal C.Valid(); | ||
var idx :| lo <= idx < hi && sq[idx] == pivot; | ||
C.AlwaysReflexive(pivot); | ||
reveal C.Striped(); | ||
} | ||
|
||
twostate lemma StripedUnchangedExceptSliceShuffled(arr: array<T>, pivot: T, lo: int, left: int, right: int, hi: int) | ||
requires 0 <= lo <= left <= right <= hi <= |arr[..]| | ||
requires Shuffled(arr, lo, left) | ||
requires UnchangedSlice(arr, left, right) | ||
requires Shuffled(arr, right, hi) | ||
requires old(C.Striped(arr[..], pivot, lo, left, right, right, hi)) | ||
ensures C.Striped(arr[..], pivot, lo, left, right, right, hi) | ||
{ | ||
reveal C.Striped(); | ||
forall i | lo <= i < left ensures Cmp(arr[..][i], pivot).Lt? { | ||
assert arr[..][i] == arr[lo..left][i - lo]; | ||
assert arr[..][i] in multiset(arr[lo..left]); | ||
} | ||
forall i | left <= i < right ensures Cmp(arr[..][i], pivot).Eq? { | ||
assert arr[..][i] == arr[left..right][i - left]; | ||
} | ||
forall i | right <= i < hi ensures Cmp(arr[..][i], pivot).Gt? { | ||
assert arr[..][i] == arr[right..hi][i - right]; | ||
assert arr[..][i] in multiset(arr[right..hi]); | ||
} | ||
} | ||
|
||
method Swap(arr: array<T>, ghost lo: int, i: int, j: int, ghost hi: int) | ||
requires 0 <= lo <= i <= j < hi <= arr.Length | ||
modifies arr | ||
ensures UnchangedExceptSliceShuffled(arr, lo, hi) | ||
ensures arr[..] == old(arr[..][i := arr[j]][j := arr[i]]) | ||
{ | ||
arr[i], arr[j] := arr[j], arr[i]; | ||
} | ||
|
||
method SwapLt( | ||
arr: array<T>, pivot: T, | ||
lo: int, left: int, mid: int, right: int, hi: int | ||
) | ||
requires 0 <= lo <= left <= mid < right <= hi <= arr.Length | ||
requires C.Striped(arr[..], pivot, lo, left, mid, right, hi) | ||
requires Cmp(arr[mid], pivot).Lt? | ||
modifies arr | ||
ensures UnchangedExceptSliceShuffled(arr, lo, hi) | ||
ensures arr[..] == old(arr[..][left := arr[mid]][mid := arr[left]]) | ||
ensures C.Striped(arr[..], pivot, lo, left + 1, mid + 1, right, hi) | ||
{ | ||
reveal C.Striped(); | ||
Swap(arr, lo, left, mid, hi); | ||
} | ||
|
||
method SwapGt( | ||
arr: array<T>, pivot: T, | ||
lo: int, left: int, mid: int, right: int, hi: int | ||
) | ||
requires 0 <= lo <= left <= mid < right <= hi <= arr.Length | ||
requires C.Striped(arr[..], pivot, lo, left, mid, right, hi) | ||
requires Cmp(arr[mid], pivot).Gt? | ||
modifies arr | ||
ensures UnchangedExceptSliceShuffled(arr, lo, hi) | ||
ensures arr[..] == old(arr[..][mid := arr[right - 1]][right - 1 := arr[mid]]) | ||
ensures C.Striped(arr[..], pivot, lo, left, mid, right - 1, hi) | ||
{ | ||
reveal C.Striped(); | ||
Swap(arr, lo, mid, right - 1, hi); | ||
} | ||
|
||
lemma SortedStripedMiddle(arr: array<T>, pivot: T, lo: int, left: int, right: int, hi: int) | ||
requires 0 <= lo <= left < right <= hi <= arr.Length | ||
requires Sortable(arr, lo, hi) | ||
requires pivot in multiset(arr[lo..hi]) | ||
requires C.Striped(arr[..], pivot, lo, left, right, right, hi) | ||
ensures Sorted(arr, left, right) | ||
{ | ||
reveal C.Striped(); | ||
forall i, j | left <= i < j < right ensures Cmp(arr[i], arr[j]).Le? { | ||
var idx :| lo <= idx < hi && arr[idx] == pivot; | ||
assert Cmp(arr[i], pivot) == Eq; | ||
assert C.CompleteonPair(arr[j], pivot); | ||
assert C.Transitive'(arr[i], pivot, arr[j]); | ||
} | ||
} | ||
|
||
lemma SortedConcat(arr: array<T>, lo: int, mid: int, hi: int) | ||
requires 0 <= lo <= mid <= hi <= arr.Length | ||
requires Sorted(arr, lo, mid) | ||
requires Sorted(arr, mid, hi) | ||
requires forall i, j | lo <= i < mid <= j < hi :: Cmp(arr[i], arr[j]).Le? | ||
ensures Sorted(arr, lo, hi) | ||
{ | ||
assert arr[lo..mid] + arr[mid..hi] == arr[lo..hi]; | ||
C.SortedConcat(arr[lo..mid], arr[mid..hi]); | ||
} | ||
|
||
twostate lemma SortedUnchangedSlice(arr: array<T>, lo: int, hi: int) | ||
requires 0 <= lo <= hi <= arr.Length | ||
requires old(Sorted(arr, lo, hi)) | ||
requires UnchangedSlice(arr, lo, hi) | ||
ensures Sorted(arr, lo, hi) | ||
{ | ||
assert arr[lo..hi] == old(arr[lo..hi]); | ||
forall i, j | lo <= i < j < hi ensures Cmp(arr[i], arr[j]).Le? { | ||
assert arr[i] == arr[lo..hi][i - lo]; | ||
assert arr[j] == arr[lo..hi][j - lo]; | ||
} | ||
} | ||
|
||
lemma SortedDutchFlag(arr: array<T>, pivot: T, lo: int, left: int, right: int, hi: int) | ||
requires 0 <= lo <= left < right <= hi <= arr.Length | ||
requires Sortable(arr, lo, hi) | ||
requires pivot in multiset(arr[lo..hi]) | ||
requires Sorted(arr, lo, left) | ||
requires Sorted(arr, right, hi) | ||
requires C.Striped(arr[..], pivot, lo, left, right, right, hi) | ||
ensures Sorted(arr, lo, hi) | ||
{ | ||
reveal C.Striped(); | ||
forall i, j | lo <= i < left <= j < right ensures Cmp(arr[i], arr[j]).Le? { | ||
var idx :| lo <= idx < hi && arr[idx] == pivot; | ||
assert Cmp(arr[i], pivot).Le?; | ||
assert Cmp(arr[j], pivot).Eq?; | ||
assert C.CompleteonPair(arr[j], pivot); | ||
assert C.Transitive'(arr[i], pivot, arr[j]); | ||
} | ||
SortedStripedMiddle(arr, pivot, lo, left, right, hi); | ||
SortedConcat(arr, lo, left, right); | ||
forall i, j | lo <= i < right <= j < hi ensures Cmp(arr[i], arr[j]).Le? { | ||
assert Cmp(arr[i], pivot).Le?; | ||
assert Cmp(arr[j], pivot).Gt?; | ||
assert C.CompleteonPair(arr[i], pivot); | ||
assert C.CompleteonPair(arr[j], pivot); | ||
assert C.Transitive'(arr[i], pivot, arr[j]); | ||
} | ||
SortedConcat(arr, lo, right, hi); | ||
} | ||
|
||
// Doesn't have the O(n log n) worst case runtime yet, need MedianofMeians algorithm implemented for that. | ||
method QuickSort(arr: array<T>, lo: int := 0, hi: int := arr.Length) | ||
requires 0 <= lo <= hi <= arr.Length | ||
requires Sortable(arr, lo, hi) | ||
decreases hi - lo | ||
modifies arr | ||
ensures Sortable(arr, lo, hi) | ||
ensures Sorted(arr, lo, hi) | ||
ensures UnchangedExceptSliceShuffled(arr, lo, hi) | ||
{ | ||
if hi - lo > 1 { | ||
var pivot := MedianOfMedians(arr, lo, hi); | ||
var left, right := DutchFlag(arr, pivot, lo, hi); | ||
|
||
label left: | ||
SortableUnchangedExceptSliceShuffled(arr, lo, hi); | ||
SortableSubslice(arr, lo, hi, lo, left); | ||
StripedNonEmpty(arr[..], pivot, lo, left, right, right, hi); | ||
QuickSort(arr, lo, left); | ||
|
||
label right: | ||
UnchangedExceptSliceShuffledExtend@left(arr, lo, lo, left, hi); | ||
SortableUnchangedExceptSliceShuffled(arr, lo, hi); | ||
SortableSubslice(arr, lo, hi, right, hi); | ||
UnchangedSliceSplit@left(arr, left, hi, arr.Length); | ||
UnchangedSliceSplit@left(arr, left, right, hi); | ||
UnchangedSliceShuffled@left(arr, right, hi); | ||
StripedUnchangedExceptSliceShuffled@left(arr, pivot, lo, left, right, hi); | ||
QuickSort(arr, right, hi); | ||
|
||
UnchangedExceptSliceShuffledExtend@right(arr, lo, right, hi, hi); | ||
SortableUnchangedExceptSliceShuffled(arr, lo, hi); | ||
UnchangedSliceSplit@right(arr, lo, left, right); | ||
SortedUnchangedSlice@right(arr, lo, left); | ||
StripedUnchangedExceptSliceShuffled@right(arr, pivot, lo, left, right, hi); | ||
SortedDutchFlag(arr, pivot, lo, left, right, hi); | ||
} | ||
} | ||
|
||
function method MedianOfMedians(arr: array<T>, lo: int, hi: int): (t: T) | ||
requires 0 <= lo < hi <= arr.Length | ||
reads arr | ||
ensures t in arr[lo..hi] | ||
{ | ||
arr[lo] // TODO | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should either implement this or drop the function and just inline this choice of pivot above. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's implement it — it's not hard and it's an important part of making the algorithm safe. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As per offline conversation we decided not to block on this, but in that case we should add a comment on QuickSort that it doesn't yet have the intended O(n log n) worst case runtime yet (which is what @cpitclaudel was referring to as "safe"). |
||
} | ||
|
||
method DutchFlag(arr: array<T>, pivot: T, lo: int, hi: int) returns (left: int, right: int) | ||
requires 0 <= lo < hi <= arr.Length | ||
requires pivot in multiset(arr[lo..hi]) | ||
modifies arr | ||
ensures UnchangedExceptSliceShuffled(arr, lo, hi) | ||
ensures lo <= left <= right <= hi | ||
ensures C.Striped(arr[..], pivot, lo, left, right, right, hi) | ||
{ | ||
left, right := lo, hi; | ||
var mid := lo; | ||
StripedInit(arr[..], pivot, lo, hi); | ||
|
||
while mid < right | ||
invariant pivot in multiset(arr[lo..hi]) | ||
invariant lo <= left <= mid <= right <= hi | ||
invariant UnchangedExceptSliceShuffled(arr, lo, hi) | ||
invariant C.Striped(arr[..], pivot, lo, left, mid, right, hi) | ||
{ | ||
reveal C.Striped(); | ||
match Cmp(arr[mid], pivot) { | ||
case Lt => | ||
SwapLt(arr, pivot, lo, left, mid, right, hi); | ||
left := left + 1; | ||
mid := mid + 1; | ||
case Gt => | ||
SwapGt(arr, pivot, lo, left, mid, right, hi); | ||
right := right - 1; | ||
case Eq => | ||
mid := mid + 1; | ||
} | ||
} | ||
|
||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This definitely needs an example in
examples
to demonstrate using it. I assume you have to not only provide a concrete class that implementsSorter<T>
and fills inCmp
, but perhaps even has to invoke a lemma or two from the trait in order to prove that it works.Is it possible to also provide a
PredicateSorter
class that is constructed around a(T, T) -> Cmp
function value? Perhaps it could even be a partial function if you also provide a ghost set of all values you intend to sort with it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to clarify, I'd strongly prefer to have an example, but the
PredicateSorter
idea can wait for a future PR.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yikes, I went to write an example just to play around with it and immediately got the
A class may only extend a trait in the same module, unless the parent trait is annotated with {:termination false}
error. That means this is currently useless unless we add{:termination false}
.Unfortunately we should treat that as a hard blocker given there's already been resistance to putting
{:termination false}
on any standard library code: #22 And especially given the discovery of dafny-lang/dafny#2500 I'm not inclined to push back on that.We can think about whether there's a good alternative for this code that doesn't use traits, but it might be better to stick with this if it's good UX and at least wait for the solution to dafny-lang/dafny#2500.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not right: the proper API to use is
PredicateSorter
, which is a regular class, defined right belowSorter
. That doesn't require:termination false
.Original code here, with examples: https://github.com/dafny-lang/compiler-bootstrap/blob/4f616822209828e48cf63d3da66ee1c010f689d4/src/Utils/Lib.Sort.dfy#L396-L420
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @cpitclaudel, I see the issue now is this PR doesn't include the
PredicateSorter
class. We should just add that as well.