Skip to content

Commit ec16261

Browse files
committed
support sorting tuples
Uses merge sort, as an obvious choice for a stable sort of tuples. A recursive data structure of singleton type, representing Peano natural numbers, is used to help with splitting a tuple into two halves in the merge sort. An alternative design would use a reference tuple, but this would require relying on `tail`, which seems more harsh on the compiler. With the recursive datastructure the predecessor operation and the successor operation are both trivial. Allows inference to preserve inferred element type even when tuple length is not known. Follow-up PRs may add further improvements, such as the ability to select an unstable sorting algorithm. The added file, typedomainnumbers.jl is not specific to sorting, thus making it a separate file. Xref JuliaLang#55571. Fixes JuliaLang#54489
1 parent 683da41 commit ec16261

File tree

5 files changed

+330
-0
lines changed

5 files changed

+330
-0
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ New library features
119119
* `Base.require_one_based_indexing` and `Base.has_offset_axes` are now public ([#56196])
120120
* New `ltruncate`, `rtruncate` and `ctruncate` functions for truncating strings to text width, accounting for char widths ([#55351])
121121
* `isless` (and thus `cmp`, sorting, etc.) is now supported for zero-dimensional `AbstractArray`s ([#55772])
122+
* `sort` now sorts tuples (#56425)
122123

123124
Standard library changes
124125
------------------------

base/Base.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ include("cartesian.jl")
106106
using .Cartesian
107107
include("multidimensional.jl")
108108

109+
include("typedomainnumbers.jl")
110+
109111
include("broadcast.jl")
110112
using .Broadcast
111113
using .Broadcast: broadcasted, broadcasted_kwsyntax, materialize, materialize!,

base/sort.jl

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,6 +1736,95 @@ julia> v
17361736
"""
17371737
sort(v::AbstractVector; kws...) = sort!(copymutable(v); kws...)
17381738

1739+
module _SortTupleStable
1740+
using
1741+
Base._TypeDomainNumbers.PositiveIntegers, Base._TypeDomainNumbers.IntegersGreaterThanOne,
1742+
Base._TypeDomainNumbers.Utils, Base._TypeDomainNumberTupleUtils, Base._TupleTypeByLength
1743+
using Base: tail
1744+
using Base.Order: Ordering, lt
1745+
export sort_tuple_stable
1746+
function merge_recursive((@nospecialize ord::Ordering), a::Tuple, b::Tuple)
1747+
ret = if a isa Tuple1OrMore
1748+
a
1749+
else
1750+
b
1751+
end
1752+
type_assert_tuple_0_or_more(ret)
1753+
end
1754+
function merge_recursive(ord::Ordering, a::Tuple1OrMore, b::Tuple1OrMore)
1755+
l = first(a)
1756+
r = first(b)
1757+
x = tail(a)
1758+
y = tail(b)
1759+
merged = if lt(ord, r, l)
1760+
let rec = type_assert_tuple_1_or_more(merge_recursive(ord, a, y))
1761+
(r, rec...)
1762+
end
1763+
else
1764+
let rec = type_assert_tuple_1_or_more(merge_recursive(ord, x, b))
1765+
(l, rec...)
1766+
end
1767+
end
1768+
type_assert_tuple_2_or_more(merged)
1769+
end
1770+
function merge_nontrivial(ord::Ordering, a::Tuple1OrMore, b::Tuple1OrMore)
1771+
ret = merge_recursive(ord, a, b)
1772+
type_assert_tuple_2_or_more(ret)
1773+
end
1774+
function split_tuple(@nospecialize tup::Tuple2OrMore)
1775+
len = type_assert_integer_greater_than_1(tuple_type_domain_length(tup))
1776+
len_l = type_assert_positive_integer(half_floor_nontrivial(len))
1777+
len_r = type_assert_positive_integer(half_ceiling_nontrivial(len))
1778+
tup_l = type_assert_tuple_1_or_more(skip_from_tail_nontrivial(tup, len_r))
1779+
tup_r = type_assert_tuple_1_or_more(skip_from_front_nontrivial(tup, len_l))
1780+
(tup_l, tup_r)
1781+
end
1782+
function sort_recursive((@nospecialize ord::Ordering), @nospecialize tup::Tuple{Any})
1783+
tup
1784+
end
1785+
function sort_recursive(ord::Ordering, tup::Tuple2OrMore)
1786+
(tup_l, tup_r) = split_tuple(tup)
1787+
sorted_l = type_assert_tuple_1_or_more(sort_recursive(ord, tup_l))
1788+
sorted_r = type_assert_tuple_1_or_more(sort_recursive(ord, tup_r))
1789+
type_assert_tuple_2_or_more(merge_nontrivial(ord, sorted_l, sorted_r))
1790+
end
1791+
function sort_tuple_stable_2_or_more(ord::Ordering, tup::Tuple2OrMore)
1792+
ret = sort_recursive(ord, tup)
1793+
type_assert_tuple_2_or_more(ret)
1794+
end
1795+
function sort_tuple_array_fallback(ord::Ordering, tup::Tuple2OrMore)
1796+
vec = if tup isa NTuple
1797+
[tup...]
1798+
else
1799+
Any[tup...]
1800+
end
1801+
sort!(vec; order = ord)
1802+
(vec...,)
1803+
end
1804+
function sort_tuple_stable((@nospecialize ord::Ordering), @nospecialize tup::Tuple)
1805+
if tup isa Tuple2OrMore
1806+
if tup isa Tuple32OrMore
1807+
sort_tuple_array_fallback(ord, tup)
1808+
else
1809+
sort_tuple_stable_2_or_more(ord, tup)
1810+
end
1811+
else
1812+
tup
1813+
end
1814+
end
1815+
end
1816+
1817+
function sort(
1818+
tup::Tuple;
1819+
lt = isless,
1820+
by = identity,
1821+
rev::Union{Nothing, Bool} = nothing,
1822+
order::Ordering = Forward,
1823+
)
1824+
o = ord(lt, by, rev, order)
1825+
_SortTupleStable.sort_tuple_stable(o, tup)
1826+
end
1827+
17391828
## partialsortperm: the permutation to sort the first k elements of an array ##
17401829

17411830
"""

base/typedomainnumbers.jl

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
# Adapted from the TypeDomainNaturalNumbers.jl package.
4+
module _TypeDomainNumbers
5+
module Zeros
6+
export Zero
7+
struct Zero end
8+
end
9+
10+
module PositiveIntegers
11+
module RecursiveStep
12+
using ...Zeros
13+
export recursive_step
14+
function recursive_step(@nospecialize t::Type)
15+
Union{Zero, t}
16+
end
17+
end
18+
module UpperBounds
19+
using ..RecursiveStep
20+
abstract type A end
21+
abstract type B{P <: recursive_step(A)} <: A end
22+
abstract type C{P <: recursive_step(B)} <: B{P} end
23+
abstract type D{P <: recursive_step(C)} <: C{P} end
24+
end
25+
using .RecursiveStep
26+
const PositiveIntegerUpperBound = UpperBounds.A
27+
const PositiveIntegerUpperBoundTighter = UpperBounds.D
28+
export
29+
natural_successor, natural_predecessor,
30+
NonnegativeInteger, NonnegativeIntegerUpperBound,
31+
PositiveInteger, PositiveIntegerUpperBound,
32+
type_assert_nonnegative_integer, type_assert_positive_integer
33+
struct PositiveInteger{
34+
Predecessor <: recursive_step(PositiveIntegerUpperBoundTighter),
35+
} <: PositiveIntegerUpperBoundTighter{Predecessor}
36+
predecessor::Predecessor
37+
global const NonnegativeInteger = recursive_step(PositiveInteger)
38+
global const NonnegativeIntegerUpperBound = recursive_step(PositiveIntegerUpperBound)
39+
global function natural_successor(p::P) where {P <: NonnegativeInteger}
40+
ret = new{P}(p)
41+
type_assert_positive_integer(ret)
42+
end
43+
end
44+
function type_assert_nonnegative_integer(@nospecialize x::NonnegativeInteger)
45+
x
46+
end
47+
function type_assert_positive_integer(@nospecialize x::PositiveInteger)
48+
x
49+
end
50+
function natural_predecessor(@nospecialize o::PositiveInteger)
51+
ret = getfield(o, :predecessor) # avoid specializing `getproperty` for each number
52+
type_assert_nonnegative_integer(ret)
53+
end
54+
end
55+
56+
module IntegersGreaterThanOne
57+
using ..PositiveIntegers
58+
export
59+
IntegerGreaterThanOne, IntegerGreaterThanOneUpperBound,
60+
type_assert_integer_greater_than_1
61+
const IntegerGreaterThanOne = let t = PositiveInteger
62+
t{P} where {P <: t}
63+
end
64+
const IntegerGreaterThanOneUpperBound = let t = PositiveIntegerUpperBound
65+
PositiveIntegers.UpperBounds.B{P} where {P <: t}
66+
end
67+
function type_assert_integer_greater_than_1(@nospecialize x::IntegerGreaterThanOne)
68+
x
69+
end
70+
end
71+
72+
module Constants
73+
using ..Zeros, ..PositiveIntegers
74+
export n0, n1
75+
const n0 = Zero()
76+
const n1 = natural_successor(n0)
77+
end
78+
79+
module Utils
80+
using ..PositiveIntegers, ..IntegersGreaterThanOne, ..Constants
81+
using Base: @assume_effects
82+
export minus_two, half_floor, half_ceiling, half_floor_nontrivial, half_ceiling_nontrivial
83+
function minus_two(@nospecialize m::IntegerGreaterThanOne)
84+
natural_predecessor(natural_predecessor(m))
85+
end
86+
@assume_effects :foldable :nothrow function half_floor(@nospecialize m::NonnegativeInteger)
87+
ret = if m isa IntegerGreaterThanOneUpperBound
88+
let n = minus_two(m), rec = half_floor(n)
89+
type_assert_positive_integer(natural_successor(rec))
90+
end
91+
else
92+
n0
93+
end
94+
type_assert_nonnegative_integer(ret)
95+
end
96+
@assume_effects :foldable :nothrow function half_ceiling(@nospecialize m::NonnegativeInteger)
97+
ret = if m isa IntegerGreaterThanOneUpperBound
98+
let n = minus_two(m), rec = half_ceiling(n)
99+
type_assert_positive_integer(natural_successor(rec))
100+
end
101+
else
102+
if m isa PositiveIntegerUpperBound
103+
n1
104+
else
105+
n0
106+
end
107+
end
108+
type_assert_nonnegative_integer(ret)
109+
end
110+
function half_floor_nontrivial(@nospecialize m::IntegerGreaterThanOne)
111+
ret = half_floor(m)
112+
type_assert_positive_integer(ret)
113+
end
114+
function half_ceiling_nontrivial(@nospecialize m::IntegerGreaterThanOne)
115+
ret = half_ceiling(m)
116+
type_assert_positive_integer(ret)
117+
end
118+
end
119+
end
120+
121+
module _TupleTypeByLength
122+
export
123+
Tuple1OrMore, Tuple2OrMore, Tuple3OrMore, Tuple4OrMore, Tuple32OrMore,
124+
type_assert_tuple_0_or_more, type_assert_tuple_1_or_more, type_assert_tuple_2_or_more,
125+
type_assert_tuple_3_or_more, type_assert_tuple_4_or_more,
126+
type_assert_tuple_1
127+
const Tuple1OrMore = Tuple{Any, Vararg}
128+
const Tuple2OrMore = Tuple{Any, Any, Vararg}
129+
const Tuple3OrMore = Tuple{Any, Any, Any, Vararg}
130+
const Tuple4OrMore = Tuple{Any, Any, Any, Any, Vararg}
131+
const Tuple32OrMore = Base.Any32
132+
function type_assert_tuple_0_or_more(@nospecialize x::Tuple)
133+
x
134+
end
135+
function type_assert_tuple_1_or_more(@nospecialize x::Tuple1OrMore)
136+
x
137+
end
138+
function type_assert_tuple_2_or_more(@nospecialize x::Tuple2OrMore)
139+
x
140+
end
141+
function type_assert_tuple_3_or_more(@nospecialize x::Tuple3OrMore)
142+
x
143+
end
144+
function type_assert_tuple_4_or_more(@nospecialize x::Tuple4OrMore)
145+
x
146+
end
147+
end
148+
149+
module _TypeDomainNumberTupleUtils
150+
using
151+
.._TypeDomainNumbers.PositiveIntegers, .._TypeDomainNumbers.IntegersGreaterThanOne,
152+
.._TypeDomainNumbers.Constants, .._TupleTypeByLength
153+
using Base: @assume_effects, front, tail
154+
export
155+
tuple_type_domain_length,
156+
skip_from_front, skip_from_tail,
157+
skip_from_front_nontrivial, skip_from_tail_nontrivial
158+
# The `@nospecialize` and `@inline` together should effectively result in specializing
159+
# on the length, without specializing on the types of the elements.
160+
@assume_effects :foldable :nothrow function tuple_type_domain_length(@nospecialize tup::Tuple)
161+
ret = if tup isa Tuple1OrMore
162+
let t = tail(tup), rec = @inline tuple_type_domain_length(t)
163+
type_assert_positive_integer(natural_successor(rec))
164+
end
165+
else
166+
n0
167+
end
168+
type_assert_nonnegative_integer(ret)
169+
end
170+
@assume_effects :foldable function skip_from_front((@nospecialize tup::Tuple), @nospecialize skip_count::NonnegativeInteger)
171+
if skip_count isa PositiveIntegerUpperBound
172+
let cm1 = natural_predecessor(skip_count), t = tail(tup)
173+
@inline skip_from_front(t, cm1)
174+
end
175+
else
176+
tup
177+
end
178+
end
179+
@assume_effects :foldable function skip_from_tail((@nospecialize tup::Tuple), @nospecialize skip_count::NonnegativeInteger)
180+
if skip_count isa PositiveIntegerUpperBound
181+
let cm1 = natural_predecessor(skip_count), t = front(tup)
182+
@inline skip_from_tail(t, cm1)
183+
end
184+
else
185+
tup
186+
end
187+
end
188+
function skip_from_front_nontrivial((@nospecialize tup::Tuple2OrMore), @nospecialize skip_count::PositiveInteger)
189+
skip_from_front(tup, skip_count)
190+
end
191+
function skip_from_tail_nontrivial((@nospecialize tup::Tuple2OrMore), @nospecialize skip_count::PositiveInteger)
192+
skip_from_tail(tup, skip_count)
193+
end
194+
end

test/sorting.jl

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,50 @@ end
9292
end
9393
@test sort(1:2000, by=x->x÷100, rev=true) == sort(1:2000, by=x->-x÷100) ==
9494
vcat(2000, (x:x+99 for x in 1900:-100:100)..., 1:99)
95+
@testset "tuples" begin
96+
tup = Tuple(0:9)
97+
@test tup === sort(tup; by = _ -> 0)
98+
@test (0, 2, 4, 6, 8, 1, 3, 5, 7, 9) === sort(tup; by = x -> isodd(x))
99+
@test (1, 3, 5, 7, 9, 0, 2, 4, 6, 8) === sort(tup; by = x -> iseven(x))
100+
end
101+
end
102+
103+
@testset "tuple sorting" begin
104+
max_unrolled_length = 31
105+
@testset "correctness" begin
106+
tup = Tuple(0:9)
107+
tup_rev = reverse(tup)
108+
@test tup === @inferred sort(tup)
109+
@test tup === sort(tup; rev = false)
110+
@test tup_rev === sort(tup; rev = true)
111+
@test tup_rev === sort(tup; lt = >)
112+
end
113+
@testset "inference" begin
114+
known_length = (Tuple{Vararg{Int, max_unrolled_length}}, Tuple{Vararg{Float64, max_unrolled_length}})
115+
unknown_length = (Tuple{Vararg{Int}}, Tuple{Vararg{Float64}})
116+
for Tup (known_length..., unknown_length...)
117+
@test Tup == Base.infer_return_type(sort, Tuple{Tup})
118+
end
119+
for Tup (known_length...,)
120+
@test Core.Compiler.is_foldable(Base.infer_effects(sort, Tuple{Tup}))
121+
end
122+
end
123+
@testset "alloc" begin
124+
function test_zero_allocated(tup::Tuple)
125+
@test iszero(@allocated sort(tup))
126+
end
127+
test_zero_allocated(ntuple(identity, max_unrolled_length))
128+
end
129+
@testset "heterogeneous" begin
130+
@testset "stability" begin
131+
tup = (0, 0x0, 0x000)
132+
@test tup === sort(tup)
133+
end
134+
tup = (1, 2, 3, missing, missing)
135+
for t (tup, (1, missing, 2, missing, 3), (missing, missing, 1, 2, 3))
136+
@test tup === @inferred sort(t)
137+
end
138+
end
95139
end
96140

97141
@testset "partialsort" begin

0 commit comments

Comments
 (0)