all: change from sort functions to slices functions

The sorting functions in the slices package are slightly faster as they
don't use reflection internally.

Change-Id: I3c0ab61336d44c1928ee2a9da12a00d914f5e637
Reviewed-on: https://go-review.googlesource.com/c/mod/+/625635
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Michael Matloob <matloob@golang.org>
Reviewed-by: Michael Matloob <matloob@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/modfile/rule.go b/modfile/rule.go
index 3e4a1d0..1a6f3ef 100644
--- a/modfile/rule.go
+++ b/modfile/rule.go
@@ -20,10 +20,11 @@
 package modfile
 
 import (
+	"cmp"
 	"errors"
 	"fmt"
 	"path/filepath"
-	"sort"
+	"slices"
 	"strconv"
 	"strings"
 	"unicode"
@@ -1633,15 +1634,13 @@
 		if !ok {
 			continue
 		}
-		less := lineLess
+		less := compareLine
 		if block.Token[0] == "exclude" && useSemanticSortForExclude {
-			less = lineExcludeLess
+			less = compareLineExclude
 		} else if block.Token[0] == "retract" {
-			less = lineRetractLess
+			less = compareLineRetract
 		}
-		sort.SliceStable(block.Line, func(i, j int) bool {
-			return less(block.Line[i], block.Line[j])
-		})
+		slices.SortStableFunc(block.Line, less)
 	}
 }
 
@@ -1746,39 +1745,38 @@
 	syntax.Stmt = stmts
 }
 
