Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions base/essentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -533,8 +533,14 @@ julia> Base.tail(())
ERROR: ArgumentError: Cannot call tail on an empty tuple.
```
"""
tail(x::Tuple) = argtail(x...)
tail(::Tuple{}) = throw(ArgumentError("Cannot call tail on an empty tuple."))
function tail(x::Tuple)
f(x::Tuple) = argtail(x...)
function f(::Tuple{})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better to use a branch for this I think.

Copy link
Member Author

@nsajko nsajko Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just here, for tail, or also for other functions? I see that first, which is very similar to tail, has a comment that says branching is not preferred:

julia/base/tuple.jl

Lines 266 to 268 in 9ef12b3

# Use dispatch to avoid a branch in first
first(::Tuple{}) = throw(ArgumentError("tuple must be non-empty"))
first(t::Tuple) = t[1]

Although perhaps that concern is outdated.

@noinline
throw(ArgumentError("Cannot call tail on an empty tuple."))
end
f(x)
end

function unwrap_unionall(@nospecialize(a))
@_foldable_meta
Expand Down
7 changes: 0 additions & 7 deletions base/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -223,13 +223,6 @@ isless(x::AbstractFloat, y::AbstractFloat) = (!isnan(x) & (isnan(y) | signless(x
isless(x::Real, y::AbstractFloat) = (!isnan(x) & (isnan(y) | signless(x, y))) | (x < y)
isless(x::AbstractFloat, y::Real ) = (!isnan(x) & (isnan(y) | signless(x, y))) | (x < y)

# Performance optimization to reduce branching
# This is useful for sorting tuples of integers
# TODO: remove this when the compiler can optimize the generic version better
# See #48724 and #48753
isless(a::Tuple{BitInteger, BitInteger}, b::Tuple{BitInteger, BitInteger}) =
isless(a[1], b[1]) | (isequal(a[1], b[1]) & isless(a[2], b[2]))

"""
isgreater(x, y)

Expand Down
132 changes: 82 additions & 50 deletions base/tuple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,14 @@ end

@eval split_rest(t::Tuple, n::Int, i=1) = ($(Expr(:meta, :aggressive_constprop)); (t[i:end-n], t[end-n+1:end]))

# Use dispatch to avoid a branch in first
first(::Tuple{}) = throw(ArgumentError("tuple must be non-empty"))
first(t::Tuple) = t[1]
function first(t::Tuple)
f(t::Tuple) = t[1]
function f(::Tuple{})
@noinline
throw(ArgumentError("tuple must be non-empty"))
end
f(t)
end

# eltype

Expand Down Expand Up @@ -577,71 +582,95 @@ function _eq(t1::Any32, t2::Any32)
end

const tuplehash_seed = UInt === UInt64 ? 0x77cfa1eef01bca90 : 0xf01bca90
hash(::Tuple{}, h::UInt) = h ⊻ tuplehash_seed
hash(t::Tuple, h::UInt) = hash(t[1], hash(tail(t), h))
function hash(t::Any32, h::UInt)
out = h ⊻ tuplehash_seed
for i = length(t):-1:1
out = hash(t[i], out)
function hash(t::Tuple, h::UInt)
f(::Tuple{}, h::UInt) = h ⊻ tuplehash_seed
f(t::Tuple, h::UInt) = hash(t[1], hash(tail(t), h))
function f(t::Any32, h::UInt)
out = h ⊻ tuplehash_seed
for i = length(t):-1:1
out = hash(t[i], out)
end
return out
end
return out
f(t, h)
end

<(::Tuple{}, ::Tuple{}) = false
<(::Tuple{}, ::Tuple) = true
<(::Tuple, ::Tuple{}) = false
function <(t1::Tuple, t2::Tuple)
a, b = t1[1], t2[1]
eq = (a == b)
if ismissing(eq)
return missing
elseif !eq
return a < b
end
return tail(t1) < tail(t2)
end
function <(t1::Any32, t2::Any32)
n1, n2 = length(t1), length(t2)
for i = 1:min(n1, n2)
a, b = t1[i], t2[i]
f(::Tuple{}, ::Tuple{}) = false
f(::Tuple{}, ::Tuple) = true
f(::Tuple, ::Tuple{}) = false
function f(t1::Tuple, t2::Tuple)
a, b = t1[1], t2[1]
eq = (a == b)
if ismissing(eq)
return missing
elseif !eq
return a < b
return a < b
end
return tail(t1) < tail(t2)
end
function f(t1::Any32, t2::Any32)
n1, n2 = length(t1), length(t2)
for i = 1:min(n1, n2)
a, b = t1[i], t2[i]
eq = (a == b)
if ismissing(eq)
return missing
elseif !eq
return a < b
end
end
return n1 < n2
end
return n1 < n2
f(t1, t2)
end

isless(::Tuple{}, ::Tuple{}) = false
isless(::Tuple{}, ::Tuple) = true
isless(::Tuple, ::Tuple{}) = false
# copy of `BitInteger` defined later during bootstrap in int.jl
const _BitInteger = Union{
Int8, Int16, Int32, Int64, Int128,
UInt8, UInt16, UInt32, UInt64, UInt128,
}

"""
isless(t1::Tuple, t2::Tuple)

Return `true` when `t1` is less than `t2` in lexicographic order.
"""
function isless(t1::Tuple, t2::Tuple)
a, b = t1[1], t2[1]
isless(a, b) || (isequal(a, b) && isless(tail(t1), tail(t2)))
end
function isless(t1::Any32, t2::Any32)
n1, n2 = length(t1), length(t2)
for i = 1:min(n1, n2)
a, b = t1[i], t2[i]
if !isequal(a, b)
return isless(a, b)
f(::Tuple{}, ::Tuple{}) = false
f(::Tuple{}, ::Tuple) = true
f(::Tuple, ::Tuple{}) = false
function f(t1::Tuple, t2::Tuple)
a, b = t1[1], t2[1]
isless(a, b) || (isequal(a, b) && isless(tail(t1), tail(t2)))
end
function f(t1::Any32, t2::Any32)
n1, n2 = length(t1), length(t2)
for i = 1:min(n1, n2)
a, b = t1[i], t2[i]
if !isequal(a, b)
return isless(a, b)
end
end
return n1 < n2
end
# Performance optimization to reduce branching
# This is useful for sorting tuples of integers
# TODO: remove this when the compiler can optimize the generic version better
# See #48724 and #48753
function f(a::Tuple{_BitInteger, _BitInteger}, b::Tuple{_BitInteger, _BitInteger})
isless(a[1], b[1]) | (isequal(a[1], b[1]) & isless(a[2], b[2]))
end
return n1 < n2
f(t1, t2)
end

## functions ##

isempty(x::Tuple{}) = true
isempty(@nospecialize x::Tuple) = false
function isempty(x::Tuple)
f(x::Tuple{}) = true
f(@nospecialize x::Tuple) = false
f(x)
end

revargs() = ()
revargs(x, r...) = (revargs(r...)..., x)
Expand Down Expand Up @@ -679,11 +708,14 @@ empty(@nospecialize x::Tuple) = ()
foreach(f, itr::Tuple) = foldl((_, x) -> (f(x); nothing), itr, init=nothing)
foreach(f, itr::Tuple, itrs::Tuple...) = foldl((_, xs) -> (f(xs...); nothing), zip(itr, itrs...), init=nothing)

circshift((@nospecialize t::Union{Tuple{},Tuple{Any}}), @nospecialize _::Integer) = t
circshift(t::Tuple{Any,Any}, shift::Integer) = iseven(shift) ? t : reverse(t)
function circshift(x::Tuple{Any,Any,Any,Vararg{Any,N}}, shift::Integer) where {N}
@inline
len = N + 3
j = mod1(shift, len)
ntuple(k -> getindex(x, k-j+ifelse(k>j,0,len)), Val(len))::Tuple
function circshift(t::Tuple, shift::Integer)
f((@nospecialize t::Union{Tuple{},Tuple{Any}}), @nospecialize _::Integer) = t
f(t::Tuple{Any,Any}, shift::Integer) = iseven(shift) ? t : reverse(t)
function f(x::Tuple{Any,Any,Any,Vararg{Any,N}}, shift::Integer) where {N}
@inline
len = N + 3
j = mod1(shift, len)
ntuple(k -> getindex(x, k-j+ifelse(k>j,0,len)), Val(len))::Tuple
end
f(t, shift)
end
12 changes: 12 additions & 0 deletions test/tuple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,18 @@ namedtup = (;a=1, b=2, c=3)
@test Val{Tuple{Int64, Vararg{Int32,N}} where N} === Val{Tuple{Int64, Vararg{Int32}}}
@test Val{Tuple{Int32, Vararg{Int64}}} === Val{Tuple{Int32, Vararg{Int64,N}} where N}

@testset "avoid method proliferation" begin
t = isone ∘ length ∘ methods
@test t(circshift, Tuple{Tuple, Integer})
@test t(hash, Tuple{Tuple, UInt})
for f in (Base.tail, first, isempty)
@test t(f, Tuple{Tuple})
end
for f in (<, isless, ==, isequal)
@test t(f, Tuple{Tuple, Tuple})
end
end

@testset "from Pair, issue #52636" begin
pair = (1 => "2")
@test (1, "2") == @inferred Tuple(pair)
Expand Down