Skip to content

Commit

Permalink
Merge pull request #25 from pi8027/topdown-tailrec
Browse files Browse the repository at this point in the history
Add top-down tail-recursive mergesort
  • Loading branch information
pi8027 authored Jun 7, 2024
2 parents 4380988 + 6890eee commit d86a047
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 7 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/docker-action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ jobs:
- 'mathcomp/mathcomp:2.2.0-coq-8.18'
- 'mathcomp/mathcomp:2.2.0-coq-8.19'
- 'mathcomp/mathcomp:2.2.0-coq-dev'
- 'mathcomp/mathcomp-dev:coq-8.17'
- 'mathcomp/mathcomp-dev:coq-8.18'
- 'mathcomp/mathcomp-dev:coq-8.19'
- 'mathcomp/mathcomp-dev:coq-dev'
Expand All @@ -66,7 +65,9 @@ jobs:
with:
opam_file: 'coq-stablesort.opam'
custom_image: ${{ matrix.image }}

export: 'OPAMWITHTEST'
env:
OPAMWITHTEST: true

# See also:
# https://github.com/coq-community/docker-coq-action#readme
Expand Down
3 changes: 2 additions & 1 deletion Make.benchmark
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ benchmark/extraction_cbv.v
benchmark/extraction_cbvacc.v

-R theories stablesort
-R benchmark benchmark
-R benchmark stablesort.benchmark

-arg -w -arg -notation-overridden
6 changes: 6 additions & 0 deletions Make.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
misc/topdown_tailrec.v

-R theories stablesort
-R misc stablesort.misc

-arg -w -arg -notation-overridden
24 changes: 22 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,40 +1,60 @@
# KNOWNTARGETS will not be passed along to CoqMakefile
KNOWNTARGETS := Makefile.coq Makefile.benchmark.coq build-benchmark \
KNOWNTARGETS := Makefile.coq Makefile.misc.coq build-misc \
Makefile.benchmark.coq build-benchmark \
clean cleanall distclean
# KNOWNFILES will not get implicit targets from the final rule, and so
# depending on them won't invoke the submake
# Warning: These files get declared as PHONY, so any targets depending
# on them always get rebuilt
KNOWNFILES := Makefile Make Make.benchmark
KNOWNFILES := Makefile Make Make.misc Make.benchmark

.DEFAULT_GOAL := invoke-coqmakefile

COQMAKEFILE = $(COQBIN)coq_makefile
COQMAKE = $(MAKE) --no-print-directory -f Makefile.coq
COQMAKE_MISC = $(MAKE) --no-print-directory -f Makefile.misc.coq
COQMAKE_BENCHMARK = $(MAKE) --no-print-directory -f Makefile.benchmark.coq

Makefile.coq: Makefile Make
$(COQMAKEFILE) -f Make -o Makefile.coq

Makefile.misc.coq: Makefile Make.misc
$(COQMAKEFILE) -f Make.misc -o Makefile.misc.coq

Makefile.benchmark.coq: Makefile Make.benchmark
$(COQMAKEFILE) -f Make.benchmark -o Makefile.benchmark.coq

invoke-coqmakefile: Makefile.coq
$(COQMAKE) $(filter-out $(KNOWNTARGETS),$(MAKECMDGOALS))

build-misc: Makefile.misc.coq invoke-coqmakefile
$(COQMAKE_MISC)

build-benchmark: Makefile.benchmark.coq invoke-coqmakefile
$(COQMAKE_BENCHMARK)

theories/%.vo: Makefile.coq
$(COQMAKE) $@

misc/%.vo: Makefile.misc.coq
$(COQMAKE_MISC) $@

benchmark/%.vo: Makefile.benchmark.coq
$(COQMAKE_BENCHMARK) $@

clean::
@if [ -f Makefile.coq ]; then $(COQMAKE) clean; fi
@if [ -f Makefile.misc.coq ]; then $(COQMAKE_MISC) clean; fi
@if [ -f Makefile.benchmark.coq ]; then $(COQMAKE_BENCHMARK) clean; fi

cleanall::
@if [ -f Makefile.coq ]; then $(COQMAKE) cleanall; fi
@if [ -f Makefile.misc.coq ]; then $(COQMAKE_MISC) cleanall; fi
@if [ -f Makefile.benchmark.coq ]; then $(COQMAKE_BENCHMARK) cleanall; fi

distclean:: cleanall
rm -f Makefile.coq Makefile.coq.conf
rm -f Makefile.misc.coq Makefile.misc.coq.conf
rm -f Makefile.benchmark.coq Makefile.benchmark.coq.conf

