Skip to content

Commit 252716e

Browse files
committed
Compiler support for optimizing PersistentDict
This is part of the work to address #51352 by attempting to allow the compiler to perform SRAO on persistent data structures like `PersistentDict` as if they were regular immutable data structures. These sorts of data structures have very complicated internals (with lots of mutation, memory sharing, etc.), but a relatively simple interface. As such, it is unlikely that our compiler will have sufficient power to optimize this interface by analyzing the implementation. We thus need to come up with some other mechanism that gives the compiler license to perform the requisite optimization. One way would be to just hardcode `PersistentDict` into the compiler, optimizing it like any of the other builtin datatypes. However, this is of course very unsatisfying. At the other end of the spectrum would be something like a generic rewrite rule system (e-graphs anyone?) that would let the PersistentDict implementation declare its interface to the compiler and the compiler would use this for optimization (in a perfect world, the actual rewrite would then be checked using some sort of formal methods). I think that would be interesting, but we're very far from even being able to design something like that (at least in Base - experiments with external AbstractInterpreters in this direction are encouraged). This PR tries to come up with a reasonable middle ground, where the compiler gets some knowledge of the protocol hardcoded without having to know about the implementation details of the data structure. The basic ideas is that `Core` provides some magic generic functions that implementations can extend. Semantically, they are not special. They dispatch as usual, and implementations are expected to work properly even in the absence of any compiler optimizations. However, the compiler is semantically permitted to perform structural optimization using these magic generic functions. In the concrete case, this PR introduces the `KeyValue` interface which consists of two generic functions, `get` and `set`. The core optimization is that the compiler is allowed to rewrite any occurrence of `get(set(x, k, v), k)` into `v` without additional legality checks. In particular, the compiler performs no type checks, conversions, etc. The higher level implementation code is expected to do all that. This approach closely matches the general direction we've been taking in external AbstractInterpreters for embedding additional semantics and optimization opportunities into Julia code (although we generally use methods there, rather than full generic functions), so I think we have some evidence that this sort of approach works reasonably well. Nevertheless, this is certainly an experiment and the interface is explicitly declared unstable. This is fully working and implemented, but the optimization currently bails on anything but the simplest cases. Filling all those cases in is not particularly hard, but should be done along with a more invasive refactoring of SROA, so we should figure out the general direction here first and then we can finish all that up in a follow-up cleanup. Before: ``` julia> using BenchmarkTools julia> function foo() a = Base.PersistentDict(:a => 1) return a[:a] end foo (generic function with 1 method) julia> @benchmark foo() BenchmarkTools.Trial: 10000 samples with 993 evaluations. Range (min … max): 32.940 ns … 28.754 μs ┊ GC (min … max): 0.00% … 99.76% Time (median): 49.647 ns ┊ GC (median): 0.00% Time (mean ± σ): 57.519 ns ± 333.275 ns ┊ GC (mean ± σ): 10.81% ± 2.22% ▃█▅ ▁▃▅▅▃▁ ▁▃▂ ▂ ▁▂▄▃▅▇███▇▃▁▂▁▁▁▁▁▁▁▁▂▂▅██████▅▂▁▁▁▁▁▁▁▁▁▁▂▃▃▇███▇▆███▆▄▃▃▂▂ ▃ 32.9 ns Histogram: frequency by time 68.6 ns < Memory estimate: 128 bytes, allocs estimate: 4. julia> @code_typed foo() CodeInfo( 1 ─ %1 = invoke Vector{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}}(Base.HashArrayMappedTries.undef::UndefInitializer, 1::Int64)::Vector{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}} │ %2 = %new(Base.HashArrayMappedTries.HAMT{Symbol, Int64}, %1, 0x00000000)::Base.HashArrayMappedTries.HAMT{Symbol, Int64} │ %3 = %new(Base.HashArrayMappedTries.Leaf{Symbol, Int64}, :a, 1)::Base.HashArrayMappedTries.Leaf{Symbol, Int64} │ %4 = Base.getfield(%2, :data)::Vector{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}} │ %5 = $(Expr(:boundscheck, true))::Bool └── goto #5 if not %5 2 ─ %7 = Base.sub_int(1, 1)::Int64 │ %8 = Base.bitcast(UInt64, %7)::UInt64 │ %9 = Base.getfield(%4, :size)::Tuple{Int64} │ %10 = $(Expr(:boundscheck, true))::Bool │ %11 = Base.getfield(%9, 1, %10)::Int64 │ %12 = Base.bitcast(UInt64, %11)::UInt64 │ %13 = Base.ult_int(%8, %12)::Bool └── goto #4 if not %13 3 ─ goto #5 4 ─ %16 = Core.tuple(1)::Tuple{Int64} │ invoke Base.throw_boundserror(%4::Vector{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}}, %16::Tuple{Int64})::Union{} └── unreachable 5 ┄ %19 = Base.getfield(%4, :ref)::MemoryRef{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}} │ %20 = Base.memoryref(%19, 1, false)::MemoryRef{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}} │ Base.memoryrefset!(%20, %3, :not_atomic, false)::MemoryRef{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}} └── goto #6 6 ─ %23 = Base.getfield(%2, :bitmap)::UInt32 │ %24 = Base.or_int(%23, 0x00010000)::UInt32 │ Base.setfield!(%2, :bitmap, %24)::UInt32 └── goto #7 7 ─ %27 = %new(Base.PersistentDict{Symbol, Int64}, %2)::Base.PersistentDict{Symbol, Int64} └── goto #8 8 ─ %29 = invoke Base.getindex(%27::Base.PersistentDict{Symbol, Int64}, 🅰️:Symbol)::Int64 └── return %29 ``` After: ``` julia> using BenchmarkTools julia> function foo() a = Base.PersistentDict(:a => 1) return a[:a] end foo (generic function with 1 method) julia> @benchmark foo() BenchmarkTools.Trial: 10000 samples with 1000 evaluations. Range (min … max): 2.459 ns … 11.320 ns ┊ GC (min … max): 0.00% … 0.00% Time (median): 2.460 ns ┊ GC (median): 0.00% Time (mean ± σ): 2.469 ns ± 0.183 ns ┊ GC (mean ± σ): 0.00% ± 0.00% ▂ █ ▁ █ ▂ █▁▁▁▁█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█▁▁▁▁█ █ 2.46 ns Histogram: log(frequency) by time 2.47 ns < Memory estimate: 0 bytes, allocs estimate: 0. julia> @code_typed foo() CodeInfo( 1 ─ return 1 ```
1 parent c30d45d commit 252716e

