Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added data-structure WeakKeyIdDict #1872

Closed
wants to merge 2 commits into from
Closed
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
237 changes: 237 additions & 0 deletions src/DataStructures/weakkeyid_dict.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# Weak key dict using object-id hashing/equality
# see also https://github.com/JuliaCollections/DataStructures.jl/pull/402
# Based on Julia's WeakKeyIdDict

# Type to wrap a WeakRef to furbish it with objectid comparison and hashing.
#
# Note that various getter and setter functions below all need to explicitly
# use `WeakRefForWeakDict(key)` instead of `key` because the automatism that
# works for `WeakRef` does not work here: for `WeakRef` the hash function is
# simply that of the wrapped object, and comparing a `WeakRef` to a value
# automatically unwraps. But this does not work for `WeakRefForWeakDict`
# because we use a custom hash function based on the `objectid` (this is
# important because it allows efficient use of objects as keys even if
# there is no effective hash function for those objects).
struct WeakRefForWeakDict
w::WeakRef
WeakRefForWeakDict(wr::WeakRef) = new(wr)

Check warning on line 17 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L17

Added line #L17 was not covered by tests
WeakRefForWeakDict(@nospecialize(v)) = new(WeakRef(v))
end

Base.:(==)(wr1::WeakRefForWeakDict, wr2::WeakRefForWeakDict) = wr1.w.value===wr2.w.value
Base.hash(wr::WeakRefForWeakDict, h::UInt) = Base.hash_uint(3h - objectid(wr.w.value))

"""
WeakKeyIdDict([itr])

`WeakKeyIdDict()` constructs a hash table where the keys are weak
references to objects which may be garbage collected even when
referenced in a hash table.

See [`Dict`](@ref) for further help. Note, unlike [`Dict`](@ref),
`WeakKeyIdDict` does not convert keys on insertion, as this would imply the key
object was unreferenced anywhere before insertion.

See also [`WeakRef`](@ref), [`WeakKeyDict`](@ref).
"""
mutable struct WeakKeyIdDict{K,V} <: AbstractDict{K,V}
ht::Dict{WeakRefForWeakDict,V}
lock::ReentrantLock
finalizer::Function
dirty::Bool

# Constructors mirror Dict's
function WeakKeyIdDict{K,V}() where V where K
t = new(Dict{WeakRefForWeakDict,V}(), ReentrantLock(), identity, 0)
t.finalizer = k -> t.dirty = true
return t
end
end
function WeakKeyIdDict{K,V}(kv) where V where K
h = WeakKeyIdDict{K,V}()
for (k,v) in kv
h[k] = v
end
return h
end
WeakKeyIdDict{K,V}(p::Pair) where V where K = setindex!(WeakKeyIdDict{K,V}(), p.second, p.first)
function WeakKeyIdDict{K,V}(ps::Pair...) where V where K
h = WeakKeyIdDict{K,V}()
sizehint!(h, length(ps))
for p in ps
h[p.first] = p.second
end
return h

Check warning on line 64 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L58-L64

Added lines #L58 - L64 were not covered by tests
end
WeakKeyIdDict() = WeakKeyIdDict{Any,Any}()

WeakKeyIdDict(kv::Tuple{}) = WeakKeyIdDict()

Check warning on line 68 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L68

Added line #L68 was not covered by tests
Base.copy(d::WeakKeyIdDict) = WeakKeyIdDict(d)

WeakKeyIdDict(ps::Pair{K,V}...) where {K,V} = WeakKeyIdDict{K,V}(ps)
WeakKeyIdDict(ps::Pair{K}...) where {K} = WeakKeyIdDict{K,Any}(ps)
WeakKeyIdDict(ps::(Pair{K,V} where K)...) where {V} = WeakKeyIdDict{Any,V}(ps)
WeakKeyIdDict(ps::Pair...) = WeakKeyIdDict{Any,Any}(ps)

Check warning on line 74 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L72-L74

Added lines #L72 - L74 were not covered by tests

function WeakKeyIdDict(kv)
try
Base.dict_with_eltype((K, V) -> WeakKeyIdDict{K, V}, kv, eltype(kv))
catch
if !Base.isiterable(typeof(kv)) || !all(x->isa(x,Union{Tuple,Pair}),kv)
throw(ArgumentError("WeakKeyIdDict(kv): kv needs to be an iterator of tuples or pairs"))
else
rethrow()

Check warning on line 83 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L83

Added line #L83 was not covered by tests
end
end
end

function _cleanup_locked(h::WeakKeyIdDict)
if h.dirty
h.dirty = false
idx = Base.skip_deleted_floor!(h.ht)
while idx != 0
if h.ht.keys[idx].w.value === nothing
Base._delete!(h.ht, idx)

Check warning on line 94 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L90-L94

Added lines #L90 - L94 were not covered by tests
end
idx = Base.skip_deleted(h.ht, idx + 1)
end

Check warning on line 97 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L96-L97

Added lines #L96 - L97 were not covered by tests
end
return h
end

