Skip to content

Commit 745ee1d

Browse files
authored
Fix none handling + map fetch rewrite (#15686)
1 parent ce47442 commit 745ee1d

2 files changed

Lines changed: 77 additions & 85 deletions

File tree

lib/elixir/lib/module/types/descr.ex

Lines changed: 36 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3252,50 +3252,32 @@ defmodule Module.Types.Descr do
32523252
# and whether the key is optional.
32533253
defp map_dnf_fetch_static(dnf, key) do
32543254
Enum.reduce(dnf, {none(), false}, fn
3255-
# Optimization: if there are no negatives
3256-
{tag, fields, []}, acc ->
3257-
field =
3258-
case fields_find(key, fields) do
3259-
{:ok, field} -> field
3260-
:error when tag == :open -> throw(:open)
3261-
:error -> map_key_tag_to_field(tag)
3262-
end
3263-
3264-
field_opt_union(field, acc, %{})
3265-
32663255
{tag, fields, negs}, acc ->
32673256
{field, bdd} = map_pop_key_bdd(tag, fields, key)
32683257

3269-
case map_split_negative_pairs_key(negs, key) do
3270-
:empty ->
3271-
acc
3272-
3273-
negative ->
3274-
field =
3275-
if map_pair_projection_keeps_full_fst?(negative, bdd) do
3276-
field
3277-
else
3278-
negs
3279-
|> map_split_negative_key(key, field, bdd)
3280-
|> Enum.reduce({none(), false}, fn {field, _}, acc ->
3281-
field_opt_union(field, acc, %{})
3282-
end)
3283-
end
3284-
3258+
# First: if a map has a none() field, then fetching from it is none() too
3259+
# Then: if there is a negative open_map(), map type is empty
3260+
with false <- map_empty?(bdd, %{}),
3261+
negative when negative != :empty <- map_split_negative_pairs_key(negs, key) do
3262+
if map_pair_projection_keeps_full_fst?(negative, bdd) do
32853263
field_opt_union(field, acc, %{})
3264+
else
3265+
negative
3266+
|> map_remove_negative(field, bdd)
3267+
|> Enum.reduce(acc, fn {field, _}, acc ->
3268+
field_opt_union(field, acc, %{})
3269+
end)
3270+
end
3271+
else
3272+
_ -> acc
32863273
end
32873274
end)
3288-
catch
3289-
:open -> {term(), true}
32903275
end
32913276

32923277
defp map_split_negative_pairs_key(negs, key) do
32933278
Enum.reduce_while(negs, [], fn
3294-
bdd_leaf(:open, []), _acc ->
3295-
{:halt, :empty}
3296-
3297-
bdd_leaf(tag, fields), neg_acc ->
3298-
{:cont, [map_pop_key_bdd(tag, fields, key) | neg_acc]}
3279+
bdd_leaf(:open, []), _acc -> {:halt, :empty}
3280+
bdd_leaf(tag, fields), neg_acc -> {:cont, [map_pop_key_bdd(tag, fields, key) | neg_acc]}
32993281
end)
33003282
end
33013283

@@ -3322,64 +3304,33 @@ defmodule Module.Types.Descr do
33223304
not field_empty?(field_difference(field, neg_field))
33233305
end
33243306

3325-
defp map_split_negative_key(negs, key, value, bdd) do
3326-
map_split_negative(negs, value, bdd, fn neg_tag, neg_fields ->
3327-
case fields_take(key, neg_fields) do
3328-
{neg_field, neg_fields} ->
3329-
{true, neg_field, map_new(neg_tag, neg_fields)}
3330-
3331-
:error ->
3332-
{false, map_key_tag_to_field(neg_tag), map_new(neg_tag, neg_fields)}
3333-
end
3334-
end)
3335-
end
3336-
33373307
# Remove negatives:
33383308
# {t, s} \ {t₁, s₁} = {t \ t₁, s} ∪ {t ∩ t₁, s \ s₁}
3339-
defp map_split_negative(negs, value, bdd, take_fun) do
3340-
Enum.reduce(negs, [{value, bdd}], fn
3341-
bdd_leaf(:open, []), _acc ->
3342-
throw(:empty)
3343-
3344-
bdd_leaf(neg_tag, neg_fields), acc ->
3345-
{found?, neg_field, neg_bdd} = take_fun.(neg_tag, neg_fields)
3346-
3347-
if not found? and neg_tag == :open do
3348-
# In case the map is open, t \ t₁ is always empty,
3349-
# t ∩ t₁ is always t, so we just need to deal with the bdd.
3350-
Enum.reduce(acc, [], fn {field, bdd}, acc ->
3309+
defp map_remove_negative(negative, value, bdd) do
3310+
Enum.reduce(negative, [{value, bdd}], fn {neg_field, neg_bdd}, acc ->
3311+
Enum.reduce(acc, [], fn {field, bdd}, acc ->
3312+
# If the negative tag is closed, then they are likely disjoint,
3313+
# so we can drastically cut down the amount of operations.
3314+
if match?(bdd_leaf(:closed, _), neg_bdd) and
3315+
map_empty?(map_intersection(bdd, neg_bdd), %{}) do
3316+
[{field, bdd} | acc]
3317+
else
3318+
intersection_field = field_intersection(field, neg_field)
3319+
3320+
if field_empty?(intersection_field) do
3321+
[{field, bdd} | acc]
3322+
else
33513323
diff_bdd = map_difference(bdd, neg_bdd)
33523324

33533325
if map_empty?(diff_bdd, %{}) do
3354-
acc
3326+
prepend_map_pair_unless_empty_diff(field, neg_field, bdd, acc)
33553327
else
3356-
[{field, diff_bdd} | acc]
3357-
end
3358-
end)
3359-
else
3360-
Enum.reduce(acc, [], fn {field, bdd}, acc ->
3361-
# If the negative tag is closed, then they are likely disjoint,
3362-
# so we can drastically cut down the amount of operations.
3363-
if neg_tag == :closed and map_empty?(map_intersection(bdd, neg_bdd), %{}) do
3364-
[{field, bdd} | acc]
3365-
else
3366-
intersection_field = field_intersection(field, neg_field)
3367-
3368-
if field_empty?(intersection_field) do
3369-
[{field, bdd} | acc]
3370-
else
3371-
diff_bdd = map_difference(bdd, neg_bdd)
3372-
3373-
if map_empty?(diff_bdd, %{}) do
3374-
prepend_map_pair_unless_empty_diff(field, neg_field, bdd, acc)
3375-
else
3376-
acc = [{intersection_field, diff_bdd} | acc]
3377-
prepend_map_pair_unless_empty_diff(field, neg_field, bdd, acc)
3378-
end
3379-
end
3328+
acc = [{intersection_field, diff_bdd} | acc]
3329+
prepend_map_pair_unless_empty_diff(field, neg_field, bdd, acc)
33803330
end
3381-
end)
3331+
end
33823332
end
3333+
end)
33833334
end)
33843335
catch
33853336
:empty -> []
@@ -3797,7 +3748,7 @@ defmodule Module.Types.Descr do
37973748
pairs =
37983749
if keep_fst? and keep_snd?,
37993750
do: [],
3800-
else: map_split_negative_key(negs, key, fst, snd)
3751+
else: map_remove_negative(negative, fst, snd)
38013752

38023753
{maybe_field_opt_union(field, fn ->
38033754
if keep_fst? do

lib/elixir/test/elixir/module/types/descr_test.exs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2613,6 +2613,47 @@ defmodule Module.Types.DescrTest do
26132613
|> map_fetch_key(:a) == {false, integer()}
26142614
end
26152615

2616+
test "map_fetch_key is independent from empty map literals" do
2617+
map =
2618+
opt_union(
2619+
closed_map(b: {term(), false}),
2620+
closed_map(a: {none(), false})
2621+
)
2622+
2623+
assert equal?(map, closed_map(b: {term(), false}))
2624+
assert map_fetch_key(map, :b) == {false, term()}
2625+
2626+
# An empty positive product with negatives must not contribute its field.
2627+
map = closed_map(a: {atom(), false})
2628+
2629+
empty =
2630+
open_map(
2631+
a: {opt_union(atom(), integer()), false},
2632+
c: {none(), false}
2633+
)
2634+
|> opt_difference(
2635+
open_map(
2636+
a: {atom(), false},
2637+
b: {term(), false}
2638+
)
2639+
)
2640+
2641+
equivalent = opt_union(map, empty)
2642+
2643+
assert empty?(empty)
2644+
assert equal?(map, equivalent)
2645+
assert map_fetch_key(equivalent, :a) == map_fetch_key(map, :a)
2646+
2647+
# Original issue reproduction.
2648+
map = closed_map(a: {term(), false})
2649+
empty = open_map(c: {none(), false})
2650+
cover = opt_union(map, empty)
2651+
equivalent = opt_difference(cover, opt_difference(cover, map))
2652+
2653+
assert equal?(map, equivalent)
2654+
assert map_fetch_key(equivalent, :a) == map_fetch_key(map, :a)
2655+
end
2656+
26162657
# Times out without a projection-only map_fetch_key path
26172658
test "map_fetch_key with projected negative maps" do
26182659
assert map_fetch_key(projected_negative_map(100), :k) == {false, open_map()}

0 commit comments

Comments
 (0)