-// lineLess returns whether li should be sorted before lj. It sorts
-// lexicographically without assigning any special meaning to tokens.
-func lineLess(li, lj *Line) bool {
+// compareLine compares li and lj. It sorts lexicographically without assigning
+// any special meaning to tokens.
+func compareLine(li, lj *Line) int {
 	for k := 0; k < len(li.Token) && k < len(lj.Token); k++ {
 		if li.Token[k] != lj.Token[k] {
-			return li.Token[k] < lj.Token[k]
+			return cmp.Compare(li.Token[k], lj.Token[k])
 		}
 	}
-	return len(li.Token) < len(lj.Token)
+	return cmp.Compare(len(li.Token), len(lj.Token))
 }
 
-// lineExcludeLess reports whether li should be sorted before lj for lines in
-// an "exclude" block.
-func lineExcludeLess(li, lj *Line) bool {
+// compareLineExclude compares li and lj for lines in an "exclude" block.
+func compareLineExclude(li, lj *Line) int {
 	if len(li.Token) != 2 || len(lj.Token) != 2 {
 		// Not a known exclude specification.
 		// Fall back to sorting lexicographically.
-		return lineLess(li, lj)
+		return compareLine(li, lj)
 	}
 	// An exclude specification has two tokens: ModulePath and Version.
 	// Compare module path by string order and version by semver rules.
 	if pi, pj := li.Token[0], lj.Token[0]; pi != pj {
-		return pi < pj
+		return cmp.Compare(pi, pj)
 	}
-	return semver.Compare(li.Token[1], lj.Token[1]) < 0
+	return semver.Compare(li.Token[1], lj.Token[1])
 }
 
-// lineRetractLess returns whether li should be sorted before lj for lines in
-// a "retract" block. It treats each line as a version interval. Single versions
-// are compared as if they were intervals with the same low and high version.
+// compareLineRetract compares li and lj for lines in a "retract" block.
+// It treats each line as a version interval. Single versions are compared as
+// if they were intervals with the same low and high version.
 // Intervals are sorted in descending order, first by low version, then by
-// high version, using semver.Compare.
-func lineRetractLess(li, lj *Line) bool {
+// high version, using [semver.Compare].
+func compareLineRetract(li, lj *Line) int {
 	interval := func(l *Line) VersionInterval {
 		if len(l.Token) == 1 {
 			return VersionInterval{Low: l.Token[0], High: l.Token[0]}
@@ -1792,9 +1790,9 @@
 	vii := interval(li)
 	vij := interval(lj)
 	if cmp := semver.Compare(vii.Low, vij.Low); cmp != 0 {
-		return cmp > 0
+		return -cmp
 	}
-	return semver.Compare(vii.High, vij.High) > 0
+	return -semver.Compare(vii.High, vij.High)
 }
 
 // checkCanonicalVersion returns a non-nil error if vers is not a canonical
diff --git a/modfile/work.go b/modfile/work.go
index 5387d0c..6f92644 100644
--- a/modfile/work.go
+++ b/modfile/work.go
@@ -6,7 +6,7 @@
 
 import (
 	"fmt"
-	"sort"
+	"slices"
 	"strings"
 )
 
@@ -315,9 +315,7 @@
 		if !ok {
 			continue
 		}
-		sort.SliceStable(block.Line, func(i, j int) bool {
-			return lineLess(block.Line[i], block.Line[j])
-		})
+		slices.SortStableFunc(block.Line, compareLine)
 	}
 }
 
diff --git a/module/module.go b/module/module.go
index 2a364b2..16e1aa7 100644
--- a/module/module.go
+++ b/module/module.go
@@ -96,10 +96,11 @@
 // Changes to the semantics in this file require approval from rsc.
 
 import (
+	"cmp"
 	"errors"
 	"fmt"
 	"path"
-	"sort"
+	"slices"
 	"strings"
 	"unicode"
 	"unicode/utf8"
@@ -657,17 +658,15 @@
 // optionally followed by a tie-breaking suffix introduced by a slash character,
 // like in "v0.0.1/go.mod".
 func Sort(list []Version) {
-	sort.Slice(list, func(i, j int) bool {
-		mi := list[i]
-		mj := list[j]
-		if mi.Path != mj.Path {
-			return mi.Path < mj.Path
+	slices.SortFunc(list, func(i, j Version) int {
+		if i.Path != j.Path {
+			return strings.Compare(i.Path, j.Path)
 		}
 		// To help go.sum formatting, allow version/file.
 		// Compare semver prefix by semver rules,
 		// file by string order.
-		vi := mi.Version
-		vj := mj.Version
+		vi := i.Version
+		vj := j.Version
 		var fi, fj string
 		if k := strings.Index(vi, "/"); k >= 0 {
 			vi, fi = vi[:k], vi[k:]
@@ -676,9 +675,9 @@
 			vj, fj = vj[:k], vj[k:]
 		}
 		if vi != vj {
-			return semver.Compare(vi, vj) < 0
+			return semver.Compare(vi, vj)
 		}
-		return fi < fj
+		return cmp.Compare(fi, fj)
 	})
 }
 
diff --git a/semver/semver.go b/semver/semver.go
index 9a2dfd3..628f8fd 100644
--- a/semver/semver.go
+++ b/semver/semver.go
@@ -22,7 +22,10 @@
 // as shorthands for vMAJOR.0.0 and vMAJOR.MINOR.0.
 package semver
 
-import "sort"
+import (
+	"slices"
+	"strings"
+)
 
 // parsed returns the parsed form of a semantic version string.
 type parsed struct {
@@ -154,19 +157,22 @@
 // ByVersion implements [sort.Interface] for sorting semantic version strings.
 type ByVersion []string
 
-func (vs ByVersion) Len() int      { return len(vs) }
-func (vs ByVersion) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
-func (vs ByVersion) Less(i, j int) bool {
-	cmp := Compare(vs[i], vs[j])
-	if cmp != 0 {
-		return cmp < 0
-	}
-	return vs[i] < vs[j]
+func (vs ByVersion) Len() int           { return len(vs) }
+func (vs ByVersion) Swap(i, j int)      { vs[i], vs[j] = vs[j], vs[i] }
+func (vs ByVersion) Less(i, j int) bool { return compareVersion(vs[i], vs[j]) < 0 }
+
+// Sort sorts a list of semantic version strings using [Compare] and falls back
+// to use [strings.Compare] if both versions are considered equal.
+func Sort(list []string) {
+	slices.SortFunc(list, compareVersion)
 }
 
-// Sort sorts a list of semantic version strings using [ByVersion].
-func Sort(list []string) {
-	sort.Sort(ByVersion(list))
+func compareVersion(a, b string) int {
+	cmp := Compare(a, b)
+	if cmp != 0 {
+		return cmp
+	}
+	return strings.Compare(a, b)
 }
 
 func parse(v string) (p parsed, ok bool) {
diff --git a/semver/semver_test.go b/semver/semver_test.go
index 937e18e..56739ad 100644
--- a/semver/semver_test.go
+++ b/semver/semver_test.go
@@ -6,6 +6,7 @@
 
 import (
 	"math/rand"
+	"slices"
 	"sort"
 	"strings"
 	"testing"
@@ -166,6 +167,44 @@
 	if !sort.IsSorted(ByVersion(versions)) {
 		t.Errorf("list is not sorted:\n%s", strings.Join(versions, "\n"))
 	}
+
+	golden := []string{
+		"bad",
+		"v1+meta",
+		"v1-alpha.beta.gamma",
+		"v1-pre",
+		"v1-pre+meta",
+		"v1.2+meta",
+		"v1.2-pre",
+		"v1.2-pre+meta",
+		"v1.0.0-alpha",
+		"v1.0.0-alpha.1",
+		"v1.0.0-alpha.beta",
+		"v1.0.0-beta",
+		"v1.0.0-beta.2",
+		"v1.0.0-beta.11",
+		"v1.0.0-rc.1",
+		"v1",
+		"v1.0",
+		"v1.0.0",
+		"v1.2",
+		"v1.2.0",
+		"v1.2.3-456",
+		"v1.2.3-456.789",
+		"v1.2.3-456-789",
+		"v1.2.3-456a",
+		"v1.2.3-pre",
+		"v1.2.3-pre+meta",
+		"v1.2.3-pre.1",
+		"v1.2.3-zzz",
+		"v1.2.3",
+		"v1.2.3+meta",
+		"v1.2.3+meta-pre",
+		"v1.2.3+meta-pre.sha.256a",
+	}
+	if !slices.Equal(versions, golden) {
+		t.Errorf("list is not sorted correctly\ngot:\n%v\nwant:\n%v", versions, golden)
+	}
 }
 
 func TestMax(t *testing.T) {
diff --git a/sumdb/dirhash/hash.go b/sumdb/dirhash/hash.go
index 51ec4db..117985a 100644
--- a/sumdb/dirhash/hash.go
+++ b/sumdb/dirhash/hash.go
@@ -16,7 +16,7 @@
 	"io"
 	"os"
 	"path/filepath"
-	"sort"
+	"slices"
 	"strings"
 )
 
@@ -36,7 +36,7 @@
 //	sha256sum $(find . -type f | sort) | sha256sum
 //
 // More precisely, the hashed summary contains a single line for each file in the list,
-// ordered by sort.Strings applied to the file names, where each line consists of
+// ordered by [slices.Sort] applied to the file names, where each line consists of
 // the hexadecimal SHA-256 hash of the file content,
 // two spaces (U+0020), the file name, and a newline (U+000A).
 //
@@ -44,7 +44,7 @@
 func Hash1(files []string, open func(string) (io.ReadCloser, error)) (string, error) {
 	h := sha256.New()
 	files = append([]string(nil), files...)
-	sort.Strings(files)
+	slices.Sort(files)
 	for _, file := range files {
 		if strings.Contains(file, "\n") {
 			return "", errors.New("dirhash: filenames with newlines are not supported")