Skip to content

Commit f70e504

Browse files
committed
Fix optimized map difference with open key
Closes #15507. Closes #15508.
1 parent 73cf46d commit f70e504

2 files changed

Lines changed: 49 additions & 1 deletion

File tree

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6390,7 +6390,30 @@ defmodule Module.Types.Descr do
63906390
defp opt_map_difference(bdd1, bdd2),
63916391
do: bdd_difference(bdd1, bdd2, &opt_map_leaf_difference/3)
63926392

6393-
defp opt_map_leaf_difference(bdd_leaf(tag, fields), bdd_leaf(:open, [{key, v2}]), type) do
6393+
# This clause optimizes differences with an open single-key right side:
6394+
#
6395+
# %{b: atom(), c: pid()} \ %{..., c: if_set(pid() | binary())}
6396+
#
6397+
# Depending on the surrounding BDD shape, the difference formulas ask the
6398+
# leaf comparison for a direct difference plus an optional intersection
6399+
# or optional union.
6400+
#
6401+
# Because the right-hand side is an open map with a single key, this branch
6402+
# assumes the result can be represented by updating the matching key
6403+
# on the left-hand side. For this pair, the direct difference is empty and
6404+
# the intersection is `%{b: atom(), c: pid()}`, which are both correct.
6405+
#
6406+
# However, when the formula needs the union, this shortcut would produce
6407+
# `%{b: atom(), c: if_set(pid() or binary())}` by preserving the left shape.
6408+
# This is not the semantic union, because the right side also includes maps
6409+
# without `:b`. In other words, we cannot always union them by updating only
6410+
# the matching key.
6411+
#
6412+
# Therefore, this clause is only used when `type` is `:intersection` or `:none`.
6413+
# `:union` falls through to the general clause below. The reason we have
6414+
# this long comment is because this was a regression in the past.
6415+
defp opt_map_leaf_difference(bdd_leaf(tag, fields), bdd_leaf(:open, [{key, v2}]), type)
6416+
when type != :union do
63946417
{found?, v1} =
63956418
case fields_find(key, fields) do
63966419
{:ok, value} -> {true, value}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,31 @@ defmodule Module.Types.DescrTest do
728728
assert opt_difference(closed_map(a: integer()), open_map(b: if_set(integer()))) == none()
729729
end
730730

731+
test "map difference of subtype" do
732+
for {k1, k2} <- [{:a, :b}, {:b, :a}, {:b, :c}, {:c, :a}, {:c, :b}, {:b, :d}] do
733+
a = closed_map([{k1, atom([:x, :y])}, {k2, pid()}])
734+
735+
b =
736+
closed_map([
737+
{k1, if_set(opt_union(atom([:y]), float()))},
738+
{k2, opt_union(pid(), opt_union(binary(), integer()))}
739+
])
740+
741+
c = open_map([{k2, if_set(opt_union(pid(), binary()))}])
742+
743+
# a <= c, while b is unrelated to both a and c.
744+
# Therefore (a \ b) <= a <= c and (a \ b) \ c is empty.
745+
# The optimized difference should match the bare difference for this shape.
746+
ab = opt_difference(a, b)
747+
748+
assert empty?(bare_difference(ab, c)),
749+
"bare difference failed for keys #{inspect({k1, k2})}"
750+
751+
assert empty?(opt_difference(ab, c)),
752+
"opt difference failed for keys #{inspect({k1, k2})}"
753+
end
754+
end
755+
731756
test "map double negation with redundant empty map" do
732757
type =
733758
closed_map(a: atom())

0 commit comments

Comments
 (0)