Skip to content

Commit 5547305

Browse files
vtjnashomus
andauthored
Fix duplicate error when using generator in Dict (JuliaLang#53151)
Fixes: JuliaLang#33147 Replaces/Closes: JuliaLang#40445 The difference here, compared to past implementations, is that we use the zero-cost `isiterable` check on every intermediate step, instead of wrapping the call in a try/catch and then trying to re-approximate the `isiterable` afterwards. Some samples: ```julia julia> Dict(i for i in 1:3) ERROR: ArgumentError: AbstractDict(kv): kv needs to be an iterator of 2-tuples or pairs Stacktrace: [1] _throw_dict_kv_error() @ Base ./dict.jl:118 [2] grow_to! @ ./dict.jl:132 [inlined] [3] dict_with_eltype @ ./abstractdict.jl:592 [inlined] [4] Dict(kv::Base.Generator{UnitRange{Int64}, typeof(identity)}) @ Base ./dict.jl:120 [5] top-level scope @ REPL[1]:1 julia> Dict(i => error("$i") for i in 1:3) ERROR: 1 Stacktrace: [1] error(s::String) @ Base ./error.jl:35 [2] (::var"mmtk#3#4")(i::Int64) @ Main ./none:0 [3] iterate @ ./generator.jl:48 [inlined] [4] grow_to! @ ./dict.jl:124 [inlined] [5] dict_with_eltype @ ./abstractdict.jl:592 [inlined] [6] Dict(kv::Base.Generator{UnitRange{Int64}, var"mmtk#3#4"}) @ Base ./dict.jl:120 [7] top-level scope @ REPL[2]:1 ``` The other unrelated change here is that `dest = empty(dest, typeof(k), typeof(v))` is made conditional, so we do not unconditionally construct an empty Dict in order to discard it and allocate an exact duplicate of it, but only do so if inference wasn't precise originally. Co-authored-by: Curtis Vogt <curtis.vogt@gmail.com>
1 parent b43edb7 commit 5547305

File tree

5 files changed

+76
-62
lines changed

5 files changed

+76
-62
lines changed

base/abstractdict.jl

+49
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,55 @@ _tablesz(x::T) where T <: Integer = x < 16 ? T(16) : one(T)<<(top_set_bit(x-one(
579579

580580
TP{K,V} = Union{Type{Tuple{K,V}},Type{Pair{K,V}}}
581581

582+
# This error is thrown if `grow_to!` cannot validate the contents of the iterator argument to it, which it does by testing the iteration protocol (isiterable) on it each time it is about to start iteration on it
583+
_throw_dict_kv_error() = throw(ArgumentError("AbstractDict(kv): kv needs to be an iterator of 2-tuples or pairs"))
584+
585+
function grow_to!(dest::AbstractDict, itr)
586+
applicable(iterate, itr) || _throw_dict_kv_error()
587+
y = iterate(itr)
588+
y === nothing && return dest
589+
kv, st = y
590+
applicable(iterate, kv) || _throw_dict_kv_error()
591+
k = iterate(kv)
592+
k === nothing && _throw_dict_kv_error()
593+
k, kvst = k
594+
v = iterate(kv, kvst)
595+
v === nothing && _throw_dict_kv_error()
596+
v, kvst = v
597+
iterate(kv, kvst) === nothing || _throw_dict_kv_error()
598+
if !(dest isa AbstractDict{typeof(k), typeof(v)})
599+
dest = empty(dest, typeof(k), typeof(v))
600+
end
601+
dest[k] = v
602+
return grow_to!(dest, itr, st)
603+
end
604+
605+
function grow_to!(dest::AbstractDict{K,V}, itr, st) where {K, V}
606+
y = iterate(itr, st)
607+
while y !== nothing
608+
kv, st = y
609+
applicable(iterate, kv) || _throw_dict_kv_error()
610+
kst = iterate(kv)
611+
kst === nothing && _throw_dict_kv_error()
612+
k, kvst = kst
613+
vst = iterate(kv, kvst)
614+
vst === nothing && _throw_dict_kv_error()
615+
v, kvst = vst
616+
iterate(kv, kvst) === nothing || _throw_dict_kv_error()
617+
if isa(k, K) && isa(v, V)
618+
dest[k] = v
619+
else
620+
new = empty(dest, promote_typejoin(K, typeof(k)), promote_typejoin(V, typeof(v)))
621+
merge!(new, dest)
622+
new[k] = v
623+
return grow_to!(new, itr, st)
624+
end
625+
y = iterate(itr, st)
626+
end
627+
return dest
628+
end
629+
630+
582631
dict_with_eltype(DT_apply, kv, ::TP{K,V}) where {K,V} = DT_apply(K, V)(kv)
583632
dict_with_eltype(DT_apply, kv::Generator, ::TP{K,V}) where {K,V} = DT_apply(K, V)(kv)
584633
dict_with_eltype(DT_apply, ::Type{Pair{K,V}}) where {K,V} = DT_apply(K, V)()

base/dict.jl

+1-39
Original file line numberDiff line numberDiff line change
@@ -114,45 +114,7 @@ const AnyDict = Dict{Any,Any}
114114
Dict(ps::Pair{K,V}...) where {K,V} = Dict{K,V}(ps)
115115
Dict(ps::Pair...) = Dict(ps)
116116

117-
function Dict(kv)
118-
try
119-
dict_with_eltype((K, V) -> Dict{K, V}, kv, eltype(kv))
120-
catch
121-
if !isiterable(typeof(kv)) || !all(x->isa(x,Union{Tuple,Pair}),kv)
122-
throw(ArgumentError("Dict(kv): kv needs to be an iterator of tuples or pairs"))
123-
else
124-
rethrow()
125-
end
126-
end
127-
end
128-
129-
function grow_to!(dest::AbstractDict{K, V}, itr) where V where K
130-
y = iterate(itr)
131-
y === nothing && return dest
132-
((k,v), st) = y
133-
dest2 = empty(dest, typeof(k), typeof(v))
134-
dest2[k] = v
135-
grow_to!(dest2, itr, st)
136-
end
137-
138-
# this is a special case due to (1) allowing both Pairs and Tuples as elements,
139-
# and (2) Pair being invariant. a bit annoying.
140-
function grow_to!(dest::AbstractDict{K,V}, itr, st) where V where K
141-
y = iterate(itr, st)
142-
while y !== nothing
143-
(k,v), st = y
144-
if isa(k,K) && isa(v,V)
145-
dest[k] = v
146-
else
147-
new = empty(dest, promote_typejoin(K,typeof(k)), promote_typejoin(V,typeof(v)))
148-
merge!(new, dest)
149-
new[k] = v
150-
return grow_to!(new, itr, st)
151-
end
152-
y = iterate(itr, st)
153-
end
154-
return dest
155-
end
117+
Dict(kv) = dict_with_eltype((K, V) -> Dict{K, V}, kv, eltype(kv))
156118

157119
empty(a::AbstractDict, ::Type{K}, ::Type{V}) where {K, V} = Dict{K, V}()
158120

base/iddict.jl

+1-12
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,7 @@ IdDict(ps::Pair{K}...) where {K} = IdDict{K,Any}(ps)
5353
IdDict(ps::(Pair{K,V} where K)...) where {V} = IdDict{Any,V}(ps)
5454
IdDict(ps::Pair...) = IdDict{Any,Any}(ps)
5555

56-
function IdDict(kv)
57-
try
58-
dict_with_eltype((K, V) -> IdDict{K, V}, kv, eltype(kv))
59-
catch
60-
if !applicable(iterate, kv) || !all(x->isa(x,Union{Tuple,Pair}),kv)
61-
throw(ArgumentError(
62-
"IdDict(kv): kv needs to be an iterator of tuples or pairs"))
63-
else
64-
rethrow()
65-
end
66-
end
67-
end
56+
IdDict(kv) = dict_with_eltype((K, V) -> IdDict{K, V}, kv, eltype(kv))
6857

6958
empty(d::IdDict, ::Type{K}, ::Type{V}) where {K, V} = IdDict{K,V}()
7059

base/weakkeydict.jl

+1-11
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,7 @@ WeakKeyDict(ps::Pair{K}...) where {K} = WeakKeyDict{K,Any}(ps)
5454
WeakKeyDict(ps::(Pair{K,V} where K)...) where {V} = WeakKeyDict{Any,V}(ps)
5555
WeakKeyDict(ps::Pair...) = WeakKeyDict{Any,Any}(ps)
5656

57-
function WeakKeyDict(kv)
58-
try
59-
Base.dict_with_eltype((K, V) -> WeakKeyDict{K, V}, kv, eltype(kv))
60-
catch
61-
if !isiterable(typeof(kv)) || !all(x->isa(x,Union{Tuple,Pair}),kv)
62-
throw(ArgumentError("WeakKeyDict(kv): kv needs to be an iterator of tuples or pairs"))
63-
else
64-
rethrow()
65-
end
66-
end
67-
end
57+
WeakKeyDict(kv) = Base.dict_with_eltype((K, V) -> WeakKeyDict{K, V}, kv, eltype(kv))
6858

6959
function _cleanup_locked(h::WeakKeyDict)
7060
if h.dirty

test/dict.jl

+24
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,30 @@ end
162162

163163
# issue #39117
164164
@test Dict(t[1]=>t[2] for t in zip((1,"2"), (2,"2"))) == Dict{Any,Any}(1=>2, "2"=>"2")
165+
166+
@testset "issue #33147" begin
167+
expected = try; Base._throw_dict_kv_error(); catch e; e; end
168+
@test_throws expected Dict(i for i in 1:2)
169+
@test_throws expected Dict(nothing for i in 1:2)
170+
@test_throws expected Dict(() for i in 1:2)
171+
@test_throws expected Dict((i, i, i) for i in 1:2)
172+
@test_throws expected Dict(nothing)
173+
@test_throws expected Dict((1,))
174+
@test_throws expected Dict(1:2)
175+
@test_throws expected Dict(((),))
176+
@test_throws expected IdDict(((),))
177+
@test_throws expected WeakKeyDict(((),))
178+
@test_throws expected IdDict(nothing)
179+
@test_throws expected WeakKeyDict(nothing)
180+
@test Dict(1:0) isa Dict
181+
@test Dict(()) isa Dict
182+
try
183+
Dict(i => error("$i") for i in 1:3)
184+
catch ex
185+
@test ex isa ErrorException
186+
@test length(Base.current_exceptions()) == 1
187+
end
188+
end
165189
end
166190

167191
@testset "empty tuple ctor" begin

0 commit comments

Comments
 (0)