Skip to content

Commit d4241a7

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 67161a3 commit d4241a7

File tree

6 files changed

+228
-64
lines changed

6 files changed

+228
-64
lines changed

base/boot.jl

+2
Original file line numberDiff line numberDiff line change
@@ -963,4 +963,6 @@ arraysize(a::Array) = a.size
963963
arraysize(a::Array, i::Int) = sle_int(i, nfields(a.size)) ? getfield(a.size, i) : 1
964964
export arrayref, arrayset, arraysize, const_arrayref
965965

966+
include(Core, "optimized_generics.jl")
967+
966968
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 = hash(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 = hash(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
"""
@@ -921,19 +946,27 @@ Base.PersistentDict{Symbol, Int64} with 1 entry:
921946
"""
922947
PersistentDict
923948

924-
PersistentDict{K,V}() where {K,V} = PersistentDict(HAMT.HAMT{K,V}())
925-
PersistentDict{K,V}(KV::Pair) where {K,V} = PersistentDict(HAMT.HAMT{K,V}(KV...))
926-
PersistentDict(KV::Pair{K,V}) where {K,V} = PersistentDict(HAMT.HAMT{K,V}(KV...))
949+
PersistentDict{K,V}() where {K, V} = KeyValue.set(PersistentDict{K,V})
950+
function PersistentDict{K,V}(KV::Pair) where {K,V}
951+
KeyValue.set(
952+
PersistentDict{K, V},
953+
nothing,
954+
KV...)
955+
end
956+
function PersistentDict(KV::Pair{K,V}) where {K,V}
957+
KeyValue.set(
958+
PersistentDict{K, V},
959+
nothing,
960+
KV...)
961+
end
927962
PersistentDict(dict::PersistentDict, pair::Pair) = PersistentDict(dict, pair...)
928963
PersistentDict{K,V}(dict::PersistentDict{K,V}, pair::Pair) where {K,V} = PersistentDict(dict, pair...)
964+
965+
929966
function PersistentDict(dict::PersistentDict{K,V}, key, val) where {K,V}
930967
key = convert(K, key)
931968
val = convert(V, val)
932-
trie = dict.trie
933-
h = hash(key)
934-
found, present, trie, i, bi, top, hs = HAMT.path(trie, key, h, #=persistent=# true)
935-
HAMT.insert!(found, present, trie, i, bi, hs, val)
936-
return PersistentDict(top)
969+
return KeyValue.set(dict, key, val)
937970
end
938971

939972
function PersistentDict(kv::Pair, rest::Pair...)
@@ -948,84 +981,60 @@ end
948981
eltype(::PersistentDict{K,V}) where {K,V} = Pair{K,V}
949982

950983
function in(key_val::Pair{K,V}, dict::PersistentDict{K,V}, valcmp=(==)) where {K,V}
951-
trie = dict.trie
952-
if HAMT.islevel_empty(trie)
953-
return false
954-
end
955-
956984
key, val = key_val
957-
958-
h = hash(key)
959-
found, present, trie, i, _, _, _ = HAMT.path(trie, key, h)
960-
if found && present
961-
leaf = @inbounds trie.data[i]::HAMT.Leaf{K,V}
962-
return valcmp(val, leaf.val) && return true
963-
end
964-
return false
985+
found = KeyValue.get(dict, key)
986+
found === nothing && return false
987+
return valcmp(val, only(found))
965988
end
966989

967990
function haskey(dict::PersistentDict{K}, key::K) where K
968-
trie = dict.trie
969-
h = hash(key)
970-
found, present, _, _, _, _, _ = HAMT.path(trie, key, h)
971-
return found && present
991+
return KeyValue.get(dict, key) !== nothing
972992
end
973993

974994
function getindex(dict::PersistentDict{K,V}, key::K) where {K,V}
975-
trie = dict.trie
976-
if HAMT.islevel_empty(trie)
977-
throw(KeyError(key))
978-
end
979-
h = hash(key)
980-
found, present, trie, i, _, _, _ = HAMT.path(trie, key, h)
981-
if found && present
982-
leaf = @inbounds trie.data[i]::HAMT.Leaf{K,V}
983-
return leaf.val
984-
end
985-
throw(KeyError(key))
995+
found = KeyValue.get(dict, key)
996+
found === nothing && throw(KeyError(key))
997+
return only(found)
986998
end
987999

9881000
function get(dict::PersistentDict{K,V}, key::K, default) where {K,V}
989-
trie = dict.trie
990-
if HAMT.islevel_empty(trie)
991-
return default
992-
end
993-
h = hash(key)
994-
found, present, trie, i, _, _, _ = HAMT.path(trie, key, h)
995-
if found && present
996-
leaf = @inbounds trie.data[i]::HAMT.Leaf{K,V}
997-
return leaf.val
998-
end
999-
return default
1001+
found = KeyValue.get(dict, key)
1002+
found === nothing && return default
1003+
return only(found)
10001004
end
10011005

1002-
function get(default::Callable, dict::PersistentDict{K,V}, key::K) where {K,V}
1006+
@noinline function KeyValue.get(dict::PersistentDict{K, V}, key) where {K, V}
10031007
trie = dict.trie
10041008
if HAMT.islevel_empty(trie)
1005-
return default
1009+
return nothing
10061010
end
10071011
h = hash(key)
10081012
found, present, trie, i, _, _, _ = HAMT.path(trie, key, h)
10091013
if found && present
10101014
leaf = @inbounds trie.data[i]::HAMT.Leaf{K,V}
1011-
return leaf.val
1015+
return (leaf.val,)
10121016
end
1013-
return default()
1017+
return nothing
10141018
end
10151019

1016-
iterate(dict::PersistentDict, state=nothing) = HAMT.iterate(dict.trie, state)
1020+
@noinline function KeyValue.get(default, dict::PersistentDict, key)
1021+
found = KeyValue.get(dict, key)
1022+
found === nothing && return default()
1023+
return only(found)
1024+
end
1025+
1026+
function get(default::Callable, dict::PersistentDict{K,V}, key::K) where {K,V}
1027+
found = KeyValue.get(dict, key)
1028+
found === nothing && return default()
1029+
return only(found)
1030+
end
10171031

10181032
function delete(dict::PersistentDict{K}, key::K) where K
1019-
trie = dict.trie
1020-
h = hash(key)
1021-
found, present, trie, i, bi, top, _ = HAMT.path(trie, key, h, #=persistent=# true)
1022-
if found && present
1023-
deleteat!(trie.data, i)
1024-
HAMT.unset!(trie, bi)
1025-
end
1026-
return PersistentDict(top)
1033+
return KeyValue.set(dict, key)
10271034
end
10281035

1036+
iterate(dict::PersistentDict, state=nothing) = HAMT.iterate(dict.trie, state)
1037+
10291038
length(dict::PersistentDict) = HAMT.length(dict.trie)
10301039
isempty(dict::PersistentDict) = HAMT.isempty(dict.trie)
10311040
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
@@ -64,12 +64,18 @@ mutable struct HAMT{K, V}
6464
bitmap::BITMAP
6565
end
6666
HAMT{K, V}() where {K, V} = HAMT(Vector{Union{Leaf{K, V}, HAMT{K, V}}}(undef, 0), zero(BITMAP))
67-
function HAMT{K,V}(k::K, v) where {K, V}
68-
v = convert(V, v)
67+
68+
@Base.assume_effects :nothrow function init_hamt(K, V, k, v)
6969
# For a single element we can't have a hash-collision
7070
trie = HAMT(Vector{Union{Leaf{K, V}, HAMT{K, V}}}(undef, 1), zero(BITMAP))
71-
trie.data[1] = Leaf{K,V}(k,v)
71+
@inbounds trie.data[1] = Leaf{K,V}(k,v)
72+
return trie
73+
end
74+
75+
function HAMT{K,V}(k::K, v) where {K, V}
76+
v = convert(V, v)
7277
bi = BitmapIndex(HashState(k))
78+
trie = init_hamt(K, V, k, v)
7379
set!(trie, bi)
7480
return trie
7581
end

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

test/compiler/irpasses.jl

+10
Original file line numberDiff line numberDiff line change
@@ -1616,3 +1616,13 @@ let m = Meta.@lower 1 + 1
16161616
end
16171617

16181618
# JET.test_opt(Core.Compiler.cfg_simplify!, (Core.Compiler.IRCode,))
1619+
1620+
# Test support for Core.OptimizedGenerics.KeyValue protocol
1621+
function persistent_dict_elim()
1622+
a = Base.PersistentDict(:a => 1)
1623+
return a[:a]
1624+
end
1625+
# Ideally we would be able to fully eliminate this,
1626+
# but currently this would require an extra round of constprop
1627+
@test_broken fully_eliminated(persistent_dict_elim)
1628+
@test code_typed(persistent_dict_elim)[1][1].code[end] == Core.ReturnNode(1)

0 commit comments

Comments
 (0)