Base.sizehint!(d::WeakKeyIdDict, newsz) = sizehint!(d.ht, newsz)

Check warning on line 102 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L102

Added line #L102 was not covered by tests
Base.empty(d::WeakKeyIdDict, ::Type{K}, ::Type{V}) where {K, V} = WeakKeyIdDict{K, V}()

Base.IteratorSize(::Type{<:WeakKeyIdDict}) = Base.SizeUnknown()

Base.islocked(wkh::WeakKeyIdDict) = islocked(wkh.lock)
Base.lock(wkh::WeakKeyIdDict) = lock(wkh.lock)
Base.unlock(wkh::WeakKeyIdDict) = unlock(wkh.lock)

Check warning on line 109 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L107-L109

Added lines #L107 - L109 were not covered by tests
Base.lock(f, wkh::WeakKeyIdDict) = lock(f, wkh.lock)
Base.trylock(f, wkh::WeakKeyIdDict) = trylock(f, wkh.lock)

Check warning on line 111 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L111

Added line #L111 was not covered by tests

function Base.setindex!(wkh::WeakKeyIdDict{K}, v, key) where K
!isa(key, K) && throw(ArgumentError("$key is not a valid key for type $K"))
# 'nothing' is not valid both because 'finalizer' will reject it,
# and because we therefore use it as a sentinel value
key === nothing && throw(ArgumentError("`nothing` is not a valid WeakKeyIdDict key"))
lock(wkh) do
_cleanup_locked(wkh)
k = getkey(wkh.ht, WeakRefForWeakDict(key), nothing)
if k === nothing
finalizer(wkh.finalizer, key)
k = WeakRefForWeakDict(key)
else
k.w.value = key

Check warning on line 125 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L125

Added line #L125 was not covered by tests
end
wkh.ht[k] = v
end
return wkh
end
function Base.get!(wkh::WeakKeyIdDict{K}, key, default) where {K}
v = lock(wkh) do
k = WeakRefForWeakDict(key)
if key !== nothing && haskey(wkh.ht, k)
wkh.ht[k]

Check warning on line 135 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L131-L135

Added lines #L131 - L135 were not covered by tests
else
wkh[k] = default

Check warning on line 137 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L137

Added line #L137 was not covered by tests
end
end
return v

Check warning on line 140 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L140

Added line #L140 was not covered by tests
end
function Base.get!(default::Base.Callable, wkh::WeakKeyIdDict{K}, key) where {K}
v = lock(wkh) do
k = WeakRefForWeakDict(key)
if key !== nothing && haskey(wkh.ht, k)
wkh.ht[k]

Check warning on line 146 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L142-L146

Added lines #L142 - L146 were not covered by tests
else
wkh[k] = default()

Check warning on line 148 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L148

Added line #L148 was not covered by tests
end
end
return v

Check warning on line 151 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L151

Added line #L151 was not covered by tests
end

function Base.getkey(wkh::WeakKeyIdDict{K}, kk, default) where K
k = lock(wkh) do
local k = getkey(wkh.ht, WeakRefForWeakDict(kk), nothing)
k === nothing && return nothing
return k.w.value

Check warning on line 158 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L154-L158

Added lines #L154 - L158 were not covered by tests
end
return k === nothing ? default : k::K

Check warning on line 160 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L160

Added line #L160 was not covered by tests
end

Base.map!(f, iter::Base.ValueIterator{<:WeakKeyIdDict})= map!(f, values(iter.dict.ht))

Check warning on line 163 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L163

Added line #L163 was not covered by tests

function Base.get(wkh::WeakKeyIdDict{K}, key, default) where {K}
key === nothing && throw(KeyError(nothing))
lock(wkh) do
return get(wkh.ht, WeakRefForWeakDict(key), default)
end
end
function Base.get(default::Base.Callable, wkh::WeakKeyIdDict{K}, key) where {K}
key === nothing && throw(KeyError(nothing))
lock(wkh) do
return get(default, wkh.ht, WeakRefForWeakDict(key))

Check warning on line 174 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L171-L174

Added lines #L171 - L174 were not covered by tests
end
end
function Base.pop!(wkh::WeakKeyIdDict{K}, key) where {K}
key === nothing && throw(KeyError(nothing))
lock(wkh) do
return pop!(wkh.ht, WeakRefForWeakDict(key))
end
end
function Base.pop!(wkh::WeakKeyIdDict{K}, key, default) where {K}
key === nothing && return default
lock(wkh) do
return pop!(wkh.ht, WeakRefForWeakDict(key), default)

Check warning on line 186 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L183-L186

Added lines #L183 - L186 were not covered by tests
end
end
function Base.delete!(wkh::WeakKeyIdDict, key)
key === nothing && return wkh
lock(wkh) do
delete!(wkh.ht, WeakRefForWeakDict(key))
end
return wkh
end
function Base.empty!(wkh::WeakKeyIdDict)
lock(wkh) do
empty!(wkh.ht)
end
return wkh
end
function Base.haskey(wkh::WeakKeyIdDict{K}, key) where {K}
key === nothing && return false
lock(wkh) do
return haskey(wkh.ht, WeakRefForWeakDict(key))