File tree

6 files changed

+228
-65
lines changed

6 files changed

+228
-65
lines changed

base/boot.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -957,7 +957,6 @@ function _hasmethod(@nospecialize(tt)) # this function has a special tfunc
957957
return Intrinsics.not_int(ccall(:jl_gf_invoke_lookup, Any, (Any, Any, UInt), tt, nothing, world) === nothing)
958958
end
959959

960-
961960
# for backward compat
962961
arrayref(inbounds::Bool, A::Array, i::Int...) = Main.Base.getindex(A, i...)
963962
const_arrayref(inbounds::Bool, A::Array, i::Int...) = Main.Base.getindex(A, i...)
@@ -969,4 +968,6 @@ export arrayref, arrayset, arraysize, const_arrayref
969968
# For convenience
970969
EnterNode(old::EnterNode, new_dest::Int) = EnterNode(new_dest)
971970

971+
include(Core, "optimized_generics.jl")
972+
972973
ccall(:jl_set_istopmod, Cvoid, (Any, Bool), Core, true)

base/compiler/ssair/passes.jl

+81-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ function is_known_call(@nospecialize(x), @nospecialize(func), ir::Union{IRCode,I
66
return singleton_type(ft) === func
77
end
88

9+
function is_known_invoke_or_call(@nospecialize(x), @nospecialize(func), ir::Union{IRCode,IncrementalCompact})
10+
isinvoke = isexpr(x, :invoke)
11+
(isinvoke || isexpr(x, :call)) || return false
12+
ft = argextype(x.args[isinvoke ? 2 : 1], ir)
13+
return singleton_type(ft) === func
14+
end
15+
916
struct SSAUse
1017
kind::Symbol
1118
idx::Int
@@ -819,6 +826,76 @@ function lift_svec_ref!(compact::IncrementalCompact, idx::Int, stmt::Expr)
819826
return
820827
end
821828

829+
function lift_leaves_keyvalue(compact::IncrementalCompact, @nospecialize(key),
830+
leaves::Vector{Any}, 𝕃ₒ::AbstractLattice)
831+
# For every leaf, the lifted value
832+
lifted_leaves = LiftedLeaves()
833+
for i = 1:length(leaves)
834+
leaf = leaves[i]
835+
cache_key = leaf
836+
if isa(leaf, AnySSAValue)
837+
(def, leaf) = walk_to_def(compact, leaf)
838+
if is_known_invoke_or_call(def, Core.OptimizedGenerics.KeyValue.set, compact)
839+
@assert isexpr(def, :invoke)
840+
if length(def.args) in (5, 6)
841+
collection = def.args[end-2]
842+
set_key = def.args[end-1]
843+
set_val_idx = length(def.args)
844+
elseif length(def.args) == 4
845+
collection = def.args[end-1]
846+
# Key is deleted
847+
# TODO: Model this
848+
return nothing
849+
elseif length(def.args) == 3
850+
collection = def.args[end]
851+
# The whole collection is deleted
852+
# TODO: Model this
853+
return nothing
854+
else
855+
return nothing
856+
end
857+
if set_key === key || (egal_tfunc(𝕃ₒ, argextype(key, compact), argextype(set_key, compact)) == Const(true))
858+
lift_arg!(compact, leaf, cache_key, def, set_val_idx, lifted_leaves)
859+
continue
860+
end
861+
# TODO: Continue walking the chain
862+
return nothing
863+
end
864+
end
865+
return nothing
866+
end
867+
return lifted_leaves
868+
end
869+
870+
function lift_keyvalue_get!(compact::IncrementalCompact, idx::Int, stmt::Expr, 𝕃ₒ::AbstractLattice)
871+
collection = stmt.args[end-1]
872+
key = stmt.args[end]
873+
874+
leaves, visited_philikes = collect_leaves(compact, collection, Any, 𝕃ₒ, phi_or_ifelse_predecessors)
875+
isempty(leaves) && return
876+
877+
lifted_leaves = lift_leaves_keyvalue(compact, key, leaves, 𝕃ₒ)
878+
lifted_leaves === nothing && return
879+
880+
result_t = Union{}
881+
for v in values(lifted_leaves)
882+
v === nothing && return
883+
result_t = tmerge(𝕃ₒ, result_t, argextype(v.val, compact))
884+
end
885+
886+
lifted_val = perform_lifting!(compact,
887+
visited_philikes, key, result_t, lifted_leaves, collection, nothing)
888+
889+
compact[idx] = lifted_val === nothing ? nothing : Expr(:call, Core.tuple, lifted_val.val)
890+
if lifted_val !== nothing
891+
if !(𝕃ₒ, compact[SSAValue(idx)][:type], result_t)
892+
compact[SSAValue(idx)][:flag] |= IR_FLAG_REFINED
893+
end
894+
end
895+
896+
return
897+
end
898+
822899
# TODO: We could do the whole lifing machinery here, but really all
823900
# we want to do is clean this up when it got inserted by inlining,
824901
# which always targets simple `svec` call or `_compute_sparams`,
@@ -1004,7 +1081,7 @@ function sroa_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing)
10041081
for ((_, idx), stmt) in compact
10051082
# check whether this statement is `getfield` / `setfield!` (or other "interesting" statement)
10061083
isa(stmt, Expr) || continue
1007-
is_setfield = is_isdefined = is_finalizer = false
1084+
is_setfield = is_isdefined = is_finalizer = is_keyvalue_get = false
10081085
field_ordering = :unspecified
10091086
if is_known_call(stmt, setfield!, compact)
10101087
4 <= length(stmt.args) <= 5 || continue
@@ -1094,6 +1171,9 @@ function sroa_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing)
10941171
lift_comparison!(isa, compact, idx, stmt, 𝕃ₒ)
10951172
elseif is_known_call(stmt, Core.ifelse, compact)
10961173
fold_ifelse!(compact, idx, stmt)
1174+
elseif is_known_invoke_or_call(stmt, Core.OptimizedGenerics.KeyValue.get, compact)
1175+
2 == (length(stmt.args) - (isexpr(stmt, :invoke) ? 2 : 1)) || continue
1176+
lift_keyvalue_get!(compact, idx, stmt, 𝕃ₒ)
10971177
elseif isexpr(stmt, :new)
10981178
refine_new_effects!(𝕃ₒ, compact, idx, stmt)
10991179
end

