From 1c9865a4026abb498161dd1b72b5d87ee47d4e42 Mon Sep 17 00:00:00 2001 From: Cody Tapscott Date: Mon, 18 Aug 2025 09:35:25 -0400 Subject: [PATCH 1/2] Make `haskey(::@Kwargs{item::Bool}, :item)` constant-fold Rather than assume the value of `.itr` for `Pairs{..., <:NamedTuple}` this introduces `nothing` as a sentinel to iterate `.data` directly, which is sufficient for this to const-prop. --- Compiler/test/inference.jl | 7 +++++++ base/essentials.jl | 4 ++-- base/iterators.jl | 20 ++++++++++---------- base/namedtuple.jl | 4 ++-- base/show.jl | 6 ++++-- test/show.jl | 2 +- 6 files changed, 26 insertions(+), 17 deletions(-) diff --git a/Compiler/test/inference.jl b/Compiler/test/inference.jl index 58cd3db49c371..c8df59a60c05e 100644 --- a/Compiler/test/inference.jl +++ b/Compiler/test/inference.jl @@ -6515,4 +6515,11 @@ end <: Bool Core.get_binding_type(m, n, xs...) end <: Type +# issue #59269 +function haskey_inference_test() + kwargs = Core.compilerbarrier(:const, Base.pairs((; item = false))) + return haskey(kwargs, :item) ? nothing : Any[] +end +@inferred haskey_inference_test() + end # module inference diff --git a/base/essentials.jl b/base/essentials.jl index f27346c4b43cb..e37ce55dac4e6 100644 --- a/base/essentials.jl +++ b/base/essentials.jl @@ -503,9 +503,9 @@ end Pairs{K, V, I, A}(data, itr) where {K, V, I, A} = $(Expr(:new, :(Pairs{K, V, I, A}), :(data isa A ? data : convert(A, data)), :(itr isa I ? itr : convert(I, itr)))) Pairs{K, V}(data::A, itr::I) where {K, V, I, A} = $(Expr(:new, :(Pairs{K, V, I, A}), :data, :itr)) Pairs{K}(data::A, itr::I) where {K, I, A} = $(Expr(:new, :(Pairs{K, eltype(A), I, A}), :data, :itr)) - Pairs(data::A, itr::I) where {I, A} = $(Expr(:new, :(Pairs{eltype(I), eltype(A), I, A}), :data, :itr)) + Pairs(data::A, itr::I) where {I, A} = $(Expr(:new, :(Pairs{I !== Nothing ? eltype(I) : keytype(A), eltype(A), I, A}), :data, :itr)) end -pairs(::Type{NamedTuple}) = Pairs{Symbol, V, NTuple{N, Symbol}, NamedTuple{names, T}} where {V, N, names, T<:NTuple{N, Any}} +pairs(::Type{NamedTuple}) = Pairs{Symbol, V, Nothing, NT} where {V, NT <: NamedTuple} """ Base.Pairs(values, keys) <: AbstractDict{eltype(keys), eltype(values)} diff --git a/base/iterators.jl b/base/iterators.jl index a47a8dc4e698e..61935c346da86 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -267,7 +267,7 @@ pairs(::IndexLinear, A::AbstractArray) = Pairs(A, LinearIndices(A)) # preserve indexing capabilities for known indexable types # faster than zip(keys(a), values(a)) for arrays pairs(tuple::Tuple) = Pairs{Int}(tuple, keys(tuple)) -pairs(nt::NamedTuple) = Pairs{Symbol}(nt, keys(nt)) +pairs(nt::NamedTuple) = Pairs{Symbol}(nt, nothing) pairs(v::Core.SimpleVector) = Pairs(v, LinearIndices(v)) pairs(A::AbstractVector) = pairs(IndexLinear(), A) # pairs(v::Pairs) = v # listed for reference, but already defined from being an AbstractDict @@ -275,40 +275,40 @@ pairs(A::AbstractVector) = pairs(IndexLinear(), A) pairs(::IndexCartesian, A::AbstractArray) = Pairs(A, Base.CartesianIndices(axes(A))) pairs(A::AbstractArray) = pairs(IndexCartesian(), A) -length(v::Pairs) = length(getfield(v, :itr)) -axes(v::Pairs) = axes(getfield(v, :itr)) -size(v::Pairs) = size(getfield(v, :itr)) +length(v::Pairs) = length(keys(v)) +axes(v::Pairs) = axes(keys(v)) +size(v::Pairs) = size(keys(v)) Base.@eval @propagate_inbounds function _pairs_elt(p::Pairs{K, V}, idx) where {K, V} return $(Expr(:new, :(Pair{K, V}), :idx, :(getfield(p, :data)[idx]))) end @propagate_inbounds function iterate(p::Pairs{K, V}, state...) where {K, V} - x = iterate(getfield(p, :itr), state...) + x = iterate(keys(p), state...) x === nothing && return x idx, next = x return (_pairs_elt(p, idx), next) end -@propagate_inbounds function iterate(r::Reverse{<:Pairs}, state=(reverse(getfield(r.itr, :itr)),)) +@propagate_inbounds function iterate(r::Reverse{<:Pairs}, state=(reverse(keys(r.itr)),)) x = iterate(state...) x === nothing && return x idx, next = x return (_pairs_elt(r.itr, idx), (state[1], next)) end -@inline isdone(v::Pairs, state...) = isdone(getfield(v, :itr), state...) +@inline isdone(v::Pairs, state...) = isdone(keys(v), state...) IteratorSize(::Type{<:Pairs{<:Any, <:Any, I}}) where {I} = IteratorSize(I) IteratorSize(::Type{<:Pairs{<:Any, <:Any, <:AbstractUnitRange, <:Tuple}}) = HasLength() function last(v::Pairs{K, V}) where {K, V} - idx = last(getfield(v, :itr)) + idx = last(keys(v)) return Pair{K, V}(idx, v[idx]) end -haskey(v::Pairs, key) = (key in getfield(v, :itr)) -keys(v::Pairs) = getfield(v, :itr) +haskey(v::Pairs, key) = key in keys(v) +keys(v::Pairs) = getfield(v, :itr) === nothing ? keys(getfield(v, :data)) : getfield(v, :itr) values(v::Pairs) = getfield(v, :data) # TODO: this should be a view of data subset by itr getindex(v::Pairs, key) = getfield(v, :data)[key] setindex!(v::Pairs, value, key) = (getfield(v, :data)[key] = value; v) diff --git a/base/namedtuple.jl b/base/namedtuple.jl index f71b13852b953..02dca86c59c33 100644 --- a/base/namedtuple.jl +++ b/base/namedtuple.jl @@ -535,7 +535,7 @@ when it is printed in the stack trace view. ```julia julia> @Kwargs{init::Int} # the internal representation of keyword arguments -Base.Pairs{Symbol, Int64, Tuple{Symbol}, @NamedTuple{init::Int64}} +Base.Pairs{Symbol, Int64, Nothing, @NamedTuple{init::Int64}} julia> sum("julia"; init=1) ERROR: MethodError: no method matching +(::Char, ::Char) @@ -578,7 +578,7 @@ Stacktrace: macro Kwargs(ex) return :(let NT = @NamedTuple $ex - Base.Pairs{keytype(NT),eltype(NT),typeof(NT.parameters[1]),NT} + Base.Pairs{keytype(NT),eltype(NT),Nothing,NT} end) end diff --git a/base/show.jl b/base/show.jl index 380006bfcf050..7016c55c57fe4 100644 --- a/base/show.jl +++ b/base/show.jl @@ -1116,6 +1116,8 @@ function show_type_name(io::IO, tn::Core.TypeName) end function maybe_kws_nt(x::DataType) + # manually-written version of + # x <: (Pairs{Symbol, eltype(NT), Nothing, NT} where NT <: NamedTuple) x.name === typename(Pairs) || return nothing length(x.parameters) == 4 || return nothing x.parameters[1] === Symbol || return nothing @@ -1125,7 +1127,7 @@ function maybe_kws_nt(x::DataType) types isa DataType || return nothing x.parameters[2] === eltype(p4) || return nothing isa(syms, Tuple) || return nothing - x.parameters[3] === typeof(syms) || return nothing + x.parameters[3] === Nothing || return nothing return p4 end return nothing @@ -3219,7 +3221,7 @@ function Base.showarg(io::IO, r::Iterators.Pairs{<:Integer, <:Any, <:Any, T}, to print(io, "pairs(IndexLinear(), ::", T, ")") end -function Base.showarg(io::IO, r::Iterators.Pairs{Symbol, <:Any, <:Any, T}, toplevel) where {T <: NamedTuple} +function Base.showarg(io::IO, r::Iterators.Pairs{Symbol, <:Any, Nothing, T}, toplevel) where {T <: NamedTuple} print(io, "pairs(::NamedTuple)") end diff --git a/test/show.jl b/test/show.jl index 60d0538e71a07..82a5a42bdfb4f 100644 --- a/test/show.jl +++ b/test/show.jl @@ -1450,7 +1450,7 @@ test_repr("(:).a") @test repr(@NamedTuple{var"#"::Int64}) == "@NamedTuple{var\"#\"::Int64}" # Test general printing of `Base.Pairs` (it should not use the `@Kwargs` macro syntax) -@test repr(@Kwargs{init::Int}) == "Base.Pairs{Symbol, $Int, Tuple{Symbol}, @NamedTuple{init::$Int}}" +@test repr(@Kwargs{init::Int}) == "Base.Pairs{Symbol, $Int, Nothing, @NamedTuple{init::$Int}}" @testset "issue #42931" begin @test repr(NTuple{4, :A}) == "Tuple{:A, :A, :A, :A}" From 3f20eec55cfc14da73b17507766751cb27c8cfdd Mon Sep 17 00:00:00 2001 From: Cody Tapscott Date: Tue, 19 Aug 2025 18:22:11 -0400 Subject: [PATCH 2/2] Restrict `merge(::NamedTuple, ::Pair{..., NamedTuple})` optimization This restricts this optimization to apply only to "fully-iterated" NamedTuple Pairs, which are the only kind generated by lowering and `Base.pairs`. Resolves #59292. --- base/namedtuple.jl | 2 +- test/namedtuple.jl | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/base/namedtuple.jl b/base/namedtuple.jl index 02dca86c59c33..37f3a3ef8436b 100644 --- a/base/namedtuple.jl +++ b/base/namedtuple.jl @@ -343,7 +343,7 @@ merge(a::NamedTuple, b::NamedTuple{()}) = a merge(a::NamedTuple{()}, b::NamedTuple{()}) = a merge(a::NamedTuple{()}, b::NamedTuple) = b -merge(a::NamedTuple, b::Iterators.Pairs{<:Any,<:Any,<:Any,<:NamedTuple}) = merge(a, getfield(b, :data)) +merge(a::NamedTuple, b::Iterators.Pairs{<:Any,<:Any,Nothing,<:NamedTuple}) = merge(a, getfield(b, :data)) merge(a::NamedTuple, b::Iterators.Zip{<:Tuple{Any,Any}}) = merge(a, NamedTuple{Tuple(b.is[1])}(b.is[2])) diff --git a/test/namedtuple.jl b/test/namedtuple.jl index b8dba5c06422e..0f54196879a43 100644 --- a/test/namedtuple.jl +++ b/test/namedtuple.jl @@ -163,6 +163,10 @@ end @test merge(NamedTuple(), [:a=>1, :b=>2, :c=>3, :a=>4, :c=>5]) == (a=4, b=2, c=5) @test merge((c=0, z=1), [:a=>1, :b=>2, :c=>3, :a=>4, :c=>5]) == (c=5, z=1, a=4, b=2) +# https://github.com/JuliaLang/julia/issues/59292 +@test merge((; a = 1), Base.Pairs((; b = 2, c = 3), (:b,))) == (a = 1, b = 2) +@test merge((; a = 1), Base.pairs((; b = 2, c = 3))) == (a = 1, b = 2, c = 3) + @test keys((a=1, b=2, c=3)) == (:a, :b, :c) @test keys(NamedTuple()) == () @test keys((a=1,)) == (:a,)