diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index e210dc6404..7e07c0f4a9 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -321,7 +321,8 @@ defmodule Module.Types.Descr do # If type contains a :dynamic part, :optional gets added there. def if_set(type) do case type do - %{dynamic: :term} -> %{dynamic: term_or_optional()} + %{dynamic: :term} when map_size(type) == 1 -> %{dynamic: term_or_optional()} + %{dynamic: :term} -> Map.put(%{type | dynamic: term_or_optional()}, :optional, 1) %{dynamic: dyn} -> Map.put(%{type | dynamic: Map.put(dyn, :optional, 1)}, :optional, 1) _ -> Map.put(type, :optional, 1) end @@ -342,10 +343,14 @@ defmodule Module.Types.Descr do defp remove_optional(descr) do case descr do %{dynamic: %{optional: _} = dynamic} when map_size(dynamic) == 1 -> - Map.delete(descr, :dynamic) + descr + |> Map.delete(:dynamic) + |> remove_optional_static() %{dynamic: %{optional: _} = dynamic} -> - %{descr | dynamic: Map.delete(dynamic, :optional)} + descr + |> Map.replace!(:dynamic, Map.delete(dynamic, :optional)) + |> remove_optional_static() _ -> remove_optional_static(descr) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index e01160d5b0..15cfcd9c0e 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -258,6 +258,15 @@ defmodule Module.Types.DescrTest do end end + describe "if_set" do + test "preserves static parts alongside dynamic term" do + type = union(atom([:value]), dynamic()) |> if_set() + + assert equal?(type, union(if_set(atom([:value])), dynamic(if_set(term())))) + refute equal?(type, dynamic(if_set(term()))) + end + end + describe "intersection" do test "bitmap" do assert intersection(integer(), union(integer(), float())) == integer() @@ -294,6 +303,12 @@ defmodule Module.Types.DescrTest do # Check for structural equivalence assert intersection(dynamic(not_set()), term()) == none() + assert equal?(intersection(if_set(dynamic(integer())), term()), dynamic(integer())) + + assert equal?( + intersection(if_set(union(atom(), dynamic())), term()), + union(atom(), dynamic()) + ) end test "tuple" do diff --git a/lib/elixir/test/elixir/module/types/map_test.exs b/lib/elixir/test/elixir/module/types/map_test.exs index fad13382f7..0c9db45fa0 100644 --- a/lib/elixir/test/elixir/module/types/map_test.exs +++ b/lib/elixir/test/elixir/module/types/map_test.exs @@ -274,6 +274,24 @@ defmodule Module.Types.MapTest do ]) end + test "optionalizing an inferred gradual value preserves its static part" do + value = typecheck!([condition?, x], if(condition?, do: :value, else: x)) + optional = if_set(value) + + assert equal?(optional, union(if_set(atom([:value])), dynamic(if_set(term())))) + refute equal?(optional, dynamic(if_set(term()))) + end + + test "reports errors from static part of optionalized gradual value" do + assert typeerror!( + [condition?, x], + ( + map = Map.from_keys([:key], if(condition?, do: :value, else: x)) + Map.fetch!(map, :key) + 1 + ) + ) =~ "incompatible types given to Kernel.+/2" + end + test "inference" do assert typecheck!( [x],