base/dict.jl

+69-60
Original file line numberDiff line numberDiff line change
@@ -887,10 +887,35 @@ _similar_for(c::AbstractDict, ::Type{T}, itr, isz, len) where {T} =
887887

888888
include("hamt.jl")
889889
using .HashArrayMappedTries
890+
using Core.OptimizedGenerics: KeyValue
890891
const HAMT = HashArrayMappedTries
891892

892893
struct PersistentDict{K,V} <: AbstractDict{K,V}
893894
trie::HAMT.HAMT{K,V}
895+
# Serves as a marker for an empty initialization
896+
@noinline function KeyValue.set(::Type{PersistentDict{K, V}}) where {K, V}
897+
new{K, V}(HAMT.HAMT{K,V}())
898+
end
899+
@noinline function KeyValue.set(::Type{PersistentDict{K, V}}, ::Nothing, key, val) where {K, V}
900+
new{K, V}(HAMT.HAMT{K, V}(key => val))
901+
end
902+
@noinline function KeyValue.set(dict::PersistentDict{K, V}, key, val) where {K, V}
903+
trie = dict.trie
904+
h = HAMT.HashState(key)
905+
found, present, trie, i, bi, top, hs = HAMT.path(trie, key, h, #=persistent=# true)
906+
HAMT.insert!(found, present, trie, i, bi, hs, val)
907+
return new{K, V}(top)
908+
end
909+
@noinline function KeyValue.set(dict::PersistentDict{K, V}, key) where {K, V}
910+
trie = dict.trie
911+
h = HAMT.HashState(key)
912+
found, present, trie, i, bi, top, _ = HAMT.path(trie, key, h, #=persistent=# true)
913+
if found && present
914+
deleteat!(trie.data, i)
915+
HAMT.unset!(trie, bi)
916+
end
917+
return new{K, V}(top)
918+
end
894919
end
895920

896921
"""
@@ -925,19 +950,27 @@ Base.PersistentDict{Symbol, Int64} with 1 entry:
925950
"""
926951
PersistentDict
927952

928-
PersistentDict{K,V}() where {K,V} = PersistentDict(HAMT.HAMT{K,V}())
929-
PersistentDict{K,V}(KV::Pair) where {K,V} = PersistentDict(HAMT.HAMT{K,V}(KV))
930-
PersistentDict(KV::Pair{K,V}) where {K,V} = PersistentDict(HAMT.HAMT{K,V}(KV))
953+
PersistentDict{K,V}() where {K, V} = KeyValue.set(PersistentDict{K,V})
954+
function PersistentDict{K,V}(KV::Pair) where {K,V}
955+
KeyValue.set(
956+
PersistentDict{K, V},
957+
nothing,
958+
KV...)
959+
end
960+
function PersistentDict(KV::Pair{K,V}) where {K,V}
961+
KeyValue.set(
962+
PersistentDict{K, V},
963+
nothing,
964+
KV...)
965+
end
931966
PersistentDict(dict::PersistentDict, pair::Pair) = PersistentDict(dict, pair...)
932967
PersistentDict{K,V}(dict::PersistentDict{K,V}, pair::Pair) where {K,V} = PersistentDict(dict, pair...)
968+
969+
933970
function PersistentDict(dict::PersistentDict{K,V}, key, val) where {K,V}
934971
key = convert(K, key)
935972
val = convert(V, val)
936-
trie = dict.trie
937-
h = HAMT.HashState(key)
938-
found, present, trie, i, bi, top, hs = HAMT.path(trie, key, h, #=persistent=# true)
939-
HAMT.insert!(found, present, trie, i, bi, hs, val)
940-
return PersistentDict(top)
973+
return KeyValue.set(dict, key, val)
941974
end
942975

943976
function PersistentDict{K,V}(KV::Pair, rest::Pair...) where {K,V}
@@ -959,84 +992,60 @@ end
959992
eltype(::PersistentDict{K,V}) where {K,V} = Pair{K,V}
960993

961994
function in(key_val::Pair{K,V}, dict::PersistentDict{K,V}, valcmp=(==)) where {K,V}
962-
trie = dict.trie
963-
if HAMT.islevel_empty(trie)
964-
return false
965-
end
966-
967995
key, val = key_val
968-
969-
h = HAMT.HashState(key)
970-
found, present, trie, i, _, _, _ = HAMT.path(trie, key, h)
971-
if found && present
972-
leaf = @inbounds trie.data[i]::HAMT.Leaf{K,V}
973-
return valcmp(val, leaf.val) && return true
974-
end
975-
return false
996+
found = KeyValue.get(dict, key)
997+
found === nothing && return false
998+
return valcmp(val, only(found))
976999
end
9771000

9781001
function haskey(dict::PersistentDict{K}, key::K) where K
979-
trie = dict.trie
980-
h = HAMT.HashState(key)
981-
found, present, _, _, _, _, _ = HAMT.path(trie, key, h)
982-
return found && present
1002+
return KeyValue.get(dict, key) !== nothing
9831003
end
9841004

9851005
function getindex(dict::PersistentDict{K,V}, key::K) where {K,V}
986-
trie = dict.trie
987-
if HAMT.islevel_empty(trie)
988-
throw(KeyError(key))
989-
end
990-
h = HAMT.HashState(key)
991-
found, present, trie, i, _, _, _ = HAMT.path(trie, key, h)
992-
if found && present
993-
leaf = @inbounds trie.data[i]::HAMT.Leaf{K,V}
994-
return leaf.val
995-
end
996-
throw(KeyError(key))
1006+
found = KeyValue.get(dict, key)
1007+
found === nothing && throw(KeyError(key))
1008+
return only(found)
9971009
end
9981010

9991011
function get(dict::PersistentDict{K,V}, key::K, default) where {K,V}
1000-
trie = dict.trie
1001-
if HAMT.islevel_empty(trie)
1002-
return default
1003-
end
1004-
h = HAMT.HashState(key)
1005-
found, present, trie, i, _, _, _ = HAMT.path(trie, key, h)
1006-
if found && present
1007-
leaf = @inbounds trie.data[i]::HAMT.Leaf{K,V}
1008-
return leaf.val
1009-
end
1010-
return default
1012+
found = KeyValue.get(dict, key)
1013+
found === nothing && return default
1014+
return only(found)
10111015
end
10121016

1013-
function get(default::Callable, dict::PersistentDict{K,V}, key::K) where {K,V}
1017+
@noinline function KeyValue.get(dict::PersistentDict{K, V}, key) where {K, V}
10141018
trie = dict.trie
10151019
if HAMT.islevel_empty(trie)
1016-
return default
1020+
return nothing
10171021
end
10181022
h = HAMT.HashState(key)
10191023
found, present, trie, i, _, _, _ = HAMT.path(trie, key, h)
10201024
if found && present
10211025
leaf = @inbounds trie.data[i]::HAMT.Leaf{K,V}
1022-
return leaf.val
1026+
return (leaf.val,)
10231027
end
1024-
return default()
1028+
return nothing
10251029
end
10261030

1027-
iterate(dict::PersistentDict, state=nothing) = HAMT.iterate(dict.trie, state)
1031+
@noinline function KeyValue.get(default, dict::PersistentDict, key)
1032+
found = KeyValue.get(dict, key)
1033+
found === nothing && return default()
1034+
return only(found)
1035+
end
1036+
1037+
function get(default::Callable, dict::PersistentDict{K,V}, key::K) where {K,V}
1038+
found = KeyValue.get(dict, key)
1039+
found === nothing && return default()
1040+
return only(found)
1041+
end
10281042

10291043
function delete(dict::PersistentDict{K}, key::K) where K
1030-
trie = dict.trie
1031-
h = HAMT.HashState(key)
1032-
found, present, trie, i, bi, top, _ = HAMT.path(trie, key, h, #=persistent=# true)
1033-
if found && present
1034-
deleteat!(trie.data, i)
1035-
HAMT.unset!(trie, bi)
1036-
end
1037-
return PersistentDict(top)
1044+
return KeyValue.set(dict, key)
10381045
end
10391046

1047+
iterate(dict::PersistentDict, state=nothing) = HAMT.iterate(dict.trie, state)
1048+
10401049
length(dict::PersistentDict) = HAMT.length(dict.trie)
10411050
isempty(dict::PersistentDict) = HAMT.isempty(dict.trie)
10421051
empty(::PersistentDict, ::Type{K}, ::Type{V}) where {K, V} = PersistentDict{K, V}()

base/hamt.jl

+9-3
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,18 @@ mutable struct HAMT{K, V}
6565
HAMT{K,V}(data, bitmap) where {K,V} = new{K,V}(data, bitmap)
6666
HAMT{K, V}() where {K, V} = new{K,V}(Vector{Union{Leaf{K, V}, HAMT{K, V}}}(undef, 0), zero(BITMAP))
6767
end
68-
function HAMT{K,V}((k,v)::Pair) where {K, V}
69-
k = convert(K, k)
70-
v = convert(V, v)
68+
69+
@Base.assume_effects :nothrow function init_hamt(K, V, k, v)
7170
# For a single element we can't have a hash-collision
7271
trie = HAMT{K,V}(Vector{Union{Leaf{K, V}, HAMT{K, V}}}(undef, 1), zero(BITMAP))
7372
trie.data[1] = Leaf{K,V}(k,v)
73+
return trie
74+
end
75+
76+
function HAMT{K,V}((k,v)::Pair) where {K, V}
77+
k = convert(K, k)
78+
v = convert(V, v)
79+
trie = init_hamt(K, V, k, v)
7480
bi = BitmapIndex(HashState(k))
7581
set!(trie, bi)
7682
return trie

base/optimized_generics.jl

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
module OptimizedGenerics
4+
5+
# This file defines interfaces that are recognized and optimized by the compiler
6+
# They are intended to be used by data structure implementations that wish to
7+
# opt into some level of compiler optimizations. These interfaces are
8+
# EXPERIMENTAL and currently intended for use by Base only. They are subject
9+
# to change or removal without notice. It is undefined behavior to add methods
10+
# to these generics that do not conform to the specified interface.
11+
#
12+
# The intended way to use these generics is that data structures will provide
13+
# appropriate implementations for a generic. In the absence of compiler
14+
# optimizations, these behave like regular methods. However, the compiler is
15+
# semantically allowed to perform certain structural optimizations on
16+
# appropriate combinations of these intrinsics without proving correctness.
17+
18+
# Compiler-recognized generics for immutable key-value stores (dicts, etc.)
19+
"""
20+
module KeyValue
21+
22+
Implements a key-value like interface where the compiler has liberty to perform
23+
the following transformations. The core optimization semantically allowed for
24+
the compiler is:
25+
26+
get(set(x, key, val), key) -> (val,)
27+
28+
where the compiler will recursively look through `x`. Keys are compared by
29+
egality.
30+
31+
Implementations must observe the following constraints:
32+
33+
1. It is undefined behavior for `get` not to return the exact (by egality) val
34+
stored for a given `key`.
35+
"""
36+
module KeyValue
37+
"""
38+
set(collection, [key [, val]])
39+
set(T, collection, key, val)
40+
41+
Set the `key` in `collection` to `val`. If `val` is omitted, deletes the
42+
value from the collection. If `key` is omitted as well, deletes all elements
43+
of the collection.
44+
"""
45+
function set end
46+
47+
"""
48+
get(collection, key)
49+
50+
Retrieve the value corresponding to `key` in `collection` as a single
51+
element tuple or `nothing` if no value corresponding to the key was found.
52+
`key`s are compared by egal.
53+
"""
54+
function get end
55+
end
56+
57+
end

0 commit comments

Comments
 (0)