Skip to content

Commit 576f123

Browse files
committed
Intersection of maps with not set keys should not be empty, closes #15131
1 parent 77e6687 commit 576f123

3 files changed

Lines changed: 96 additions & 28 deletions

File tree

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

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2894,12 +2894,17 @@ defmodule Module.Types.Descr do
28942894
# Case 1: we are removing an open map with one field. Just do the difference of that field.
28952895
if neg_tag == :open and map_size(neg_fields) == 1 do
28962896
[{key, value}] = Map.to_list(neg_fields)
2897-
t_diff = difference(Map.get(fields, key, map_key_tag_to_type(tag)), value)
28982897

2899-
if empty?(t_diff) do
2900-
:bdd_bot
2898+
if tag == :closed and not is_map_key(fields, key) and not optional_static?(value) do
2899+
map1
29012900
else
2902-
bdd_leaf(tag, Map.put(fields, key, t_diff))
2901+
t_diff = difference(Map.get(fields, key, map_key_tag_to_type(tag)), value)
2902+
2903+
if empty?(t_diff) do
2904+
:bdd_bot
2905+
else
2906+
bdd_leaf(tag, Map.put(fields, key, t_diff))
2907+
end
29032908
end
29042909
else
29052910
# Case 2: the maps have all but one key in common. Do the difference of that key.
@@ -2948,25 +2953,20 @@ defmodule Module.Types.Descr do
29482953

29492954
# Both closed: the result is closed.
29502955
defp map_literal_intersection(:closed, map1, :closed, map2) do
2951-
new_fields =
2952-
symmetrical_intersection(map1, map2, fn _, type1, type2 ->
2953-
non_empty_intersection!(type1, type2)
2954-
end)
2955-
2956-
if map_size(new_fields) < map_size(map1) or map_size(new_fields) < map_size(map2) do
2957-
throw(:empty)
2956+
if map_size(map1) > map_size(map2) do
2957+
:maps.iterator(map1) |> :maps.next() |> map_literal_intersection_closed(map2, [])
2958+
else
2959+
:maps.iterator(map2) |> :maps.next() |> map_literal_intersection_closed(map1, [])
29582960
end
2959-
2960-
{:closed, new_fields}
29612961
end
29622962

29632963
# Open and closed: result is closed, all fields from open should be in closed, except not_set ones.
29642964
defp map_literal_intersection(:open, open, :closed, closed) do
2965-
:maps.iterator(open) |> :maps.next() |> map_literal_intersection_loop(closed)
2965+
:maps.iterator(open) |> :maps.next() |> map_literal_intersection_open_closed(closed)
29662966
end
29672967

29682968
defp map_literal_intersection(:closed, closed, :open, open) do
2969-
:maps.iterator(open) |> :maps.next() |> map_literal_intersection_loop(closed)
2969+
:maps.iterator(open) |> :maps.next() |> map_literal_intersection_open_closed(closed)
29702970
end
29712971

29722972
# At least one tag is a tag-domain pair.
@@ -3021,23 +3021,56 @@ defmodule Module.Types.Descr do
30213021
if map_size(new_domains) == 0, do: :closed, else: new_domains
30223022
end
30233023

3024-
defp map_literal_intersection_loop(:none, acc), do: {:closed, acc}
3024+
defp map_literal_intersection_open_closed(:none, acc), do: {:closed, acc}
30253025

3026-
defp map_literal_intersection_loop({key, type1, iterator}, acc) do
3026+
defp map_literal_intersection_open_closed({key, type1, iterator}, acc) do
30273027
case acc do
30283028
%{^key => type2} ->
30293029
acc = %{acc | key => non_empty_intersection!(type1, type2)}
3030-
:maps.next(iterator) |> map_literal_intersection_loop(acc)
3030+
map_literal_intersection_open_closed(:maps.next(iterator), acc)
30313031

30323032
_ ->
30333033
# If the key is optional in the open map, we can ignore it
30343034
case type1 do
3035-
%{optional: 1} -> :maps.next(iterator) |> map_literal_intersection_loop(acc)
3035+
%{optional: 1} -> map_literal_intersection_open_closed(:maps.next(iterator), acc)
30363036
_ -> throw(:empty)
30373037
end
30383038
end
30393039
end
30403040