Check warning on line 205 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L202-L205

Added lines #L202 - L205 were not covered by tests
end
end
function Base.getindex(wkh::WeakKeyIdDict{K}, key) where {K}
key === nothing && throw(KeyError(nothing))
lock(wkh) do
return getindex(wkh.ht, WeakRefForWeakDict(key))

Check warning on line 211 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L208-L211

Added lines #L208 - L211 were not covered by tests
end
end
Base.isempty(wkh::WeakKeyIdDict) = length(wkh) == 0
function Base.length(t::WeakKeyIdDict)
lock(t) do
_cleanup_locked(t)
return length(t.ht)
end
end

function Base.iterate(t::WeakKeyIdDict{K,V}, state...) where {K, V}
return lock(t) do
while true
y = iterate(t.ht, state...)
y === nothing && return nothing
wkv, state = y
k = wkv[1].w.value
GC.safepoint() # ensure `k` is now gc-rooted
k === nothing && continue # indicates `k` is scheduled for deletion
kv = Pair{K,V}(k::K, wkv[2])
return (kv, state)
end

Check warning on line 233 in src/DataStructures/weakkeyid_dict.jl

View check run for this annotation

Codecov / codecov/patch

src/DataStructures/weakkeyid_dict.jl#L233

Added line #L233 was not covered by tests
end
end

Base.filter!(f, d::WeakKeyIdDict) = Base.filter_in_one_pass!(f, d)
1 change: 1 addition & 0 deletions src/Oscar.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ using Preferences
using LazyArtifacts

include("imports.jl")
include("DataStructures/weakkeyid_dict.jl")

include("utils/utils.jl")

Expand Down
69 changes: 69 additions & 0 deletions test/DataStructures/weakkeyid_dict-test.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
@testset "WeakKeyIdDict" begin
WeakKeyIdDict = Oscar.WeakKeyIdDict
A = [1]
B = [2]
C = [3]

# construction
wkd = WeakKeyIdDict()
wkd[A] = 2
wkd[B] = 3
wkd[C] = 4
dd = convert(Dict{Any,Any},wkd)
@test WeakKeyIdDict(dd) == wkd
@test convert(WeakKeyIdDict{Any, Any}, dd) == wkd
@test isa(WeakKeyIdDict(dd), WeakKeyIdDict{Any,Any})
@test WeakKeyIdDict(A=>2, B=>3, C=>4) == wkd
@test isa(WeakKeyIdDict(A=>2, B=>3, C=>4), WeakKeyIdDict{Array{Int,1},Int})
@test WeakKeyIdDict(a=>i+1 for (i,a) in enumerate([A,B,C]) ) == wkd
@test WeakKeyIdDict([(A,2), (B,3), (C,4)]) == wkd
@test WeakKeyIdDict(Pair(A,2), Pair(B,3), Pair(C,4)) == wkd
@test copy(wkd) == wkd

@test length(wkd) == 3
@test !isempty(wkd)
res = pop!(wkd, C)
@test res == 4
@test C ∉ keys(wkd)
@test 4 ∉ values(wkd)
@test length(wkd) == 2
@test !isempty(wkd)
wkd = filter!( p -> p.first != B, wkd)
@test B ∉ keys(wkd)
@test 3 ∉ values(wkd)
@test length(wkd) == 1
@test WeakKeyIdDict(Pair(A, 2)) == wkd
@test !isempty(wkd)

wkd = empty!(wkd)
@test wkd == empty(wkd)
@test typeof(wkd) == typeof(empty(wkd))
@test length(wkd) == 0
@test isempty(wkd)
@test isa(wkd, WeakKeyIdDict)

@test_throws ArgumentError WeakKeyIdDict([1, 2, 3])

# WeakKeyIdDict does not convert keys
@test_throws ArgumentError WeakKeyIdDict{Int,Any}(5.0=>1)

# WeakKeyIdDict hashes with object-id
AA = copy(A)
GC.@preserve A AA begin
wkd = WeakKeyIdDict(A=>1, AA=>2)
@test length(wkd)==2
kk = collect(keys(wkd))
@test kk[1]==kk[2]
@test kk[1]!==kk[2]
end

# WeakKeyIdDict compares to other dicts:
@test IdDict(A=>1)!=WeakKeyIdDict(A=>1)
@test Dict(A=>1)==WeakKeyIdDict(A=>1)
@test Dict(copy(A)=>1)!=WeakKeyIdDict(A=>1)

# issue #26939
d26939 = WeakKeyIdDict()
d26939[big"1.0" + 1.1] = 1
GC.gc() # make sure this doesn't segfault
end
2 changes: 2 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ testlist = [

"printing.jl",

"DataStructures/weakkeyid_dict-test.jl",

"PolyhedralGeometry/runtests.jl",
"Combinatorics/runtests.jl",

Expand Down
Loading