.PHONY: invoke-coqmakefile $(KNOWNFILES)
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ slices in the input.
- Additional dependencies:
- [MathComp](https://math-comp.github.io) 1.13.0 or later
- [Paramcoq](https://github.com/coq-community/paramcoq) 1.1.3 or later
- [Mczify](https://github.com/math-comp/mczify) (required only for the test suite)
- [Equations](https://github.com/mattam82/Coq-Equations) (required only for the test suite)
- Coq namespace: `stablesort`
- Related publication(s):
- [A bargain for mergesorts (functional pearl) — How to prove your mergesort correct and stable, almost for free](https://arxiv.org/abs/2403.08173) doi:[10.48550/arXiv.2403.08173](https://doi.org/10.48550/arXiv.2403.08173)
Expand Down
5 changes: 5 additions & 0 deletions coq-stablesort.opam
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,16 @@ In addition, each of the above two kinds of mergesort functions has a smooth
slices in the input."""

build: [make "-j%{jobs}%"]
# The filter below has been added by hand to avoid running the test suite with
# Coq 8.17.
run-test: [ [make "-j%{jobs}%" "build-misc" ] { coq:version < "8.17~" | "8.18~" <= coq:version } ]
install: [make "install"]
depends: [
"coq" {>= "8.13"}
"coq-mathcomp-ssreflect" {>= "1.13.0"}
"coq-paramcoq" {>= "1.1.3"}
"coq-mathcomp-zify" {with-test}
"coq-equations" {with-test}
]

tags: [
Expand Down
18 changes: 16 additions & 2 deletions meta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,6 @@ tested_coq_opam_versions:
repo: 'mathcomp/mathcomp'
- version: '2.2.0-coq-dev'
repo: 'mathcomp/mathcomp'
- version: 'coq-8.17'
repo: 'mathcomp/mathcomp-dev'
- version: 'coq-8.18'
repo: 'mathcomp/mathcomp-dev'
- version: 'coq-8.19'
Expand All @@ -169,9 +167,25 @@ dependencies:
version: '{>= "1.1.3"}'
description: |-
[Paramcoq](https://github.com/coq-community/paramcoq) 1.1.3 or later
- opam:
name: coq-mathcomp-zify
version: '{with-test}'
description: |-
[Mczify](https://github.com/math-comp/mczify) (required only for the test suite)
- opam:
name: coq-equations
version: '{with-test}'
description: |-
[Equations](https://github.com/mattam82/Coq-Equations) (required only for the test suite)
test_target: build-misc
namespace: stablesort

action_appendix: |2-
export: 'OPAMWITHTEST'
env:
OPAMWITHTEST: true
documentation: |-
## Credits
The mergesort functions and the stability proofs provided in this library are
Expand Down
139 changes: 139 additions & 0 deletions misc/topdown_tailrec.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
(* A verified version of top-down tail-recursive mergesort, presented in *)
(* Sections 4.1 and 4.4.1 of the paper *)
From mathcomp Require Import ssreflect ssrfun ssrbool eqtype ssrnat seq path.
From mathcomp Require Import zify.
From stablesort Require Import param stablesort.
From Equations Require Import Equations.

Set Implicit Arguments.
Unset Strict Implicit.
Unset Printing Implicit Defensive.

Lemma if_nilp (T S : Type) (s : seq T) (x y : S) :
(if nilp s then x else y) = if s is [::] then x else y.
Proof. by case: s. Qed.

Section Revmerge.

Context (T : Type) (leT : rel T).

Fixpoint merge_rec (xs ys accu : seq T) : seq T :=
if xs is x :: xs' then
(fix merge_rec' (ys accu : seq T) :=
if ys is y :: ys' then
if leT x y then
merge_rec xs' ys (x :: accu) else merge_rec' ys' (y :: accu)
else
catrev xs accu) ys accu
else catrev ys accu.

Definition revmerge (xs ys : seq T) : seq T := merge_rec xs ys [::].

Lemma revmergeE (xs ys : seq T) : revmerge xs ys = rev (merge leT xs ys).
Proof.
rewrite /revmerge /rev; move: xs ys [::].
by elim=> [|x xs IHxs]; elim=> [|y ys IHys] accu //=; case: ifP => /=.
Qed.

End Revmerge.

Module Abstract.
Section Abstract.

Context (T R : Type) (leT : rel T).
Context (merge merge' : R -> R -> R) (singleton : T -> R) (empty : R).

(* The abstract top-down tail-recursive mergesort (Section 4.4.1) *)
Equations sort_rec (xs : seq T) (b : bool) (n fuel : nat) :
R * seq T by struct fuel :=
(* The following three cases ar absurd because [0 < n <= size xs] and *)
(* [n <= fuel] should hold. Nevertheless, we add them to make [sort_rec] *)
(* total and to make its correctness proof easier. *)
sort_rec xs _ _ 0 => (empty, xs);
sort_rec xs _ 0 _ => (empty, xs);
sort_rec [::] _ _ _ => (empty, [::]);
(* end absurd cases *)
sort_rec (x :: xs) _ 1 _ => (singleton x, xs);
sort_rec xs b n fuel.+1 =>
let n1 := n./2 in
let (s1, xs') := sort_rec xs (~~ b) n1 fuel in
let (s2, xs'') := sort_rec xs' (~~ b) (n - n1) fuel in
((if b then merge' s1 s2 else merge s1 s2), xs'').

#[using="All"]
Definition sort (xs : seq T) : R :=
if xs is [::] then empty else let n := size xs in (sort_rec xs true n n).1.

End Abstract.

Parametricity sort.

Check warning on line 69 in misc/topdown_tailrec.v

View workflow job for this annotation

GitHub Actions / build (mathcomp/mathcomp:2.0.0-coq-8.18)

Notation sub is deprecated since mathcomp 2.0.0. Use Sub instead.

Check warning on line 69 in misc/topdown_tailrec.v

View workflow job for this annotation

GitHub Actions / build (mathcomp/mathcomp:2.1.0-coq-8.18)

Notation sub is deprecated since mathcomp 2.0.0. Use Sub instead.

Check warning on line 69 in misc/topdown_tailrec.v

View workflow job for this annotation

GitHub Actions / build (mathcomp/mathcomp:2.2.0-coq-8.18)

Notation sub is deprecated since mathcomp 2.0.0. Use Sub instead.

End Abstract.

Section Concrete.

Context (T : Type) (leT : rel T).
Let geT x y := leT y x.

(* The concrete top-down tail-recursive mergesort (Section 4.1) *)
Equations sort_rec (xs : seq T) (b : bool) (n fuel : nat) :
seq T * seq T by struct fuel :=
(* begin absurd cases (cf. Abstract.sort_rec) *)
sort_rec xs _ _ 0 => ([::], xs);
sort_rec xs _ 0 _ => ([::], xs);
sort_rec [::] _ _ _ => ([::], [::]);
(* end absurd cases *)
sort_rec (x :: xs) _ 1 _ => ([:: x], xs);
sort_rec xs b n fuel.+1 =>
let n1 := n./2 in
let (s1, xs') := sort_rec xs (~~ b) n1 fuel in
let (s2, xs'') := sort_rec xs' (~~ b) (n - n1) fuel in
(if b then revmerge geT s2 s1 else revmerge leT s1 s2, xs'').

Definition sort (xs : seq T) : seq T :=
if xs is [::] then [::] else let n := size xs in (sort_rec xs true n n).1.

Notation merge := (path.merge leT) (only parsing).
Notation merge' :=
(fun xs ys => rev (path.merge geT (rev ys) (rev xs))) (only parsing).

(* The proof of Equation (5) *)
Lemma asort_mergeE :
Abstract.sort leT merge merge' (fun x => [:: x]) [::] =1 sort.
Proof.
rewrite /Abstract.sort /sort => xs; rewrite -!if_nilp.
congr (if _ then _ else _.1).
pose condrev b (p : seq T * seq T) := ((if b then p.1 else rev p.1), p.2).
set rhs := RHS; have ->: rhs = condrev true rhs by case: rhs.
rewrite {}/rhs; move: {2 4}(size xs) => fuel.
apply_funelim (sort_rec xs true (size xs) fuel);
try by move=> *; case: (b in condrev b).
move=> x {}xs b n {}fuel IHl IHr.
rewrite Abstract.sort_rec_equation_5 /= {}IHl /= {IHr}(IHr [::]) /=.
case: (sort_rec (x :: xs)) => s1 xs' /=; case: sort_rec => s2 xs'' /=.
by rewrite !revmergeE /condrev; case: b; rewrite /= !revK.
Qed.

(* The proof of Equation (6) *)
Lemma asort_catE : Abstract.sort leT cat cat (fun x => [:: x]) [::] =1 id.
Proof.
rewrite /Abstract.sort => xs.
rewrite (_ : Abstract.sort_rec _ _ _ _ _ _ _ _ =
(take (size xs) xs, drop (size xs) xs)).
by rewrite take_size; case: xs.
move: {2 4}(size xs) (leqnn (size xs)) => fuel.
apply_funelim
(Abstract.sort_rec cat cat (fun x => [:: x]) [::] xs true (size xs) fuel).
- by move=> {}xs _ [] //; rewrite take0 drop0.
- by move=> {}xs; rewrite take0 drop0.
- by [].
- by move=> x {}xs; rewrite /= take0 drop0.
move=> x {}xs b n {}fuel IHl IHr; rewrite ltnS => n_lt_fuel.
rewrite [LHS]/= {}IHl 1?{}(IHr [::]) 1?if_same; try lia.
rewrite -takeD drop_drop; congr (take _ _, drop _ _); lia.
Qed.

End Concrete.

Canonical sort_stable :=
StableSort sort Abstract.sort Abstract.sort_R asort_mergeE asort_catE.
1 change: 1 addition & 0 deletions theories/param.v
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Parametricity False.
Parametricity eq.
Parametricity or.
Parametricity Acc.
Parametricity unit.
Parametricity bool.
Parametricity option.
Parametricity prod.
Expand Down

0 comments on commit d86a047

Please sign in to comment.