3041+
defp map_literal_intersection_closed(:none, map, acc) do
3042+
fields = :maps.from_list(acc)
3043+
3044+
# If the number of fields match, then it is empty unless the mismatched fields are not set
3045+
if map_size(map) != map_size(fields) do
3046+
:maps.fold(
3047+
fn
3048+
key, value, _acc when is_map_key(fields, key) or value == @not_set -> :ok
3049+
_key, _value, _acc -> throw(:empty)
3050+
end,
3051+
:ok,
3052+
map
3053+
)
3054+
end
3055+
3056+
{:closed, fields}
3057+
end
3058+
3059+
defp map_literal_intersection_closed({key, type1, iterator}, map, acc) do
3060+
case map do
3061+
%{^key => type2} ->
3062+
acc = [{key, non_empty_intersection!(type1, type2)} | acc]
3063+
map_literal_intersection_closed(:maps.next(iterator), map, acc)
3064+
3065+
# If the field is literally not set, we are fine
3066+
_ when type1 == @not_set ->
3067+
map_literal_intersection_closed(:maps.next(iterator), map, acc)
3068+
3069+
_ ->
3070+
throw(:empty)
3071+
end
3072+
end
3073+
30413074
defp non_empty_intersection!(type1, type2) do
30423075
type = intersection(type1, type2)
30433076
if empty?(type), do: throw(:empty), else: type

lib/elixir/lib/module/types/pattern.ex

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,14 @@ defmodule Module.Types.Pattern do
424424

425425
defp badpattern_error(expr, index, tag, stack, context) do
426426
meta = error_meta(expr, stack)
427-
error = {:badpattern, meta, index, tag, context}
427+
428+
error =
429+
if is_integer(index) do
430+
{:badpattern, meta, index, tag, context}
431+
else
432+
{:badmatch, meta, expr, context}
433+
end
434+
428435
error(__MODULE__, error, meta, stack, context)
429436
end
430437

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

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,24 @@ defmodule Module.Types.DescrTest do
329329
|> equal?(closed_map(tag: atom([true]), halted: atom([true]), assigns: term()))
330330
end
331331

332+
# Closed maps with not set keys should have no impact
333+
test "map closed with not set keys" do
334+
assert intersection(closed_map(a: integer()), closed_map(a: integer(), b: not_set())) ==
335+
closed_map(a: integer())
336+
337+
assert intersection(closed_map(a: integer(), b: not_set()), closed_map(a: integer())) ==
338+
closed_map(a: integer())
339+
340+
assert intersection(closed_map(a: integer()), closed_map(a: not_set())) ==
341+
none()
342+
343+
assert intersection(
344+
closed_map(a: integer(), b: not_set()),
345+
closed_map(a: integer(), c: not_set())
346+
) ==
347+
closed_map(a: integer())
348+
end
349+
332350
test "map with domain keys" do
333351
# %{..., int => t1, atom => t2} and %{int => t3}
334352
# intersection is %{int => t1 and t3, atom => none}
@@ -554,25 +572,35 @@ defmodule Module.Types.DescrTest do
554572
end
555573

556574
test "map" do
557-
assert empty?(difference(open_map(), open_map()))
558-
assert empty?(difference(open_map(), term()))
559-
assert equal?(difference(open_map(), none()), open_map())
575+
assert difference(open_map(), open_map()) == none()
576+
assert difference(open_map(), term()) == none()
577+
assert difference(open_map(), none()) == open_map()
578+
560579
assert empty?(difference(closed_map(a: integer()), open_map()))
561-
assert empty?(difference(closed_map(a: integer()), closed_map(a: integer())))
562-
assert empty?(difference(closed_map(a: integer()), open_map(a: integer())))
563-
assert empty?(difference(closed_map(a: integer()), open_map(b: if_set(integer()))))
564580

565581
assert difference(closed_map(a: integer(), b: if_set(atom())), closed_map(a: integer()))
566582
|> difference(closed_map(a: integer(), b: atom()))
567583
|> empty?()
568584

585+
refute empty?(difference(open_map(), empty_map()))
586+
587+
# Difference with single field closed map on rhs
588+
assert difference(closed_map(a: integer()), closed_map(a: integer())) == none()
589+
569590
assert difference(open_map(a: atom()), closed_map(b: integer()))
570591
|> equal?(open_map(a: atom()))
571592

572-
refute empty?(difference(open_map(), empty_map()))
573-
574593
assert difference(open_map(a: integer()), closed_map(b: boolean()))
575594
|> equal?(open_map(a: integer()))
595+
596+
# Difference with single field open map on rhs (they are optimized)
597+
assert difference(closed_map(a: integer()), open_map(a: integer())) == none()
598+
assert difference(closed_map(a: integer()), open_map(a: if_set(integer()))) == none()
599+
600+
assert difference(closed_map(a: integer()), open_map(b: integer())) ==
601+
closed_map(a: integer())
602+
603+
assert difference(closed_map(a: integer()), open_map(b: if_set(integer()))) == none()
576604
end
577605

578606
test "map (struct optimizations)" do

0 commit comments

Comments
 (0)