From 2c969aaf6dac63114de772fb6b328a65acfe0030 Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Sun, 27 Jul 2025 15:45:08 -0700 Subject: [PATCH 1/7] Implement sort function for Glojure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add SortSlice function in pkg/lang/sort.go that performs stable in-place sorting - Add Compare function to support Clojure's compare semantics (nil handling, cross-type numeric comparison) - Update ToSlice to handle all required types: nil→empty, IPersistentVector, IPersistentMap, string→char array - Add transformation in rewrite.clj to replace java.util.Arrays.sort with SortSlice - Add transformation for clojure.lang.Util.compare to use Compare function The implementation matches Clojure JVM semantics: - Stable sort (equal elements maintain order) - Comparator contract (-1/0/1 return values) - Proper nil handling (nil sorts before non-nil) - Support for custom comparators --- Makefile | 7 ++ internal/deps/pull.go | 2 - pkg/gen/gljimports/gljimports_darwin_amd64.go | 2 + pkg/gen/gljimports/gljimports_darwin_arm64.go | 2 + pkg/gen/gljimports/gljimports_js_wasm.go | 2 + pkg/gen/gljimports/gljimports_linux_amd64.go | 2 + pkg/gen/gljimports/gljimports_linux_arm64.go | 2 + .../gljimports/gljimports_windows_amd64.go | 2 + pkg/gen/gljimports/gljimports_windows_arm.go | 2 + pkg/lang/slices.go | 47 +++++++- pkg/lang/sort.go | 104 ++++++++++++++++++ pkg/stdlib/glojure/core.glj | 4 +- scripts/rewrite-core/rewrite.clj | 6 + 13 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 pkg/lang/sort.go diff --git a/Makefile b/Makefile index db4f4548..51cf3cee 100644 --- a/Makefile +++ b/Makefile @@ -60,3 +60,10 @@ $(TEST_TARGETS): gocmd .PHONY: test test: vet $(TEST_TARGETS) + +.PHONY: format +format: + @if go fmt ./... | grep -q .; then \ + echo "Files were formatted. Please commit the changes."; \ + exit 1; \ + fi diff --git a/internal/deps/pull.go b/internal/deps/pull.go index 594830d3..3072c424 100644 --- a/internal/deps/pull.go +++ b/internal/deps/pull.go @@ -1,3 +1 @@ package deps - - diff --git a/pkg/gen/gljimports/gljimports_darwin_amd64.go b/pkg/gen/gljimports/gljimports_darwin_amd64.go index a2aec26b..9e7c3739 100644 --- a/pkg/gen/gljimports/gljimports_darwin_amd64.go +++ b/pkg/gen/gljimports/gljimports_darwin_amd64.go @@ -3466,6 +3466,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.CloneThreadBindingFrame", github_com_glojurelang_glojure_pkg_lang.CloneThreadBindingFrame) + _register("github.com/glojurelang/glojure/pkg/lang.Compare", github_com_glojurelang_glojure_pkg_lang.Compare) _register("github.com/glojurelang/glojure/pkg/lang.Comparer", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Comparer)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.ConcatStrings", github_com_glojurelang_glojure_pkg_lang.ConcatStrings) _register("github.com/glojurelang/glojure/pkg/lang.Conj", github_com_glojurelang_glojure_pkg_lang.Conj) @@ -3888,6 +3889,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.SliceSet", github_com_glojurelang_glojure_pkg_lang.SliceSet) + _register("github.com/glojurelang/glojure/pkg/lang.SortSlice", github_com_glojurelang_glojure_pkg_lang.SortSlice) _register("github.com/glojurelang/glojure/pkg/lang.StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.Stacker", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Stacker)(nil)).Elem()) diff --git a/pkg/gen/gljimports/gljimports_darwin_arm64.go b/pkg/gen/gljimports/gljimports_darwin_arm64.go index ab06fe1e..f4133476 100644 --- a/pkg/gen/gljimports/gljimports_darwin_arm64.go +++ b/pkg/gen/gljimports/gljimports_darwin_arm64.go @@ -3466,6 +3466,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.CloneThreadBindingFrame", github_com_glojurelang_glojure_pkg_lang.CloneThreadBindingFrame) + _register("github.com/glojurelang/glojure/pkg/lang.Compare", github_com_glojurelang_glojure_pkg_lang.Compare) _register("github.com/glojurelang/glojure/pkg/lang.Comparer", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Comparer)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.ConcatStrings", github_com_glojurelang_glojure_pkg_lang.ConcatStrings) _register("github.com/glojurelang/glojure/pkg/lang.Conj", github_com_glojurelang_glojure_pkg_lang.Conj) @@ -3888,6 +3889,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.SliceSet", github_com_glojurelang_glojure_pkg_lang.SliceSet) + _register("github.com/glojurelang/glojure/pkg/lang.SortSlice", github_com_glojurelang_glojure_pkg_lang.SortSlice) _register("github.com/glojurelang/glojure/pkg/lang.StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.Stacker", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Stacker)(nil)).Elem()) diff --git a/pkg/gen/gljimports/gljimports_js_wasm.go b/pkg/gen/gljimports/gljimports_js_wasm.go index e99d3d9b..adfec5ae 100644 --- a/pkg/gen/gljimports/gljimports_js_wasm.go +++ b/pkg/gen/gljimports/gljimports_js_wasm.go @@ -3466,6 +3466,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.CloneThreadBindingFrame", github_com_glojurelang_glojure_pkg_lang.CloneThreadBindingFrame) + _register("github.com/glojurelang/glojure/pkg/lang.Compare", github_com_glojurelang_glojure_pkg_lang.Compare) _register("github.com/glojurelang/glojure/pkg/lang.Comparer", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Comparer)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.ConcatStrings", github_com_glojurelang_glojure_pkg_lang.ConcatStrings) _register("github.com/glojurelang/glojure/pkg/lang.Conj", github_com_glojurelang_glojure_pkg_lang.Conj) @@ -3888,6 +3889,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.SliceSet", github_com_glojurelang_glojure_pkg_lang.SliceSet) + _register("github.com/glojurelang/glojure/pkg/lang.SortSlice", github_com_glojurelang_glojure_pkg_lang.SortSlice) _register("github.com/glojurelang/glojure/pkg/lang.StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.Stacker", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Stacker)(nil)).Elem()) diff --git a/pkg/gen/gljimports/gljimports_linux_amd64.go b/pkg/gen/gljimports/gljimports_linux_amd64.go index e5310d57..9d7331e7 100644 --- a/pkg/gen/gljimports/gljimports_linux_amd64.go +++ b/pkg/gen/gljimports/gljimports_linux_amd64.go @@ -3466,6 +3466,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.CloneThreadBindingFrame", github_com_glojurelang_glojure_pkg_lang.CloneThreadBindingFrame) + _register("github.com/glojurelang/glojure/pkg/lang.Compare", github_com_glojurelang_glojure_pkg_lang.Compare) _register("github.com/glojurelang/glojure/pkg/lang.Comparer", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Comparer)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.ConcatStrings", github_com_glojurelang_glojure_pkg_lang.ConcatStrings) _register("github.com/glojurelang/glojure/pkg/lang.Conj", github_com_glojurelang_glojure_pkg_lang.Conj) @@ -3888,6 +3889,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.SliceSet", github_com_glojurelang_glojure_pkg_lang.SliceSet) + _register("github.com/glojurelang/glojure/pkg/lang.SortSlice", github_com_glojurelang_glojure_pkg_lang.SortSlice) _register("github.com/glojurelang/glojure/pkg/lang.StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.Stacker", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Stacker)(nil)).Elem()) diff --git a/pkg/gen/gljimports/gljimports_linux_arm64.go b/pkg/gen/gljimports/gljimports_linux_arm64.go index 852fcb1c..7e427803 100644 --- a/pkg/gen/gljimports/gljimports_linux_arm64.go +++ b/pkg/gen/gljimports/gljimports_linux_arm64.go @@ -3466,6 +3466,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.CloneThreadBindingFrame", github_com_glojurelang_glojure_pkg_lang.CloneThreadBindingFrame) + _register("github.com/glojurelang/glojure/pkg/lang.Compare", github_com_glojurelang_glojure_pkg_lang.Compare) _register("github.com/glojurelang/glojure/pkg/lang.Comparer", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Comparer)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.ConcatStrings", github_com_glojurelang_glojure_pkg_lang.ConcatStrings) _register("github.com/glojurelang/glojure/pkg/lang.Conj", github_com_glojurelang_glojure_pkg_lang.Conj) @@ -3888,6 +3889,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.SliceSet", github_com_glojurelang_glojure_pkg_lang.SliceSet) + _register("github.com/glojurelang/glojure/pkg/lang.SortSlice", github_com_glojurelang_glojure_pkg_lang.SortSlice) _register("github.com/glojurelang/glojure/pkg/lang.StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.Stacker", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Stacker)(nil)).Elem()) diff --git a/pkg/gen/gljimports/gljimports_windows_amd64.go b/pkg/gen/gljimports/gljimports_windows_amd64.go index 8376188e..6de5d4b4 100644 --- a/pkg/gen/gljimports/gljimports_windows_amd64.go +++ b/pkg/gen/gljimports/gljimports_windows_amd64.go @@ -3466,6 +3466,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.CloneThreadBindingFrame", github_com_glojurelang_glojure_pkg_lang.CloneThreadBindingFrame) + _register("github.com/glojurelang/glojure/pkg/lang.Compare", github_com_glojurelang_glojure_pkg_lang.Compare) _register("github.com/glojurelang/glojure/pkg/lang.Comparer", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Comparer)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.ConcatStrings", github_com_glojurelang_glojure_pkg_lang.ConcatStrings) _register("github.com/glojurelang/glojure/pkg/lang.Conj", github_com_glojurelang_glojure_pkg_lang.Conj) @@ -3888,6 +3889,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.SliceSet", github_com_glojurelang_glojure_pkg_lang.SliceSet) + _register("github.com/glojurelang/glojure/pkg/lang.SortSlice", github_com_glojurelang_glojure_pkg_lang.SortSlice) _register("github.com/glojurelang/glojure/pkg/lang.StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.Stacker", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Stacker)(nil)).Elem()) diff --git a/pkg/gen/gljimports/gljimports_windows_arm.go b/pkg/gen/gljimports/gljimports_windows_arm.go index 100d9ee4..ff2506e9 100644 --- a/pkg/gen/gljimports/gljimports_windows_arm.go +++ b/pkg/gen/gljimports/gljimports_windows_arm.go @@ -3466,6 +3466,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*ChunkedCons", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.ChunkedCons)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.CloneThreadBindingFrame", github_com_glojurelang_glojure_pkg_lang.CloneThreadBindingFrame) + _register("github.com/glojurelang/glojure/pkg/lang.Compare", github_com_glojurelang_glojure_pkg_lang.Compare) _register("github.com/glojurelang/glojure/pkg/lang.Comparer", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Comparer)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.ConcatStrings", github_com_glojurelang_glojure_pkg_lang.ConcatStrings) _register("github.com/glojurelang/glojure/pkg/lang.Conj", github_com_glojurelang_glojure_pkg_lang.Conj) @@ -3888,6 +3889,7 @@ func RegisterImports(_register func(string, interface{})) { _register("github.com/glojurelang/glojure/pkg/lang.SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*SliceSeq", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.SliceSeq)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.SliceSet", github_com_glojurelang_glojure_pkg_lang.SliceSet) + _register("github.com/glojurelang/glojure/pkg/lang.SortSlice", github_com_glojurelang_glojure_pkg_lang.SortSlice) _register("github.com/glojurelang/glojure/pkg/lang.StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil)).Elem()) _register("github.com/glojurelang/glojure/pkg/lang.*StackFrame", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.StackFrame)(nil))) _register("github.com/glojurelang/glojure/pkg/lang.Stacker", reflect.TypeOf((*github_com_glojurelang_glojure_pkg_lang.Stacker)(nil)).Elem()) diff --git a/pkg/lang/slices.go b/pkg/lang/slices.go index 469cde58..bb4a3513 100644 --- a/pkg/lang/slices.go +++ b/pkg/lang/slices.go @@ -11,9 +11,48 @@ func SliceSet(slc any, idx int, val any) { } func ToSlice(x any) []any { + // Handle nil - Clojure returns empty array for nil if IsNil(x) { - return nil + return []any{} } + + // Handle []any - return as-is + if slice, ok := x.([]any); ok { + return slice + } + + // Handle IPersistentVector + if vec, ok := x.(IPersistentVector); ok { + count := vec.Count() + res := make([]any, count) + for i := 0; i < count; i++ { + res[i] = vec.Nth(i) + } + return res + } + + // Handle IPersistentMap - convert to array of MapEntry objects + if m, ok := x.(IPersistentMap); ok { + seq := m.Seq() + res := make([]any, 0, m.Count()) + for seq != nil { + res = append(res, seq.First()) // Each element is a MapEntry + seq = seq.Next() + } + return res + } + + // Handle string - convert to character array + if s, ok := x.(string); ok { + runes := []rune(s) // Important: use runes for proper Unicode handling + res := make([]any, len(runes)) + for i, ch := range runes { + res[i] = NewChar(ch) // Convert each rune to Char + } + return res + } + + // Handle ISeq if s, ok := x.(ISeq); ok { res := make([]interface{}, 0, Count(x)) for s := Seq(s); s != nil; s = s.Next() { @@ -21,6 +60,8 @@ func ToSlice(x any) []any { } return res } + + // Handle reflection-based slice/array xVal := reflect.ValueOf(x) if xVal.Kind() == reflect.Slice || xVal.Kind() == reflect.Array { res := make([]interface{}, xVal.Len()) @@ -29,5 +70,7 @@ func ToSlice(x any) []any { } return res } - panic(fmt.Errorf("ToSlice not supported on type: %T", x)) + + // Error with Clojure-style message + panic(NewIllegalArgumentError(fmt.Sprintf("Unable to convert: %T to Object[]", x))) } diff --git a/pkg/lang/sort.go b/pkg/lang/sort.go new file mode 100644 index 00000000..2fe2e8b5 --- /dev/null +++ b/pkg/lang/sort.go @@ -0,0 +1,104 @@ +package lang + +import ( + "fmt" + "sort" +) + +// SortSlice performs an in-place stable sort on the given array using the provided comparator. +// This matches java.util.Arrays.sort semantics: +// - Stable sort (equal elements maintain their relative order) +// - In-place modification of the array +// - Comparator returns -1 for less than, 0 for equal, 1 for greater than +func SortSlice(slice []any, comp any) error { + // comp is a Clojure function that acts as a comparator + compFn, ok := comp.(IFn) + if !ok { + panic(NewIllegalArgumentError("Comparator must be a function")) + } + + // Use sort.SliceStable for stable sorting (maintains relative order of equal elements) + sort.SliceStable(slice, func(i, j int) bool { + // Call the comparator function with the two elements + result := compFn.Invoke(slice[i], slice[j]) + + // Comparator returns: + // -1 if first arg is less than second + // 0 if args are equal + // 1 if first arg is greater than second + // We return true for "less than" case + resultInt, ok := AsInt(result) + if !ok { + panic(NewIllegalArgumentError(fmt.Sprintf("Comparator must return a number, got %T", result))) + } + return resultInt < 0 + }) + + return nil +} + +// Compare implements Clojure's compare function. +// Returns a negative number, zero, or a positive number when x is logically +// 'less than', 'equal to', or 'greater than' y. +// Handles nil values (nil is less than everything except nil). +func Compare(x, y any) int { + // Handle nil cases first + if IsNil(x) { + if IsNil(y) { + return 0 + } + return -1 + } + if IsNil(y) { + return 1 + } + + // Handle numbers - convert to float64 for comparison + xNum, xIsNum := AsNumber(x) + yNum, yIsNum := AsNumber(y) + if xIsNum && yIsNum { + xFloat := AsFloat64(xNum) + yFloat := AsFloat64(yNum) + if xFloat < yFloat { + return -1 + } else if xFloat > yFloat { + return 1 + } + return 0 + } + + // Handle strings + if xStr, xOk := x.(string); xOk { + if yStr, yOk := y.(string); yOk { + if xStr < yStr { + return -1 + } else if xStr > yStr { + return 1 + } + return 0 + } + } + + // Handle keywords + if xKw, xOk := x.(Keyword); xOk { + if yKw, yOk := y.(Keyword); yOk { + return Compare(xKw.String(), yKw.String()) + } + } + + // Handle symbols + if xSym, xOk := x.(Symbol); xOk { + if ySym, yOk := y.(Symbol); yOk { + // Compare namespace first + nsComp := Compare(xSym.Namespace(), ySym.Namespace()) + if nsComp != 0 { + return nsComp + } + // Then compare name + return Compare(xSym.Name(), ySym.Name()) + } + } + + // If we can't compare, panic with an error + panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare %T with %T", x, y))) +} diff --git a/pkg/stdlib/glojure/core.glj b/pkg/stdlib/glojure/core.glj index 8f907ecd..4fc89cdd 100644 --- a/pkg/stdlib/glojure/core.glj +++ b/pkg/stdlib/glojure/core.glj @@ -830,7 +830,7 @@ { :inline (fn [x y] `(. glojure.lang.Util compare ~x ~y)) :added "1.0"} - [x y] (. glojure.lang.Util (compare x y))) + [x y] (github.com$glojurelang$glojure$pkg$lang.Compare x y)) (defmacro and "Evaluates exprs one at a time, from left to right. If a form @@ -3090,7 +3090,7 @@ ([^java.util.Comparator comp coll] (if (seq coll) (let [a (to-array coll)] - (. java.util.Arrays (sort a comp)) + (github.com$glojurelang$glojure$pkg$lang.SortSlice a comp) (with-meta (seq a) (meta coll))) ()))) diff --git a/scripts/rewrite-core/rewrite.clj b/scripts/rewrite-core/rewrite.clj index 506b4241..d9d18e74 100644 --- a/scripts/rewrite-core/rewrite.clj +++ b/scripts/rewrite-core/rewrite.clj @@ -435,6 +435,8 @@ ;; builtin "Equals" (sexpr-replace 'clojure.lang.Util/equiv 'github.com$glojurelang$glojure$pkg$lang.Equiv) (sexpr-replace 'clojure.lang.Util/equals 'github.com$glojurelang$glojure$pkg$lang.Equals) + (sexpr-replace '(. clojure.lang.Util (compare x y)) '(github.com$glojurelang$glojure$pkg$lang.Compare x y)) + (sexpr-replace '(. x (meta)) '(.Meta x)) (sexpr-replace 'clojure.lang.Symbol/intern 'github.com$glojurelang$glojure$pkg$lang.NewSymbol) @@ -938,6 +940,10 @@ (node-replace "(.pattern ^java.util.regex.Pattern p)" "(.String ^regexp.*Regexp p)") + ;; Arrays.sort replacement for Glojure sort function + (sexpr-replace '(. java.util.Arrays (sort a comp)) + '(github.com$glojurelang$glojure$pkg$lang.SortSlice a comp)) + ])) (defn rewrite-core [zloc] From f90f9c7213b3a095bb8916ffc635ef9bb7b33dc5 Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Fri, 1 Aug 2025 21:13:55 -0700 Subject: [PATCH 2/7] Add sort tests Signed-off-by: James Hamlin --- internal/persistent/vector/vector.go | 8 +- pkg/lang/slices.go | 11 ++ pkg/lang/sort.go | 57 +++++++- test/glojure/test_glojure/sort.glj | 201 +++++++++++++++++++++++++++ 4 files changed, 266 insertions(+), 11 deletions(-) create mode 100644 test/glojure/test_glojure/sort.glj diff --git a/internal/persistent/vector/vector.go b/internal/persistent/vector/vector.go index d460496e..31ead381 100644 --- a/internal/persistent/vector/vector.go +++ b/internal/persistent/vector/vector.go @@ -46,10 +46,10 @@ type Vector interface { // Iterator is an iterator over vector elements. It can be used like this: // -// for it := v.Iterator(); it.HasElem(); it.Next() { -// elem := it.Elem() -// // do something with elem... -// } +// for it := v.Iterator(); it.HasElem(); it.Next() { +// elem := it.Elem() +// // do something with elem... +// } type Iterator interface { // Elem returns the element at the current position. Elem() interface{} diff --git a/pkg/lang/slices.go b/pkg/lang/slices.go index bb4a3513..bb99c661 100644 --- a/pkg/lang/slices.go +++ b/pkg/lang/slices.go @@ -42,6 +42,17 @@ func ToSlice(x any) []any { return res } + // Handle Set - convert to array of values + if s, ok := x.(*Set); ok { + seq := s.Seq() + res := make([]any, 0, s.Count()) + for seq != nil { + res = append(res, seq.First()) + seq = seq.Next() + } + return res + } + // Handle string - convert to character array if s, ok := x.(string); ok { runes := []rune(s) // Important: use runes for proper Unicode handling diff --git a/pkg/lang/sort.go b/pkg/lang/sort.go index 2fe2e8b5..094c4f6f 100644 --- a/pkg/lang/sort.go +++ b/pkg/lang/sort.go @@ -10,7 +10,7 @@ import ( // - Stable sort (equal elements maintain their relative order) // - In-place modification of the array // - Comparator returns -1 for less than, 0 for equal, 1 for greater than -func SortSlice(slice []any, comp any) error { +func SortSlice(slice []any, comp any) { // comp is a Clojure function that acts as a comparator compFn, ok := comp.(IFn) if !ok { @@ -22,19 +22,24 @@ func SortSlice(slice []any, comp any) error { // Call the comparator function with the two elements result := compFn.Invoke(slice[i], slice[j]) - // Comparator returns: + // Handle both boolean and numeric comparators + // Boolean comparator: returns true if i < j + // Numeric comparator: returns negative if i < j + if boolResult, ok := result.(bool); ok { + return boolResult + } + + // Numeric comparator returns: // -1 if first arg is less than second // 0 if args are equal // 1 if first arg is greater than second // We return true for "less than" case resultInt, ok := AsInt(result) if !ok { - panic(NewIllegalArgumentError(fmt.Sprintf("Comparator must return a number, got %T", result))) + panic(NewIllegalArgumentError(fmt.Sprintf("Comparator must return a boolean or number, got %T", result))) } return resultInt < 0 }) - - return nil } // Compare implements Clojure's compare function. @@ -87,8 +92,8 @@ func Compare(x, y any) int { } // Handle symbols - if xSym, xOk := x.(Symbol); xOk { - if ySym, yOk := y.(Symbol); yOk { + if xSym, xOk := x.(*Symbol); xOk { + if ySym, yOk := y.(*Symbol); yOk { // Compare namespace first nsComp := Compare(xSym.Namespace(), ySym.Namespace()) if nsComp != 0 { @@ -99,6 +104,44 @@ func Compare(x, y any) int { } } + // Handle characters + if xChar, xOk := x.(Char); xOk { + if yChar, yOk := y.(Char); yOk { + if xChar < yChar { + return -1 + } else if xChar > yChar { + return 1 + } + return 0 + } + } + + // Handle vectors (including MapEntry which is a vector) + if xVec, xOk := x.(IPersistentVector); xOk { + if yVec, yOk := y.(IPersistentVector); yOk { + xCount := xVec.Count() + yCount := yVec.Count() + minCount := xCount + if yCount < minCount { + minCount = yCount + } + // Compare element by element + for i := 0; i < minCount; i++ { + cmp := Compare(xVec.Nth(i), yVec.Nth(i)) + if cmp != 0 { + return cmp + } + } + // If all compared elements are equal, shorter vector is less + if xCount < yCount { + return -1 + } else if xCount > yCount { + return 1 + } + return 0 + } + } + // If we can't compare, panic with an error panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare %T with %T", x, y))) } diff --git a/test/glojure/test_glojure/sort.glj b/test/glojure/test_glojure/sort.glj new file mode 100644 index 00000000..c829e023 --- /dev/null +++ b/test/glojure/test_glojure/sort.glj @@ -0,0 +1,201 @@ +;; Tests for sort function +;; Ensures Glojure behavior matches Clojure + +(ns glojure.test-glojure.sort + (:require [glojure.test :refer :all] + [glojure.string :as s])) + +(deftest test-sort-basic + (testing "Basic sort functionality" + ;; Numbers + (is (= '(1 2 3 4 5) (sort [3 1 4 2 5]))) + (is (= '(1 1 3 4 5) (sort [3 1 4 1 5]))) ; duplicates preserved + (is (= '(1.0 2.5 3 4.7) (sort [4.7 1.0 3 2.5]))) ; mixed numeric types + + ;; Strings + (is (= '("apple" "banana" "cherry") (sort ["cherry" "apple" "banana"]))) + (is (= '("" "a" "ab" "b") (sort ["b" "a" "" "ab"]))) ; empty string sorts first + + ;; Keywords + (is (= '(:a :b :c) (sort [:c :a :b]))) + (is (= '(:a/x :b/x :c/x) (sort [:c/x :a/x :b/x]))) ; namespaced keywords + + ;; Symbols + (is (= '(a b c) (sort '[c a b]))) + (is (= '(a/x b/x c/x) (sort '[c/x a/x b/x]))) ; namespaced symbols + + ;; Empty collection + (is (= '() (sort []))) + (is (= '() (sort '()))) + (is (= '() (sort nil))) ; nil returns empty seq + + ;; Single element + (is (= '(42) (sort [42]))) + + ;; Already sorted + (is (= '(1 2 3) (sort [1 2 3]))) + + ;; Reverse sorted + (is (= '(1 2 3) (sort [3 2 1]))))) + +(deftest test-sort-with-comparator + (testing "Sort with custom comparator" + ;; Reverse sort + (is (= '(5 4 3 2 1) (sort (fn [a b] (compare b a)) [3 1 4 5 2]))) + (is (= '(5 4 3 2 1) (sort > [3 1 4 5 2]))) ; using > as comparator + + ;; Case-insensitive string sort + (is (= '("apple" "Banana" "cherry") + (sort (fn [a b] (compare (s/lower-case a) (s/lower-case b))) + ["cherry" "apple" "Banana"]))) + + ;; Sort by string length + (is (= '("a" "bb" "ccc" "dddd") + (sort (fn [a b] (compare (count a) (count b))) + ["ccc" "a" "dddd" "bb"]))) + + ;; Sort maps by a specific key + (let [data [{:name "John" :age 30} + {:name "Jane" :age 25} + {:name "Bob" :age 35}]] + (is (= [{:name "Jane" :age 25} + {:name "John" :age 30} + {:name "Bob" :age 35}] + (sort (fn [a b] (compare (:age a) (:age b))) data)))))) + +(deftest test-sort-nil-handling + (testing "Nil handling in sort" + ;; nil sorts before everything + (is (= '(nil 1 2 3) (sort [3 nil 1 2]))) + (is (= '(nil nil 1 2) (sort [2 nil 1 nil]))) + (is (= '(nil "a" "b") (sort ["b" nil "a"]))) + + ;; With custom comparator that handles nil + (is (= '(3 2 1 nil) + (sort (fn [a b] + (cond + (nil? a) 1 ; nil goes to end + (nil? b) -1 + :else (compare b a))) + [nil 1 2 3]))))) + +(deftest test-sort-different-collection-types + (testing "Sort works on different collection types" + ;; Vector + (is (= '(1 2 3) (sort [3 1 2]))) + + ;; List + (is (= '(1 2 3) (sort '(3 1 2)))) + + ;; Set + (is (= '(1 2 3) (sort #{3 1 2}))) + + ;; Map entries (sorted as vectors) + (let [result (sort {:b 2 :a 1 :c 3})] + (is (= 3 (count result))) + (is (every? vector? result)) + (is (= :a (first (first result))))) + + ;; String (converts to character sequence) + (is (= '(\a \b \c \d) (sort "dcba"))) + + ;; Range + (is (= '(0 1 2 3 4) (sort (reverse (range 5))))))) + +(deftest test-sort-stability + (testing "Sort is stable" + ;; Create items that compare equal but are distinguishable + (let [items [{:id 1 :value 1} + {:id 2 :value 2} + {:id 3 :value 1} + {:id 4 :value 2} + {:id 5 :value 1}] + sorted (sort (fn [a b] (compare (:value a) (:value b))) items)] + ;; Items with same value should maintain relative order + (is (= [{:id 1 :value 1} + {:id 3 :value 1} + {:id 5 :value 1} + {:id 2 :value 2} + {:id 4 :value 2}] + sorted))))) + +(deftest test-sort-metadata-preservation + (testing "Sort preserves metadata" + (let [coll ^{:my-meta true} [3 1 2] + sorted (sort coll)] + (is (= '(1 2 3) sorted)) + (is (= true (:my-meta (meta sorted))))))) + +(deftest test-sort-edge-cases + (testing "Sort edge cases" + ;; Large collection + (let [large (repeatedly 1000 #(rand-int 100)) + sorted (sort large)] + (is (= 1000 (count sorted))) + (is (apply <= sorted))) ; verify it's actually sorted + + ;; All equal elements + (is (= '(1 1 1 1) (sort [1 1 1 1]))) + + ;; Mixed positive/negative numbers + (is (= '(-3 -1 0 1 3) (sort [1 -1 3 0 -3]))))) + +(deftest test-sort-error-cases + (testing "Sort error cases" + ;; Invalid comparator (not a function) + (is (thrown? go/any (sort "not-a-function" [1 2 3]))) + + ;; Comparator returns non-numeric + (is (thrown? go/any + (sort (fn [a b] "not-a-number") [1 2 3]))) + + ;; Uncomparable types (this might throw or might have undefined behavior) + ;; Clojure's behavior here is to throw ClassCastException + (is (thrown? go/any + (sort [1 "a" :b]))))) + +(deftest test-compare-function + (testing "Compare function behavior" + ;; Numbers + (is (= -1 (compare 1 2))) + (is (= 0 (compare 2 2))) + (is (= 1 (compare 3 2))) + (is (= -1 (compare 1.5 2))) + (is (= 1 (compare 2.5 2))) + + ;; Strings + (is (= -1 (compare "a" "b"))) + (is (= 0 (compare "hello" "hello"))) + (is (= 1 (compare "z" "a"))) + + ;; Keywords + (is (= -1 (compare :a :b))) + (is (= 0 (compare :x :x))) + + ;; Symbols + (is (= -1 (compare 'a 'b))) + (is (= 0 (compare 'x 'x))) + + ;; nil handling + (is (= -1 (compare nil 1))) + (is (= -1 (compare nil "a"))) + (is (= -1 (compare nil :a))) + (is (= 0 (compare nil nil))) + (is (= 1 (compare 1 nil))) + + ;; Different numeric types + (is (= 0 (compare 1 1.0))) + (is (= 0 (compare 1.0 1))))) + +(deftest test-sort-maps-behavior + (testing "Sorting maps produces map entries" + (let [m {:b 2 :a 1 :c 3} + sorted (sort m)] + ;; Each element should be a map entry (vector of [k v]) + (is (every? vector? sorted)) + (is (every? #(= 2 (count %)) sorted)) + ;; Should be sorted by key + (is (= [[:a 1] [:b 2] [:c 3]] sorted))))) + +;; Run tests +(run-tests) From e49ab6cbe10688307f0a97fcead5bfd2eab0b14a Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Fri, 1 Aug 2025 22:00:04 -0700 Subject: [PATCH 3/7] Update number compare Signed-off-by: James Hamlin --- pkg/lang/numbers.go | 10 ++++++++++ pkg/lang/sort.go | 11 ++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pkg/lang/numbers.go b/pkg/lang/numbers.go index 35b3e23e..10d121f2 100644 --- a/pkg/lang/numbers.go +++ b/pkg/lang/numbers.go @@ -189,6 +189,16 @@ func (nm *NumberMethods) Equiv(x, y any) bool { return Ops(x).Combine(Ops(y)).Equiv(x, y) } +func (nm *NumberMethods) Compare(x, y any) int { + ops := Ops(x).Combine(Ops(y)) + if ops.LT(x, y) { + return -1 + } else if ops.LT(y, x) { + return 1 + } + return 0 +} + func (nm *NumberMethods) Floats(x any) []float32 { return x.([]float32) } diff --git a/pkg/lang/sort.go b/pkg/lang/sort.go index 094c4f6f..f6bbfd67 100644 --- a/pkg/lang/sort.go +++ b/pkg/lang/sort.go @@ -58,18 +58,11 @@ func Compare(x, y any) int { return 1 } - // Handle numbers - convert to float64 for comparison + // Handle numbers using the Numbers.Compare method xNum, xIsNum := AsNumber(x) yNum, yIsNum := AsNumber(y) if xIsNum && yIsNum { - xFloat := AsFloat64(xNum) - yFloat := AsFloat64(yNum) - if xFloat < yFloat { - return -1 - } else if xFloat > yFloat { - return 1 - } - return 0 + return Numbers.Compare(xNum, yNum) } // Handle strings From fd3dcad3dabd2732ba269782713ab3af613ffb19 Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Fri, 1 Aug 2025 22:14:34 -0700 Subject: [PATCH 4/7] Use Comparer interface a la Java Comparable Signed-off-by: James Hamlin --- pkg/lang/apersistentvector.go | 1 + pkg/lang/keyword.go | 7 +++ pkg/lang/mapentry.go | 28 +++++++++++ pkg/lang/sort.go | 80 +++++++----------------------- pkg/lang/subvector.go | 26 ++++++++++ pkg/lang/symbol.go | 24 +++++++++ pkg/lang/vector.go | 26 ++++++++++ test/glojure/test_glojure/sort.glj | 55 ++++++++++++++++++++ 8 files changed, 186 insertions(+), 61 deletions(-) diff --git a/pkg/lang/apersistentvector.go b/pkg/lang/apersistentvector.go index 47b90b9e..20b05f33 100644 --- a/pkg/lang/apersistentvector.go +++ b/pkg/lang/apersistentvector.go @@ -13,6 +13,7 @@ type ( IPersistentVector IHashEq Reversible + Comparer } apvSeq struct { diff --git a/pkg/lang/keyword.go b/pkg/lang/keyword.go index 1d9a10ad..f678c305 100644 --- a/pkg/lang/keyword.go +++ b/pkg/lang/keyword.go @@ -98,3 +98,10 @@ func (k Keyword) ApplyTo(args ISeq) interface{} { func (k Keyword) Hash() uint32 { return k.hash } + +func (k Keyword) Compare(other any) int { + if otherKw, ok := other.(Keyword); ok { + return strings.Compare(k.String(), otherKw.String()) + } + panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare Keyword with %T", other))) +} diff --git a/pkg/lang/mapentry.go b/pkg/lang/mapentry.go index 80f4bb39..922ebfb7 100644 --- a/pkg/lang/mapentry.go +++ b/pkg/lang/mapentry.go @@ -1,5 +1,7 @@ package lang +import "fmt" + // MapEntry represents a key-value pair in a map. type MapEntry struct { hasheq uint32 @@ -118,3 +120,29 @@ func (me *MapEntry) ValAt(key any) any { func (me *MapEntry) ValAtDefault(key, notFound any) any { return apersistentVectorValAtDefault(me, key, notFound) } + +func (me *MapEntry) Compare(other any) int { + otherVec, ok := other.(IPersistentVector) + if !ok { + panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare MapEntry with %T", other))) + } + + myCount := me.Count() + otherCount := otherVec.Count() + + // Compare lengths first + if myCount < otherCount { + return -1 + } else if myCount > otherCount { + return 1 + } + + // Compare element by element + for i := 0; i < myCount; i++ { + cmp := Compare(me.Nth(i), otherVec.Nth(i)) + if cmp != 0 { + return cmp + } + } + return 0 +} diff --git a/pkg/lang/sort.go b/pkg/lang/sort.go index f6bbfd67..e7d59019 100644 --- a/pkg/lang/sort.go +++ b/pkg/lang/sort.go @@ -3,6 +3,7 @@ package lang import ( "fmt" "sort" + "strings" ) // SortSlice performs an in-place stable sort on the given array using the provided comparator. @@ -28,7 +29,7 @@ func SortSlice(slice []any, comp any) { if boolResult, ok := result.(bool); ok { return boolResult } - + // Numeric comparator returns: // -1 if first arg is less than second // 0 if args are equal @@ -47,7 +48,12 @@ func SortSlice(slice []any, comp any) { // 'less than', 'equal to', or 'greater than' y. // Handles nil values (nil is less than everything except nil). func Compare(x, y any) int { - // Handle nil cases first + // Identity check + if x == y { + return 0 + } + + // Handle nil cases if IsNil(x) { if IsNil(y) { return 0 @@ -59,41 +65,19 @@ func Compare(x, y any) int { } // Handle numbers using the Numbers.Compare method - xNum, xIsNum := AsNumber(x) - yNum, yIsNum := AsNumber(y) - if xIsNum && yIsNum { - return Numbers.Compare(xNum, yNum) + if xNum, xIsNum := AsNumber(x); xIsNum { + return Numbers.Compare(xNum, y) } - // Handle strings - if xStr, xOk := x.(string); xOk { - if yStr, yOk := y.(string); yOk { - if xStr < yStr { - return -1 - } else if xStr > yStr { - return 1 - } - return 0 - } - } - - // Handle keywords - if xKw, xOk := x.(Keyword); xOk { - if yKw, yOk := y.(Keyword); yOk { - return Compare(xKw.String(), yKw.String()) - } + // Check if x implements Comparer interface + if xComp, ok := x.(Comparer); ok { + return xComp.Compare(y) } - // Handle symbols - if xSym, xOk := x.(*Symbol); xOk { - if ySym, yOk := y.(*Symbol); yOk { - // Compare namespace first - nsComp := Compare(xSym.Namespace(), ySym.Namespace()) - if nsComp != 0 { - return nsComp - } - // Then compare name - return Compare(xSym.Name(), ySym.Name()) + // Handle strings (built-in type, doesn't implement Comparer) + if xStr, xOk := x.(string); xOk { + if yStr, yOk := y.(string); yOk { + return strings.Compare(xStr, yStr) } } @@ -109,32 +93,6 @@ func Compare(x, y any) int { } } - // Handle vectors (including MapEntry which is a vector) - if xVec, xOk := x.(IPersistentVector); xOk { - if yVec, yOk := y.(IPersistentVector); yOk { - xCount := xVec.Count() - yCount := yVec.Count() - minCount := xCount - if yCount < minCount { - minCount = yCount - } - // Compare element by element - for i := 0; i < minCount; i++ { - cmp := Compare(xVec.Nth(i), yVec.Nth(i)) - if cmp != 0 { - return cmp - } - } - // If all compared elements are equal, shorter vector is less - if xCount < yCount { - return -1 - } else if xCount > yCount { - return 1 - } - return 0 - } - } - - // If we can't compare, panic with an error - panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare %T with %T", x, y))) + // Default error - cannot compare + panic(NewIllegalArgumentError(fmt.Sprintf("%T cannot be cast to Comparable", x))) } diff --git a/pkg/lang/subvector.go b/pkg/lang/subvector.go index e48c30e4..b502f282 100644 --- a/pkg/lang/subvector.go +++ b/pkg/lang/subvector.go @@ -165,3 +165,29 @@ func (v *SubVector) Invoke(args ...any) any { func (v *SubVector) HashEq() uint32 { return apersistentVectorHashEq(&v.hasheq, v) } + +func (v *SubVector) Compare(other any) int { + otherVec, ok := other.(IPersistentVector) + if !ok { + panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare SubVector with %T", other))) + } + + myCount := v.Count() + otherCount := otherVec.Count() + + // Compare lengths first + if myCount < otherCount { + return -1 + } else if myCount > otherCount { + return 1 + } + + // Compare element by element + for i := 0; i < myCount; i++ { + cmp := Compare(v.Nth(i), otherVec.Nth(i)) + if cmp != 0 { + return cmp + } + } + return 0 +} diff --git a/pkg/lang/symbol.go b/pkg/lang/symbol.go index 009ddbf6..ff882b60 100644 --- a/pkg/lang/symbol.go +++ b/pkg/lang/symbol.go @@ -1,6 +1,7 @@ package lang import ( + "fmt" "strings" ) @@ -48,6 +49,29 @@ func (s *Symbol) Name() string { return s.name } +func (s *Symbol) Compare(other any) int { + otherSym, ok := other.(*Symbol) + if !ok { + panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare Symbol with %T", other))) + } + + // Compare namespace first + if s.ns != otherSym.ns { + if s.ns == "" && otherSym.ns != "" { + return -1 + } + if s.ns != "" && otherSym.ns == "" { + return 1 + } + if nsComp := strings.Compare(s.ns, otherSym.ns); nsComp != 0 { + return nsComp + } + } + + // Then compare name + return strings.Compare(s.name, otherSym.name) +} + func (s *Symbol) FullName() string { return s.String() } diff --git a/pkg/lang/vector.go b/pkg/lang/vector.go index 26578ee1..a5754341 100644 --- a/pkg/lang/vector.go +++ b/pkg/lang/vector.go @@ -272,6 +272,32 @@ func (v *Vector) AsTransient() ITransientCollection { } } +func (v *Vector) Compare(other any) int { + otherVec, ok := other.(IPersistentVector) + if !ok { + panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare Vector with %T", other))) + } + + myCount := v.Count() + otherCount := otherVec.Count() + + // Compare lengths first + if myCount < otherCount { + return -1 + } else if myCount > otherCount { + return 1 + } + + // Compare element by element + for i := 0; i < myCount; i++ { + cmp := Compare(v.Nth(i), otherVec.Nth(i)) + if cmp != 0 { + return cmp + } + } + return 0 +} + func toSlice(x any) []any { if x == nil { return nil diff --git a/test/glojure/test_glojure/sort.glj b/test/glojure/test_glojure/sort.glj index c829e023..c03af81f 100644 --- a/test/glojure/test_glojure/sort.glj +++ b/test/glojure/test_glojure/sort.glj @@ -197,5 +197,60 @@ ;; Should be sorted by key (is (= [[:a 1] [:b 2] [:c 3]] sorted))))) +(deftest test-non-comparable-types + (testing "Non-comparable types throw errors" + ;; Lists are not comparable + (is (thrown? go/any (compare '(1 2) '(1 2)))) + (is (thrown? go/any (sort ['(1 2) '(3 4)]))) + + ;; Maps are not comparable + (is (thrown? go/any (compare {:a 1} {:b 2}))) + (is (thrown? go/any (sort [{:a 1} {:b 2}]))) + + ;; Sets are not comparable + (is (thrown? go/any (compare #{1 2} #{3 4}))) + (is (thrown? go/any (sort [#{1 2} #{3 4}]))) + + ;; Mixed incompatible types + (is (thrown? go/any (compare 1 :a))) + (is (thrown? go/any (compare "string" 'symbol))) + (is (thrown? go/any (compare :keyword [1 2 3]))))) + +(deftest test-vector-comparison + (testing "Vector comparison details" + ;; Vectors compare lexicographically + (is (= -1 (compare [1 2] [1 3]))) + (is (= 1 (compare [1 3] [1 2]))) + (is (= 0 (compare [1 2 3] [1 2 3]))) + + ;; Shorter vectors are less than longer vectors with same prefix + (is (= -1 (compare [1 2] [1 2 3]))) + (is (= 1 (compare [1 2 3] [1 2]))) + + ;; Nested vectors + (is (= -1 (compare [[1 2] [3 4]] [[1 2] [3 5]]))) + (is (= 0 (compare [[1 2] [3 4]] [[1 2] [3 4]]))) + + ;; SubVectors behave like vectors + (let [v [1 2 3 4 5] + sv1 (subvec v 1 3) ; [2 3] + sv2 (subvec v 2 4)] ; [3 4] + (is (= -1 (compare sv1 sv2))) + (is (= -1 (compare sv1 [3 4])))))) + +(deftest test-symbol-namespace-comparison + (testing "Symbols compare namespace-first" + ;; No namespace < with namespace + (is (= -1 (compare 'x 'a/x))) + (is (= 1 (compare 'a/x 'x))) + + ;; Different namespaces + (is (= -1 (compare 'a/x 'b/x))) + (is (= 1 (compare 'b/x 'a/x))) + + ;; Same namespace, different names + (is (= -1 (compare 'ns/a 'ns/b))) + (is (= 1 (compare 'ns/b 'ns/a))))) + ;; Run tests (run-tests) From 117117d68468cb9f510f7fc141adeeed7587e43a Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Fri, 1 Aug 2025 22:45:55 -0700 Subject: [PATCH 5/7] Sort test files for consistent run order Signed-off-by: James Hamlin --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 51cf3cee..279747aa 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ STDLIB := $(STDLIB_ORIGINALS:scripts/rewrite-core/originals/%=%) STDLIB_ORIGINALS := $(addprefix scripts/rewrite-core/originals/,$(STDLIB)) STDLIB_TARGETS := $(addprefix pkg/stdlib/glojure/,$(STDLIB:.clj=.glj)) -TEST_FILES := $(shell find ./test -name '*.glj') +TEST_FILES := $(shell find ./test -name '*.glj' | sort) TEST_TARGETS := $(addsuffix .test,$(TEST_FILES)) GOPLATFORMS := darwin_arm64 darwin_amd64 linux_arm64 linux_amd64 windows_amd64 windows_arm js_wasm From 60d90fa992139a5cd0a5e4b11dc873d95f0ec3dc Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Fri, 1 Aug 2025 22:46:00 -0700 Subject: [PATCH 6/7] Fix sort-by Signed-off-by: James Hamlin --- pkg/stdlib/glojure/core.glj | 2 +- scripts/rewrite-core/rewrite.clj | 3 +++ test/glojure/test_glojure/sort.glj | 21 ++++++++++----------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pkg/stdlib/glojure/core.glj b/pkg/stdlib/glojure/core.glj index 4fc89cdd..dbf53637 100644 --- a/pkg/stdlib/glojure/core.glj +++ b/pkg/stdlib/glojure/core.glj @@ -3106,7 +3106,7 @@ ([keyfn coll] (sort-by keyfn compare coll)) ([keyfn ^java.util.Comparator comp coll] - (sort (fn [x y] (. comp (compare (keyfn x) (keyfn y)))) coll))) + (sort (fn [x y] (comp (keyfn x) (keyfn y))) coll))) (defn dorun "When lazy sequences are produced via functions that have side diff --git a/scripts/rewrite-core/rewrite.clj b/scripts/rewrite-core/rewrite.clj index d9d18e74..fd6a3477 100644 --- a/scripts/rewrite-core/rewrite.clj +++ b/scripts/rewrite-core/rewrite.clj @@ -944,6 +944,9 @@ (sexpr-replace '(. java.util.Arrays (sort a comp)) '(github.com$glojurelang$glojure$pkg$lang.SortSlice a comp)) + ;; comparators are simple functions in Glojure + (sexpr-replace '(. comp (compare (keyfn x) (keyfn y))) + '(comp (keyfn x) (keyfn y))) ])) (defn rewrite-core [zloc] diff --git a/test/glojure/test_glojure/sort.glj b/test/glojure/test_glojure/sort.glj index c03af81f..82140d29 100644 --- a/test/glojure/test_glojure/sort.glj +++ b/test/glojure/test_glojure/sort.glj @@ -1,5 +1,5 @@ ;; Tests for sort function -;; Ensures Glojure behavior matches Clojure +;; Ensures Glojure behavior matches Clojure as closely as possible (ns glojure.test-glojure.sort (:require [glojure.test :refer :all] @@ -51,8 +51,7 @@ ;; Sort by string length (is (= '("a" "bb" "ccc" "dddd") - (sort (fn [a b] (compare (count a) (count b))) - ["ccc" "a" "dddd" "bb"]))) + (sort-by count ["ccc" "a" "dddd" "bb"]))) ;; Sort maps by a specific key (let [data [{:name "John" :age 30} @@ -202,15 +201,15 @@ ;; Lists are not comparable (is (thrown? go/any (compare '(1 2) '(1 2)))) (is (thrown? go/any (sort ['(1 2) '(3 4)]))) - + ;; Maps are not comparable (is (thrown? go/any (compare {:a 1} {:b 2}))) (is (thrown? go/any (sort [{:a 1} {:b 2}]))) - + ;; Sets are not comparable (is (thrown? go/any (compare #{1 2} #{3 4}))) (is (thrown? go/any (sort [#{1 2} #{3 4}]))) - + ;; Mixed incompatible types (is (thrown? go/any (compare 1 :a))) (is (thrown? go/any (compare "string" 'symbol))) @@ -222,15 +221,15 @@ (is (= -1 (compare [1 2] [1 3]))) (is (= 1 (compare [1 3] [1 2]))) (is (= 0 (compare [1 2 3] [1 2 3]))) - + ;; Shorter vectors are less than longer vectors with same prefix (is (= -1 (compare [1 2] [1 2 3]))) (is (= 1 (compare [1 2 3] [1 2]))) - + ;; Nested vectors (is (= -1 (compare [[1 2] [3 4]] [[1 2] [3 5]]))) (is (= 0 (compare [[1 2] [3 4]] [[1 2] [3 4]]))) - + ;; SubVectors behave like vectors (let [v [1 2 3 4 5] sv1 (subvec v 1 3) ; [2 3] @@ -243,11 +242,11 @@ ;; No namespace < with namespace (is (= -1 (compare 'x 'a/x))) (is (= 1 (compare 'a/x 'x))) - + ;; Different namespaces (is (= -1 (compare 'a/x 'b/x))) (is (= 1 (compare 'b/x 'a/x))) - + ;; Same namespace, different names (is (= -1 (compare 'ns/a 'ns/b))) (is (= 1 (compare 'ns/b 'ns/a))))) From 8464f2167f44ccc76a387a9d53aae65191ccd47f Mon Sep 17 00:00:00 2001 From: James Hamlin Date: Fri, 1 Aug 2025 23:05:51 -0700 Subject: [PATCH 7/7] Add more sort-by tests Signed-off-by: James Hamlin --- pkg/lang/slices.go | 16 ++++++------- pkg/lang/subvector.go | 6 ++--- test/glojure/test_glojure/sort.glj | 36 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/pkg/lang/slices.go b/pkg/lang/slices.go index bb99c661..3f1d4b46 100644 --- a/pkg/lang/slices.go +++ b/pkg/lang/slices.go @@ -15,12 +15,12 @@ func ToSlice(x any) []any { if IsNil(x) { return []any{} } - + // Handle []any - return as-is if slice, ok := x.([]any); ok { return slice } - + // Handle IPersistentVector if vec, ok := x.(IPersistentVector); ok { count := vec.Count() @@ -30,7 +30,7 @@ func ToSlice(x any) []any { } return res } - + // Handle IPersistentMap - convert to array of MapEntry objects if m, ok := x.(IPersistentMap); ok { seq := m.Seq() @@ -41,7 +41,7 @@ func ToSlice(x any) []any { } return res } - + // Handle Set - convert to array of values if s, ok := x.(*Set); ok { seq := s.Seq() @@ -52,7 +52,7 @@ func ToSlice(x any) []any { } return res } - + // Handle string - convert to character array if s, ok := x.(string); ok { runes := []rune(s) // Important: use runes for proper Unicode handling @@ -62,7 +62,7 @@ func ToSlice(x any) []any { } return res } - + // Handle ISeq if s, ok := x.(ISeq); ok { res := make([]interface{}, 0, Count(x)) @@ -71,7 +71,7 @@ func ToSlice(x any) []any { } return res } - + // Handle reflection-based slice/array xVal := reflect.ValueOf(x) if xVal.Kind() == reflect.Slice || xVal.Kind() == reflect.Array { @@ -81,7 +81,7 @@ func ToSlice(x any) []any { } return res } - + // Error with Clojure-style message panic(NewIllegalArgumentError(fmt.Sprintf("Unable to convert: %T to Object[]", x))) } diff --git a/pkg/lang/subvector.go b/pkg/lang/subvector.go index b502f282..b08772bf 100644 --- a/pkg/lang/subvector.go +++ b/pkg/lang/subvector.go @@ -171,17 +171,17 @@ func (v *SubVector) Compare(other any) int { if !ok { panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare SubVector with %T", other))) } - + myCount := v.Count() otherCount := otherVec.Count() - + // Compare lengths first if myCount < otherCount { return -1 } else if myCount > otherCount { return 1 } - + // Compare element by element for i := 0; i < myCount; i++ { cmp := Compare(v.Nth(i), otherVec.Nth(i)) diff --git a/test/glojure/test_glojure/sort.glj b/test/glojure/test_glojure/sort.glj index 82140d29..02e84fdf 100644 --- a/test/glojure/test_glojure/sort.glj +++ b/test/glojure/test_glojure/sort.glj @@ -11,6 +11,7 @@ (is (= '(1 2 3 4 5) (sort [3 1 4 2 5]))) (is (= '(1 1 3 4 5) (sort [3 1 4 1 5]))) ; duplicates preserved (is (= '(1.0 2.5 3 4.7) (sort [4.7 1.0 3 2.5]))) ; mixed numeric types + (is (= '(-5 0 1.5 2 3.14 10) (sort [3.14 2 1.5 10 -5 0]))) ;; Strings (is (= '("apple" "banana" "cherry") (sort ["cherry" "apple" "banana"]))) @@ -251,5 +252,40 @@ (is (= -1 (compare 'ns/a 'ns/b))) (is (= 1 (compare 'ns/b 'ns/a))))) +(deftest sort-by-clojuredocs-examples + (testing "Examples from ClojureDocs sort-by documentation" + + (let [words ["banana" "apple" "cherry" "date"]] + (is (= '("date" "apple" "banana" "cherry") + (sort-by count words)))) + + (let [words ["banana" "apple" "cherry" "date"]] + (is (= (sort-by count > words) + '("banana" "cherry" "apple" "date")))) + + (let [people [{:name "Alice" :age 30 :city "NYC"} + {:name "Bob" :age 25 :city "LA"} + {:name "Charlie" :age 35 :city "NYC"} + {:name "David" :age 25 :city "LA"}]] + (is (= (sort-by (juxt :city :age) people) + '({:name "Bob" :age 25 :city "LA"} + {:name "David" :age 25 :city "LA"} + {:name "Alice" :age 30 :city "NYC"} + {:name "Charlie" :age 35 :city "NYC"})))) + + (let [numbers [3 1 4 1 5 9 2 6]] + (is (= '(3 9 6 1 4 1 5 2) + (sort-by #(mod % 3) numbers)))) + + (let [items [nil "hello" 42 :keyword]] + (is (= (sort-by str items) + '(nil 42 :keyword "hello")))) + + (is (= (sort-by identity []) '())) + (is (= (sort-by count []) '())) + + (is (= (sort-by identity [42]) '(42))) + (is (= (sort-by count ["hello"]) '("hello"))))) + ;; Run tests (run-tests)